false, 'wide_high' => false, 'wide' => false, 'wide_print' => false, 'filtered' => false, 'utheme' => false, // 'true' means that the static cache cannot cache with this parameter by default 'active_filter' => true, 'redirected' => true, 'redirect_url' => true, 'redirect' => true, 'redirect_passon' => true, // Google Analytics 'utm_source' => false, 'utm_medium' => false, 'utm_campaign' => false, 'utm_term' => false, 'utm_content' => false, ]; inform_non_canonical_parameter('#^(.*_)?(max|start|sort)$#'); if (function_exists('get_value')) { $is_non_canonical = false; $canonical_keep_params = explode(',', (get_value('canonical_keep_params') === null) ? 'keep_devtest' : get_value('canonical_keep_params')); if ((get_site_default_lang() != user_lang()) || ((get_option('detect_lang_forum') == '1') && (!is_guest())) || (get_option('detect_lang_browser') == '1')) { $canonical_keep_params[] = 'keep_lang'; } foreach (array_merge(array_keys($_GET), ['keep_session'/*may be inserted later*/]) as $key) { if ((is_string($key)) && (substr($key, 0, 5) == 'keep_') && (!@in_array($key, $canonical_keep_params))) { $NON_CANONICAL_PARAMS[$key] = true; $is_non_canonical = true; } } /* Doing this redirect is too risky. Some code (including user code) may redirect back to inject missing parameters. if (($is_non_canonical) && (get_bot_type() !== null)) { // Force bots onto the canonical URL if there were non-standard keep parameters, as they may ignore even the canonical meta tag. $non_canonical = []; if (is_array($NON_CANONICAL_PARAMS)) { foreach (array_keys($NON_CANONICAL_PARAMS) as $n) { $non_canonical[$n] = null; } } set_http_status_code(301); require_code('urls'); header('Location: ' . escape_header(get_self_url(true, false, $non_canonical))); // assign_refresh not used, as it is a pre-page situation exit(); } */ } global $PAGE_STRING, $LAST_COMCODE_PARSED_TITLE; $PAGE_STRING = null; $LAST_COMCODE_PARSED_TITLE = ''; global $PT_PAIR_CACHE_CP; $PT_PAIR_CACHE_CP = []; global $COMCODE_PAGE_RUNTIME_CACHE; $COMCODE_PAGE_RUNTIME_CACHE = []; do_site_prep(); } /** * Attach some extra JavaScript to . Don't use this too commonly, it's not a 'tidy' way of doing things. * * @sets_output_state * * @param mixed $data JavaScript to attach (string or Tempcode) */ function attach_to_javascript($data) { global $JAVASCRIPT; if ($JAVASCRIPT === null) { $JAVASCRIPT = new Tempcode(); } $JAVASCRIPT->attach($data); } /** * Attach some HTML to the screen header. * * @sets_output_state * * @param mixed $data HTML to attach, provided in HTML format (string or Tempcode) */ function attach_to_screen_header($data) { global $EXTRA_HEAD; if ($EXTRA_HEAD === null) { $EXTRA_HEAD = new Tempcode(); } $EXTRA_HEAD->attach($data); } /** * Mark another parameter non-canonical, so that search engines won't consider it when indexing URLs. * * @sets_output_state * * @param ID_TEXT $param Parameter name * @param boolean $block_page_from_static_cache_if_present If set to true and if this parameter is present in the URL then no static cache save will happen */ function inform_non_canonical_parameter(string $param, bool $block_page_from_static_cache_if_present = true) { global $NON_CANONICAL_PARAMS; if ($param[0] == '#') { // A regexp foreach (array_keys($_GET) as $key) { if (is_numeric($key)) { $key = strval($key); } if (preg_match($param, $key) != 0) { inform_non_canonical_parameter($key); } } } else { $NON_CANONICAL_PARAMS[$param] = $block_page_from_static_cache_if_present; } } /** * Attach a message to the page output. * For response-embedded messaging where the code isn't failing per-se, but it's more like a predictable messaging situation occurred. * For failure-like messages call trigger_error which will in turn call attach_message itself. * * @sets_output_state * * @param mixed $message The message to show, provided in plain-text format or HTML Tempcode * @param ID_TEXT $type The 'template' to use * @set inform notice warn * @param boolean $put_in_helper_panel Whether to put into the helper panel instead of the normal header area * @param boolean $log_error Whether to log the error * @return string Blank string so it can be chained in the Tempcode compiler. You will rarely want to use this return value. It's kind of a failsafe. */ function attach_message($message, string $type = 'inform', bool $put_in_helper_panel = false, bool $log_error = false) : string { $message_eval = (is_object($message) ? $message->evaluate() : escape_html($message)); $message_hash = hash('sha256', $message_eval); if ($message_eval == '') { return ''; // Empty message } if ((error_reporting() == 0) && ($type == 'warn')) { return ''; // Suppressing errors } global $ATTACHED_MESSAGES, $ATTACHED_MESSAGES_RAW, $LATE_ATTACHED_MESSAGES, $SITE_INFO; if (isset($ATTACHED_MESSAGES_RAW[$message_hash])) { return ''; // Already shown } // We wait until after checking if the message was already shown before counting towards infinite loop to prevent premature terminations check_for_infinite_loop('attach_message', [$message_eval], 2); $ATTACHED_MESSAGES_RAW[$message_hash] = [$message_eval, $type]; if ($log_error) { require_code('urls'); $php_error_label = $message_eval . ' @ ' . get_self_url_easy(); $may_log_error = ((!running_script('cron_bridge')) || (@filemtime(get_custom_file_base() . '/data_custom/errorlog.php') < time() - 60 * 5)); if ($may_log_error) { if ((function_exists('syslog')) && (GOOGLE_APPENGINE)) { syslog(LOG_ERR, $php_error_label); } if (php_function_allowed('error_log')) { switch ($type) { case 'inform': case 'notice': @error_log(brand_name() . ': INFO ' . $php_error_label, 0); break; case 'warn': @error_log(brand_name() . ': WARNING ' . $php_error_label, 0); break; } } require_code('failure'); $trace = get_html_trace(); relay_error_notification('[html]' . $message_eval . $trace->evaluate() . '[/html]'); } } if ($type == 'warn') { require_code('failure'); $webservice_result = get_webservice_result($message); if ($webservice_result !== null) { if ((is_object($message)) && (!empty($message->pure_lang))) { $message = $message->evaluate(); } elseif (is_object($message)) { $message = escape_html($message->evaluate()); } else { $message = escape_html($message); } require_code('templates_tooltip'); $message = tooltip($message, $webservice_result, false); $message = protect_from_escaping($message); } if (current_fatalistic() > 0) { fatal_exit($message); } } // FUDGE: No messages on the slideshow overlay as it messes with the full-screen layout if (!function_exists('get_page_name') || (get_page_name() != 'galleries') || (get_param_integer('slideshow', 0) == 0)) { $message_tpl = do_template('MESSAGE', [ '_GUID' => 'ec843c8619d21fbeeb512686ea300a17', 'TYPE' => $type, 'MESSAGE' => is_string($message) ? escape_html($message) : $message, ]); if ($put_in_helper_panel) { set_helper_panel_text($message_tpl, true, false); } else { if ($GLOBALS['TEMPCODE_OUTPUT_STARTED']) { if ($LATE_ATTACHED_MESSAGES === null) { $LATE_ATTACHED_MESSAGES = new Tempcode(); } $LATE_ATTACHED_MESSAGES->attach($message_tpl); } else { if ($ATTACHED_MESSAGES === null) { $ATTACHED_MESSAGES = new Tempcode(); } $ATTACHED_MESSAGES->attach($message_tpl); } } } if ($GLOBALS['REFRESH_URL'][0] != '') { $map = [ 'r_session_id' => get_session_id(), 'r_type' => $type, 'r_time' => time(), ]; $map += insert_lang('r_message', $message_eval, 5, null, true, null, null, true, null, null, true, true); $GLOBALS['SITE_DB']->query_insert('messages_to_render', $map); } return ''; } /** * Get the relative URL to the logo for the current zone. * * @param ?ID_TEXT $zone_name The zone being operated within (null: auto-detect) * @return URLPATH The relative URL to the logo for the current zone */ function get_logo_url(?string $zone_name = null) : string { global $ZONE; if ($zone_name === null) { $zone_name = $ZONE['zone_name']; } $image = find_theme_image('logo/' . $zone_name . '-logo', true); if ($image == '') { // Fallback to welcome zone logo $image = find_theme_image('logo/-logo'); } return $image; } /** * Get the Tempcode for the breadcrumbs. * * @param boolean $show_self Whether to show a self segment * @return Tempcode The breadcrumbs */ function breadcrumbs(bool $show_self = true) : object { static $cache = []; if (isset($cache[$show_self])) { return $cache[$show_self]; } global $BREADCRUMB_SET_PARENTS, $BREADCRUMBS; // Special hard-coded link to sitemap structure for Admin and CMS zones $zone = get_zone_name(); if ((($zone == 'adminzone') || ($zone == 'cms')) && (get_option('deeper_admin_breadcrumbs') == '1') && (get_page_name() != 'login')) { require_code('site_adminzone'); $BREADCRUMB_SET_PARENTS = array_merge(adminzone_extended_breadcrumbs(), $BREADCRUMB_SET_PARENTS); } // Substitutions if ((addon_installed('breadcrumbs')) && (function_exists('xml_parser_create'))) { // We may need to add in the final label so Source_breadcrumb_substitution_loader::load_breadcrumb_substitutions can see and match on it; we will then pop that final label knowing that load_breadcrumb_substitutions altered the BREADCRUMB_SET_SELF global if it needed to require_code('breadcrumbs'); $needs_pop = false; if (($GLOBALS['BREADCRUMB_SET_SELF'] !== null) || ($GLOBALS['DISPLAYED_TITLE'] !== null)) { $last_segment_label = isset($BREADCRUMB_SET_PARENTS[count($BREADCRUMB_SET_PARENTS) - 1]) ? $BREADCRUMB_SET_PARENTS[count($BREADCRUMB_SET_PARENTS) - 1][1] : null; $new_last_segment_label = ($GLOBALS['BREADCRUMB_SET_SELF'] !== null) ? $GLOBALS['BREADCRUMB_SET_SELF'] : $GLOBALS['DISPLAYED_TITLE']; if (($last_segment_label === null) || ((is_object($new_last_segment_label) ? $new_last_segment_label->evaluate() : $new_last_segment_label) != (is_object($last_segment_label) ? $last_segment_label->evaluate() : $last_segment_label))) { $BREADCRUMB_SET_PARENTS[] = [null, $last_segment_label]; $needs_pop = true; } } $BREADCRUMB_SET_PARENTS = Source_breadcrumb_substitution_loader::load_breadcrumb_substitutions($BREADCRUMB_SET_PARENTS); if ($needs_pop) { array_pop($BREADCRUMB_SET_PARENTS); } } // Render if ($BREADCRUMBS === null) { $BREADCRUMBS = breadcrumbs_get_default_stub($show_self); } $out = new Tempcode(); $out->attach($BREADCRUMBS); $cache[$show_self] = $out; return $out; } /** * Get the Tempcode for the default breadcrumbs stub. This isn't entirely a default, because it does work with breadcrumb_set_parents. We refer to it as a default as it is possible to override the whole breadcrumbs environment via the special BREADCRUMBS global variable. * * @param boolean $link_to_self_entrypoint Whether we'll be providing a link to where we are currently at * @return Tempcode The default breadcrumb stub */ function breadcrumbs_get_default_stub(bool $link_to_self_entrypoint = true) : object { global $BREADCRUMB_SET_PARENTS, $DISPLAYED_TITLE, $BREADCRUMB_SET_SELF; $stub = new Tempcode(); // Specified parents $stub->attach(breadcrumb_segments_to_tempcode($BREADCRUMB_SET_PARENTS, $link_to_self_entrypoint)); // Self-link if ($link_to_self_entrypoint) { $label = (($BREADCRUMB_SET_SELF === null) && ($DISPLAYED_TITLE !== null)) ? protect_from_escaping($DISPLAYED_TITLE) : $BREADCRUMB_SET_SELF; if ($label !== null) { $label_eval = is_object($label) ? $label->evaluate() : $label; $last_breadcrumb_label_eval = mixed(); $last_breadcrumb_label_eval = (empty($BREADCRUMB_SET_PARENTS)) ? '' : $BREADCRUMB_SET_PARENTS[count($BREADCRUMB_SET_PARENTS) - 1][1]; if (is_object($last_breadcrumb_label_eval)) { $last_breadcrumb_label_eval = $last_breadcrumb_label_eval->evaluate(); } if ($label_eval != $last_breadcrumb_label_eval) { if (!empty($BREADCRUMB_SET_PARENTS)) { $stub->attach(do_template('BREADCRUMB_SEPARATOR')); } $stub->attach(do_template('BREADCRUMB_LONE_WRAP', ['_GUID' => '769236ef4f20a0a05cee6d7a335eaf9f', 'LABEL' => $label])); } } } return $stub; } /** * Convert breadcrumb segments to Tempcode breadcrumbs. * * @param array $segments The segments in array format * @param ?mixed $link_to_self_entrypoint Whether we'll be providing a link to where we are currently at (by reference, gets set to false in some circumstances) (null: don't save by reference) * @return Tempcode The segments in Tempcode0 */ function breadcrumb_segments_to_tempcode(array $segments, &$link_to_self_entrypoint = null) : object { $out = new Tempcode(); foreach ($segments as $i => $bit) { list($entry_point, $label) = $bit; $tooltip = isset($bit[2]) ? $bit[2] : ''; if (!(((is_string($tooltip)) && ($tooltip == '')) || ((is_object($tooltip)) && ($tooltip->is_empty())))) { $link_to_self_entrypoint = false; // Tooltip implies that we are defining end-point ourselves } if ($i != 0) { $out->attach(do_template('BREADCRUMB_SEPARATOR')); } if ($entry_point == '') { $out->attach(do_template('BREADCRUMB_LONE_WRAP', ['_GUID' => 'fcf371ee4a071ebdd170dc16a55f36dd', 'LABEL' => $label])); $link_to_self_entrypoint = false; // Empty part implies that we are defining end-point ourselves } else { $url = page_link_to_tempcode_url($entry_point); $out->attach(do_template('BREADCRUMB_LINK_WRAP', ['_GUID' => 'f7e8a83d61bde871ab182dec7da84ccc', 'LABEL' => $label, 'URL' => $url, 'TOOLTIP' => $tooltip])); } } return $out; } /** * Put a list of parents in for the breadcrumbs. * * @sets_output_state * * @param array $parents The list of parent entry points (pairs: entry point, title) */ function breadcrumb_set_parents(array $parents) { global $BREADCRUMB_SET_PARENTS; $BREADCRUMB_SET_PARENTS = $parents; } /** * Set the current title. * * @sets_output_state * * @param mixed $title The title, provided in plain-text format or HTML Tempcode */ function breadcrumb_set_self($title) { global $BREADCRUMB_SET_SELF; $BREADCRUMB_SET_SELF = $title; } /** * Add a feed (RSS/Atom) URL to the metadata for this page. * * @sets_output_state * * @param URLPATH $url The URL to the feed (if this starts with ?, then the backend script will be prepended automatically) * @param SHORT_TEXT $title The title of the feed */ function inject_feed_url(string $url, string $title) { global $FEED_URLS; $FEED_URLS[] = ['title' => $title, 'url' => $url]; } /** * Set the helper panel text. * * @sets_output_state * * @param ?Tempcode $text The text (null: none) * @param boolean $append Whether to append * @param boolean $put_in_box Whether to add a box around the parameter */ function set_helper_panel_text(?object $text, bool $append = true, bool $put_in_box = true) { if ($put_in_box) { $text = put_in_standard_box($text); } global $HELPER_PANEL_TEXT; if ($append) { if (is_string($HELPER_PANEL_TEXT)) { $HELPER_PANEL_TEXT = make_string_tempcode($HELPER_PANEL_TEXT); } $HELPER_PANEL_TEXT->attach($text); } else { $HELPER_PANEL_TEXT = $text; } } /** * Set the helper panel tutorial. * * @sets_output_state * * @param ?ID_TEXT $tutorial The page name of the tutorial (must be an existing one on the brand site) (null: none) */ function set_helper_panel_tutorial(?string $tutorial) { global $HELPER_PANEL_TUTORIAL; $HELPER_PANEL_TUTORIAL = $tutorial; } /** * Sets the short title, used for screen header text if set. * * @sets_output_state * * @param string $title The short title */ function set_short_title(string $title) { global $SHORT_TITLE, $FORCE_SET_TITLE; if (!$FORCE_SET_TITLE) { $SHORT_TITLE = ($title == '') ? null : $title; } } /** * Prepare for the main do_site() page renderer. */ function do_site_prep() { erase_rejected_cookies(); load_zone_data(); // SEO redirection require_code('urls'); if (can_try_url_schemes()) { $ruri = $_SERVER['REQUEST_URI']; $url_scheme = get_option('url_scheme'); if (($url_scheme == 'PG') || ($url_scheme == 'HTM')) { if ( (!headers_sent()) && (running_script('index')) && ($GLOBALS['RELATIVE_PATH'] == get_zone_name()/*i.e. a proper zone*/) && ($_SERVER['REQUEST_METHOD'] != 'POST') && (get_param_integer('keep_failover', null) !== 0) && ((strpos($ruri, '/pg/') === false) || ($url_scheme != 'PG')) && ((strpos($ruri, '.htm') === false) || ($url_scheme != 'HTM')) && ((substr(preg_replace('#\?.*$#', '', $ruri), -1) != '/') || (get_option('url_scheme_omit_default_zone_pages') == '0')) ) { require_code('permissions'); set_http_status_code(301); if ($GLOBALS['DEV_MODE']) { header('Redirect-Reason: Correct URL scheme'); } header('Location: ' . escape_header(get_self_url(true))); // assign_refresh not used, as it is a pre-page situation exit(); } } } // Search engine having session in URL, we don't like this if ((get_bot_type() !== null) && ($_SERVER['REQUEST_METHOD'] != 'POST') && (get_param_string('keep_session', null) !== null)) { //Too risky, what if something sets it at run-time. Relying on canonical URL is better. //set_http_status_code(301); //header('Location: ' . escape_header(get_self_url(true, false, ['keep_session' => null, 'keep_print' => null]))); // assign_refresh not used, as it is a pre-page situation //exit(); } if ((running_script('index')) && (!is_cli())) { $request_hostname = get_request_hostname(); if (get_value('disable_cookie_checks') !== '1') { // Detect bad cookie domain (reasonable approximation) $cookie_domain = @ltrim(get_cookie_domain(), '.'); if (!empty($cookie_domain) && !empty($request_hostname)) { if (substr($request_hostname, -strlen($cookie_domain)) != $cookie_domain) { attach_message(do_lang_tempcode('INCORRECT_COOKIE_DOMAIN', escape_html($cookie_domain), escape_html($request_hostname)), 'warn'); } } // Detect bad cookie path $cookie_path = get_cookie_path(); $access_path = $_SERVER['SCRIPT_NAME']; if (!empty($cookie_path) && !empty($access_path)) { if (substr($access_path, 0, strlen($cookie_path)) != $cookie_path) { attach_message(do_lang_tempcode('INCORRECT_COOKIE_PATH', escape_html($cookie_path), escape_html($access_path)), 'warn'); } } } } if (running_script('index')) { process_url_monikers(); } // Bulk advance loading global $SMART_CACHE; if (isset($SMART_CACHE)) { $_comcode_pages_needed = $SMART_CACHE->get('comcode_pages_needed'); if ($_comcode_pages_needed !== null) { $comcode_pages_needed = []; foreach ($_comcode_pages_needed as $_comcode_page_needed => $_) { $__comcode_page_needed = @unserialize($_comcode_page_needed); if ($__comcode_page_needed !== false) { $comcode_pages_needed[] = $__comcode_page_needed; } } if (count($comcode_pages_needed) < 20) { _load_comcodes_page_from_cache($comcode_pages_needed); } } } } /** * Load up details for the current zone. */ function check_has_page_access() { $real_zone = load_zone_data(); // The most important security check get_member(); // Make sure we've loaded our backdoor if installed require_code('permissions'); global $ZONE; if ($ZONE['zone_require_session'] == 1) { set_no_clickjacking_csp(); } if (($ZONE['zone_name'] != '') && ($ZONE['zone_require_session'] == 1) && (get_page_name() != 'login') && (!session_considered_confirmed())) { access_denied((($real_zone == 'data') || (has_zone_access(get_member(), $ZONE['zone_name']))) ? 'ZONE_ACCESS_SESSION' : 'ZONE_ACCESS', $ZONE['zone_name'], true); } else { if (($real_zone == 'data') || (has_zone_access(get_member(), $ZONE['zone_name']))) { if ((running_script('index')) && /*Actually we will allow Guest denying to the front page even though that is a bit weird ((get_page_name()!=$ZONE['zone_default_page']) || ($real_zone!='')) && */(!has_page_access(get_member(), get_page_name(), $ZONE['zone_name'], true))) { access_denied('PAGE_ACCESS'); } } else { if (get_page_name() != 'login') { access_denied('ZONE_ACCESS', $ZONE['zone_name'], true); } } } } /** * Find whether we should consider the current user's session confirmed, for security purposes. * * @return boolean Whether we should */ function session_considered_confirmed() : bool { global $SESSION_CONFIRMED_CACHE; return (is_guest()) || (is_httpauth_login()) || ((get_session_id() != '') && ($SESSION_CONFIRMED_CACHE)); } /** * Load up details for the current zone. * * @return ID_TEXT The "real" zone name (not actually the zone name, but the zone name wants details to load for) */ function load_zone_data() : string { global $ZONE, $RELATIVE_PATH; $zone_name = get_zone_name(); $real_zone = (($RELATIVE_PATH == '_tests') || ($RELATIVE_PATH == 'data') || ($RELATIVE_PATH == 'data_custom')) ? get_param_string('zone', '') : $zone_name; /** A map of the current zone that is running. * * @global array $ZONE */ if ($ZONE === null) { $ZONE = persistent_cache_get(['ZONE', $real_zone]); if ($ZONE === null) { find_all_zones(); // Optimisation, this will load up our zone list which *usually* includes caching of current zone, as this likely is needed somewhere anyway global $ALL_ZONES_TITLED_CACHE; if (isset($ALL_ZONES_TITLED_CACHE[$real_zone][3])) { $ZONE = $ALL_ZONES_TITLED_CACHE[$real_zone][3]; } } if ($ZONE === null) { $zones = $GLOBALS['SITE_DB']->query_select('zones', ['*'], ['zone_name' => $real_zone], '', 1); // Auto-create zone if ((!array_key_exists(0, $zones)) && (is_dir(get_file_base() . '/' . $real_zone . '/' . 'pages'))) { $zones = $GLOBALS['SITE_DB']->query_select('zones', ['*'], ['zone_name' => preg_replace('#\d#', '', $real_zone)], '', 1); if (array_key_exists(0, $zones)) { // Ah, we have a zone with the same name, except the numbers stripped -- this can be a template $map = $zones[0]; $map['zone_name'] = $real_zone; $map = insert_lang('zone_title', get_translated_text($zones[0]['zone_title']), 1) + $map; $map = insert_lang('zone_header_text', get_translated_text($zones[0]['zone_header_text']), 1) + $map; $access = $GLOBALS['SITE_DB']->query_select('group_zone_access', ['group_id'], ['zone_name' => $zones[0]['zone_name']]); foreach ($access as $a) { $GLOBALS['SITE_DB']->query_insert('group_zone_access', ['group_id' => $a['group_id'], 'zone_name' => $real_zone]); } } else { $zone_default_page = DEFAULT_ZONE_PAGE_NAME; if ($real_zone == 'forum') { // A bit of an architectural fudge, but people get confused why it doesn't come back the same $zone_default_page = 'forumview'; } if ($real_zone == 'docs') { $zone_default_page = 'tutorials'; } $map = [ 'zone_name' => $real_zone, 'zone_default_page' => $zone_default_page, 'zone_theme' => 'default', 'zone_require_session' => 0, ]; $map += insert_lang('zone_title', $real_zone, 1); $map += insert_lang('zone_header_text', $real_zone, 1); } $GLOBALS['SITE_DB']->query_insert('zones', $map); $zones = $GLOBALS['SITE_DB']->query_select('zones', ['*'], ['zone_name' => $real_zone], '', 1); } if (array_key_exists(0, $zones)) { $ZONE = $zones[0]; persistent_cache_set(['ZONE', $real_zone], $ZONE); } // Missing zone if ($ZONE === null) { $zones = $GLOBALS['SITE_DB']->query_select('zones', ['*'], ['zone_name' => ''], '', 1); $ZONE = $zones[0]; warn_exit(do_lang_tempcode('BAD_ZONE', escape_html($real_zone))); } unset($zones); } } return $real_zone; } /** * Process URL monikers, changing 'id' GET param to be correct. * * @param boolean $redirect_if_non_canonical Do a redirect if we're not on the canonical URL * @param boolean $env_change Change environmental $_GET parameters (otherwise returns by reference) * @param ?ID_TEXT $page The page name to do it for, as it would appear in the URL (null: read from the environment) * @param ?ID_TEXT $zone The zone name to do it for (null: read from the environment) * @param ?ID_TEXT $type The screen type to do it for (null: read from the environment / really not passed) * @param ?ID_TEXT $url_id The ID to do it for (null: read from the environment / really not passed) * @param boolean $consider_nulls_as_unpassed If any nulls are passed it's considered as 'really not passed' rather than 'read from environment' for $type and $url_id * @return boolean Found via moniker */ function process_url_monikers(bool $redirect_if_non_canonical = true, bool $env_change = true, ?string &$page = null, ?string &$zone = null, ?string &$type = null, ?string &$url_id = null, bool $consider_nulls_as_unpassed = true) : bool { if ($env_change) { static $run_once = false; if ($run_once) { return false; } $run_once = true; } $_page = $page; if ($page === null) { $page = get_page_name(); } if ($zone === null) { $zone = get_zone_name(); } if (($type === null) && ($consider_nulls_as_unpassed)) { $type = get_param_string('type', null, INPUT_FILTER_GET_COMPLEX); } if (($url_id === null) && ($consider_nulls_as_unpassed)) { $url_id = get_param_string('id', null, INPUT_FILTER_GET_COMPLEX); } if (url_monikers_enabled()) { // Monikers relative to the zone $page_place = _request_page($page, $zone); if (($page_place !== false) && ($page_place[0] == 'REDIRECT')) { $page_place_r = $page_place; $page_place = _request_page($page_place[1]['r_to_page'], $page_place[1]['r_to_zone']); if (($page_place !== false) && (substr($page_place[0], 0, 7) != 'COMCODE') || ($type === null)) { // We're viewing the Comcode page behind this redirect, or it's not a Comcode page so nothing is underneath it $page = $page_place_r[1]['r_to_page']; $zone = $page_place_r[1]['r_to_zone']; } } if (($page_place === false) || ((substr($page_place[0], 0, 7) == 'COMCODE') && ($type !== null/*looking deeper than a normal Comcode page*/))) { // This code branch is finding absolute monikers (easy case), or monikers pointing to a Comcode page (special case)... // Reassemble source URL moniker from incorrectly-derived URL components $url_moniker = ''; $url_moniker .= ($_page === null) ? get_param_string('page', '', INPUT_FILTER_GET_COMPLEX) : $_page; /* Has to be unadulterated, $page has /s/-/_ */ if ($type !== null) { $url_moniker .= '/' . $type; } if ($url_id !== null) { $url_moniker .= '/' . $url_id; } // ... and query it $sql = 'SELECT * FROM ' . get_table_prefix() . 'url_id_monikers WHERE '; $sql .= db_string_equal_to('m_moniker', '/' . $url_moniker) . ' OR '; // Absolute $sql .= db_string_equal_to('m_moniker', $url_moniker) . ' AND ' . db_string_equal_to('m_resource_type', '') . ' AND ' . db_string_equal_to('m_resource_id', $zone) . ' OR '; // Comcode page // (dash replacement...) $sql .= db_string_equal_to('m_moniker', '/' . str_replace('-', '_', $url_moniker)) . ' OR '; // Absolute $sql .= db_string_equal_to('m_moniker', str_replace('-', '_', $url_moniker)) . ' AND ' . db_string_equal_to('m_resource_type', '') . ' AND ' . db_string_equal_to('m_resource_id', $zone); // Comcode page $monikers = $GLOBALS['SITE_DB']->query($sql, 1); if (array_key_exists(0, $monikers)) { if (_request_page($monikers[0]['m_resource_page'], $zone) !== false) { // ... if operable within the zone we're in // Bind to correct new values if ($env_change) { global $PAGE_NAME_CACHE, $GETTING_PAGE_NAME; $PAGE_NAME_CACHE = $monikers[0]['m_resource_page']; $GETTING_PAGE_NAME = false; } if ($monikers[0]['m_resource_type'] == '') { if ($env_change) { $_GET['page'] = $monikers[0]['m_resource_page']; unset($_GET['type']); unset($_GET['id']); } else { $page = $monikers[0]['m_resource_page']; $type = null; $url_id = null; } } else { if ($env_change) { $_GET['page'] = $monikers[0]['m_resource_page']; $_GET['type'] = $monikers[0]['m_resource_type']; $_GET['id'] = $monikers[0]['m_resource_id']; } else { $page = $monikers[0]['m_resource_page']; $type = $monikers[0]['m_resource_type']; $url_id = $monikers[0]['m_resource_id']; } } // Check for (and handle) deprecation handle_moniker_deprecation_redirect($zone, $monikers[0], $redirect_if_non_canonical, true); return true; } } } // Does this URL arrangement support monikers? We find out by interrogating hooks to see if the moniker can sit under the module's zone+page+type stub. if (($url_id !== null) && ($redirect_if_non_canonical)) { // NB: Comcode page monikers would have been handled in the code above $type = get_param_string('type', 'browse'); $looking_for = '_SEARCH:' . $page . ':' . $type . ':_WILD'; $hooks = find_all_hooks('systems', 'content_meta_aware'); $ob_info = null; foreach (array_keys($hooks) as $hook) { require_code('content'); $ob = get_content_object($hook); if ($ob === null) { continue; } $ob_info = $ob->info(); if ($ob_info === null) { continue; } if (($ob_info['view_page_link_pattern'] === null) || (!$ob_info['support_url_monikers'])) { continue; } $ob_info['view_page_link_pattern'] = preg_replace('#:[^:]*$#', ':_WILD', $ob_info['view_page_link_pattern']); if ($ob_info['view_page_link_pattern'] == $looking_for) { if (is_numeric($url_id)) { // Lookup and redirect to moniker $correct_moniker = find_id_moniker(['page' => $page, 'type' => get_param_string('type', 'browse'), 'id' => $url_id], $zone); if (($correct_moniker !== null) && ($correct_moniker != $url_id) && (get_param_integer('keep_failover', null) !== 0) && ($_SERVER['REQUEST_METHOD'] != 'POST')) { // test is very unlikely to fail. Will only fail if the title of the resource was numeric - in which case the moniker was chosen to be the ID (NOT the number in the title, as that would have created ambiguity). set_http_status_code(301); $_new_url = build_url(['page' => '_SELF', 'id' => $correct_moniker], '_SELF', [], true); $new_url = $_new_url->evaluate(); if ($GLOBALS['DEV_MODE']) { header('Redirect-Reason: Corrected moniker'); } header('Location: ' . escape_header($new_url)); // assign_refresh not used, as it is a pre-page situation exit(); } } else { // Look up the moniker row $table = 'url_id_monikers' . $GLOBALS['SITE_DB']->prefer_index('url_id_monikers', 'uim_moniker'); $monikers = $GLOBALS['SITE_DB']->query_select($table, ['*'], ['m_resource_page' => $page, 'm_resource_type' => get_param_string('type', 'browse'), 'm_moniker' => $url_id]); if (!array_key_exists(0, $monikers)) { // Uh oh // Assume that it wasn't a moniker after all if (!$ob_info['id_field_numeric']) { return false; } // Okay it was deleted or never existed then?! Just set to -1 as nothing will have that ID, and we'll get an error from the module when bootstrapping is fully finished $_GET['id'] = '-1'; warn_exit(do_lang_tempcode('MISSING_RESOURCE')); } // Map back 'id' if ($env_change) { $_GET['id'] = $monikers[0]['m_resource_id']; // We need to know the ID number rather than the moniker } else { $url_id = $monikers[0]['m_resource_id']; } // Check for (and handle) deprecation handle_moniker_deprecation_redirect($zone, $monikers[0], $redirect_if_non_canonical); return true; } return false; } } } } return false; } /** * Redirect to the latest moniker, if appropriate. * * @param ID_TEXT $zone The zone * @param array $moniker_row The current moniker row * @param boolean $redirect_if_non_canonical Do a redirect if we're not on the canonical URL * @param boolean $is_complete_url_under_zone Is the moniker representing the full URL (as opposed to being underneath a module)? */ function handle_moniker_deprecation_redirect(string $zone, array $moniker_row, bool $redirect_if_non_canonical, bool $is_complete_url_under_zone = false) { $deprecated = $moniker_row['m_deprecated'] == 1; if (($redirect_if_non_canonical) && ($deprecated) && ($_SERVER['REQUEST_METHOD'] != 'POST') && (get_param_integer('keep_failover', null) !== 0)) { $_moniker_row = $moniker_row; unset($_moniker_row['id']); unset($_moniker_row['m_moniker']); unset($_moniker_row['m_moniker_reversed']); unset($_moniker_row['m_manually_chosen']); $_moniker_row['m_deprecated'] = 0; $correct_moniker_rows = $GLOBALS['SITE_DB']->query_select('url_id_monikers', ['m_moniker'], $_moniker_row, '', 1); $correct_moniker = array_key_exists(0, $correct_moniker_rows) ? $correct_moniker_rows[0]['m_moniker'] : null; if (($correct_moniker !== null) && ($correct_moniker != $moniker_row['m_moniker'])) { // Just in case database corruption means ALL are deprecated set_http_status_code(301); if ($is_complete_url_under_zone) { $_new_url = build_url(['page' => $correct_moniker], '_SELF', [], true); } else { $_new_url = build_url(['page' => '_SELF', 'id' => $correct_moniker], '_SELF', [], true); } $new_url = $_new_url->evaluate(); if ($GLOBALS['DEV_MODE']) { header('Redirect-Reason: Deprecated moniker'); } header('Location: ' . escape_header($new_url)); // assign_refresh not used, as it is a pre-page situation exit(); } } } /** * This is it - the start of rendering of a website page. * Take in all inputs, sends them to the correct functions to process, gathers up all the outputs, sticks them together and echoes them. */ function do_site() { $out_evaluated = null; global $ZONE; // Note any special handling needed $special_page_type = get_param_string('special_page_type', 'view'); $keep_markers = get_param_integer('keep_markers', 0); $show_edit_links = get_param_integer('show_edit_links', 0); global $KEEP_MARKERS, $SHOW_EDIT_LINKS; /** Whether we will be embedding template markers in the output. * * @sets_output_state * * @global ?array $KEEP_MARKERS */ $KEEP_MARKERS = ($keep_markers == 1) || ($special_page_type == 'show_markers'); /** Whether we will be embedding edit links in the output. * * @sets_output_state * * @global ?array $SHOW_EDIT_LINKS */ $SHOW_EDIT_LINKS = ($show_edit_links == 1) || ($special_page_type == 'show_edit_links'); if ($SHOW_EDIT_LINKS) { require_css('themes_editor'); } if (($special_page_type != 'view') && ($special_page_type != 'show_markers')) { require_code('view_modes'); initialise_special_page_types($special_page_type); } $doing_special_page_type = ($special_page_type != 'view') && ($special_page_type != 'show_markers') && ($special_page_type != 'show_edit_links') && ($special_page_type != 'memory') && ((has_privilege(get_member(), 'view_profiling_modes')) || ($GLOBALS['IS_ACTUALLY_ADMIN'])); if (((get_option('grow_template_meta_tree') == '1') && (running_script('index'))) || (get_param_integer('keep_grow_template_meta_tree', 0) == 1)) { global $RECORD_TEMPLATES_USED; $RECORD_TEMPLATES_USED = true; } // Pace.js progress bar if (get_theme_option('enable_pace') == '1') { require_css('pace'); require_javascript('pace'); } // Any messages to output? if (get_param_integer('redirected', 0) == 1) { $messages = $GLOBALS['SITE_DB']->query_select('messages_to_render', ['r_message', 'r_type', 'r_time'], ['r_session_id' => get_session_id(),], 'ORDER BY r_time DESC'); foreach ($messages as $message) { $message_text = get_translated_tempcode('messages_to_render', $message, 'r_message'); if ($message_text === null) { // Ignore look-up errors and just skip the message continue; } attach_message($message_text, $message['r_type']); } if (!empty($messages)) { $GLOBALS['SITE_DB']->query_delete('messages_to_render', ['r_session_id' => get_session_id()], ' OR r_time<' . strval(time() - 60 * 60)); } } // Site message(s) if (running_script('index') && addon_installed('site_messaging')) { require_code('site_messaging'); show_site_messages(); } // Public warning about a backed-up mail queue? if ((get_option('mail_queue') == '1') && (get_option('enable_mail_queue_large_warning') == '1')) { $count_queued = $GLOBALS['SITE_DB']->query_select_value('logged_mail_messages', 'COUNT(*)', ['m_queued' => 1]); $batch_size = intval(get_option('max_queued_mails_per_cron_cycle')); if ($count_queued >= ($batch_size * 3)) { attach_message(do_lang_tempcode('MAIL_QUEUE_LARGE'), 'notice'); } } // Warning if dev-mode is on if (($GLOBALS['DEV_MODE']) && (get_param_integer('wide_high', 0) == 0) && (get_param_integer('keep_hide_dev_mode_message', 0) == 0)) { static $done_message = false; if (!$done_message) { attach_message(do_lang_tempcode('DEV_MODE_ON'), 'notice'); $done_message = true; } } // Allow the site to be closed $site_closed = get_option('site_closed'); if (($site_closed != '0') && (!has_privilege(get_member(), 'access_closed_site')) && (!$GLOBALS['IS_ACTUALLY_ADMIN'])) { require_code('site2'); closed_site_exit(); } // Warning about whether the Setup Wizard still needs running $zone = get_zone_name(); if ((($zone == 'adminzone') || ($zone == 'cms')) && (get_param_integer('wide_high', 0) == 0) && (get_param_integer('keep_wide_high', 0) == 0)) { if ((get_param_integer('cancel_sw_warn', 0) == 1) || (!addon_installed('setupwizard'))) { set_value('setupwizard_completed', '1', true); } else { $_done_sw_once = get_value('setupwizard_completed', null, true); $done_sw_once = $_done_sw_once !== null; if ((!$done_sw_once) && (get_page_name() != 'admin_setupwizard') && (has_actual_page_access(get_member(), 'admin_setupwizard'))) { require_lang('config'); $setupwizard_url = build_url(['page' => 'admin_setupwizard'], get_module_zone('admin_setupwizard')); $cancel_sw_url = get_self_url(false, true, ['cancel_sw_warn' => 1]); attach_message(do_lang_tempcode('SETUPWIZARD_NOT_RUN', escape_html($setupwizard_url->evaluate()), escape_html($cancel_sw_url->evaluate())), 'notice'); } } } // Web Standards mode? $webstandards_check = get_param_integer('keep_webstandards_check', get_param_integer('webstandards_check', 0)); $show_edit_links = get_param_integer('show_edit_links', 0); $webstandards_mode = ( (($GLOBALS['IS_ACTUALLY_ADMIN']) || ($GLOBALS['FORUM_DRIVER']->is_staff(get_member()))) && (($special_page_type == 'code') || (($webstandards_check == 1) && ($GLOBALS['REFRESH_URL'][0] == '') && ($show_edit_links == 0)))); // Not a permission - a matter of performance // Load up our frames into strings. Note that the header and the footer are fixed already. $middle = request_page(get_page_name(), true, null, null, false, true); if ($middle->is_empty_shell()) { set_http_status_code(404); $title = get_screen_title('ERROR_OCCURRED'); $text = do_lang_tempcode('NO_PAGE_OUTPUT'); $middle = warn_screen($title, $text, false); } $out = globalise($middle, null, '', true); // Web Standards mode if ($webstandards_mode) { require_code('view_modes'); $out_evaluated = $out->evaluate(null); check_xhtml_webstandards($out_evaluated, ($special_page_type == 'code' && (get_param_integer('preview_mode', null) === null)), get_param_integer('preview_mode', 0)); } // Static cache save_static_caching($out); // Save template tree if ($GLOBALS['RECORD_TEMPLATES_USED']) { require_code('themes_meta_tree'); global $CSSS, $JAVASCRIPTS; if (!isset($out->metadata)) { $out->metadata = ['children' => []]; } foreach (array_keys($CSSS) as $css) { $out->metadata['children'][] = create_template_tree_metadata(TEMPLATE_TREE_NODE__TEMPLATE_INSTANCE, 'css/' . $css . '.css'); } foreach (array_keys($JAVASCRIPTS) as $javascript) { $out->metadata['children'][] = create_template_tree_metadata(TEMPLATE_TREE_NODE__TEMPLATE_INSTANCE, 'javascript/' . $javascript . '.js'); } if (running_script('index')) { record_template_tree_used($out); } } // Something to do now rather than output normal screen? if ($doing_special_page_type) { special_page_types($special_page_type, $out, $out_evaluated); } if ((in_safe_mode()) && (!isset($GLOBALS['SITE_INFO']['safe_mode']))) { global $SITE_INFO; $safe_mode_via_config = (isset($SITE_INFO['safe_mode'])) && ($SITE_INFO['safe_mode'] == '1'); $disable_safe_mode_url = get_self_url(true, true, ['keep_safe_mode' => $safe_mode_via_config ? 0 : null]); attach_message(do_lang_tempcode('CURRENTLY_HAS_KEEP_SAFE_MODE', escape_html($disable_safe_mode_url)), 'notice'); } if (current_fatalistic() > 0) { $disable_fatalistic_url = get_self_url(true, true, ['keep_fatalistic' => null]); attach_message(do_lang_tempcode('CURRENTLY_HAS_KEEP_FATALISTIC', escape_html($disable_fatalistic_url)), 'notice'); } // We calculated the time before outputting so that latency and bandwidth do not adversely affect the result global $PAGE_START_TIME, $PAGE_STRING; $page_generation_time = (microtime(true) - $PAGE_START_TIME) * 1000.0; // Output if (!$GLOBALS['QUICK_REDIRECT']) { $need_enforce = (((!has_cookies()) || !allowed_cookies('ESSENTIAL')) && (get_bot_type() === null) && (get_option('sessions_in_urls') == '1')); if ($need_enforce) { if ($out_evaluated === null) { $out_evaluated = $out->evaluate(null); } require_code('users'); enforce_sessioned_html($out_evaluated); echo $out_evaluated; } else { if ($out_evaluated !== null) { echo $out_evaluated; } else { $GLOBALS['FINISHING_OUTPUT'] = true; $out->evaluate_echo(null); } } } // Stats if ($PAGE_STRING !== null) { cms_register_shutdown_function_safe(function () use ($page_generation_time) { log_stats(null, intval($page_generation_time)); }); } // When someone hits the Admin Zone if (($ZONE !== null) && ($ZONE['zone_name'] == 'adminzone')) { require_code('global4'); // Log the access if this session did not already hit the Admin Zone if (!already_in_log('ACCESSED_ADMIN_ZONE', get_session_id(true))) { log_it('ACCESSED_ADMIN_ZONE', get_session_id(true)); // Security feature admins can turn on for notifications require_code('notifications'); $current_username = $GLOBALS['FORUM_DRIVER']->get_username(get_member()); $subject = do_lang('AFA_NOTIFICATION_MAIL_SUBJECT', $current_username, get_site_name(), get_ip_address()); $mail = do_notification_lang('AFA_NOTIFICATION_MAIL', comcode_escape(get_site_name()), comcode_escape($current_username), comcode_escape(get_ip_address())); Source_notification_dispatcher::dispatch_notification('core_staff:adminzone_dashboard_accessed', null, $subject, $mail); } // Send very basic software details to homesite if enabled require_code('telemetry'); if ((is_encryption_enabled_telemetry()) && (!is_local_machine()) && (get_option('telemetry') == '2') && (get_value_newer_than('last_call_home', time() - (60 * 60 * 24)) === null)) { require_code('version2'); $count_members = $GLOBALS['FORUM_DRIVER']->get_num_members(); $count_daily_hits = $GLOBALS['SITE_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . get_table_prefix() . 'stats WHERE date_and_time>' . strval(time() - 60 * 60 * 24)); $url = get_brand_base_url() . '/data/endpoint.php/cms_homesite/user_stats/'; $__payload = [ 'version' => cms_version_pretty(), 'count_members' => $count_members, 'count_daily_hits' => $count_daily_hits, ]; $_payload = encrypt_data_site_telemetry(serialize($__payload)); $payload = json_encode($_payload); if ($payload === false) { cms_error_log(brand_name() . ' telemetry: WARNING Failed to JSON encode payload to send telemetry stats.'); } else { $error_code = null; $error_message = ''; $response = cms_fsock_request($payload, $url, $error_code, $error_message, 3.0); if (($response === null) || ($error_message != '')) { cms_error_log(brand_name() . ' telemetry: WARNING Could not forward site statistics to the developers. ' . $error_message . (($response === null) ? '' : escape_html($response))); // Maybe something happened with the keys, or the base URL changed? Try re-registering in the background. cms_register_shutdown_function_safe(function () { register_site_telemetry(); }); } } set_value('last_call_home', strval(time())); } } // Execute Cron bridge if we want web requests to run scheduled tasks if ((running_script('index')) && (get_option('enable_web_request_scheduler') == '1')) { // NB: We put this in two registers so it executes after most other shutdown functions, but not before the database disconnects. cms_register_shutdown_function_if_available(function () { cms_register_shutdown_function_if_available(function () { // NB: We run timing checks inside the register shutdown function to prevent overlaps if ( (get_value_newer_than('cron_currently_running', time() - (60 * 60), true) !== '1') && // Don't run if scheduled tasks are running now unless it's been over an hour (get_value_newer_than('last_cron', time() - 60) === null) // Don't run if scheduled tasks ran in the last 60 seconds ) { @ignore_user_abort(true); // Use fastcgi_finish_request if available to prevent blocking if (function_exists('fastcgi_finish_request')) { @fastcgi_finish_request(); } require_code('cron'); cron_run(); } }); }); } } /** * Do any static cache saving that we want to do. * * @param mixed $out Output to cache (string or Tempcode) * @param string $mime_type Mime type to use * @return boolean Whether saving could have happened */ function save_static_caching($out, string $mime_type = 'text/html') : bool { require_code('static_cache'); $debugging = debugging_static_cache(); $url = get_self_url_easy(); // Initial assessments of whether we can cache... // We cannot cache if there's a redirect $headers_sent = headers_list(); foreach ($headers_sent as $header) { if (preg_match('#^Location:#i', $header) != 0) { if ($debugging) { if (php_function_allowed('error_log')) { @error_log(brand_name() . ' static cache: DEBUG redirect was present when trying to save'); } } return false; } } global $INVALIDATED_FAST_SPIDER_CACHE; if ($INVALIDATED_FAST_SPIDER_CACHE) { if ($debugging) { if (php_function_allowed('error_log')) { @error_log(brand_name() . ' static cache: DEBUG caching explicitly blocked by $INVALIDATED_FAST_SPIDER_CACHE'); } } return false; } global $HTTP_STATUS_CODE; if (($HTTP_STATUS_CODE != 200) && ($mime_type != 'text/html')) { if ($debugging) { if (php_function_allowed('error_log')) { @error_log(brand_name() . ' static cache: DEBUG HTTP status is ' . strval($HTTP_STATUS_CODE) . ' and non-HTML so no http-equiv'); } } return false; } if (!can_static_cache_request(true)) { if ($debugging) { if (php_function_allowed('error_log')) { @error_log(brand_name() . ' static cache: DEBUG Static cache not available according to can_static_cache_request() on ' . $url); } } return false; } // Work out what to cache... if (is_object($out)) { $out_evaluated = $out->evaluate(null); $static_cache = $out_evaluated; } else { $static_cache = $out; } // Deeper assessments about what we can cache... if (!$GLOBALS['STATIC_CACHE_ENABLED']) { if ($debugging) { if (php_function_allowed('error_log')) { @error_log(brand_name() . ' static cache: DEBUG Internal signal to not cache from Tempcode on ' . $url); } } return false; // Something in the output tree decided this was not cacheable } // Cache... $url = static_cache_current_url(); // Log if ($debugging) { if (php_function_allowed('error_log')) { @error_log(brand_name() . ' static cache: DEBUG cached on ' . $url); } } // Remove any sessions etc $static_cache = preg_replace('#(&|&|&amp;|%3Aamp%3A|\?)?(keep_session|for_session|keep_devtest|keep_failover)(=|%3D)\w+#', '', $static_cache); // Add URL identifier $static_cache .= "\n\n" . ''; // Add mime type $static_cache .= "\n\n" . ''; $file_extension = ($mime_type == 'text/xml') ? '.xml' : '.htm'; // Add HTTP response code and select HTTP headers (HTML-only) if ($mime_type == 'text/html') { $meta_extra = ''; if (substr(strval($HTTP_STATUS_CODE), 0, 1) != '2') { $meta_extra .= ''; // Search engines likely won't support "meta http-equiv status", so let's just tell them not to index $noindex_meta = ''; if (strpos($static_cache, $noindex_meta) === false) { $meta_extra .= ''; } } // Criteria: // - likely supported under http-equiv // - actually set by the software // - has actual chance of being present during something statically cached // - is not already represented in HTML output in some (other?) way already (e.g. unlike X-Robots-Tag) // - is not set algorithmically by the static cache itself (e.g. unlike Content-Type, or caching, or Vary) $supported_http_equiv_header_patterns = '#^(Content-Security-Policy|Cross-Origin-Opener-Policy):\s*(.*)$#'; foreach (headers_list() as $header) { $matches = []; if (preg_match($supported_http_equiv_header_patterns, $header, $matches) != 0) { $meta_extra .= ''; } } $static_cache = str_ireplace('', $meta_extra . '', $static_cache); } // Work out cache path on disk $fast_cache_path = get_custom_file_base() . '/caches/static/' . cms_base64_encode($url, true, true); $fast_cache_path_failover_mode = $fast_cache_path; $bot_type = get_bot_type(); if ($bot_type === null) { $fast_cache_path .= '__non-bot'; } if (!array_key_exists('has_js', $_COOKIE)) { $fast_cache_path .= '__no-js'; } if (is_mobile()) { $fast_cache_path .= '__mobile'; $fast_cache_path_failover_mode .= '__mobile'; } $fast_cache_path_failover_mode .= '__failover_mode'; // Save (TODO: Seems hard-coded update time of 5 hours should be using configuration instead) if (!is_file($fast_cache_path . $file_extension) || filemtime($fast_cache_path . $file_extension) < time() - 60 * 60 * 5) { write_static_cache_file($fast_cache_path . $file_extension, $static_cache, true); } // Save for failover mode global $SITE_INFO; $supports_failover_mode = (isset($SITE_INFO['failover_mode'])) && ($SITE_INFO['failover_mode'] != 'off'); if ($supports_failover_mode) { if (!is_file($fast_cache_path_failover_mode . $file_extension) || filemtime($fast_cache_path_failover_mode . $file_extension) < time() - 60 * 60 * 5) { // Add failover messages if (!empty($SITE_INFO['failover_message_place_after'])) { $static_cache = str_replace($SITE_INFO['failover_message_place_after'], $SITE_INFO['failover_message_place_after'] . $SITE_INFO['failover_message'], $static_cache); } if (!empty($SITE_INFO['failover_message_place_before'])) { $static_cache = str_replace($SITE_INFO['failover_message_place_before'], $SITE_INFO['failover_message'] . $SITE_INFO['failover_message_place_before'], $static_cache); } // Disable all form controls $static_cache = preg_replace('#<(textarea|input|select|button)#', '<$1 disabled="disabled"', $static_cache); write_static_cache_file($fast_cache_path_failover_mode . $file_extension, $static_cache, false); } if (!empty($SITE_INFO['failover_apache_rewritemap_file'])) { $url_stem = $url; $url_stem = str_replace(get_base_url() . '/', '', $url_stem); if (preg_match('#^' . $SITE_INFO['failover_apache_rewritemap_file'] . '$#', $url_stem) != 0) { if (is_mobile()) { $rewritemap_file = get_custom_file_base() . '/data_custom/failover_rewritemap__mobile.txt'; } else { $rewritemap_file = get_custom_file_base() . '/data_custom/failover_rewritemap.txt'; } $rewritemap_file_contents = cms_file_get_contents_safe($rewritemap_file, FILE_READ_LOCK); if (strpos($rewritemap_file_contents, "\n" . $url_stem . ' ') === false) { require_code('files'); $rewritemap_file_contents .= "\n" . $url_stem . ' ' . $fast_cache_path . '__failover_mode' . $file_extension; cms_file_put_contents_safe($rewritemap_file, $rewritemap_file_contents, FILE_WRITE_FIX_PERMISSIONS); } } } } return true; } /** * Write out a static cache file. * * @param PATH $fast_cache_path Cache file path * @param string $out_evaluated Cache contents * @param boolean $support_output_compression Whether to allow output compression */ function write_static_cache_file(string $fast_cache_path, string $out_evaluated, bool $support_output_compression) { require_code('files'); cms_file_put_contents_safe($fast_cache_path, $out_evaluated, FILE_WRITE_FIX_PERMISSIONS); if ($support_output_compression) { require_code('web_resources2'); compress_cms_stub_file($fast_cache_path); //unlink($fast_cache_path); Actually, we should not assume all user agents support compression } } /** * Take the specified parameters, and try to find the corresponding page, then execute a function to load the page (load_html_page/load_comcode_page). * * @param ID_TEXT $codename The codename of the page to load * @param boolean $required Whether it is required for this page to exist (shows an error if it doesn't) -- otherwise, it will just return null * @param ?ID_TEXT $zone The zone the page is being loaded in (null: as shown by access URL) * @param ?ID_TEXT $page_type The type of page - for if you know it (null: don't know it) * @param boolean $being_included Whether the page is being included from another * @param boolean $redirect_check Whether to check for redirects (normally you would) * @param boolean $page_missing Whether the page is missing (returned by reference) * @return Tempcode The page */ function request_page(string $codename, bool $required, ?string $zone = null, ?string $page_type = null, bool $being_included = false, bool $redirect_check = true, bool &$page_missing = false) : object { global $SITE_INFO; if ($zone === null) { $zone = get_zone_name(); } if (($zone == 'site') && (get_option('single_public_zone') == '1')) { $zone = ''; // Might have been explicitly said in Tempcode, for example } $details = _request_page($codename, $zone, $page_type, null, $redirect_check); global $REQUEST_PAGE_NEST_LEVEL; $REQUEST_PAGE_NEST_LEVEL++; if ($REQUEST_PAGE_NEST_LEVEL > 20) { $REQUEST_PAGE_NEST_LEVEL = 0; attach_message(do_lang_tempcode('STOPPED_RECURSIVE_RESOURCE_INCLUDE', escape_html($codename), escape_html(do_lang('PAGE'))), 'warn', false, true); return new Tempcode(); } // Run hooks, if any exist $hooks = find_all_hook_obs('systems', 'upon_page_load', 'Hook_upon_page_load_'); foreach ($hooks as $ob) { $ob->run($codename, $required, $zone, $page_type, $being_included, $details); } if ($details === false) { if ($required) { if (get_option('url_scheme') == 'SIMPLE') { $details = _request_page('404', '', null, null, true); } } if ($details === false) { if ($required) { require_code('site2'); $ret = page_not_found($codename, $zone); $REQUEST_PAGE_NEST_LEVEL--; return $ret; } $REQUEST_PAGE_NEST_LEVEL--; return new Tempcode(); } } switch ($details[0]) { case 'MODULES_CUSTOM': $path = isset($details[3]) ? $details[3] : zone_black_magic_filterer($details[1] . (($details[1] == '') ? '' : '/') . 'pages/modules_custom/' . $details[2] . '.php', true); $ret = load_module_page($path, $details[2]); $REQUEST_PAGE_NEST_LEVEL--; return $ret; case 'MODULES': $path = isset($details[3]) ? $details[3] : zone_black_magic_filterer($details[1] . (($details[1] == '') ? '' : '/') . 'pages/modules/' . $details[2] . '.php', true); $ret = load_module_page($path, $details[2]); $REQUEST_PAGE_NEST_LEVEL--; return $ret; case 'COMCODE_CUSTOM': $path = isset($details[4]) ? $details[4] : zone_black_magic_filterer($details[1] . (($details[1] == '') ? '' : '/') . 'pages/comcode_custom/' . $details[3] . '/' . $details[2] . '.txt', true); if (((isset($SITE_INFO['no_disk_sanity_checks'])) && ($SITE_INFO['no_disk_sanity_checks'] == '1') && (get_custom_file_base() == get_file_base())) || (@is_file(get_custom_file_base() . '/' . $path))) { $ret = load_comcode_page($path, $details[1], $details[2], get_custom_file_base(), $being_included); $REQUEST_PAGE_NEST_LEVEL--; return $ret; } // no break (as probably been deleted since persistent cache was filled) case 'COMCODE_CUSTOM_PURE': $path = isset($details[4]) ? $details[4] : zone_black_magic_filterer($details[1] . (($details[1] == '') ? '' : '/') . 'pages/comcode_custom/' . $details[3] . '/' . $details[2] . '.txt', true); if (((isset($SITE_INFO['no_disk_sanity_checks'])) && ($SITE_INFO['no_disk_sanity_checks'] == '1')) || (@is_file(get_file_base() . '/' . $path))) { $ret = load_comcode_page($path, $details[1], $details[2], get_file_base(), $being_included); $REQUEST_PAGE_NEST_LEVEL--; return $ret; } // no break (as probably been deleted since persistent cache was filled) case 'COMCODE': $path = isset($details[4]) ? $details[4] : zone_black_magic_filterer($details[1] . (($details[1] == '') ? '' : '/') . 'pages/comcode/' . $details[3] . '/' . $details[2] . '.txt', true); if (((isset($SITE_INFO['no_disk_sanity_checks'])) && ($SITE_INFO['no_disk_sanity_checks'] == '1')) || (@is_file(get_file_base() . '/' . $path))) { $ret = load_comcode_page($path, $details[1], $details[2], null, $being_included); $REQUEST_PAGE_NEST_LEVEL--; return $ret; } // no break (as probably been deleted since persistent cache was filled) case 'HTML_CUSTOM': require_code('site_html_pages'); $path = isset($details[4]) ? $details[4] : zone_black_magic_filterer($details[1] . (($details[1] == '') ? '' : '/') . 'pages/html_custom/' . $details[3] . '/' . $details[2] . '.htm', true); $ret = make_string_tempcode(load_html_page($path)); $REQUEST_PAGE_NEST_LEVEL--; return $ret; case 'HTML': require_code('site_html_pages'); $path = isset($details[4]) ? $details[4] : zone_black_magic_filterer($details[1] . (($details[1] == '') ? '' : '/') . 'pages/html/' . $details[3] . '/' . $details[2] . '.htm', true); $ret = make_string_tempcode(load_html_page($path)); $REQUEST_PAGE_NEST_LEVEL--; return $ret; case 'MINIMODULES_CUSTOM': $path = isset($details[3]) ? $details[3] : zone_black_magic_filterer($details[1] . (($details[1] == '') ? '' : '/') . 'pages/minimodules_custom/' . $codename . '.php', true); $ret = load_minimodule_page($path); $REQUEST_PAGE_NEST_LEVEL--; return $ret; case 'MINIMODULES': $path = isset($details[3]) ? $details[3] : zone_black_magic_filterer($details[1] . (($details[1] == '') ? '' : '/') . 'pages/minimodules/' . $codename . '.php', true); $ret = load_minimodule_page($path); $REQUEST_PAGE_NEST_LEVEL--; return $ret; case 'REDIRECT': $redirect = $details[1]; if ($required) { global $REDIRECTED_TO_CACHE; $REDIRECTED_TO_CACHE = $redirect; } if (strpos($redirect['r_to_page'], ':') !== false) { $bits = page_link_decode($redirect['r_to_zone'] . ':' . $redirect['r_to_page']); } else { $bits = [$redirect['r_to_zone'], ['page' => $redirect['r_to_page']]]; } // Transparent redirection? if (($redirect['r_is_transparent'] == 1) || ($being_included)) { if (($being_included) && (!has_page_access(get_member(), $redirect['r_to_page'], $redirect['r_to_zone'], true))) { if ($being_included) { return new Tempcode(); } access_denied('PAGE_ACCESS'); } foreach ($bits[1] as $key => $val) { if ($key != 'page') { $_GET[$key] = $val; } } if (($redirect['r_to_page'] != $codename) || ($redirect['r_to_zone'] != $zone)) { $ret = request_page($redirect['r_to_page'], $required, $redirect['r_to_zone'], null, $being_included, false/*Don't want redirect loops*/); $REQUEST_PAGE_NEST_LEVEL--; return $ret; } } else { $url = build_url($bits[1], $redirect['r_to_zone'], [], true); set_http_status_code(301); $ret = redirect_screen(null, $url, do_lang_tempcode('REDIRECTED_LINK'), true); $REQUEST_PAGE_NEST_LEVEL--; return $ret; } break; } $REQUEST_PAGE_NEST_LEVEL--; $page_missing = true; return new Tempcode(); } /** * Take the specified parameters, and try to find the corresponding page (caching front-end). * * @param ID_TEXT $codename The codename of the page to load * @param ID_TEXT $zone The zone the page is being loaded in * @param ?ID_TEXT $page_type The type of page - for if you know it (null: don't know it) * @param ?LANGUAGE_NAME $lang Language name (null: user's language) * @param boolean $redirect_check Whether to check for redirects (normally you would) * @return ~array A list of details (false: page not found) */ function _request_page(string $codename, string $zone, ?string $page_type = null, ?string $lang = null, bool $redirect_check = true) { $details = persistent_cache_get(['PAGE_INFO', $codename, $zone, $page_type, $lang, $redirect_check]); if ($details === null || $details === false/*caching negativity breaks things if the page subsequently appears - even adding a page does a pre-check so would net a cached false*/) { $details = __request_page($codename, $zone, $page_type, null, $redirect_check); persistent_cache_set(['PAGE_INFO', $codename, $zone, $page_type, $lang, $redirect_check], $details); } return $details; } /** * Take the specified parameters, and try to find the corresponding page. * * @param ID_TEXT $codename The codename of the page to load * @param ID_TEXT $zone The zone the page is being loaded in * @param ?ID_TEXT $page_type The type of page - for if you know it (null: don't know it) * @param ?LANGUAGE_NAME $lang Language name (null: user's language) * @param boolean $redirect_check Whether to check for redirects (normally you would) * @return ~array A list of details (false: page not found) * @ignore */ function __request_page(string $codename, string $zone, ?string $page_type = null, ?string $lang = null, bool $redirect_check = true) { if ($lang === null) { require_code('lang'); $lang = user_lang(); } if ($codename == 'login') { // Special case $login_zone = get_module_zone('login'); $path = zone_black_magic_filterer($login_zone . (($login_zone == '') ? '' : '/') . 'pages/modules_custom/' . $codename . '.php', true); if (@is_file(get_file_base() . '/' . $path)) { return ['MODULES_CUSTOM', $login_zone, $codename, $path]; } $path = zone_black_magic_filterer($login_zone . (($login_zone == '') ? '' : '/') . 'pages/modules/' . $codename . '.php', true); if (@is_file(get_file_base() . '/' . $path)) { return ['MODULES', $login_zone, $codename, $path]; } } // Redirect if (($redirect_check) && (get_value('no_priority_redirects') !== '1') && (addon_installed('redirects_editor'))) { $test = _request_page__redirects($codename, $zone); if ($test !== false) { return $test; } } // If we know the page type if ($page_type !== null) { switch ($page_type) { case 'modules_custom': if (!in_safe_mode()) { $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/modules_custom/' . $codename . '.php', true); if (@is_file(get_file_base() . '/' . $path)) { return ['MODULES_CUSTOM', $zone, $codename, $path]; } } break; case 'modules': $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/modules/' . $codename . '.php', true); if (@is_file(get_file_base() . '/' . $path)) { return ['MODULES', $zone, $codename, $path]; } break; case 'comcode_custom': if (!in_safe_mode()) { if (get_param_integer('keep_theme_test', 0) == 1) { $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/comcode_custom/' . $lang . '/' . $GLOBALS['FORUM_DRIVER']->get_theme() . '__' . $codename . '.txt', true); if (@is_file(get_custom_file_base() . '/' . $path)) { return ['COMCODE_CUSTOM', $zone, $GLOBALS['FORUM_DRIVER']->get_theme() . '__' . $codename, $lang, $path]; } } $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/comcode_custom/' . $lang . '/' . $codename . '.txt', true); if (@is_file(get_custom_file_base() . '/' . $path)) { return ['COMCODE_CUSTOM', $zone, $codename, $lang, $path]; } if (get_custom_file_base() != get_file_base()) { // For multisite installs we also will search the root site's Custom Comcode pages $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/comcode_custom/' . $lang . '/' . $codename . '.txt', true); if (@is_file(get_file_base() . '/' . $path)) { return ['COMCODE_CUSTOM_PURE', $zone, $codename, $lang, $path]; } } } // no break case 'comcode': $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/comcode/' . $lang . '/' . $codename . '.txt', true); if (@is_file(get_file_base() . '/' . $path)) { return ['COMCODE', $zone, $codename, $lang, $path]; } break; case 'html_custom': if (!in_safe_mode()) { $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/html_custom/' . $lang . '/' . $codename . '.htm', true); if (@is_file(get_custom_file_base() . '/' . $path)) { return ['HTML_CUSTOM', $zone, $codename, $lang, $path]; } } break; case 'html': $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/html/' . $lang . '/' . $codename . '.htm', true); if (@is_file(get_file_base() . '/' . $path)) { return ['HTML', $zone, $codename, $lang, $path]; } break; case 'minimodules_custom': if (!in_safe_mode()) { $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/minimodules_custom/' . $lang . '/' . $codename . '.php', true); if (@is_file(get_file_base() . '/' . $path)) { return ['MINIMODULES_CUSTOM', $zone, $codename, $path]; } } break; case 'minimodules': $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/minimodules/' . $lang . '/' . $codename . '.php', true); if (@is_file(get_file_base() . '/' . $path)) { return ['MINIMODULES', $zone, $codename, $path]; } break; } return false; } // We have a priority list to find our page if (!in_safe_mode()) { $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/minimodules_custom/' . $codename . '.php', true); if (@is_file(get_file_base() . '/' . $path)) { return ['MINIMODULES_CUSTOM', $zone, $codename, $path]; } } if (!in_safe_mode()) { $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/modules_custom/' . $codename . '.php', true); if (@is_file(get_file_base() . '/' . $path)) { return ['MODULES_CUSTOM', $zone, $codename, $path]; } } $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/modules/' . $codename . '.php', true); if (@is_file(get_file_base() . '/' . $path)) { return ['MODULES', $zone, $codename, $path]; } if (!in_safe_mode()) { if (get_param_integer('keep_theme_test', 0) == 1) { $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/comcode_custom/' . $lang . '/' . $GLOBALS['FORUM_DRIVER']->get_theme() . '__' . $codename . '.txt', true); if (@is_file(get_custom_file_base() . '/' . $path)) { return ['COMCODE_CUSTOM', $zone, $GLOBALS['FORUM_DRIVER']->get_theme() . '__' . $codename, $lang, $path]; } } $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/comcode_custom/' . $lang . '/' . $codename . '.txt', true); if (@is_file(get_custom_file_base() . '/' . $path)) { return ['COMCODE_CUSTOM', $zone, $codename, $lang, $path]; } if (get_custom_file_base() != get_file_base()) { // For multisite installs we also will search the root site's Custom Comcode pages $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/comcode_custom/' . $lang . '/' . $codename . '.txt', true); if (@is_file(get_file_base() . '/' . $path)) { return ['COMCODE_CUSTOM_PURE', $zone, $codename, $lang, $path]; } } } if (!in_safe_mode()) { $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/html_custom/' . $lang . '/' . $codename . '.htm', true); if (@is_file(get_custom_file_base() . '/' . $path)) { return ['HTML_CUSTOM', $zone, $codename, $lang, $path]; } } $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/minimodules/' . $codename . '.php', true); if (@is_file(get_file_base() . '/' . $path)) { return ['MINIMODULES', $zone, $codename, $path]; } // As a fallback consider it might not yet have been translated $fallback_lang = fallback_lang(); $site_lang = get_site_default_lang(); $langs_to_try = []; if ($lang != $site_lang) { $langs_to_try[] = $site_lang; } if ($lang != $fallback_lang) { $langs_to_try[] = $fallback_lang; } foreach ($langs_to_try as $fallback_lang) { if (!in_safe_mode()) { $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/comcode_custom/' . $fallback_lang . '/' . $codename . '.txt', true); if (@is_file(get_custom_file_base() . '/' . $path)) { return ['COMCODE_CUSTOM', $zone, $codename, $fallback_lang, $path]; } if (get_custom_file_base() != get_file_base()) { // For multisite installs we also will search the root site's Custom Comcode pages $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/comcode_custom/' . $fallback_lang . '/' . $codename . '.txt', true); if (@is_file(get_file_base() . '/' . $path)) { return ['COMCODE_CUSTOM_PURE', $zone, $codename, $fallback_lang, $path]; } } $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/html_custom/' . $fallback_lang . '/' . $codename . '.htm', true); if (@is_file(get_custom_file_base() . '/' . $path)) { return ['HTML_CUSTOM', $zone, $codename, $fallback_lang, $path]; } } } // Or check for default pages $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/comcode/' . $lang . '/' . $codename . '.txt', true); if (@is_file(get_file_base() . '/' . $path)) { return ['COMCODE', $zone, $codename, $lang, $path]; } $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/html/' . $lang . '/' . $codename . '.htm', true); if (@is_file(get_file_base() . '/' . $path)) { return ['HTML', $zone, $codename, $lang, $path]; } foreach ($langs_to_try as $fallback_lang) { $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/comcode/' . $fallback_lang . '/' . $codename . '.txt', true); if (@is_file(get_file_base() . '/' . $path)) { return ['COMCODE', $zone, $codename, $fallback_lang, $path]; } $path = zone_black_magic_filterer($zone . (($zone == '') ? '' : '/') . 'pages/html/' . $fallback_lang . '/' . $codename . '.htm', true); if (@is_file(get_file_base() . '/' . $path)) { return ['HTML', $zone, $codename, $fallback_lang, $path]; } } // Redirect if (($redirect_check) && (addon_installed('redirects_editor'))) { $test = _request_page__redirects($codename, $zone, true/*include wildcards as this is lowest priority*/); if ($test !== false) { return $test; } } return false; } /** * Take the specified parameters, and try to find a redirect for the corresponding page. * * @param ID_TEXT $codename The codename of the page to load * @param ID_TEXT $zone The zone the page is being loaded in * @param boolean $wildcard_mode Whether to also search in wildcard mode * @return ~array A list of details (false: page not found) * @ignore */ function _request_page__redirects(string $codename, string $zone, bool $wildcard_mode = false) { $codename = cms_mb_strtolower($codename); static $internal_cache = []; if (isset($internal_cache[$codename][$zone][$wildcard_mode])) { return $internal_cache[$codename][$zone][$wildcard_mode]; } global $REDIRECT_CACHE; if ($REDIRECT_CACHE === null) { load_redirect_cache(); } if (isset($REDIRECT_CACHE)) { if (isset($REDIRECT_CACHE[$zone][$codename])) { $redirect = [$REDIRECT_CACHE[$zone][$codename]]; } elseif (($wildcard_mode) && (isset($REDIRECT_CACHE[$zone]['*'])) && (substr($codename, 0, 6) != 'panel_')) { $redirect = [$REDIRECT_CACHE[$zone]['*']]; } elseif (($wildcard_mode) && (isset($REDIRECT_CACHE['*'][$codename]))) { $redirect = [$REDIRECT_CACHE['*'][$codename]]; } else { $redirect = []; } } else { $query = 'SELECT * FROM ' . get_table_prefix() . 'redirects WHERE (' . db_string_equal_to('r_from_zone', $zone); if ($wildcard_mode) { $query .= ' OR ' . db_string_equal_to('r_from_zone', '*'); } $query .= ') AND (' . db_string_equal_to('r_from_page', $codename); if (($wildcard_mode) && (substr($codename, 0, 6) != 'panel_')) { $query .= ' OR ' . db_string_equal_to('r_from_page', '*'); } $query .= ') ORDER BY r_from_zone DESC'; // The ordering ensures '*' comes last, as we want to deprioritise this $redirect = $GLOBALS['SITE_DB']->query($query, 1, 0, false, true); } if (isset($redirect[0])) { $ret = ['REDIRECT', $redirect[0]]; $internal_cache[$codename][$zone][$wildcard_mode] = $ret; return $ret; } $internal_cache[$codename][$zone][$wildcard_mode] = false; return false; } /** * Get a Comcode page from the cache. * * @param ID_TEXT $codename The codename of the page * @param ID_TEXT $zone The zone the page is being loaded from * @param ID_TEXT $theme The theme * @return ?array The page row (null: none found) */ function load_comcode_page_from_cache(string $codename, string $zone, string $theme) : ?array { $tuple = [$codename, $zone, $theme]; global $SMART_CACHE; if ($SMART_CACHE !== null) { $SMART_CACHE->append('comcode_pages_needed', serialize($tuple)); } $ret = _load_comcodes_page_from_cache([$tuple]); return isset($ret[0]) ? $ret[0] : null; } /** * Wraps load_comcode_page_from_cache for bulk loading. * * @param array $pages Details of pages to load * @return array Database rows * * @ignore */ function _load_comcodes_page_from_cache(array $pages) : array { global $COMCODE_PAGE_RUNTIME_CACHE; $ret = []; $needs_query = false; $sql = 'SELECT * FROM ' . get_table_prefix() . 'cached_comcode_pages a JOIN ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'comcode_pages b ON a.the_page=b.the_page AND a.the_zone=b.the_zone'; $sql .= ' WHERE 1=0'; foreach ($pages as $page) { $sz = serialize($page); if (isset($COMCODE_PAGE_RUNTIME_CACHE[$sz])) { $ret[] = $COMCODE_PAGE_RUNTIME_CACHE[$sz]; } else { list($codename, $zone, $theme) = $page; $sql .= ' OR '; $sql .= db_string_equal_to('a.the_page', $codename) . ' AND ' . db_string_equal_to('a.the_zone', $zone) . ' AND ' . db_string_equal_to('the_theme', $theme); $needs_query = true; } } if ($needs_query) { $_ret = $GLOBALS['SITE_DB']->query($sql, null, 0, false, false, ['string_index' => 'LONG_TRANS__COMCODE', 'cc_page_title' => '?SHORT_TRANS']); foreach ($_ret as $_page) { $sz = serialize([$_page['the_page'], $_page['the_zone'], $_page['the_theme']]); $COMCODE_PAGE_RUNTIME_CACHE[$sz] = $_page; $ret[] = $_page; } } return $ret; } /** * Get the parsed contents of a Comcode page. * * @param PATH $string The relative (to the software's base directory) path to the page (e.g. pages/comcode/EN/example.txt) * @param ID_TEXT $zone The zone the page is being loaded from * @param ID_TEXT $codename The codename of the page * @param ?PATH $file_base The file base to load from (null: standard) * @param boolean $being_included Whether the page is being included from another * @return Tempcode The page */ function load_comcode_page(string $string, string $zone, string $codename, ?string $file_base = null, bool $being_included = false) : object { if (strlen($codename) < 1) { warn_exit(do_lang_tempcode('EMPTY_CODENAME')); } if ($file_base === null) { $file_base = get_file_base(); } if (!$being_included) { $GLOBALS['TITLE_CALLED'] = true; } $is_panel = (substr($codename, 0, 6) == 'panel_') || ((strpos($codename, 'panel_') !== false) && (get_param_integer('keep_theme_test', 0) == 1)); if ($zone == '' && $codename == '404') { set_http_status_code(404); } if ($zone == 'adminzone') { require_code('site_adminzone'); adminzone_special_cases($codename); } if ($codename == 'sitemap') { inject_feed_url('?mode=comcode_pages&select=' . urlencode($zone), do_lang('zones:COMCODE_PAGES')); } global $PAGE_STRING, $COMCODE_PARSE_TITLE, $LAST_COMCODE_PARSED_TITLE; $COMCODE_PARSE_TITLE = null; if (($PAGE_STRING === null) && (!$being_included) && (!$is_panel)) { $PAGE_STRING = $string; } $validated = 1; if (addon_installed('validation')) { $validated = intval(get_option('validate_new_comcode_pages')); } $new_comcode_page_row = [ 'the_zone' => $zone, 'the_page' => $codename, 'p_parent_page' => '', 'p_validated' => $validated, 'p_edit_date' => null, 'p_add_date' => null, 'p_submitter' => null, 'p_show_as_edit' => 0, 'p_include_on_sitemap' => null, 'p_order' => 0, ]; if (!is_file($file_base . '/' . $string)) { warn_exit(do_lang_tempcode('MISSING_PAGE', escape_html($zone . ':' . $codename))); } global $KEEP_MARKERS, $SHOW_EDIT_LINKS, $INJECT_HIDDEN_TEMPLATE_NAMES; if ((has_caching_for('comcode_page', $codename)) && (get_param_integer('keep_print', 0) == 0) && !$KEEP_MARKERS && !$SHOW_EDIT_LINKS && !$INJECT_HIDDEN_TEMPLATE_NAMES) { $support_smart_decaching = support_smart_decaching(); if (is_browser_decaching()) { $comcode_page = $GLOBALS['SITE_DB']->query_select('cached_comcode_pages', ['string_index', 'cc_page_title'], ['the_page' => $codename, 'the_zone' => $zone, 'the_theme' => $GLOBALS['FORUM_DRIVER']->get_theme()], '', 1, 0, false, []); if (isset($comcode_page[0])) { if ($comcode_page[0]['string_index'] !== null) { delete_lang($comcode_page[0]['string_index']); } $GLOBALS['SITE_DB']->query_delete('cached_comcode_pages', ['the_page' => $codename, 'the_zone' => $zone]); $GLOBALS['COMCODE_PAGE_RUNTIME_CACHE'] = []; } } $theme = $GLOBALS['FORUM_DRIVER']->get_theme(); if ($GLOBALS['PERSISTENT_CACHE'] !== null) { if ($support_smart_decaching) { $mtime = filemtime($file_base . '/' . $string); if ($mtime > time()) { $mtime = time(); // Timezone error, we have to assume that cache is ok rather than letting us get in a loop decaching the file. It'll get fixed automatically in a few hours when the hours of the timezone difference passes. } $pcache = persistent_cache_get(['COMCODE_PAGE', $codename, $zone, $theme, user_lang()], $mtime); } else { $pcache = persistent_cache_get(['COMCODE_PAGE', $codename, $zone, $theme, user_lang()]); } } else { $pcache = null; } if ($pcache === null) { $comcode_page_row = load_comcode_page_from_cache($codename, $zone, $theme); if ($comcode_page_row !== null) { if ($support_smart_decaching) { $mtime = filemtime($file_base . '/' . $string); if ($mtime > time()) { $mtime = time(); // Timezone error, we have to assume that cache is ok rather than letting us get in a loop decaching the file. It'll get fixed automatically in a few hours when the hours of the timezone difference passes. } } if ((!$support_smart_decaching) || ((($comcode_page_row['p_edit_date'] !== null) && ($comcode_page_row['p_edit_date'] >= $mtime)) || (($comcode_page_row['p_edit_date'] === null) && ($comcode_page_row['p_add_date'] !== null) && ($comcode_page_row['p_add_date'] >= $mtime)))) { // Make sure it has not been edited since last edited or created // Optimised path for empty panels when no super-fast persistent cache if (($support_smart_decaching/*only should do an fstat if this is enabled*/) && (($being_included) || ($is_panel)) && ($GLOBALS['PERSISTENT_CACHE'] === null) && (filesize($file_base . '/' . $string) == 0)) { return new Tempcode(); } $just_comcode_page_row = db_map_restrict($comcode_page_row, ['the_page', 'the_zone', 'the_theme', 'string_index']); $db_set = get_translated_tempcode('cached_comcode_pages', $just_comcode_page_row, 'string_index', null, user_lang(), true, true/*,true*/); } else { $mtime = filemtime($file_base . '/' . $string); if ($mtime > time()) { $mtime = time(); // Timezone error, we have to assume that cache is ok rather than letting us get in a loop decaching the file. It'll get fixed automatically in a few hours when the hours of the timezone difference passes. } $GLOBALS['SITE_DB']->query_update('comcode_pages', ['p_edit_date' => $mtime], ['the_page' => $codename, 'the_zone' => $zone], '', 1); $GLOBALS['SITE_DB']->query_delete('cached_comcode_pages', ['the_zone' => $zone, 'the_page' => $codename]); delete_lang($comcode_page_row['string_index']); $GLOBALS['COMCODE_PAGE_RUNTIME_CACHE'] = []; $db_set = null; $comcode_page_row = null; } } else { $db_set = null; $comcode_page_row = null; } if ($db_set !== null) { $index = $comcode_page_row['string_index']; $title_to_use = $comcode_page_row['cc_page_title']; if ($title_to_use !== null) { $title_to_use = get_translated_text($title_to_use, null, null, true); if ($title_to_use === null) { $title_to_use = $codename; } } $html = $db_set; $raw_comcode = get_translated_text($comcode_page_row['string_index']); } else { $comcode_page = $GLOBALS['SITE_DB']->query_select('comcode_pages', ['*'], ['the_page' => $codename, 'the_zone' => $zone], '', 1); if (isset($comcode_page[0])) { $comcode_page_row = $comcode_page[0]; } require_code('site2'); $new_comcode_page_row['p_add_date'] = filectime($file_base . '/' . $string); require_code('global4'); $new_comcode_page_row['p_include_on_sitemap'] = comcode_page_include_on_sitemap($zone, $codename) ? 1 : 0; list($html, $title_to_use, $comcode_page_row, $raw_comcode) = _load_comcode_page_not_cached($string, $zone, $codename, $file_base, $comcode_page_row, $new_comcode_page_row, $being_included); } persistent_cache_set(['COMCODE_PAGE', $codename, $zone, $theme, user_lang()], [$html, $title_to_use, $comcode_page_row, $raw_comcode]); } else { list($html, $title_to_use, $comcode_page_row, $raw_comcode) = $pcache; } } else { require_code('site2'); $new_comcode_page_row['p_add_date'] = filectime($file_base . '/' . $string); require_code('global4'); $new_comcode_page_row['p_include_on_sitemap'] = comcode_page_include_on_sitemap($zone, $codename) ? 1 : 0; list($html, $comcode_page_row, $title_to_use, $raw_comcode) = _load_comcode_page_cache_off($string, $zone, $codename, $file_base, $new_comcode_page_row, $being_included); } require_code('global4'); if ( (!comcode_page_include_on_sitemap($zone, $codename, $comcode_page_row)) && (get_page_name() == $codename/*Top-level*/) ) { attach_to_screen_header(''); // XHTMLXHTML } $no_title_yet = ($GLOBALS['DISPLAYED_TITLE'] === null); $filtered_title_to_use = null; if ((!$is_panel) && ((!$being_included) || ($no_title_yet))) { if (!cms_empty_safe($title_to_use)) { get_screen_title($title_to_use, false); // Little hack - this will force DISPLAYED_TITLE to get set. $filtered_title_to_use = strip_html($title_to_use); } seo_meta_load_for('comcode_page', $zone . ':' . $codename, $filtered_title_to_use); } $LAST_COMCODE_PARSED_TITLE = $title_to_use; if (($html->is_empty_shell()) && ($being_included || $is_panel)) { return $html; } if ((function_exists('has_edit_comcode_page_permission')) && (has_edit_comcode_page_permission($zone, $codename, $comcode_page_row['p_submitter']))) { $redirect = get_self_url(true, false, ['redirect' => null, 'redirected' => null]); if ((($codename == 'panel_left') || ($codename == 'panel_right')) && (has_actual_page_access(get_member(), 'admin_zones')) && (get_theme_option('zone_editor_enabled') == '1') && (get_value('hide_zone_editor') !== '1')) { $edit_url = build_url(['page' => 'admin_zones', 'type' => '_editor', 'id' => get_zone_name(), 'redirect' => protect_url_parameter($redirect)], get_module_zone('admin_zones')); } else { $edit_url = build_url(['page' => 'cms_comcode_pages', 'type' => '_edit', 'page_link' => $zone . ':' . $codename, /*'lang' => user_lang(), */'redirect' => protect_url_parameter($redirect)], get_module_zone('cms_comcode_pages')); } } else { $edit_url = new Tempcode(); } $add_child_url = new Tempcode(); if (has_add_comcode_page_permission($zone)) { if (strpos($raw_comcode, 'main_comcode_page_children') !== false) { $add_child_url = (get_option('is_on_comcode_page_children') == '1') ? build_url(['page' => 'cms_comcode_pages', 'type' => '_edit', 'may_choose_template' => '1', 'parent_page' => $codename, 'page_link' => $zone . ':'/*Don't make too many assumptions about user flow , 'lang' => user_lang()*//*, 'redirect' => protect_url_parameter($redirect)*/], get_module_zone('cms_comcode_pages')) : new Tempcode(); } } $warning_details = new Tempcode(); if (($comcode_page_row['p_validated'] !== null) && ($comcode_page_row['p_validated'] == 0) && (!$being_included)) { require_code('site2'); $warning_details = get_page_warning_details($zone, $codename, $edit_url); } if ((!$is_panel) && ($title_to_use !== null) && ((!$being_included) || ($no_title_yet))) { global $PT_PAIR_CACHE_CP; $PT_PAIR_CACHE_CP[$codename]['cc_page_title'] = ($title_to_use === null) ? do_lang_tempcode('NA_EM') : protect_from_escaping(escape_html($title_to_use)); $PT_PAIR_CACHE_CP[$codename]['p_parent_page'] = $comcode_page_row['p_parent_page']; $comcode_breadcrumbs = comcode_breadcrumbs($codename, $zone, get_param_string('keep_page_root', ''), ($comcode_page_row['p_parent_page'] != '') && (has_privilege(get_member(), 'open_virtual_roots')) && (get_option('virtual_root_links') == '1')); breadcrumb_set_parents($comcode_breadcrumbs); if ($being_included) { $GLOBALS['METADATA']['title'] = '[semihtml]' . $title_to_use . '[/semihtml]'; } else { set_extra_request_metadata([ 'title' => ($title_to_use == '') ? null : ('[semihtml]' . $title_to_use . '[/semihtml]'), // We need to pass as we cannot assume we have a cache row in the database 'identifier' => $zone . ':' . $codename, ], $comcode_page_row, 'comcode_page', $zone . ':' . $codename); } } global $SCREEN_TEMPLATE_CALLED; $st = $SCREEN_TEMPLATE_CALLED; $ret = do_template('COMCODE_PAGE_SCREEN', [ '_GUID' => '0fc4fe4f27e54aaaa2b7e4848c02bacb', 'IS_PANEL' => $is_panel, 'BEING_INCLUDED' => $being_included, 'SUBMITTER' => strval($comcode_page_row['p_submitter']), 'TAGS' => (get_theme_option('show_content_tagging') == '0') ? /*optimisation, can be intensive with many page includes*/new Tempcode() : get_loaded_tags('comcode_pages'), 'WARNING_DETAILS' => $warning_details, 'EDIT_DATE_RAW' => ($comcode_page_row['p_edit_date'] === null) ? '' : strval($comcode_page_row['p_edit_date']), 'SHOW_AS_EDIT' => $comcode_page_row['p_show_as_edit'] == 1, 'CONTENT' => $html, 'EDIT_URL' => $edit_url, 'ADD_CHILD_URL' => $add_child_url, 'NAME' => $codename, 'NATIVE_ZONE' => $zone, ]); if (($is_panel) || ($being_included)) { $SCREEN_TEMPLATE_CALLED = $st; } return $ret; } /** * Get a route from a known Comcode page back to the declared root of the tree. * * @param ID_TEXT $the_page The Comcode page name * @param ID_TEXT $the_zone The Comcode page zone * @param ID_TEXT $root The virtual root * @param boolean $include_link Whether not to put a link at this point in the navigation tree (usually, because the viewer is already at it) * @param integer $jumps The number of jumps we have gone through so far (cuts out after 10 as a failsafe) * @return array The breadcrumbs */ function comcode_breadcrumbs(string $the_page, string $the_zone, string $root = '', bool $include_link = false, int $jumps = 0) : array { // Cut off broken recursion if ($jumps == 10) { return []; // Probably a loop } if ($the_page == '') { return []; } // Find link $map = ['page' => $the_page]; if ($jumps == 0) { $map['keep_page_root'] = $the_page; } elseif ($root != '') { $map['keep_page_root'] = $root; } $page_link = build_page_link($map, $the_zone); // Find title global $PT_PAIR_CACHE_CP; if (!array_key_exists($the_page, $PT_PAIR_CACHE_CP)) { $test = _request_page__redirects($the_page, $the_zone); if ($test !== false) { $_the_page = $test[1]['r_to_page']; $_the_zone = $test[1]['r_to_zone']; } else { $_the_page = $the_page; $_the_zone = $the_zone; } $page_rows = $GLOBALS['SITE_DB']->query_select('cached_comcode_pages a JOIN ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'comcode_pages b ON a.the_page=b.the_page AND a.the_zone=b.the_zone', ['cc_page_title', 'p_parent_page'], ['a.the_page' => $_the_page, 'a.the_zone' => $_the_zone], '', 1, 0, false, ['cc_page_title' => '?SHORT_TRANS']); if (!array_key_exists(0, $page_rows)) { global $DISPLAYED_TITLE; push_output_state(); $DISPLAYED_TITLE = new Tempcode(); request_page($the_page, false, $the_zone, null, true); // It's not cached, force the issue and then try again... $temp_title = $DISPLAYED_TITLE; restore_output_state(); $page_rows = $GLOBALS['SITE_DB']->query_select('cached_comcode_pages a JOIN ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'comcode_pages b ON a.the_page=b.the_page AND a.the_zone=b.the_zone', ['cc_page_title', 'p_parent_page'], ['a.the_page' => $_the_page, 'a.the_zone' => $_the_zone], '', 1, 0, false, ['cc_page_title' => '?SHORT_TRANS']); if (!array_key_exists(0, $page_rows)) { // Oh well, fallback (maybe page doesn't exist yet, ?)... $PT_PAIR_CACHE_CP[$the_page] = []; $PT_PAIR_CACHE_CP[$the_page]['cc_page_title'] = $temp_title->evaluate(); if ($PT_PAIR_CACHE_CP[$the_page]['cc_page_title'] == '') { $PT_PAIR_CACHE_CP[$the_page]['cc_page_title'] = $the_page; } $PT_PAIR_CACHE_CP[$the_page]['p_parent_page'] = ''; } } if (array_key_exists(0, $page_rows)) { $PT_PAIR_CACHE_CP[$the_page] = $page_rows[0]; $_title = get_translated_text($PT_PAIR_CACHE_CP[$the_page]['cc_page_title'], null, null, true); if ($_title === null) { $_title = $the_page; } $PT_PAIR_CACHE_CP[$the_page]['cc_page_title'] = $_title; } } $title = $PT_PAIR_CACHE_CP[$the_page]['cc_page_title']; if ($title === null) { $title = $the_page; } // End of recursion if ($the_page == $root) { if (!$include_link) { return []; } return [[$page_link, is_object($title) ? $title : protect_from_escaping(escape_html($title))]]; } // Cut off broken recursion if ($PT_PAIR_CACHE_CP[$the_page]['p_parent_page'] == $the_page) { fatal_exit(do_lang_tempcode('RECURSIVE_TREE_CHAIN', escape_html($the_page), 'comcode_page')); } // Our point in the chain $segments = []; if ($include_link) { $segments[] = [$page_link, is_object($title) ? $title : protect_from_escaping(escape_html($title)), ($jumps == 0) ? do_lang('VIRTUAL_ROOT') : '']; } else { if (!(((is_string($title)) && ($title == '')) || ((is_object($title)) && ($title->is_empty())))) { $segments[] = ['', is_object($title) ? $title : protect_from_escaping(escape_html($title))]; } } // Further recursion $below = comcode_breadcrumbs($PT_PAIR_CACHE_CP[$the_page]['p_parent_page'], $the_zone, $root, true, $jumps + 1); return array_merge($below, $segments); } /** * Log statistics for the page view. * * @param ?string $page_link Page link being viewed (null: work it out) * @param integer $pg_time The time taken for page loading in milliseconds */ function log_stats(?string $page_link, int $pg_time) { if (!addon_installed('stats')) { return; } if ((get_option('site_closed') != '0') && (get_option('stats_when_closed') == '0')) { return; } $time = time(); if ($page_link === null) { $page_link = get_current_page_link(true, 255); } if ((get_option('super_logging') == '1') && ($_SERVER['REQUEST_METHOD'] == 'POST')) { $post2 = []; foreach ($_POST as $key => $val) { if (!is_password_field(strval($key))) { $post2[$key] = $val; } } $post = json_encode($post2); } else { $post = ''; } $ip = get_ip_address(); global $IS_ACTUALLY; $member_id = ($IS_ACTUALLY === null) ? get_member() : $IS_ACTUALLY; // We want to suppress DB errors for logging stats but still log/relay the error require_code('failure'); set_throw_errors(true); try { $GLOBALS['SITE_DB']->query_insert('stats', [ 'date_and_time' => $time, 'page_link' => $page_link, 'post' => $post, 'referer_url' => cms_mb_substr($_SERVER['HTTP_REFERER'], 0, 255), 'ip' => $ip, 'member_id' => $member_id, 'session_id' => get_pseudo_session_id(), 'browser' => cms_mb_substr(get_browser_string(), 0, 255), 'operating_system' => cms_mb_substr(get_os_string(), 0, 255), 'requested_language' => substr(preg_replace('#[,;].*$#', '', $_SERVER['HTTP_ACCEPT_LANGUAGE']), 0, 10), 'milliseconds' => intval($pg_time), 'tracking_code' => cms_mb_substr(get_param_string('_t', ''), 0, 80), ], false); } catch (Exception $e) { // cms_error_log(brand_name() . ' database: WARNING ' . $e->getMessage()); // DB already logs it } set_throw_errors(false); /* NB: We cannot always assume the scheduler is running, so randomly clear the stats if it hasn't in the last hour. This, however, is not enough for GDPR compliance; we need the Cron as well. */ if (mt_rand(0, 100) == 1) { $last_cron = get_value('last_cron'); if (($last_cron === null) || (intval($last_cron) < time() - 60 * 60)) { cms_register_shutdown_function_safe(function () { require_code('stats'); cleanup_stats(); }); } } global $SITE_INFO; if (isset($SITE_INFO['throttle_bandwidth_views_per_meg'])) { $increment = statistical_update_model('values', intval(get_value('page_views'))); if ($increment != 0) { set_value('page_views', strval(intval(get_value('page_views')) + 1), false, true); } } }