/** main program for visitors
 *
 * this routine is called from /index.php. It is the main program for visitors.
 *
 * @return void page sent to the browser
 * @todo cleanup login/logout-code
 */
function main_index()
{
    global $USER;
    global $CFG;
    global $LANGUAGE;
    /** initialise the program, setup database, read configuration, etc. */
    require_once $CFG->progdir . '/init.php';
    initialise();
    was_version_check();
    // this never returns if versions don't match
    // TODO: cleanup like in main_admin()
    // handle login/logout/continuation so we quickly find out which user is calling
    if (isset($_GET['logout'])) {
        /** loginlib.php contains both login- and logout-routines */
        require_once $CFG->progdir . '/lib/loginlib.php';
        was_logout();
        // may or may not return here
    } elseif (isset($_GET['login'])) {
        /** loginlib.php contains both login- and logout-routines */
        require_once $CFG->progdir . '/lib/loginlib.php';
        was_login(magic_unquote($_GET['login']));
        // may or may not return here
    } elseif (isset($_COOKIE[$CFG->session_name])) {
        /** dbsessionlib.php contains our own database based session handler */
        require_once $CFG->progdir . '/lib/dbsessionlib.php';
        dbsession_setup($CFG->session_name);
        if (dbsession_exists(magic_unquote($_COOKIE[$CFG->session_name]))) {
            session_start();
        }
    }
    // At this point we either have a valid session with a logged-in user
    // (indicated via existence of $_SESSION) or we are dealing with an anonymous
    // visitor with non-existing $_SESSION. Keep track of the number of calls
    // this user makes (may be logged lateron on logout).
    if (isset($_SESSION)) {
        if (!isset($_SESSION['session_counter'])) {
            // first time after login, record start time of session
            $_SESSION['session_counter'] = 1;
            $_SESSION['session_start'] = strftime("%Y-%m-%d %T");
        } else {
            $_SESSION['session_counter']++;
        }
    }
    // Now is the time to create a USER object, even when the visitor is just a passerby
    // because we can then determine easily if a visitor is allowed certain things, e.g.
    // view a protected area or something
    /** useraccount.class.php is used to define the USER object */
    require_once $CFG->progdir . '/lib/useraccount.class.php';
    if (isset($_SESSION) && isset($_SESSION['user_id'])) {
        $USER = new Useraccount($_SESSION['user_id']);
        $USER->is_logged_in = TRUE;
        $_SESSION['language_key'] = $LANGUAGE->get_current_language();
        // remember language set via _GET or otherwise
    } else {
        $USER = new Useraccount();
        $USER->is_logged_in = FALSE;
    }
    // Check for the special preview-mode
    // This allows a webmaster to preview a page in the correct environment (theme)
    // even when the page is under embargo. Note that the node_id and area_id are
    // retrieved from the session; the user only has a cryptic preview-code.
    // See pagemanagerlib.php for more information (function task_page_preview()).
    $in_preview_mode = FALSE;
    if ($USER->is_logged_in) {
        $preview_code_from_url = get_parameter_string('preview');
        if (!is_null($preview_code_from_url) && isset($_SESSION['preview_salt']) && isset($_SESSION['preview_node'])) {
            $hash = md5($_SESSION['preview_salt'] . $_SESSION['preview_node']);
            if ($hash === $preview_code_from_url) {
                $node_id = intval($_SESSION['preview_node']);
                $area_id = intval($_SESSION['preview_area']);
                $area = db_select_single_record('areas', '*', array('area_id' => $area_id));
                if ($area === FALSE) {
                    logger("Fatal error 070: cannot preview node '{$node_id}' in area '{$area_id}'");
                    error_exit('070');
                } else {
                    $tree = tree_build($area_id);
                    $in_preview_mode = TRUE;
                }
            }
        }
    }
    if ($in_preview_mode == FALSE) {
        $requested_area = get_requested_area();
        $requested_node = get_requested_node();
        $req_area_str = is_null($requested_area) ? "NULL" : strval($requested_area);
        $req_node_str = is_null($requested_node) ? "NULL" : strval($requested_node);
        if (($area = calculate_area($requested_area, $requested_node)) === FALSE) {
            logger("Fatal error 080: no valid area (request: area='{$req_area_str}', node='{$req_node_str}')");
            error_exit('080');
            // no such area
        }
        $area_id = intval($area['area_id']);
        // If $USER has no permission to view area $area_id, we simply bail out.
        // Rationale: if the user is genuine, she knows about logging in first.
        // If the user is NOT logged in and tries to view a protected area, I'd consider
        // it malicious, and in that case I won't even confirm the existence of
        // the requested area. (If a cracker simply tries areas 0,1,.. and sometimes is greeted
        // with 'please enter credentials' and sometimes with 'area does not exist', this
        // provides information to the cracker. I don't want that). Note that the error code
        // is the same as the one for non-existing area. In other words: for an unauthorised
        // visitor an existing private area is just as non-existent as a non-existing public area.
        if (db_bool_is(TRUE, $area['is_private']) && !$USER->has_intranet_permissions(ACL_ROLE_INTRANET_ACCESS, $area_id)) {
            logger(sprintf("Fatal error 080: no view permissions for area '%d' (request: area='%s', node='%s')", $area_id, $req_area_str, $req_node_str));
            error_exit('080');
            // no such area
        }
        // still here?
        // then we've got a valid $area_id and corresponding $area record.
        // now we need to figure out which $node_id to use
        $tree = tree_build($area_id);
        if (($node_id = calculate_node_id($tree, $area_id, $requested_node)) === FALSE) {
            logger(sprintf("Fatal error 080: no valid node within area '%d' (request: area='%s', node='%s')", $area_id, $req_area_str, $req_node_str));
            error_exit('080');
            // no such area
        }
    }
    // At this point we have the following in our hands
    // - a valid $area_id
    // - a valid $node_id
    // - the complete tree from area $area_id in $tree
    // - the area record from database in $area
    // - the node record from database in $tree[$node_id]['record']
    // - a flag that signals preview mode in $in_preview_mode
    // We are on our way to generate a full page with content and all,
    // but otoh we MIGHT be in the middle of a redirect, so we may have to
    // leave without showing anything at all...
    if (!empty($tree[$node_id]['record']['link_href'])) {
        update_statistics($node_id);
        if (isset($_SESSION)) {
            session_write_close();
        }
        redirect_and_exit(htmlspecialchars($tree[$node_id]['record']['link_href']));
        // exit; redirect_and_exit() never returns
    }
    /** themelib contains the theme factory */
    require_once $CFG->progdir . '/lib/themelib.php';
    // And now we know about the $area, we can carry on determining which $theme to use.
    //
    $theme = theme_factory($area['theme_id'], $area_id, $node_id);
    if ($theme === FALSE) {
        logger("Fatal error 090: cannot setup theme '{$area['theme_id']}' in area '{$area_id}'");
        error_exit('090');
    }
    // Tell the theme about the preview mode
    $theme->set_preview_mode($in_preview_mode);
    // Now all we need to do is let the module connected to node $node_id generate output
    $module_id = $tree[$node_id]['record']['module_id'];
    module_view($theme, $area_id, $node_id, $module_id);
    // Remember this visitor
    update_statistics($node_id);
    // Finally, send output to user
    $theme->send_output();
    if (isset($_SESSION)) {
        session_write_close();
    }
    // done!
    exit;
}
/** main program for serving files
 *
 * this routine is called from /file.php.
 *
 * This routine is responsible for serving files to the visitor.
 * These files are stored in a (virtual) file hierarchy that looks
 * like this.
 *
 * <pre>
 * /areas/areaname
 *       /another
 *       /stillmore
 *       ...
 * /users/username
 *       /another
 *       /stillmore
 *       ...
 * /groups/groupname
 *        /another
 *        /stillmore
 *        ...
 * /websiteatschool/program
 *                 /manual
 *                 /languages
 * </pre>
 *
 * This structure maps to the real file system as follows.  The (virtual)
 * directories /areas, /users and /groups correspond to the fysical
 * directories {$CFG->datadir}/areas, {$CFG->datadir}/users and
 * {$CFG->datadir}/groups respectively. The subdirectories correspond to
 * a (unique) area, user or group and serve as a file repository for that
 * area, user or group.
 *
 * The (virtual) top-level directory /websiteatschool is a special case.
 * It is used to serve the currently running website program code and the
 * user-defined translations of active languages.
 *
 * Before any file is transmitted to the visitor the access privileges
 * are checked.  The following rules apply.
 *
 * Access control for the /areas subdirectory
 *
 *  - an area must be active before any files are served
 *  - the visitor must have access to the private area if files are to be served
 *  - non-existing files yield a 404 Not Found error
 *  - non-existing areas also yield a 404 Not Found error
 *  - if the visitor has no access to the private area, also a 404 Not Found error is returned
 *
 * Access control for /users and /groups
 *
 *  - a user/group must be active before any files are served
 *  - non-existing users/groups yield 404 Not Found
 *  - non-existing files in existing directories also yield 404 Not Found
 *
 * Access control for /websiteatschool
 *
 *  - there is no limit on downloading the currently active program code or user-defined translations of active languages
 *
 * Note:
 * The check on '..' in the requested filename would be inconclusive if the $path
 * is encoded in invalid UTF-8: the overlong sequence 2F C0 AE 2E 2F eventually
 * yields 2F 2E 2E 2F or '/../'. Reference: RFC3629 section 10. However, we use
 * the filename processed with get_requested_filename() which already checks for
 * utf8 validity, which rules out the trick with overlong sequences.
 *
 * @return void file sent to the browser OR 404 not found on error
 */
function main_file()
{
    global $USER;
    global $CFG;
    global $WAS_SCRIPT_NAME;
    global $LANGUAGE;
    /** initialise the program, setup database, read configuration, etc. */
    require_once $CFG->progdir . '/init.php';
    initialise();
    was_version_check();
    // this never returns if versions don't match
    /** utility routines for manipulating files */
    require_once $CFG->progdir . '/lib/filelib.php';
    $filename = get_requested_filename();
    if (is_null($filename)) {
        error_exit404();
    }
    // 0 -- is the visitor logged in
    if (isset($_COOKIE[$CFG->session_name])) {
        /** dbsessionlib.php contains our own database based session handler */
        require_once $CFG->progdir . '/lib/dbsessionlib.php';
        dbsession_setup($CFG->session_name);
        if (dbsession_exists(magic_unquote($_COOKIE[$CFG->session_name]))) {
            session_start();
            if (!isset($_SESSION['session_counter'])) {
                // first time after login, record start time of session
                $_SESSION['session_counter'] = 1;
                $_SESSION['session_start'] = strftime("%Y-%m-%d %T");
            } else {
                $_SESSION['session_counter']++;
            }
        }
    }
    /** useraccount.class.php is used to define the USER object */
    require_once $CFG->progdir . '/lib/useraccount.class.php';
    if (isset($_SESSION) && isset($_SESSION['user_id'])) {
        $USER = new Useraccount($_SESSION['user_id']);
        $USER->is_logged_in = TRUE;
        $_SESSION['language_key'] = $LANGUAGE->get_current_language();
        // remember language set via _GET or otherwise
        session_write_close();
        // we no longer need this here, everything relevant is now in $USER
    } else {
        $USER = new Useraccount();
        $USER->is_logged_in = FALSE;
    }
    //
    // 1 -- does the visitor want to download the source code
    //
    $path_components = explode('/', trim(strtr($filename, '\\', '/'), '/'));
    if (strtolower($path_components[0]) == 'websiteatschool') {
        $source = isset($path_components[1]) ? strtolower($path_components[1]) : 'program';
        download_source($source);
        exit;
    }
    //
    // 2 -- no source code requested. check out regular files
    //
    $path = '/' . implode('/', $path_components);
    // 2A -- always disallow attempts to escape from tree via parent directory tricks
    if (in_array('..', $path_components)) {
        logger(sprintf("%s(): access denied for file '%s': no tricks with '/../': return 404 Not Found", __FUNCTION__, $path), WLOG_DEBUG);
        error_exit404($path);
    }
    // 2B -- check the 1st and 2nd component of the requested file
    switch ($path_components[0]) {
        case 'areas':
            $area_path = isset($path_components[1]) ? $path_components[1] : '';
            $fields = array('area_id', 'is_private');
            $where = array('is_active' => TRUE, 'path' => $area_path);
            $table = 'areas';
            if (($record = db_select_single_record($table, $fields, $where)) === FALSE) {
                logger(sprintf("%s(): access denied for file '%s': non-existing or inactive area: return 404 Not Found", __FUNCTION__, $path), WLOG_DEBUG);
                error_exit404($path);
            }
            $area_id = intval($record['area_id']);
            if (db_bool_is(TRUE, $record['is_private']) && !$USER->has_intranet_permissions(ACL_ROLE_INTRANET_ACCESS, $area_id)) {
                logger(sprintf("%s(): access denied for file '%s' in private area '%d': return 404 Not Found", __FUNCTION__, $path, $area_id), WLOG_DEBUG);
                error_exit404($path);
            }
            break;
        case 'users':
            $user_path = isset($path_components[1]) ? $path_components[1] : '';
            $fields = array('user_id');
            $where = array('path' => $user_path, 'is_active' => TRUE);
            $table = 'users';
            if (($record = db_select_single_record($table, $fields, $where)) === FALSE) {
                logger(sprintf("%s(): access denied for file '%s': non-existing or inactive user: return 404 Not Found", __FUNCTION__, $path), WLOG_DEBUG);
                error_exit404($path);
            }
            break;
        case 'groups':
            $group_path = isset($path_components[1]) ? $path_components[1] : '';
            $fields = array('group_id');
            $where = array('path' => $group_path, 'is_active' => TRUE);
            $table = 'groups';
            if (($record = db_select_single_record($table, $fields, $where)) === FALSE) {
                logger(sprintf("%s(): access denied for file '%s': non-existing or inactive group: return 404 Not Found", __FUNCTION__, $path), WLOG_DEBUG);
                error_exit404($path);
            }
            break;
        default:
            logger(sprintf("%s(): access denied for file '%s': subdirectory '%s' not recognised: return 404 Not Found", __FUNCTION__, $path, $path_components[0]), WLOG_DEBUG);
            error_exit404($path);
            break;
    }
    // 2C -- still here? 1st and 2nd components are good but does the file exist?
    if (!is_file($CFG->datadir . $path)) {
        logger(sprintf("%s(): access denied for file '%s': file does not exist: return 404 Not Found", __FUNCTION__, $path), WLOG_DEBUG);
        error_exit404($path);
    }
    //
    // At this point we confident that the file exists within the data directory and also that
    // the visitor is allowed access to the file. Now send the file to the visitor.
    //
    $name = basename($path);
    if (($bytes_sent = send_file_from_datadir($path, $name)) === FALSE) {
        logger(sprintf("Failed to send '%s' using filename '%s'", $path, $name));
        $retval = FALSE;
    } else {
        logger(sprintf("Success sending '%s' using filename '%s', size = %d bytes", $path, $name, $bytes_sent), WLOG_DEBUG);
        $retval = TRUE;
    }
    exit;
}
/** continue the session from the previous call OR exit
 *
 * This tries to resume the session that was initiated before
 * (when the user logged in succesfully). If the session cannot
 * be resumed, we logout the user, show the login screen and exit.
 * In other words: this routine guarantees that a valid session
 * exists if and when this routine returns. If the routine returns,
 * it returns the user_id.
 *
 * @return void|int on success the $user_id is returned else a logout + exit is forced
 * @uses $CFG;
 * @uses dbsessionlib.php
 */
function admin_continue_session()
{
    global $CFG;
    /** dbsessionlib.php contains our own database based session handler */
    require_once $CFG->progdir . '/lib/dbsessionlib.php';
    dbsession_setup($CFG->session_name);
    if (dbsession_exists($_COOKIE[$CFG->session_name])) {
        session_start();
    }
    if (!isset($_SESSION)) {
        admin_logout_and_exit();
    } else {
        if (!isset($_SESSION['session_counter'])) {
            // first time after login, record start time of session
            $_SESSION['session_counter'] = 1;
            $_SESSION['session_start'] = strftime("%Y-%m-%d %T");
        } else {
            $_SESSION['session_counter']++;
        }
        $user_id = $_SESSION['user_id'];
    }
    return $user_id;
}
/** end a session (logout the user) and maybe redirect
 *
 * This routine ends the current session if it exists (as indicated by the
 * cookie presented by the user's browser). An empty value is sent
 * to the browser (effectively deleting the cookie) and also
 * the session is ended. The routine ends either with
 * showing a generic login dialog OR a redirection to
 * a user-defined page.
 *
 * Note that as a rule this routine does NOT return but instead calls exit().
 * However, there are cases where this routine DOES return, notably when
 * no session appears to be established (no cookie submitted by the browser
 * or a non-existing/expired session). If the routine does return, the status
 * is equivalent to a logged out user; no session exists so the user simply 
 * should not be logged in.
 *
 * @return void this function sometimes never returns
 * @uses $CFG
 * @uses dbsession_setup()
 */
function was_logout()
{
    global $CFG;
    if (isset($_COOKIE[$CFG->session_name])) {
        /** install our own database based session handler */
        require_once $CFG->progdir . '/lib/dbsessionlib.php';
        dbsession_setup($CFG->session_name);
        if (dbsession_exists($_COOKIE[$CFG->session_name])) {
            session_start();
            $redirect = isset($_SESSION['redirect']) ? $_SESSION['redirect'] : '';
            $user_id = $_SESSION['user_id'];
            $logmessage = 'logout: \'' . $_SESSION['username'] . '\' (' . $user_id . '): success';
            $logmessage .= ' (session started ' . $_SESSION['session_start'] . ', count=' . $_SESSION['session_counter'] . ')';
            $_SESSION = array();
            // get rid of all session variables
            /* kill the session/cookie in the user's browser (with the correct cookie parameters)... */
            $a = session_get_cookie_params();
            setcookie($CFG->session_name, '', time() - 86400, $a['path'], $a['domain'], $a['secure']);
            /* ...and also in the sessions table */
            session_destroy();
            /* log the event */
            logger($logmessage, WLOG_INFO, $user_id);
            if (!empty($redirect)) {
                header('Location: ' . $redirect);
                echo '<a href="' . $redirect . '">' . htmlspecialchars($redirect) . '</a>';
            } else {
                show_login(LOGIN_PROCEDURE_NORMAL, t('logout_successful', 'loginlib'));
            }
        } else {
            /* kill the cookie in the user's browser even when the session does no longer exist in database */
            $a = session_get_cookie_params();
            setcookie($CFG->session_name, '', time() - 86400, $a['path'], $a['domain'], $a['secure']);
            logger('logout: reset user\'s cookie even without corresponding session record in database', WLOG_DEBUG);
            /* since we don't know where to go next (no redirect), we'll go to login screen #1 */
            show_login(LOGIN_PROCEDURE_NORMAL, t('logout_forced', 'loginlib'));
        }
        exit;
    } else {
        // nothing because no cookie was/is set
        logger('logout: nothing to do');
    }
}