$x) { if ($x == $path) { unset($_CREATED_FILES[$i]); } } } } /** * Get the contents of a file, with locking support. * * @param PATH $path File path. * @return ~string File contents (false: error) */ function cms_file_get_contents_safe($path) { $tmp = fopen($path, 'rb'); if ($tmp === false) { return false; } flock($tmp, LOCK_SH); $contents = stream_get_contents($tmp); flock($tmp, LOCK_UN); fclose($tmp); return $contents; } /** * Return the file in the URL by downloading it over HTTP. If a byte limit is given, it will only download that many bytes. It outputs warnings, returning null, on error. * * @param URLPATH $url The URL to download * @param ?integer $byte_limit The number of bytes to download. This is not a guarantee, it is a minimum (null: all bytes) * @range 1 max * @param boolean $trigger_error Whether to throw a Composr error, on error * @param boolean $no_redirect Whether to block redirects (returns null when found) * @param string $ua The user-agent to identify as * @param ?array $post_params An optional array of POST parameters to send; if this is null, a GET request is used (null: none) * @param ?array $cookies An optional array of cookies to send (null: none) * @param ?string $accept 'accept' header value (null: don't pass one) * @param ?string $accept_charset 'accept-charset' header value (null: don't pass one) * @param ?string $accept_language 'accept-language' header value (null: don't pass one) * @param ?resource $write_to_file File handle to write to (null: do not do that) * @param ?string $referer The HTTP referer (null: none) * @param ?array $auth A pair: authentication username and password (null: none) * @param float $timeout The timeout (for connecting/stalling, not for overall download time); usually it is rounded up to the nearest second, depending on the downloader implementation * @param boolean $raw_post Whether to treat the POST parameters as a raw POST (rather than using MIME) * @param ?array $files Files to send. Map between field to file path (null: none) * @param ?array $extra_headers Extra headers to send (null: none) * @param ?string $http_verb HTTP verb (null: auto-decide based on other parameters) * @param string $raw_content_type The content type to use if a raw HTTP post * @return ?string The data downloaded (null: error) */ function http_download_file($url, $byte_limit = null, $trigger_error = true, $no_redirect = false, $ua = 'Composr', $post_params = null, $cookies = null, $accept = null, $accept_charset = null, $accept_language = null, $write_to_file = null, $referer = null, $auth = null, $timeout = 6.0, $raw_post = false, $files = null, $extra_headers = null, $http_verb = null, $raw_content_type = 'application/xml') { require_code('files2'); cms_profile_start_for('http_download_file'); $ret = _http_download_file($url, $byte_limit, $trigger_error, $no_redirect, $ua, $post_params, $cookies, $accept, $accept_charset, $accept_language, $write_to_file, $referer, $auth, $timeout, $raw_post, $files, $extra_headers, $http_verb, $raw_content_type); cms_profile_end_for('http_download_file', $url); return $ret; } /** * Load a fresh output state. * * @sets_output_state * * @param boolean $just_tempcode Whether to only restore the Tempcode execution part of the state. * @param boolean $true_blank Whether to go for a completely blank state (no defaults!), not just a default fresh state. * * @ignore */ function _load_blank_output_state($just_tempcode = false, $true_blank = false) { /* Now lots of stuff all relating to output state (unless commented, these GLOBALs should not be written to directly, we have API calls for it) */ if (!$just_tempcode) { global $HTTP_STATUS_CODE; /** Record of the HTTP status code being set. * * @sets_output_state * * @global string $HTTP_STATUS_CODE */ $HTTP_STATUS_CODE = '200'; global $METADATA; $METADATA = array(); global $ATTACHED_MESSAGES, $ATTACHED_MESSAGES_RAW, $LATE_ATTACHED_MESSAGES; $ATTACHED_MESSAGES = null; /** Raw data of attached messages. * * @sets_output_state * * @global ?array $ATTACHED_MESSAGES_RAW */ $ATTACHED_MESSAGES_RAW = array(); $LATE_ATTACHED_MESSAGES = null; global $SEO_KEYWORDS, $SEO_DESCRIPTION, $SHORT_TITLE; $SEO_KEYWORDS = null; $SEO_DESCRIPTION = null; /** Shortened title to use only within header text and thus tag (if not set, $DISPLAYED_TITLE will be used). * * @sets_output_state * * @global ?string $SHORT_TITLE */ $SHORT_TITLE = null; global $BREADCRUMBS, $BREADCRUMB_SET_PARENTS, $DISPLAYED_TITLE, $FORCE_SET_TITLE, $BREADCRUMB_SET_SELF; $BREADCRUMBS = null; $BREADCRUMB_SET_PARENTS = array(); /** The screen title that was set (i.e. <h1>). * * @sets_output_state * * @global ?string $DISPLAYED_TITLE */ $DISPLAYED_TITLE = null; $FORCE_SET_TITLE = false; $BREADCRUMB_SET_SELF = null; global $FEED_URL, $FEED_URL_2; $FEED_URL = null; $FEED_URL_2 = null; global $REFRESH_URL, $FORCE_META_REFRESH, $QUICK_REDIRECT; $REFRESH_URL[0] = ''; $REFRESH_URL[1] = 0; $FORCE_META_REFRESH = false; $QUICK_REDIRECT = false; global $EXTRA_HEAD, $EXTRA_FOOT; $EXTRA_HEAD = null; $EXTRA_FOOT = null; global $HELPER_PANEL_TEXT, $HELPER_PANEL_TUTORIAL; $HELPER_PANEL_TEXT = ''; $HELPER_PANEL_TUTORIAL = ''; // Register basic CSS and JavaScript requirements global $JAVASCRIPT, $JAVASCRIPTS, $CSSS, $JAVASCRIPTS_DEFAULT; $JAVASCRIPT = null; /** List of required JavaScript files. * * @sets_output_state * * @global ?array $JAVASCRIPTS */ $JAVASCRIPTS = $true_blank ? array() : $JAVASCRIPTS_DEFAULT; /** List of required CSS files. * * @sets_output_state * * @global ?array $CSSS */ $CSSS = $true_blank ? array() : array('no_cache' => true, 'global' => true); } global $CYCLES, $TEMPCODE_SETGET; /** Stores Tempcode CYCLE values during execution. * * @sets_output_state * * @global array $CYCLE */ $CYCLES = array(); /** Stores Tempcode variable values during execution. * * @sets_output_state * * @global array $TEMPCODE_SETGET */ $TEMPCODE_SETGET = array(); } /** * Push the output state on the stack and create a fresh one. * * @sets_output_state * * @param boolean $just_tempcode Whether to only restore the Tempcode execution part of the state. * @param boolean $true_blank Whether to go for a completely blank state (no defaults!), not just a default fresh state. */ function push_output_state($just_tempcode = false, $true_blank = false) { global $OUTPUT_STATE_STACK, $OUTPUT_STATE_VARS; $current_state = array(); foreach ($OUTPUT_STATE_VARS as $var) { $current_state[$var] = isset($GLOBALS[$var]) ? $GLOBALS[$var] : null; } array_push($OUTPUT_STATE_STACK, $current_state); _load_blank_output_state($just_tempcode, $true_blank); } /** * Restore the last output state on the stack, or a fresh one if none was pushed. * * @sets_output_state * * @param boolean $just_tempcode Whether to only restore the Tempcode execution part of the state. * @param boolean $merge_current Whether to merge the current output state in. * @param ?array $keep Settings to keep / merge if possible (null: none). */ function restore_output_state($just_tempcode = false, $merge_current = false, $keep = null) { global $OUTPUT_STATE_STACK; if ($keep === null) { $keep = array(); } $mergeable_arrays = array('METADATA' => true, 'JAVASCRIPTS' => true, 'CSSS' => true, 'TEMPCODE_SETGET' => true, 'CYCLES' => true); $mergeable_tempcode = array('EXTRA_HEAD' => true, 'EXTRA_FOOT' => true, 'JAVASCRIPT' => true); $old_state = array_pop($OUTPUT_STATE_STACK); if ($old_state === null) { _load_blank_output_state($just_tempcode); } else { foreach ($old_state as $var => $val) { if ((!$just_tempcode) || ($var == 'CYCLES') || ($var == 'TEMPCODE_SETGET')) { $merge_array = (($merge_current) && (is_array($val)) && (isset($mergeable_arrays[$var]))); $merge_tempcode = (($merge_current) && (isset($val->codename/*faster than is_object*/)) && (isset($mergeable_tempcode[$var]))); $mergeable = $merge_array || $merge_tempcode; if (($keep === array()) || (!in_array($var, $keep)) || ($mergeable)) { if ($merge_array) { if ($GLOBALS[$var] === null) { $GLOBALS[$var] = array(); } $GLOBALS[$var] = array_merge($val, $GLOBALS[$var]); } elseif ($merge_tempcode) { if ($GLOBALS[$var] === null) { $GLOBALS[$var] = new Tempcode(); } $GLOBALS[$var]->attach($val); } elseif (!$merge_current || !isset($GLOBALS[$var]) || $GLOBALS[$var] === array() || $GLOBALS[$var] === false || $GLOBALS[$var] === '' || $var == 'REFRESH_URL') { $GLOBALS[$var] = $val; } } } } } } /** * Turn the Tempcode lump into a standalone page. * * @param ?Tempcode $middle The Tempcode to put into a nice frame (null: support output streaming mode) * @param ?mixed $message 'Additional' message (null: none) * @param string $type The type of special message * @set inform warn "" * @param boolean $include_header_and_footer Whether to include the header/footer/panels * @param boolean $show_border Whether to include a full screen rendering layout (will be overridable by 'show_border' GET parameter if present or if main page view) * @return Tempcode Standalone page */ function globalise($middle, $message = null, $type = '', $include_header_and_footer = false, $show_border = false) { if (!$include_header_and_footer) { // FUDGE $old = mixed(); if (isset($_GET['wide_high'])) { $old = $_GET['wide_high']; } $_GET['wide_high'] = '1'; } require_code('site'); if ($message !== null) { attach_message($message, $type); } restore_output_state(true); // Here we reset some Tempcode environmental stuff, because template compilation or preprocessing may have dirtied things $show_border = (get_param_integer('show_border', $show_border ? 1 : 0) == 1); if (!$show_border && !running_script('index')) { $global = do_template('STANDALONE_HTML_WRAP', array( '_GUID' => 'fe818a6fb0870f0b211e8e52adb23f26', 'TITLE' => ($GLOBALS['DISPLAYED_TITLE'] === null) ? do_lang_tempcode('NA') : $GLOBALS['DISPLAYED_TITLE'], 'FRAME' => running_script('iframe'), 'TARGET' => '_self', 'CONTENT' => $middle, )); if ($GLOBALS['OUTPUT_STREAMING'] || $middle !== null) { $global->handle_symbol_preprocessing(); } return $global; } global $TEMPCODE_CURRENT_PAGE_OUTPUTTING; if (($middle !== null) && (isset($TEMPCODE_CURRENT_PAGE_OUTPUTTING))) { // Error happened after output and during MIDDLE processing, so bind MIDDLE as an error $middle->handle_symbol_preprocessing(); $global = $TEMPCODE_CURRENT_PAGE_OUTPUTTING; $global->singular_bind('MIDDLE', $middle); // NB: We also considered the idea of using document.write() as a way to reset the output stream, but JavaScript execution will not happen before the parser (even if you force a flush and delay) } else { global $DOING_OUTPUT_PINGS; if (headers_sent() && !$DOING_OUTPUT_PINGS) { $global = do_template('STANDALONE_HTML_WRAP', array( '_GUID' => 'd579b62182a0f815e0ead1daa5904793', 'TITLE' => ($GLOBALS['DISPLAYED_TITLE'] === null) ? do_lang_tempcode('NA') : $GLOBALS['DISPLAYED_TITLE'], 'FRAME' => false, 'TARGET' => '_self', 'CONTENT' => $middle, )); } else { $global = do_template('GLOBAL_HTML_WRAP', array( '_GUID' => '592faa2c0e8bf2dc3492de2c11ca7131', 'MIDDLE' => $middle, )); } if ($GLOBALS['OUTPUT_STREAMING'] || $middle !== null) { $global->handle_symbol_preprocessing(); } } if (get_value('xhtml_strict') === '1') { require_code('global4'); $global = make_xhtml_strict($global); } if ((!$include_header_and_footer) && ($old !== null)) { $_GET['wide_high'] = $old; } return $global; } /** * Attach some XHTML to the screen footer. * * @sets_output_state * * @param mixed $data XHTML to attach (Tempcode or string) */ function attach_to_screen_footer($data) { global $EXTRA_FOOT; if ($EXTRA_FOOT === null) { $EXTRA_FOOT = new Tempcode(); } $EXTRA_FOOT->attach($data); } /** * Add some metadata for the request. * * @sets_output_state * * @param array $metadata Extra metadata * @param ?array $row Content row to automatically grab data from, if we also have $content_type (null: unknown) * @param ?ID_TEXT $content_type Content type (null: unknown) * @param ?ID_TEXT $content_id Content ID (null: unknown) */ function set_extra_request_metadata($metadata, $row = null, $content_type = null, $content_id = null) { global $METADATA; $METADATA += $metadata; if ($content_type !== null) { require_code('content'); $cma_ob = get_content_object($content_type); if ($cma_ob !== null) { $cma_info = $cma_ob->info(); if ($cma_ob === null) { $content_type = null; } } else { $content_type = null; } } if ($row !== null && $content_type !== null) { // Add in generic data... $cma_mappings = array( 'created' => 'add_time_field', 'creator' => isset($cma_info['author_field']) ? 'author_field' : 'submitter_field', 'publisher' => 'submitter_field', 'modified' => 'edit_time_field', 'title' => 'title_field', 'description' => 'description_field', 'views' => 'views_field', 'validated' => 'validated_field', 'type' => 'content_type_universal_label', ); foreach ($cma_mappings as $meta_type => $cma_field) { if (!isset($METADATA[$meta_type])) { if ($cma_field == 'content_type_universal_label' || isset($row[$cma_info[$cma_field]])) { switch ($meta_type) { case 'type': $val_raw = $cma_info[$cma_field]; $val = $val_raw; break; case 'created': case 'modified': $val_raw = strval($row[$cma_info[$cma_field]]); $val = date('Y-m-d', $row[$cma_info[$cma_field]]); break; case 'publisher': case 'creator': if ($cma_field == 'author_field') { $val_raw = $row[$cma_info[$cma_field]]; $val = $val_raw; } else { $val_raw = strval($row[$cma_info[$cma_field]]); $val = $GLOBALS['FORUM_DRIVER']->get_username($row[$cma_info[$cma_field]]); } break; case 'title': if ($cma_info['title_field_dereference']) { $val_raw = get_translated_text($row[$cma_info[$cma_field]], $cma_info['connection']); } else { $val_raw = $row[$cma_info[$cma_field]]; } if ($content_type === 'comcode_page') { // FUDGE if ($content_id === ':start') { $val_raw = get_site_name(); } else { $val_raw = titleify($val_raw); } } if ((!isset($cma_info['title_field_supports_comcode'])) || (!$cma_info['title_field_supports_comcode'])) { $val = comcode_escape($val_raw); } else { $val = $val_raw; } break; case 'description': if (is_integer($row[$cma_info[$cma_field]])) { $val_raw = get_translated_text($row[$cma_info[$cma_field]], $cma_info['connection']); } else { $val_raw = $row[$cma_info[$cma_field]]; } $val = $val_raw; break; case 'views': $val_raw = strval($row[$cma_info[$cma_field]]); $val = $val_raw; break; case 'validated': $val_raw = strval($row[$cma_info[$cma_field]]); $val = $val_raw; break; default: $val_raw = $row[$cma_info[$cma_field]]; $val = $val_raw; break; } if ($val !== null) { $METADATA[$meta_type] = $val; $METADATA[$meta_type . '_RAW'] = $val_raw; } } } } // Add in image... $image_url = ''; if ($cma_info['thumb_field'] !== null) { if ((strpos($cma_info['thumb_field'], 'CALL:') !== false) && ($content_id !== null)) { $image_url = call_user_func(trim(substr($cma_info['thumb_field'], 5)), array('id' => $content_id), $row); } else { if ($content_type === 'image') { // FUDGE (we don't actually want thumb in this case, we want full image) $image_url = $row['url']; } else { $image_url = $row[$cma_info['thumb_field']]; } } if ($image_url != '') { if ($cma_info['thumb_field_is_theme_image']) { $image_url = find_theme_image($image_url, true); } else { if (url_is_local($image_url)) { $image_url = get_custom_base_url() . '/' . $image_url; } } } } if ((empty($image_url)) && ($cma_info['alternate_icon_theme_image'] != '') && ($content_id !== ':start'/*TODO: Fix in v11*/)) { $METADATA['image'] = find_theme_image($cma_info['alternate_icon_theme_image'], true); } if (!empty($image_url)) { $METADATA['image'] = $image_url; } // Add all $cma_info $METADATA += $cma_info; unset($METADATA['connection']); $METADATA['content_type_label_trans'] = do_lang($cma_info['content_type_label']); } if ($content_type !== null) { $METADATA['content_type'] = $content_type; } if ($content_id !== null) { $METADATA['content_id'] = $content_id; } } /** * Set the HTTP status code for the request. * * @sets_output_state * * @param string $code The HTTP status code (should be numeric) */ function set_http_status_code($code) { global $HTTP_STATUS_CODE; $HTTP_STATUS_CODE = $code; // So we can keep track if ((!headers_sent()) && (function_exists('browser_matches')) && (!browser_matches('ie')) && (strpos(cms_srv('SERVER_SOFTWARE'), 'IIS') === false)) { switch ($code) { case '301': header('HTTP/1.0 301 Moved Permanently'); break; case '400': header('HTTP/1.0 400 Bad Request'); break; case '401': header('HTTP/1.0 401 Unauthorized'); break; case '403': header('HTTP/1.0 403 Forbidden'); break; case '404': header('HTTP/1.0 404 Not Found'); break; case '429': header('HTTP/1.0 429 Too Many Requests'); break; case '500': header('HTTP/1.0 500 Internal server error'); break; } } } /** * Search for a template. * * @param ID_TEXT $codename The codename of the template being loaded * @param ?LANGUAGE_NAME $lang The language to load the template in (templates can embed language references) (null: users own language) * @param ID_TEXT $theme The theme to use * @param string $suffix File type suffix of template file (e.g. .tpl) * @param string $directory Subdirectory type to look in * @set templates css * @param boolean $non_custom_only Whether to only search in the default templates * @param boolean $fallback_other_themes Allow fallback to other themes, in case it is defined only in a specific theme we would not noprmally look in * @return ?array List of parameters needed for the _do_template function to be able to load the template (null: could not find the template) */ function find_template_place($codename, $lang, $theme, $suffix, $directory, $non_custom_only = false, $fallback_other_themes = true) { global $FILE_ARRAY, $CURRENT_SHARE_USER; static $tp_cache = array(); $sz = serialize(array($codename, $lang, $theme, $suffix, $directory, $non_custom_only)); if (isset($tp_cache[$sz])) { return $tp_cache[$sz]; } if (addon_installed('less') && $suffix == '.css') { $test = find_template_place($codename, $lang, $theme, '.less', $directory, $non_custom_only, false); if (!is_null($test)) { $tp_cache[$sz] = $test; return $test; } } $prefix_default = get_file_base() . '/themes/'; $prefix = ($theme == 'default' || $theme == 'admin') ? $prefix_default : (get_custom_file_base() . '/themes/'); if (!isset($FILE_ARRAY)) { if ((is_file($prefix . $theme . '/' . $directory . '_custom/' . $codename . $suffix)) && (!in_safe_mode()) && (!$non_custom_only)) { $place = array($theme, '/' . $directory . '_custom/', $suffix); } elseif (is_file($prefix . $theme . '/' . $directory . '/' . $codename . $suffix)) { $place = array($theme, '/' . $directory . '/', $suffix); } elseif (($CURRENT_SHARE_USER !== null) && ($theme !== 'default') && (is_file(get_file_base() . '/themes/' . $theme . '/' . $directory . '_custom/' . $codename . $suffix)) && (!$non_custom_only)) { $place = array($theme, '/' . $directory . '_custom/', $suffix); } elseif (($CURRENT_SHARE_USER !== null) && ($theme !== 'default') && (is_file(get_file_base() . '/themes/' . $theme . '/' . $directory . '/' . $codename . $suffix))) { $place = array($theme, '/' . $directory . '/', $suffix); } elseif (($CURRENT_SHARE_USER !== null) && (is_file(get_custom_file_base() . '/themes/default/' . $directory . '_custom/' . $codename . $suffix)) && (!$non_custom_only)) { $place = array('default', '/' . $directory . '_custom/', $suffix); } elseif (($CURRENT_SHARE_USER !== null) && (is_file(get_custom_file_base() . '/themes/default/' . $directory . '/' . $codename . $suffix))) { $place = array('default', '/' . $directory . '/', $suffix); } elseif ((is_file($prefix_default . 'default' . '/' . $directory . '_custom/' . $codename . $suffix)) && (!in_safe_mode()) && (!$non_custom_only)) { $place = array('default', '/' . $directory . '_custom/', $suffix); } elseif (is_file($prefix_default . 'default' . '/' . $directory . '/' . $codename . $suffix)) { $place = array('default', '/' . $directory . '/', $suffix); } else { $place = null; } if (($place === null) && (!$non_custom_only) && ($fallback_other_themes)) { // Get desperate, search in themes other than current and default $dh = opendir(get_file_base() . '/themes'); while (($possible_theme = readdir($dh))) { if (($possible_theme[0] !== '.') && ($possible_theme !== 'default') && ($possible_theme !== $theme) && ($possible_theme !== 'map.ini') && ($possible_theme !== 'index.html')) { $full_path = get_custom_file_base() . '/themes/' . $possible_theme . '/' . $directory . '_custom/' . $codename . $suffix; if (is_file($full_path)) { $place = array($possible_theme, '/' . $directory . '_custom/', $suffix); break; } } } closedir($dh); } } else { $place = array('default', '/' . $directory . '/', $suffix); } $tp_cache[$sz] = $place; return $place; } /** * Find whether panels and the header/footer areas won't be shown. * * @return BINARY Result. */ function is_wide_high() { global $IS_WIDE_HIGH_CACHE; if ($IS_WIDE_HIGH_CACHE !== null) { return $IS_WIDE_HIGH_CACHE; } $IS_WIDE_HIGH_CACHE = get_param_integer('wide_high', get_param_integer('keep_wide_high', get_param_integer('wide_print', 0))); return $IS_WIDE_HIGH_CACHE; } /** * Find whether panels will be shown. * * @return BINARY Result. */ function is_wide() { global $IS_WIDE_CACHE; if ($IS_WIDE_CACHE !== null) { return $IS_WIDE_CACHE; } global $ZONE; $IS_WIDE_CACHE = get_param_integer('wide', get_param_integer('keep_wide', (is_wide_high() == 1) ? 1 : 0)); if ($IS_WIDE_CACHE == 0) { return 0; } // Need to check it is allowed $theme = $GLOBALS['FORUM_DRIVER']->get_theme(); $ini_path = (($theme == 'default' || $theme == 'admin') ? get_file_base() : get_custom_file_base()) . '/themes/' . $theme . '/theme.ini'; if (is_file($ini_path)) { require_code('files'); $details = better_parse_ini_file($ini_path); if ((isset($details['supports_wide'])) && ($details['supports_wide'] == '0')) { $IS_WIDE_CACHE = 0; return $IS_WIDE_CACHE; } } return $IS_WIDE_CACHE; } /** * Fixes bad unicode (utf-8) in the input. Useful when input may be dirty, e.g. from a txt file, or from a potential hacker. * The fix is imperfect, it will actually treat the input as ISO-8859-1 if not valid utf-8, then reconvert. Some limited scrambling is considered better than a stack trace. * This function does nothing if we are not using utf-8. * * @param string $input Input string * @param boolean $definitely_unicode If we know the input is meant to be unicode * @return string Guaranteed valid utf-8, if we're using it, otherwise the same as the input string */ function fix_bad_unicode($input, $definitely_unicode = false) { // Fix bad unicode if (get_charset() == 'utf-8' || $definitely_unicode) { if (is_numeric($input) || preg_match('#[^\x00-\x7f]#', $input) == 0) { return $input; // No non-ASCII characters } $test_string = $input; // avoid being destructive $test_string = preg_replace('#[\x09\x0A\x0D\x20-\x7E]#', '', $test_string); // ASCII $test_string = preg_replace('#[\xC2-\xDF][\x80-\xBF]#', '', $test_string); // non-overlong 2-byte $test_string = preg_replace('#\xE0[\xA0-\xBF][\x80-\xBF]#', '', $test_string); // excluding overlongs $test_string = preg_replace('#[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}#', '', $test_string); // straight 3-byte $test_string = preg_replace('#\xED[\x80-\x9F][\x80-\xBF]#', '', $test_string); // excluding surrogates $test_string = preg_replace('#\xF0[\x90-\xBF][\x80-\xBF]{2}#', '', $test_string); // planes 1-3 $test_string = preg_replace('#[\xF1-\xF3][\x80-\xBF]{3}#', '', $test_string); // planes 4-15 $test_string = preg_replace('#\xF4[\x80-\x8F][\x80-\xBF]{2}#', '', $test_string); // plane 16 if ($test_string !== '') { // All ASCII/unicode characters stripped, so if anything is remaining it must be some kind of corruption $input = utf8_encode($input); } } return $input; } /** * Get string length, with utf-8 awareness where possible/required. * * @param string $in The string to get the length of. * @param boolean $force Whether to force unicode as on. * @return integer The string length. */ function cms_mb_strlen($in, $force = false) { if (!$force && get_charset() != 'utf-8') { return strlen($in); } if (function_exists('mb_strlen')) { return @mb_strlen($in, $force ? 'utf-8' : get_charset()); // @ is because there could be invalid unicode involved } if (function_exists('iconv_strlen')) { return @iconv_strlen($in, $force ? 'utf-8' : get_charset()); } return strlen($in); } /** * Return part of a string, with utf-8 awareness where possible/required. * * @param string $in The subject. * @param integer $from The start position. * @param ?integer $amount The length to extract (null: all remaining). * @param boolean $force Whether to force unicode as on. * @return ~string String part (false: $start was over the end of the string). */ function cms_mb_substr($in, $from, $amount = null, $force = false) { if ($amount === null) { $amount = cms_mb_strlen($in, $force) - $from; } if ($in == '' || strlen($in) == $from) { return ''; // Workaround PHP bug/inconsistency (https://bugs.php.net/bug.php?id=72320) } if ((!$force) && (get_charset() != 'utf-8')) { return substr($in, $from, $amount); } if (function_exists('iconv_substr')) { return @iconv_substr($in, $from, $amount, $force ? 'utf-8' : get_charset()); } if (function_exists('mb_substr')) { return @mb_substr($in, $from, $amount, $force ? 'utf-8' : get_charset()); } $ret = substr($in, $from, $amount); $end = ord(substr($ret, -1)); if (($end >= 192) && ($end <= 223)) { $ret .= substr($in, $from + $amount, 1); } if ($from != 0) { $start = ord(substr($ret, 0, 1)); if (($start >= 192) && ($start <= 223)) { $ret = substr($in, $from - 1, 1) . $ret; } } return $ret; } /** * Workaround for when we can't enable LC_CTYPE on the locale - temporarily enable it when we really need it. * * @param boolean $start Whether to start the workaround (as opposed to ending it). */ function _local_ctype_hack($start) { $ctype_hack = (do_lang('locale_ctype_hack') == '1'); if ($ctype_hack) { if ($start) { static $proper_locale = null; if ($proper_locale === null) { $proper_locale = explode(',', do_lang('locale')); } setlocale(LC_CTYPE, $proper_locale); } else { static $fallback_locale = null; if ($fallback_locale === null) { $fallback_locale = explode(',', do_lang('locale')); } setlocale(LC_CTYPE, $fallback_locale); } } } /** * Make a string title-case, with utf-8 awareness where possible/required. * * @param string $in Subject. * @return string Result. */ function cms_mb_ucwords($in) { _local_ctype_hack(true); if (get_charset() != 'utf-8') { $ret = ucwords($in); } elseif (function_exists('mb_convert_case')) { $ret = @mb_convert_case($in, MB_CASE_TITLE, get_charset()); } else { $ret = ucwords($in); } _local_ctype_hack(false); return $ret; } /** * Make a string lowercase, with utf-8 awareness where possible/required. * * @param string $in Subject. * @return string Result. */ function cms_mb_strtolower($in) { _local_ctype_hack(true); if (get_charset() != 'utf-8') { $ret = strtolower($in); } elseif (function_exists('mb_strtolower')) { $ret = @mb_strtolower($in, get_charset()); } else { $ret = strtolower($in); } _local_ctype_hack(false); return $ret; } /** * Make a string uppercase, with utf-8 awareness where possible/required. * * @param string $in Subject. * @return string Result. */ function cms_mb_strtoupper($in) { _local_ctype_hack(true); if (get_charset() != 'utf-8') { $ret = strtoupper($in); } elseif (function_exists('mb_strtoupper')) { $ret = @mb_strtoupper($in, get_charset()); } else { $ret = strtoupper($in); } _local_ctype_hack(false); return $ret; } /** * Find if we a string is ASCII, and hence we can use non-UTF-safe functions on it. * * @param string $x String to test * @return boolean Whether it is ASCII */ function is_ascii_string($x) { $l = strlen($x); for ($i = 0; $i < $l; $i++) { if (ord($x[$i]) >= 128) { return false; } } return true; } /** * Find whether a file/directory is writeable. This function is designed to get past that the PHP is_writable function does not work properly on Windows. * * @param PATH $path The file path * @return boolean Whether the file is writeable */ function is_writable_wrap($path) { if (strtoupper(substr(PHP_OS, 0, 3)) != 'WIN') { return is_writable($path); } if (!file_exists($path)) { return false; } if (is_dir($path)) { /*if (false) { // ideal, but too dangerous as sometimes you can write files but not delete again $test = @fopen($path . '/cms.delete.me', GOOGLE_APPENGINE ? 'wb' : 'wt'); if ($test !== false) { fclose($test); unlink($path . '/cms.delete.me'); return true; } return false; }*/ return is_writable($path); // imperfect unfortunately; but unlikely to cause a problem } else { $test = @fopen($path, 'ab'); if ($test !== false) { fclose($test); return true; } return false; } } /** * Discern the cause of a file-write error, and show an appropriate error message. * * @param PATH $path File path that could not be written (full path, not relative) */ function intelligent_write_error($path) { require_code('files2'); _intelligent_write_error($path); } /** * Discern the cause of a file-write error, and return an appropriate error message. * * @param PATH $path File path that could not be written * @return mixed Message (Tempcode or string) */ function intelligent_write_error_inline($path) { require_code('files2'); return _intelligent_write_error_inline($path); } /** * Find whether we have no forum on this website. * * @return boolean Whether we have no forum on this website */ function has_no_forum() { if (get_forum_type() == 'none') { return true; } if ((get_forum_type() == 'cns') && (!addon_installed('cns_forum'))) { return true; } return false; } /** * Check to see if an addon is installed. * * @param ID_TEXT $addon The addon name * @param boolean $non_bundled_too Whether to check non-bundled addons (ones without an addon_registry hook) * @return boolean Whether it is */ function addon_installed($addon, $non_bundled_too = false) { global $ADDON_INSTALLED_CACHE, $SITE_INFO; if ($ADDON_INSTALLED_CACHE == array()) { if (function_exists('persistent_cache_get')) { $ADDON_INSTALLED_CACHE = persistent_cache_get('ADDONS_INSTALLED'); } } if (isset($ADDON_INSTALLED_CACHE[$addon])) { return $ADDON_INSTALLED_CACHE[$addon]; } $addon = filter_naughty($addon, true); $answer = is_file(get_file_base() . '/sources/hooks/systems/addon_registry/' . $addon . '.php') || is_file(get_file_base() . '/sources_custom/hooks/systems/addon_registry/' . $addon . '.php'); if ((!$answer) && ($non_bundled_too) && (!running_script('install'))) { $test = $GLOBALS['SITE_DB']->query_select_value_if_there('addons', 'addon_name', array('addon_name' => $addon)); if ($test !== null) { $answer = true; } } $ADDON_INSTALLED_CACHE[$addon] = $answer; if (function_exists('persistent_cache_set')) { persistent_cache_set('ADDONS_INSTALLED', $ADDON_INSTALLED_CACHE); } return $answer; } /** * Convert a float to a "technical string representation of a float". Inverted with floatval. * * @param float $num The number * @param integer $decs_wanted The number of decimals to keep * @param boolean $only_needed_decs Whether to trim trailing zeros * @return string The string converted */ function float_to_raw_string($num, $decs_wanted = 2, $only_needed_decs = false) { $str = number_format($num, $decs_wanted, '.', ''); $dot_pos = strpos($str, '.'); $decs_here = ($dot_pos === false) ? 0 : (strlen($str) - $dot_pos - 1); if ($decs_here < $decs_wanted) { for ($i = 0; $i < $decs_wanted - $decs_here; $i++) { $str .= '0'; } } elseif ($decs_here > $decs_wanted) { $str = substr($str, 0, strlen($str) - $decs_here + $decs_wanted); if ($decs_wanted == 0 && !$only_needed_decs) { $str = rtrim($str, '.'); } } if ($only_needed_decs && $decs_wanted != 0) { $str = rtrim(rtrim($str, '0'), '.'); } return $str; } /** * Format the given float number as a nicely formatted string (using the locale). Inverted with float_unformat. * * @param float $val The value to format * @param integer $decs_wanted The number of fractional digits * @param boolean $only_needed_decs Whether to trim trailing zeros * @return string Nicely formatted string */ function float_format($val, $decs_wanted = 2, $only_needed_decs = false) { $locale = localeconv(); if ($locale['thousands_sep'] == '') { $locale['thousands_sep'] = ','; } $str = number_format($val, $decs_wanted, $locale['decimal_point'], $locale['thousands_sep']); $dot_pos = strpos($str, '.'); $decs_here = ($dot_pos === false) ? 0 : (strlen($str) - $dot_pos - 1); if ($decs_here < $decs_wanted) { for ($i = 0; $i < $decs_wanted - $decs_here; $i++) { $str .= '0'; } } elseif ($decs_here > $decs_wanted) { $str = substr($str, 0, strlen($str) - $decs_here + $decs_wanted); if ($decs_wanted == 0) { $str = rtrim($str, '.'); } } if ($only_needed_decs && $decs_wanted != 0) { $str = rtrim(rtrim($str, '0'), '.'); } return $str; } /** * Take the given formatted float number and convert it to a native float. The inverse of float_format. * * @param string $str The formatted float number using the locale. * @param boolean $no_thousands_sep Whether we do *not* expect a thousands separator, which means we can be a bit smarter. * @return float Native float */ function float_unformat($str, $no_thousands_sep = false) { $locale = localeconv(); // Simplest case? if (preg_match('#^\d+$#', $str) != 0) { // E.g. "123" return floatval($str); } if ($no_thousands_sep) { // We can assume a "." is a decimal point then? if (preg_match('#^\d+\.\d+$#', $str) != 0) { // E.g. "123.456" return floatval($str); } } // Looks like English-format? It couldn't be anything else because thousands_sep always comes before decimal_point if (preg_match('#^[\d,]+\.\d+$#', $str) != 0) { // E.g. "123,456.789" return floatval($str); } // Now it must e E.g. "123.456,789" or "123.456", or something from another language which uses other separators... if ($locale['thousands_sep'] != '') { $str = str_replace($locale['thousands_sep'], '', $str); } $str = str_replace($locale['decimal_point'], '.', $str); return floatval($str); } /** * Format the given integer number as a nicely formatted string (using the locale). * * @param integer $val The value to format * @return string Nicely formatted string */ function integer_format($val) { static $locale = null; if ($locale === null) { $locale = localeconv(); if ($locale['thousands_sep'] == '') { $locale['thousands_sep'] = ','; } } return number_format($val, 0, $locale['decimal_point'], $locale['thousands_sep']); } /** * Sort a list of maps by the string length of a particular key ID in the maps. * * @param array $rows List of maps to sort * @param mixed $sort_key Either an integer sort key (to sort by integer key ID of contained arrays) or a String sort key (to sort by string key ID of contained arrays). */ function sort_maps_by__strlen($rows, $sort_key) { global $M_SORT_KEY; $M_SORT_KEY = $sort_key; if (count($rows) < 2) { if (($GLOBALS['DEV_MODE']) && (count($rows) == 1)) { call_user_func('_strlen_sort', current($rows), current($rows)); // Just to make sure there's no crash bug in the sort function } return; } @uasort($rows, '_strlen_sort'); // @ is to stop PHP bug warning about altered array contents when Tempcode copies are evaluated internally } /** * Helper function for usort to sort a list by string length. * * @param string $a The first string to compare * @param string $b The second string to compare * @return integer The comparison result (0 for equal, -1 for less, 1 for more) * @ignore */ function _strlen_sort($a, $b) { if (!isset($a)) { $a = ''; } if (!isset($b)) { $b = ''; } if ($a == $b) { return 0; } if (!is_string($a)) { global $M_SORT_KEY; return (strlen($a[$M_SORT_KEY]) < strlen($b[$M_SORT_KEY])) ? -1 : 1; } return (strlen($a) < strlen($b)) ? -1 : 1; } /** * Sort a list of maps by a particular key ID in the maps. Does not (and should not) preserve list indices, but does preserve associative key indices. * * @param array $rows List of maps to sort * @param mixed $sort_keys Either an integer sort key (to sort by integer key ID of contained arrays) or a Comma-separated list of sort keys (to sort by string key ID of contained arrays; prefix '!' a key to reverse the sort order for it). * @param boolean $preserve_order_if_possible Don't shuffle order unnecessarily (i.e. do a merge sort) */ function sort_maps_by(&$rows, $sort_keys, $preserve_order_if_possible = false) { if ($rows == array()) { return; } global $M_SORT_KEY; $M_SORT_KEY = $sort_keys; if ($preserve_order_if_possible) { merge_sort($rows, '_multi_sort'); } else { if (count($rows) < 2) { if (($GLOBALS['DEV_MODE']) && (count($rows) == 1)) { call_user_func('_multi_sort', current($rows), current($rows)); // Just to make sure there's no crash bug in the sort function } return; } $first_key = key($rows); if ((is_integer($first_key)) && (array_unique(array_map('is_integer', array_keys($rows))) === array(true))) { usort($rows, '_multi_sort'); } else { uasort($rows, '_multi_sort'); } } } /** * Do a user sort, preserving order where reordering not needed. Based on a PHP manual comment at http://php.net/manual/en/function.usort.php * * @param array $array Sort array * @param mixed $cmp_function Comparison function */ function merge_sort(&$array, $cmp_function = 'strcmp') { // Arrays of size<2 require no action. if (count($array) < 2) { if (($GLOBALS['DEV_MODE']) && (count($array) == 1)) { call_user_func($cmp_function, current($array), current($array)); // Just to make sure there's no crash bug in the sort function } return; } // Split the array in half $halfway = intval(floatval(count($array)) / 2.0); $array1 = array_slice($array, 0, $halfway); $array2 = array_slice($array, $halfway); // Recurse to sort the two halves merge_sort($array1, $cmp_function); merge_sort($array2, $cmp_function); // If all of $array1 is <= all of $array2, just append them. if (call_user_func($cmp_function, end($array1), reset($array2)) < 1) { $array = array_merge($array1, $array2); return; } // Merge the two sorted arrays into a single sorted array $array = array(); reset($array1); reset($array2); $ptr1 = 0; $ptr2 = 0; $cnt1 = count($array1); $cnt2 = count($array2); while (($ptr1 < $cnt1) && ($ptr2 < $cnt2)) { if (call_user_func($cmp_function, current($array1), current($array2)) < 1) { $key = key($array1); if (is_integer($key)) { $array[] = current($array1); } else { $array[$key] = current($array1); } $ptr1++; next($array1); } else { $key = key($array2); if (is_integer($key)) { $array[] = current($array2); } else { $array[$key] = current($array2); } $ptr2++; next($array2); } } // Merge the remainder while ($ptr1 < $cnt1) { $key = key($array1); if (is_integer($key)) { $array[] = current($array1); } else { $array[$key] = current($array1); } $ptr1++; next($array1); } while ($ptr2 < $cnt2) { $key = key($array2); if (is_integer($key)) { $array[] = current($array2); } else { $array[$key] = current($array2); } $ptr2++; next($array2); } } /** * Helper function to sort a list of maps by the value at $key in each of those maps. * * @param array $a The first to compare * @param array $b The second to compare * @return integer The comparison result (0 for equal, -1 for less, 1 for more) * @ignore */ function _multi_sort($a, $b) { global $M_SORT_KEY; $keys = explode(',', is_string($M_SORT_KEY) ? $M_SORT_KEY : strval($M_SORT_KEY)); $first_key = $keys[0]; if ($first_key[0] === '!') { $first_key = substr($first_key, 1); } if ((is_string($a[$first_key])) || (is_object($a[$first_key]))) { $ret = 0; do { $key = array_shift($keys); $backwards = ($key[0] === '!'); if ($backwards) { $key = substr($key, 1); } $av = $a[$key]; $bv = $b[$key]; if (is_object($av)) { $av = $av->evaluate(); } if (is_object($bv)) { $bv = $bv->evaluate(); } if ($backwards) { // Flip around if ((is_numeric($av)) && (is_numeric($bv))) { $ret = -strnatcasecmp($av, $bv); } else { $ret = -strcasecmp($av, $bv); } } else { if ((is_numeric($av)) && (is_numeric($bv))) { $ret = strnatcasecmp($av, $bv); } else { $ret = strcasecmp($av, $bv); } } } while ((count($keys) !== 0) && ($ret === 0)); return $ret; } do { $key = array_shift($keys); if ($key[0] === '!') { // Flip around $key = substr($key, 1); $ret = ($a[$key] > $b[$key]) ? -1 : (($a[$key] == $b[$key]) ? 0 : 1); } else { $ret = ($a[$key] > $b[$key]) ? 1 : (($a[$key] == $b[$key]) ? 0 : -1); } } while ((count($keys) !== 0) && ($ret == 0)); return $ret; } /** * Require all code relating to the Conversr forum */ function cns_require_all_forum_stuff() { require_lang('cns'); require_code('cns_members'); require_code('cns_topics'); require_code('cns_posts'); require_code('cns_moderation'); require_code('cns_groups'); require_code('cns_forums'); require_code('cns_general'); } /** * Create file with unique file name, but works around compatibility issues between servers. Note that the file is NOT automatically deleted. You should also delete it using "@unlink", as some servers have problems with permissions. * * @param string $prefix The prefix of the temporary file name. * @return ~string The name of the temporary file (false: error). */ function cms_tempnam($prefix = 'cms') { require_code('files2'); return _cms_tempnam($prefix); } /** * Peek at a stack element. * * @param array $array The stack to peek in * @param integer $depth_down The depth into the stack we are peaking * @return mixed The result of the peeking */ function array_peek($array, $depth_down = 1) { $count = count($array); if ($count - $depth_down < 0) { return null; } return $array[$count - $depth_down]; } /** * Make a value suitable for use in an XML ID. * * @param string $param The value to escape * @return string The escaped value */ function fix_id($param) { if (preg_match('#^[A-Za-z][\w]*$#', $param) !== 0) { return $param; // Optimisation } $length = strlen($param); $new = ''; for ($i = 0; $i < $length; $i++) { $char = $param[$i]; switch ($char) { case '[': $new .= '_opensquare_'; break; case ']': $new .= '_closesquare_'; break; case ''': case '\'': $new .= '_apostophe_'; break; case '-': $new .= '_minus_'; break; case ' ': $new .= '_space_'; break; case '+': $new .= '_plus_'; break; case '*': $new .= '_star_'; break; case '/': $new .= '__'; break; default: $ascii = ord($char); if ((($i !== 0) && ($char === '_')) || (($ascii >= 48) && ($ascii <= 57)) || (($ascii >= 65) && ($ascii <= 90)) || (($ascii >= 97) && ($ascii <= 122))) { $new .= $char; } else { $new .= '_' . strval($ascii) . '_'; } break; } } if ($new === '') { $new = 'zero_length'; } if ($new[0] === '_') { $new = 'und_' . $new; } return $new; } /** * See if the current URL matches the given Composr match-keys. * * @param mixed $match_keys Match keys (comma-separated list of match-keys, or array of) * @param boolean $support_post Check against POSTed data too * @param ?array $current_params Parameters to check against (null: get from environment GET/POST) - if set, $support_post is ignored) * @param ?ID_TEXT $current_zone_name Current zone name (null: get from environment) * @param ?ID_TEXT $current_page_name Current page name (null: get from environment) * @return boolean Whether there is a match */ function match_key_match($match_keys, $support_post = false, $current_params = null, $current_zone_name = null, $current_page_name = null) { $req_func = $support_post ? 'either_param_string' : 'get_param_string'; if ($current_zone_name === null) { global $IN_SELF_ROUTING_SCRIPT; if (!$IN_SELF_ROUTING_SCRIPT) { return false; } $current_zone_name = get_zone_name(); } if ($current_page_name === null) { $current_page_name = get_page_name(); } $potentials = is_array($match_keys) ? $match_keys : explode(',', $match_keys); foreach ($potentials as $potential) { $parts = is_array($potential) ? $potential : explode(':', $potential); if (($parts[0] == '_WILD') || ($parts[0] == '_SEARCH')) { $parts[0] = $current_zone_name; } if ((!isset($parts[1])) || ($parts[1] == '_WILD') || (($parts[1] == '_WILD_NOT_START') && ($current_page_name != get_zone_default_page($parts[0])))) { $parts[1] = $current_page_name; } if (($parts[0] == 'site') && (get_option('collapse_user_zones') == '1')) { $parts[0] = ''; } $zone_matches = (($parts[0] == $current_zone_name) || ((strpos($parts[0], '*') !== false) && (simulated_wildcard_match($current_zone_name, $parts[0], true)))); $page_matches = ((($parts[1] == '') && ($current_page_name == get_zone_default_page($current_zone_name))) || ($parts[1] == $current_page_name) || ((strpos($parts[1], '*') !== false) && (simulated_wildcard_match($current_page_name, $parts[1], true)))); if (($zone_matches) && ($page_matches)) { $bad = false; for ($i = 2; $i < count($parts); $i++) { if ($parts[$i] != '') { if (($i == 2) && (strpos($parts[$i], '=') === false)) { $parts[$i] = 'type=' . $parts[$i]; } elseif (($i == 3) && (strpos($parts[$i], '=') === false)) { $parts[$i] = 'id=' . $parts[$i]; } } $subparts = explode('=', $parts[$i]); if ($subparts[0] == 'type') { $default = 'browse'; } else { $default = ''; } if (count($subparts) != 2) { $bad = true; continue; } $env_val = ($current_params === null) ? call_user_func_array($req_func, array($subparts[0], null)) : (isset($current_params[$subparts[0]]) ? $current_params[$subparts[0]] : null); if ($subparts[1] == '_WILD') { if ($env_val !== null) { $subparts[1] = $env_val; // null won't match to a wildcard } } else { if ($env_val === null) { $env_val = $default; } } if ($env_val !== $subparts[1]) { $bad = true; continue; } } if (!$bad) { return true; } } } return false; } /** * Get the name of the page in the URL or active script. * * @return ID_TEXT The current page/script name */ function get_page_or_script_name() { global $IN_SELF_ROUTING_SCRIPT; if ($IN_SELF_ROUTING_SCRIPT) { return get_page_name(); } return current_script(); } /** * Get the name of the page in the URL (by convention: the current page). * This works on the basis of the 'page' parameter and does not require index.php be the active script. * It will do dash to underscore substitution as required. * * @return ID_TEXT The current page name */ function get_page_name() { global $PAGE_NAME_CACHE; if (isset($PAGE_NAME_CACHE)) { return $PAGE_NAME_CACHE; } global $ZONE, $GETTING_PAGE_NAME, $BOOTSTRAPPING; if ($GETTING_PAGE_NAME) { return 'unknown'; } $GETTING_PAGE_NAME = true; $page = get_param_string('page', '', true); if (strlen($page) > 80) { warn_exit(do_lang_tempcode('INTERNAL_ERROR')); } if (($page == '') && ($ZONE !== null)) { $page = $ZONE['zone_default_page']; if ($page === null) { $page = ''; } } if (strpos($page, '..') !== false) { $page = filter_naughty($page); } $simplified_algorithm = $BOOTSTRAPPING; // fix_page_name_dashing calls request_page, which won't work reliably during bootstrapping if ($simplified_algorithm) { $page = str_replace('-', '_', $page); } else { $page = fix_page_name_dashing(get_zone_name(), $page); } if (!$GETTING_PAGE_NAME) { // It's been changed by process_url_monikers, which was called indirectly by fix_page_name_dashing return $PAGE_NAME_CACHE; } if (($ZONE !== null) && (!$simplified_algorithm)) { $PAGE_NAME_CACHE = $page; } $GETTING_PAGE_NAME = false; return $page; } /** * Fix a page name that may have been given dashes for SEO reasons. * * @param string $zone Zone. * @param string $page Page. * @return string The fixed page name. */ function fix_page_name_dashing($zone, $page) { if (strpos($page, '/') !== false) { return $page; // It's a moniker that hasn't been processed yet } // Fix page-name dashes if needed if (strpos($page, '-') !== false) { require_code('site'); $test = _request_page($page, $zone, null, null, true); if ($test === false) { $_page = str_replace('-', '_', $page); $test = _request_page($_page, $zone); if ($test !== false) { $page = $_page; } } } return $page; } /** * Take a list of maps, and make one of the values of each array the index of a map to the map. * * list_to_map is very useful for handling query results. * Let's imagine you get the result of SELECT id,title FROM sometable. * list_to_map turns the array of rows into a map between the id key and each row. * * @param string $map_value The key key of our maps that reside in our map * @param array $list The list of maps * @return array The collapsed map */ function list_to_map($map_value, $list) { $i = 0; $new_map = array(); foreach ($list as $map) { $key = $map[$map_value]; $new_map[$key] = $map; $i++; } if ($i > 0) { return $new_map; } return array(); } /** * Take a list of maps of just two elements, and make it into a single map * * @param string $key The key key of our maps that reside in our map * @param string $value The value key of our maps that reside in our map * @param array $list The map of maps * @return array The collapsed map */ function collapse_2d_complexity($key, $value, $list) { $new_map = array(); foreach ($list as $map) { $new_map[$map[$key]] = $map[$value]; } return $new_map; } /** * Take a list of maps of just one element, and make it into a single map * * @param ?string $key The key of our maps that reside in our map (null: first key) * @param array $list The map of maps * @return array The collapsed map */ function collapse_1d_complexity($key, $list) { $new_array = array(); foreach ($list as $map) { if ($key === null) { $new_array[] = array_shift($map); } else { $new_array[] = $map[$key]; } } return $new_array; } /** * Used by cms_strip_tags to handle whether to strip a tag. * * @param array $matches Array of matches * @return string Substituted tag text * * @ignore */ function _cms_strip_tags_callback($matches) { global $STRIP_TAGS_TAGS, $STRIP_TAGS_TAGS_AS_ALLOW; $tag_covered = stripos($STRIP_TAGS_TAGS, '<' . $matches[1] . '>'); if ((($STRIP_TAGS_TAGS_AS_ALLOW) && ($tag_covered !== false)) || ((!$STRIP_TAGS_TAGS_AS_ALLOW) && ($tag_covered === false))) { return $matches[0]; } return ''; } /** * Strip HTML and PHP tags from a string. * Equivalent to PHP's strip_tags, whose $allowable_tags parameter is expected to be deprecated in PHP 7.3 (https://wiki.php.net/rfc/deprecations_php_7_3). * * @param string $str Subject * @param string $tags Comma-separated list of tags * @param boolean $tags_as_allow Whether tags represents a whitelist (set for false to allow all by default and make $tags a blacklist) * @return string Result */ function cms_strip_tags($str, $tags, $tags_as_allow = true) { global $STRIP_TAGS_TAGS, $STRIP_TAGS_TAGS_AS_ALLOW; $STRIP_TAGS_TAGS = $tags; $STRIP_TAGS_TAGS_AS_ALLOW = $tags_as_allow; return preg_replace_callback('#</?([^\s<>]+)(\s[^<>]*)?' . '>#', '_cms_strip_tags_callback', $str); } /** * Find whether an IP address is valid * * @param IP $ip IP address to check. * @return boolean Whether the IP address is valid. */ function is_valid_ip($ip) { if ($ip == '') { return false; } $parts = array(); if ((strpos($ip, '.') !== false) && (preg_match('#^(\d+)\.(\d+)\.(\d+)\.(\d+)$#', $ip, $parts) != 0)) { if (intval($parts[1]) > 255) { return false; } if (intval($parts[2]) > 255) { return false; } if (intval($parts[3]) > 255) { return false; } if (intval($parts[4]) > 255) { return false; } return true; } if ((strpos($ip, ':') !== false) && (preg_match('#^[\d:a-fA-F]*$#', $ip) != 0)) { return true; } return false; } /** * Attempt to get the clean IP address of the current user * * @param integer $amount The number of groups to include in the IP address (rest will be replaced with *'s). For IP6, this is doubled. * @set 1 2 3 4 * @param ?IP $ip IP address to use, normally left null (null: current user's) * @return IP The users IP address (blank: could not find a valid one) */ function get_ip_address($amount = 4, $ip = null) { require_code('config'); if ((get_value('cloudflare_workaround') === '1') && (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) && (isset($_SERVER['REMOTE_ADDR']))) { $regexp = '^(204\.93\.240\.|204\.93\.177\.|199\.27\.|173\.245\.|103\.21\.|103\.22\.|103\.31\.|141\.101\.|108\.162\.|190\.93\.|188\.114\.|197\.234\.|198\.41\.|162\.)'; if (preg_match('#' . $regexp . '#', $_SERVER['REMOTE_ADDR']) != 0) { $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR']; unset($_SERVER['HTTP_X_FORWARDED_FOR']); } } if ($ip === null) { /* Presents too many security and maintenance problems. Can easily be faked, or changed. $fw = cms_srv('HTTP_X_FORWARDED_FOR'); if (cms_srv('HTTP_CLIENT_IP') != '') { $fw = cms_srv('HTTP_CLIENT_IP'); } if (($fw != '') && ($fw != '127.0.0.1') && (substr($fw, 0, 8) != '192.168.') && (substr($fw, 0, 3) != '10.') && (is_valid_ip($fw)) && ($fw != cms_srv('SERVER_ADDR'))) { $ip = $fw; } else */ $ip = cms_srv('REMOTE_ADDR'); } global $SITE_INFO; if (($amount == 3) && (!empty($SITE_INFO['full_ips'])) && ($SITE_INFO['full_ips'] == '1')) { // Extra configurable security $amount = 4; } return normalise_ip_address($ip, $amount); } /** * Normalise a provided IP address * * @param IP $ip The IP address to normalise * @param ?integer $amount Amount to mask out (null: do not) * @return IP The normalised IP address */ function normalise_ip_address($ip, $amount = null) { $raw_ip = $ip; static $ip_cache = array(); if (isset($ip_cache[$raw_ip][$amount])) { return $ip_cache[$raw_ip][$amount]; } // Bizarro-filter (found "in the wild") $pos = strpos($ip, ','); if ($pos !== false) { $ip = substr($ip, 0, $pos); } // ...and another if (strpos($ip, '%') !== false) { $ip = preg_replace('#%14$#', '', $ip); } if (!is_valid_ip($ip)) { $ip_cache[$raw_ip][$amount] = ''; return ''; } // Normalise if (strpos($ip, '.') === false) { // IPv6 if (substr_count($ip, ':') < 7) { $ip = str_replace('::', str_repeat(':', (7 - substr_count($ip, ':')) + 2), $ip); } $parts = explode(':', $ip); for ($i = 0; $i < (is_null($amount) ? 8 : ($amount * 2)); $i++) { $parts[$i] = isset($parts[$i]) ? str_pad($parts[$i], 4, '0', STR_PAD_LEFT) : '0000'; } if (!is_null($amount)) { for ($i = $amount * 2; $i < 8; $i++) { $parts[$i] = '*'; } } $ip_cache[$raw_ip][$amount] = implode(':', $parts); } else { // IPv4 $parts = explode('.', $ip); for ($i = 0; $i < (is_null($amount) ? 4 : $amount); $i++) { if (!array_key_exists($i, $parts)) { $parts[$i] = '0'; } } if (!is_null($amount)) { for ($i = $amount; $i < 4; $i++) { $parts[$i] = '*'; } } $ip_cache[$raw_ip][$amount] = implode('.', $parts); } return $ip_cache[$raw_ip][$amount]; } /** * Exit with debug data, only for a specific IP address. * * @param IP $ip IP address of tester * @param mixed $data Data to display */ function me_debug($ip, $data) { if (get_ip_address() == $ip) { @var_dump($data); exit(); } } /** * Get a string of the users web browser * * @return string The web browser string */ function get_browser_string() { return cms_srv('HTTP_USER_AGENT'); } /** * Get the user's operating system * * @return string The operating system string */ function get_os_string() { if (cms_srv('HTTP_UA_OS') != '') { return cms_srv('HTTP_UA_OS'); } elseif (cms_srv('HTTP_USER_AGENT') != '') { // E.g. Mozilla/4.5 [en] (X11; U; Linux 2.2.9 i586) // We need to get the stuff in the brackets $matches = array(); if (preg_match('#\(([^\)]*)\)#', cms_srv('HTTP_USER_AGENT'), $matches) != 0) { $ret = $matches[1]; $ret = preg_replace('#^compatible; (MSIE[^;]*; )?#', '', $ret); return $ret; } } return ''; } /** * Find if Cron is installed * * @param boolean $absolutely_sure Whether Cron really needs to be installed (if set to false it will be assumed installed on dev-mode) * @return boolean Whether Cron is installed */ function cron_installed($absolutely_sure = false) { $test = get_param_integer('keep_has_cron', null); if ($test !== null) { return $test == 1; } if (!$absolutely_sure) { if ($GLOBALS['DEV_MODE']) { return true; } } $last_cron = get_value('last_cron'); if ($last_cron === null) { return false; } return intval($last_cron) > (time() - 60 * 60 * 5); } /** * Compare two IP addresses for potential correlation. Not as simple as equality due to '*' syntax. * * @param string $wild The general IP address that is potentially wildcarded * @param IP $full The specific IP address we are checking * @return boolean Whether the IP addresses correlate */ function compare_ip_address($wild, $full) { $wild_parts = explode((strpos($full, '.') !== false) ? '.' : ':', $wild); $full_parts = explode((strpos($full, '.') !== false) ? '.' : ':', $full); foreach ($wild_parts as $i => $wild_part) { if (($wild_part != '*') && ($wild_part != $full_parts[$i])) { return false; } } return true; } /** * Compare two IP addresses for potential correlation. Not as simple as equality due to '*' syntax. IP4-only variant * * @param string $wild The general IP address that is potentially wildcarded * @param array $full_parts The exploded parts of the specific IP address we are checking * @return boolean Whether the IP addresses correlate */ function compare_ip_address_ip4($wild, $full_parts) { $wild_parts = explode('.', $wild); foreach ($wild_parts as $i => $wild_part) { if (($wild_part != '*') && ($wild_part != $full_parts[$i])) { return false; } } return true; } /** * Compare two IP addresses for potential correlation. Not as simple as equality due to '*' syntax. IP6-only variant * * @param string $wild The general IP address that is potentially wildcarded * @param array $full_parts The exploded parts of the specific IP address we are checking * @return boolean Whether the IP addresses correlate */ function compare_ip_address_ip6($wild, $full_parts) { $wild_parts = explode(':', $wild); foreach ($wild_parts as $i => $wild_part) { if (($wild_part != '*') && ($wild_part != $full_parts[$i])) { return false; } } return true; } /** * Check to see if an IP address is banned. * * @param string $ip The IP address to check for banning * @param boolean $force_db Force check via database * @param boolean $handle_uncertainties Handle uncertainities (used for the external bans - if true, we may return null, showing we need to do an external check). Only works with $force_db. * @return ?boolean Whether the IP address is banned (null: unknown) */ function ip_banned($ip, $force_db = false, $handle_uncertainties = false) { // NB: This function will make the first query called, so we will be a bit smarter, checking for errors static $cache = array(); if ($handle_uncertainties) { if (array_key_exists($ip, $cache)) { return $cache[$ip]; } } if (!addon_installed('securitylogging')) { return false; } if ($ip == '') { return false; } // Check exclusions first $_exclusions = get_option('spam_check_exclusions'); $exclusions = explode(',', $_exclusions); foreach ($exclusions as $exclusion) { if (trim($ip) == $exclusion) { return false; } } global $SITE_INFO; if ((!$force_db) && (((isset($SITE_INFO['known_suexec'])) && ($SITE_INFO['known_suexec'] == '1')) || (is_writable_wrap(get_file_base() . '/.htaccess')))) { $bans = array(); $ban_count = preg_match_all('#\n(deny from|require not ip) (.*)#i', cms_file_get_contents_safe(get_file_base() . '/.htaccess'), $bans); $ip_bans = array(); for ($i = 0; $i < $ban_count; $i++) { $ip_bans[$bans[1][$i]] = array('ip' => $bans[1][$i]); } } else { $ip_bans = function_exists('persistent_cache_get') ? persistent_cache_get('IP_BANS') : null; if ($ip_bans === null) { $ip_bans = $GLOBALS['SITE_DB']->query_select('banned_ip', array('*'), null, '', null, null, true); if (!is_array($ip_bans)) { // LEGACY $ip_bans = $GLOBALS['SITE_DB']->query_select('usersubmitban_ip', array('*'), null, '', null, null, true); } if ($ip_bans !== null) { persistent_cache_set('IP_BANS', $ip_bans); } } if ($ip_bans === null) { critical_error('DATABASE_FAIL'); } } $ip4 = (strpos($ip, '.') !== false); if ($ip4) { $ip_parts = explode('.', $ip); } else { $ip_parts = explode(':', $ip); } $self_ip = null; foreach ($ip_bans as $ban) { if ((isset($ban['i_ban_until'])) && ($ban['i_ban_until'] < time())) { $GLOBALS['SITE_DB']->query('DELETE FROM ' . get_table_prefix() . 'banned_ip WHERE i_ban_until IS NOT NULL AND i_ban_until<' . strval(time())); continue; } if ((($ip4) && (compare_ip_address_ip4($ban['ip'], $ip_parts))) || ((!$ip4) && (compare_ip_address_ip6($ban['ip'], $ip_parts)))) { if ($self_ip === null) { $self_host = cms_srv('HTTP_HOST'); if (($self_host == '') || (preg_match('#^localhost[\.\:$]#', $self_host) != 0)) { $self_ip = ''; } else { $self_ip = cms_gethostbyname($self_host); if ($self_ip == $self_host) { $self_ip = cms_srv('SERVER_ADDR'); } } } if (($self_ip != '') && (compare_ip_address($ban['ip'], $self_ip))) { continue; } if (compare_ip_address($ban['ip'], '127.0.0.1')) { continue; } if (compare_ip_address($ban['ip'], 'fe00:0000:0000:0000:0000:0000:0000:0000')) { continue; } if (array_key_exists('i_ban_positive', $ban)) { $ret = ($ban['i_ban_positive'] == 1); } else { $ret = true; } if ($handle_uncertainties) { $cache[$ip] = $ret; } return $ret; } } $ret = $handle_uncertainties ? null : false; if ($handle_uncertainties) { $cache[$ip] = $ret; } return $ret; } /** * Log an action * * @param ID_TEXT $type The type of activity just carried out (a language string ID) * @param ?SHORT_TEXT $a The most important parameter of the activity (e.g. D) (null: none) * @param ?SHORT_TEXT $b A secondary (perhaps, human readable) parameter of the activity (e.g. caption) (null: none) * @return ?AUTO_LINK Log ID (null: did not save a log) */ function log_it($type, $a = null, $b = null) { require_code('global4'); return _log_it($type, $a, $b); } /** * Escape a string to fit within PHP double quotes. * * @param string $in String in * @return string Resultant string */ function php_addslashes($in) { global $PHP_REP_FROM, $PHP_REP_TO; return str_replace($PHP_REP_FROM, $PHP_REP_TO, $in); } /** * Remove any duplication inside the list of rows (each row being a map). Duplication is defined by rows with correspinding IDs. * * @param array $rows The rows to remove duplication of * @param string $id_field The ID field * @return array The filtered rows */ function remove_duplicate_rows($rows, $id_field = 'id') { $ids_seen = array(); $rows2 = array(); foreach ($rows as $row) { if (!array_key_exists($row[$id_field], $ids_seen)) { $rows2[] = $row; } $ids_seen[$row[$id_field]] = true; } return $rows2; } /** * Update the member tracker for the currently viewing user. * * @param ID_TEXT $page The page * @param ID_TEXT $type The type * @param ID_TEXT $id The ID */ function member_tracking_update($page, $type, $id) { if (get_value('no_member_tracking') === '1') { return; } if (!$GLOBALS['SITE_DB']->table_is_locked('member_tracking')) { $GLOBALS['SITE_DB']->query('DELETE FROM ' . get_table_prefix() . 'member_tracking WHERE mt_time<' . strval(time() - 60 * intval(get_option('users_online_time'))) . ' OR (mt_member_id=' . strval(get_member()) . ' AND ' . db_string_equal_to('mt_type', $type) . ' AND ' . db_string_equal_to('mt_id', $id) . ' AND ' . db_string_equal_to('mt_page', $page) . ')'); } $GLOBALS['SITE_DB']->query_insert('member_tracking', array( 'mt_member_id' => get_member(), 'mt_cache_username' => $GLOBALS['FORUM_DRIVER']->get_username(get_member(), true), 'mt_time' => time(), 'mt_page' => $page, 'mt_type' => $type, 'mt_id' => $id ), false, true); // Ignore errors for race conditions } /** * Find whether the current user is invisible. * * @return boolean Whether the current user is invisible */ function is_invisible() { global $SESSION_CACHE; $s = get_session_id(); return ((isset($SESSION_CACHE[$s])) && ($SESSION_CACHE[$s]['session_invisible'] == 1)); } /** * Get the number of users on the site in the last 5 minutes. The function also maintains the statistic via the sessions table. * * @return integer The number of users on the site */ function get_num_users_site() { if (get_value('disable_user_online_counting') === '1') { return 1; } global $NUM_USERS_SITE_CACHE, $PEAK_USERS_EVER_CACHE, $PEAK_USERS_WEEK_CACHE; $users_online_time_seconds = 60 * intval(get_option('users_online_time')); $NUM_USERS_SITE_CACHE = get_value_newer_than('users_online', time() - $users_online_time_seconds / 2); /* Refreshes half way through the user online time, to approximate accuracy */ if ($NUM_USERS_SITE_CACHE === null) { $NUM_USERS_SITE_CACHE = get_value('users_online'); $count = 0; require_code('users2'); get_users_online(false, null, $count); $NUM_USERS_SITE_CACHE = strval($count); if (!$GLOBALS['SITE_DB']->table_is_locked('values')) { set_value('users_online', $NUM_USERS_SITE_CACHE); } } if ((intval($NUM_USERS_SITE_CACHE) > intval(get_option('maximum_users'))) && (intval(get_option('maximum_users')) > 1) && (get_page_name() != 'login') && (!has_privilege(get_member(), 'access_overrun_site')) && (!running_script('cron_bridge'))) { set_http_status_code('503'); critical_error('BUSY', do_lang('TOO_MANY_USERS')); } if (addon_installed('stats')) { // Store a peak record if there is one $PEAK_USERS_EVER_CACHE = get_value('user_peak'); if (($PEAK_USERS_EVER_CACHE === null) || ($PEAK_USERS_EVER_CACHE == '')) { $_peak_users_user = $GLOBALS['SITE_DB']->query_select_value_if_there('usersonline_track', 'MAX(peak)', null, '', true); $PEAK_USERS_EVER_CACHE = ($_peak_users_user === null) ? $NUM_USERS_SITE_CACHE : strval($_peak_users_user); if (!$GLOBALS['SITE_DB']->table_is_locked('values')) { set_value('user_peak', $PEAK_USERS_EVER_CACHE); } } if (intval($NUM_USERS_SITE_CACHE) > intval($PEAK_USERS_EVER_CACHE)) { // New record $GLOBALS['SITE_DB']->query_insert('usersonline_track', array('date_and_time' => time(), 'peak' => intval($NUM_USERS_SITE_CACHE)), false, true); if (!$GLOBALS['SITE_DB']->table_is_locked('values')) { set_value('user_peak', $NUM_USERS_SITE_CACHE); } } // Store a 7-day-cycle peak record if we've made one $PEAK_USERS_WEEK_CACHE = get_value_newer_than('user_peak_week', time() - $users_online_time_seconds / 2); $store_anyway = false; if (($PEAK_USERS_WEEK_CACHE === null) || ($PEAK_USERS_WEEK_CACHE == '')) { $store_anyway = true; } if ((intval($NUM_USERS_SITE_CACHE) > intval($PEAK_USERS_WEEK_CACHE)) || ($store_anyway)) { $PEAK_USERS_WEEK_CACHE = $NUM_USERS_SITE_CACHE; // But also delete anything else in the last 7 days that was less than the new weekly peak record, to keep the stats clean (we only want 7 day peaks to be stored) $GLOBALS['SITE_DB']->query('DELETE FROM ' . get_table_prefix() . 'usersonline_track WHERE date_and_time>' . strval(time() - 60 * 60 * 24 * 7) . ' AND peak<=' . $PEAK_USERS_WEEK_CACHE, null, null, true); // Set record for week set_value('user_peak_week', $PEAK_USERS_WEEK_CACHE); $GLOBALS['SITE_DB']->query_insert('usersonline_track', array('date_and_time' => time(), 'peak' => intval($PEAK_USERS_WEEK_CACHE)), false, true); } } return intval($NUM_USERS_SITE_CACHE); } /** * Get the largest amount of users ever to be on the site at the same time. * * @return integer The number of peak users */ function get_num_users_peak() { global $PEAK_USERS_EVER_CACHE; return intval($PEAK_USERS_EVER_CACHE); } /** * Get the specified string, but with all characters escaped. * * @param mixed $string The input string * @return string The escaped string */ function escape_html($string) { //if ($string === '') return $string; // Optimisation, but doesn't work well if (isset($string->codename)/*faster than is_object*/) { return $string; } /*if ($GLOBALS['XSS_DETECT']) { Useful for debugging if (ocp_is_escaped($string)) { @var_dump(debug_backtrace()); @exit('String double-escaped'); } }*/ global $XSS_DETECT, $ESCAPE_HTML_OUTPUT, $DECLARATIONS_STATE; $ret = @htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, get_charset()); if (defined('I_UNDERSTAND_XSS') && !$DECLARATIONS_STATE[I_UNDERSTAND_XSS]) { $ESCAPE_HTML_OUTPUT[$ret] = true; } if ($XSS_DETECT) { ocp_mark_as_escaped($ret); } return $ret; } /** * See's if the current browser matches some special property code. Assumes users are keeping up on newish browsers (except for IE users, who are 6+) * * @param string $code The property code * @set android ios wysiwyg windows mac linux odd_os mobile ie ie8 ie8+ ie9 ie9+ gecko safari odd_browser chrome bot simplified_attachments_ui itunes * @param ?string $comcode Comcode that might be WYSIWYG edited; used to determine whether WYSIWYG may load when we'd prefer it to not do so (null: none) * @return boolean Whether there is a match */ function browser_matches($code, $comcode = null) { global $BROWSER_MATCHES_CACHE; if (isset($BROWSER_MATCHES_CACHE[$code])) { return $BROWSER_MATCHES_CACHE[$code]; } $browser = strtolower(cms_srv('HTTP_USER_AGENT')); $os = strtolower(cms_srv('HTTP_UA_OS')) . ' ' . $browser; $is_safari = strpos($browser, 'applewebkit') !== false; $is_chrome = strpos($browser, 'chrome/') !== false; $is_gecko = (strpos($browser, 'gecko') !== false) && !$is_safari; $is_ie = ((strpos($browser, 'msie') !== false) || (strpos($browser, 'trident') !== false) || (strpos($browser, 'edge/') !== false)); $is_ie8 = (strpos($browser, 'msie 8') !== false) && ($is_ie); $is_ie9 = (strpos($browser, 'msie 9') !== false) && ($is_ie); $is_ie8_plus = $is_ie; // Below IE8 not supported/recognised $is_ie9_plus = $is_ie && !$is_ie8; switch ($code) { case 'simplified_attachments_ui': $BROWSER_MATCHES_CACHE[$code] = !$is_ie8 && !$is_ie9 && get_option('simplified_attachments_ui') == '1' && get_option('complex_uploader') == '1' && has_js(); return $BROWSER_MATCHES_CACHE[$code]; case 'itunes': $BROWSER_MATCHES_CACHE[$code] = (get_param_integer('itunes', 0) == 1) || (strpos($browser, 'itunes') !== false); return $BROWSER_MATCHES_CACHE[$code]; case 'bot': $BROWSER_MATCHES_CACHE[$code] = (get_bot_type() !== null); return $BROWSER_MATCHES_CACHE[$code]; case 'android': $BROWSER_MATCHES_CACHE[$code] = strpos($browser, 'android') !== false; return $BROWSER_MATCHES_CACHE[$code]; case 'ios': $BROWSER_MATCHES_CACHE[$code] = strpos($browser, 'iphone') !== false || strpos($browser, 'ipad') !== false; return $BROWSER_MATCHES_CACHE[$code]; case 'wysiwyg': if ((get_option('wysiwyg') == '0') || ((is_mobile()) && ((is_null($comcode)) || (strpos($comcode, 'html]') === false)))) { $BROWSER_MATCHES_CACHE[$code] = false; return false; } $BROWSER_MATCHES_CACHE[$code] = (strpos($browser, 'android') === false); // Using CKEditor, which does not yet support Android return $BROWSER_MATCHES_CACHE[$code]; case 'windows': $BROWSER_MATCHES_CACHE[$code] = (strpos($os, 'windows') !== false) || (strpos($os, 'win32') !== false); return $BROWSER_MATCHES_CACHE[$code]; case 'mac': $BROWSER_MATCHES_CACHE[$code] = strpos($os, 'mac') !== false; return $BROWSER_MATCHES_CACHE[$code]; case 'linux': $BROWSER_MATCHES_CACHE[$code] = strpos($os, 'linux') !== false; return $BROWSER_MATCHES_CACHE[$code]; case 'odd_os': $BROWSER_MATCHES_CACHE[$code] = (strpos($os, 'windows') === false) && (strpos($os, 'mac') === false) && (strpos($os, 'linux') === false); return $BROWSER_MATCHES_CACHE[$code]; case 'mobile': $BROWSER_MATCHES_CACHE[$code] = is_mobile(); return $BROWSER_MATCHES_CACHE[$code]; case 'ie': $BROWSER_MATCHES_CACHE[$code] = $is_ie; return $BROWSER_MATCHES_CACHE[$code]; case 'ie8': $BROWSER_MATCHES_CACHE[$code] = $is_ie8; return $BROWSER_MATCHES_CACHE[$code]; case 'ie8+': $BROWSER_MATCHES_CACHE[$code] = $is_ie8_plus; return $BROWSER_MATCHES_CACHE[$code]; case 'ie9': $BROWSER_MATCHES_CACHE[$code] = $is_ie9; return $BROWSER_MATCHES_CACHE[$code]; case 'ie9+': $BROWSER_MATCHES_CACHE[$code] = $is_ie9_plus; return $BROWSER_MATCHES_CACHE[$code]; case 'chrome': $BROWSER_MATCHES_CACHE[$code] = $is_chrome; return $BROWSER_MATCHES_CACHE[$code]; case 'gecko': $BROWSER_MATCHES_CACHE[$code] = $is_gecko; return $BROWSER_MATCHES_CACHE[$code]; case 'safari': $BROWSER_MATCHES_CACHE[$code] = $is_safari; return $BROWSER_MATCHES_CACHE[$code]; case 'odd_browser': $BROWSER_MATCHES_CACHE[$code] = !$is_safari && !$is_gecko && !$is_ie; return $BROWSER_MATCHES_CACHE[$code]; } // Should never get here return false; } /** * Look at the user's browser, and decide if they are viewing on a mobile device or not. * * @param ?string $user_agent The user agent (null: get from environment, current user's browser) * @param boolean $truth Whether to always tell the truth (even if the current page does not have mobile support) * @return boolean Whether the user is using a mobile device */ function is_mobile($user_agent = null, $truth = false) { $user_agent_given = ($user_agent !== null); global $IS_MOBILE_CACHE, $IS_MOBILE_TRUTH_CACHE; if (!$user_agent_given) { if (($truth ? $IS_MOBILE_TRUTH_CACHE : $IS_MOBILE_CACHE) !== null) { return $truth ? $IS_MOBILE_TRUTH_CACHE : $IS_MOBILE_CACHE; } } if ((!function_exists('get_option')) || (get_option('mobile_support') == '0')) { if (function_exists('get_option')) { $IS_MOBILE_CACHE = false; $IS_MOBILE_TRUTH_CACHE = false; } return false; } if ($user_agent === null) { $user_agent = cms_srv('HTTP_USER_AGENT'); } global $SITE_INFO; if (((!isset($SITE_INFO['assume_full_mobile_support'])) || ($SITE_INFO['assume_full_mobile_support'] != '1')) && (isset($GLOBALS['FORUM_DRIVER'])) && (!$truth) && (running_script('index')) && (($theme = $GLOBALS['FORUM_DRIVER']->get_theme()) != 'default')) { $ini_path = (($theme == 'default' || $theme == 'admin') ? get_file_base() : get_custom_file_base()) . '/themes/' . $theme . '/theme.ini'; if (is_file($ini_path)) { $page = get_param_string('page', ''); // We intentionally do not use get_page_name, as that requires URL Monikers to work, which are not available early in boot (as needed by static cache) require_code('files'); $details = better_parse_ini_file($ini_path); if (!empty($details['mobile_pages'])) { if (substr($details['mobile_pages'], 0, 1) == '#' && substr($details['mobile_pages'], -1) == '#') { if (preg_match($details['mobile_pages'], get_zone_name() . ':' . $page) == 0) { $IS_MOBILE_CACHE = false; return false; } } else { if (preg_match('#(^|,)\s*' . preg_quote($page, '#') . '\s*(,|$)#', $details['mobile_pages']) == 0 && preg_match('#(^|,)\s*' . preg_quote(get_zone_name() . ':' . $page, '#') . '\s*(,|$)#', $details['mobile_pages']) == 0) { $IS_MOBILE_CACHE = false; return false; } } } } } if (!$user_agent_given && !$truth) { $val = get_param_integer('keep_mobile', null); if ($val !== null) { $result = ($val == 1); if (isset($GLOBALS['FORUM_DRIVER'])) { if ($truth) { $IS_MOBILE_TRUTH_CACHE = $result; } else { $IS_MOBILE_CACHE = $result; } } return $result; } } // The set of browsers (also change in static_cache.php) $browsers = array( // Implication by technology claims 'WML', 'WAP', 'Wap', 'MIDP', // Mobile Information Device Profile // Generics 'Mobile', 'Smartphone', 'WebTV', // Well known/important browsers/brands 'Mobile Safari', // Usually Android 'Android', 'iPhone', 'iPod', 'Opera Mobi', 'Opera Mini', 'BlackBerry', 'Windows Phone', 'nook browser', // Barnes and Noble ); $exceptions = array( 'iPad', ); if (((!isset($SITE_INFO['no_extra_mobiles'])) || ($SITE_INFO['no_extra_mobiles'] != '1')) && (is_file(get_file_base() . '/text_custom/mobile_devices.txt'))) { require_code('files'); $mobile_devices = better_parse_ini_file((get_file_base() . '/text_custom/mobile_devices.txt')); foreach ($mobile_devices as $key => $val) { if ($val == 1) { $browsers[] = $key; } else { $exceptions[] = $key; } } } // The test $result = (preg_match('/(' . implode('|', $browsers) . ')/i', $user_agent) != 0) && (preg_match('/(' . implode('|', $exceptions) . ')/i', $user_agent) == 0); if (!$user_agent_given) { if (isset($GLOBALS['FORUM_DRIVER'])) { if ($truth) { $IS_MOBILE_TRUTH_CACHE = $result; } else { $IS_MOBILE_CACHE = $result; } } } return $result; } /** * Get the name of a webcrawler bot, or null if no bot detected * * @param ?string $agent User agent (null: read from environment) * @return ?string Webcrawling bot name (null: not a bot) */ function get_bot_type($agent = null) { $agent_given = ($agent !== null); if (!$agent_given) { global $BOT_TYPE_CACHE; if ($BOT_TYPE_CACHE !== false) { return $BOT_TYPE_CACHE; } $agent = cms_srv('HTTP_USER_AGENT'); } if (strpos($agent, 'WebKit') !== false || strpos($agent, 'Trident') !== false || strpos($agent, 'MSIE') !== false || strpos($agent, 'Firefox') !== false || strpos($agent, 'Opera') !== false) { if (strpos($agent, 'bot') === false) { // Quick exit path if (!$agent_given) { $BOT_TYPE_CACHE = null; } return null; } } $agent = strtolower($agent); global $BOT_MAP_CACHE, $SITE_INFO; if ($BOT_MAP_CACHE === null) { if (((!isset($SITE_INFO['no_extra_bots'])) || ($SITE_INFO['no_extra_bots'] != '1')) && (is_file(get_file_base() . '/text_custom/bots.txt'))) { require_code('files'); $BOT_MAP_CACHE = better_parse_ini_file(get_file_base() . '/text_custom/bots.txt'); } else { $BOT_MAP_CACHE = array( 'zyborg' => 'Looksmart', 'googlebot' => 'Google', 'mediapartners-google' => 'Google Adsense', 'teoma' => 'Teoma', 'jeeves' => 'Ask Jeeves', 'ultraseek' => 'Infoseek', 'ia_archiver' => 'Alexa/Archive.org', 'msnbot' => 'Bing', 'bingbot' => 'Bing', 'mantraagent' => 'LookSmart', 'wisenutbot' => 'Looksmart', 'paros' => 'Paros', 'sqworm' => 'Aol.com', 'baidu' => 'Baidu', 'facebookexternalhit' => 'Facebook', 'yandex'=> 'Yandex', 'daum' => 'Daum', 'ahrefsbot' => 'Ahrefs', 'mj12bot' => 'Majestic-12', 'blexbot' => 'webmeup', 'duckduckbot' => 'DuckDuckGo', ); } } foreach ($BOT_MAP_CACHE as $id => $name) { if ($name == '') { continue; } if (strpos($agent, $id) !== false) { if (!$agent_given) { $BOT_TYPE_CACHE = $name; } return $name; } } if ((strpos($agent, 'bot') !== false) || (strpos($agent, 'spider') !== false)) { $to_a = strpos($agent, ' '); if ($to_a === false) { $to_a = strlen($agent); } $to_b = strpos($agent, '/'); if ($to_b === false) { $to_b = strlen($agent); } $name = substr($agent, 0, min($to_a, $to_b)); if (!$agent_given) { $BOT_TYPE_CACHE = $name; } return $name; } if (!$agent_given) { $BOT_TYPE_CACHE = null; } return null; } /** * Determine whether the user's browser supports cookies or not. * Unfortunately this function will only return true once a user has been to the site more than once... Composr will set a cookie, and if it perseveres, that indicates cookies work. * * @return boolean Whether the user has definitely got cookies */ function has_cookies() // Will fail on users first visit, but then will catch on { global $HAS_COOKIES_CACHE; if ($HAS_COOKIES_CACHE !== null) { return $HAS_COOKIES_CACHE; } /*if (($GLOBALS['DEV_MODE']) && (get_param_integer('keep_debug_has_cookies', 0) == 0) && (!running_script('commandr'))) We know this works by now, was tested for years. Causes annoyance when developing { $_COOKIE = array(); return false; }*/ if (isset($_COOKIE['has_cookies'])) { $HAS_COOKIES_CACHE = true; return true; } require_code('users_active_actions'); cms_setcookie('has_cookies', '1'); $HAS_COOKIES_CACHE = false; return false; } /** * Determine whether the user's browser supports JavaScript or not. * Unfortunately this function will only return true once a user has been to the site more than once... JavaScript will set a cookie, indicating it works. * * @return boolean Whether the user has definitely got JavaScript */ function has_js() { if (!function_exists('get_option')) { return true; } if (get_option('detect_javascript') == '0') { return true; } if (get_param_integer('keep_has_js', 0) == 1) { return true; } if (get_param_integer('keep_has_js', null) === 0) { return false; } return ((array_key_exists('js_on', $_COOKIE)) && ($_COOKIE['js_on'] == '1')); } /** * Turn an array into a humanely readable string. * * @param array $array Array to convert * @param boolean $already_stripped Whether PHP magic-quotes have already been cleaned out for the array * @return string A humanely readable version of the array. */ function flatten_slashed_array($array, $already_stripped = false) { $ret = ''; foreach ($array as $key => $val) { if (is_array($val)) { $val = flatten_slashed_array($val); } if (!$already_stripped && get_magic_quotes_gpc()) { $val = stripslashes($val); } $ret .= '<param>' . (is_integer($key) ? strval($key) : $key) . '=' . $val . '</param>' . "\n"; // $key may be integer, due to recursion line for list fields, above } return $ret; } /** * Get a word-filtered version of the specified text. * * @param string $text Text to filter * @return string Filtered version of the input text */ function wordfilter_text($text) { if (!addon_installed('wordfilter')) { return $text; } require_code('wordfilter'); return check_wordfilter($text, null, true); } /** * Assign this to explicitly declare that a variable may be of mixed type, and initialise to null. * * @return ?mixed Of mixed type (null: default) */ function mixed() { return null; } /** * Get meta information for specified resource * * @param ID_TEXT $type The type of resource (e.g. download) * @param ID_TEXT $id The ID of the resource * @return array A pair: The first element is the meta keyword string for the specified resource, and the other is the meta description string. */ function seo_meta_get_for($type, $id) { $cache = function_exists('persistent_cache_get') ? persistent_cache_get(array('seo', $type, $id)) : null; if ($cache !== null) { return $cache; } $where = array('meta_for_type' => $type, 'meta_for_id' => $id); $cache = array('', ''); $rows = $GLOBALS['SITE_DB']->query_select('seo_meta_keywords', array('meta_keyword'), $where, 'ORDER BY id'); foreach ($rows as $row) { if ($cache[0] != '') { $cache[0] .= ','; } $cache[0] .= get_translated_text($row['meta_keyword']); } $rows = $GLOBALS['SITE_DB']->query_select('seo_meta', array('meta_description'), $where, '', 1); if (array_key_exists(0, $rows)) { $cache[1] = get_translated_text($rows[0]['meta_description']); } persistent_cache_set(array('seo', $type, $id), $cache); return $cache; } /** * Load the specified resource's meta information into the system for use on this page. * Also, if the title is specified then this is used for the page title. * * @sets_output_state * * @param ID_TEXT $type The type of resource (e.g. download) * @param ID_TEXT $id The ID of the resource * @param ?string $title The page-specific title to use, in Comcode or plain-text format with possible HTML entities included [Comcode will later be stripped] (null: none) */ function seo_meta_load_for($type, $id, $title = null) { if (!$GLOBALS['IS_VIRTUALISED_REQUEST']) { $result = seo_meta_get_for($type, $id); global $SEO_KEYWORDS, $SEO_DESCRIPTION, $SHORT_TITLE; if ($result[0] != '') { $SEO_KEYWORDS = array_map('trim', explode(',', trim($result[0], ','))); } if ($result[1] != '') { $SEO_DESCRIPTION = $result[1]; } if ($title !== null) { set_short_title(str_replace('–', '-', str_replace('©', '(c)', str_replace(''', '\'', $title)))); } } // Otherwise don't bother (this is an optimisation) } /** * Get Tempcode for tags, based on loaded up from SEO keywords (seo_meta_load_for). * * @param ?ID_TEXT $limit_to The search code for this tag content (e.g. downloads) (null: there is none) * @param ?array $the_tags Explicitly pass a list of tags instead (null: use loaded ones) * @return Tempcode Loaded tag output (or blank if there are none) */ function get_loaded_tags($limit_to = null, $the_tags = null) { if (get_value('no_tags') === '1') { return new Tempcode(); } if (!addon_installed('search')) { return new Tempcode(); } if ($the_tags === null) { global $SEO_KEYWORDS; $the_tags = $SEO_KEYWORDS; } $tags = array(); if ($the_tags !== null) { $search_limiter_no = array('all_defaults' => '1'); if ($limit_to !== null) { $search_limiter_no['search_' . $limit_to] = '1'; $search_limiter_no['all_defaults'] = '0'; } if ($limit_to !== null) { $search_limiter_yes = array(); $search_limiter_yes['search_' . $limit_to] = '1'; $search_limiter_yes['all_defaults'] = '0'; } else { $search_limiter_yes = $search_limiter_no; } foreach ($the_tags as $tag) { $tag = trim($tag); if ($tag == '') { continue; } $tags[] = array( 'TAG' => $tag, 'LINK_LIMITEDSCOPE' => build_url(array('page' => 'search', 'type' => 'results', 'content' => '"' . $tag . '"', 'only_search_meta' => '1') + $search_limiter_yes, get_module_zone('search')), 'LINK_FULLSCOPE' => build_url(array('page' => 'search', 'type' => 'results', 'content' => '"' . $tag . '"', 'only_search_meta' => '1') + $search_limiter_no, get_module_zone('search')), ); } } return do_template('TAGS', array('_GUID' => '2cd542a245bc7d1c3f10e858e8fc5159', 'TAGS' => $tags, 'TYPE' => ($limit_to === null) ? '' : $limit_to)); } /** * Get the default page for a zone. * * @param ID_TEXT $zone_name Zone name * @return ID_TEXT Default page */ function get_zone_default_page($zone_name) { if ($zone_name == '_SELF') { $zone_name = get_zone_name(); } /*$p_test = function_exists('persistent_cache_get') ? persistent_cache_get(array('ZONE', $zone_name)) : null; Better to get from ALL_ZONES_TITLED, less cache volume if ($p_test !== null) { return $p_test['zone_default_page']; }*/ global $ZONE; if (($ZONE['zone_name'] == $zone_name) && ($ZONE['zone_default_page'] !== null)) { return $ZONE['zone_default_page']; } else { global $ZONE_DEFAULT_PAGES_CACHE; if (!isset($ZONE_DEFAULT_PAGES_CACHE[$zone_name])) { $_zone_default_page = null; if (function_exists('persistent_cache_get')) { $temp = persistent_cache_get('ALL_ZONES_TITLED'); if ($temp !== null) { $_zone_default_page = array(); foreach ($temp as $_temp) { list($_zone_name, , $zone_default_page) = $_temp; $_zone_default_page[] = array('zone_name' => $_zone_name, 'zone_default_page' => $zone_default_page); } } } if ($_zone_default_page === null) { $_zone_default_page = $GLOBALS['SITE_DB']->query_select('zones', array('zone_name', 'zone_default_page'), null/*Load multiple so we can cache for performance array('zone_name' => $zone_name)*/, 'ORDER BY zone_title', 50/*reasonable limit; zone_title is sequential for default zones*/); } $ZONE_DEFAULT_PAGES_CACHE[$zone_name] = 'start'; $ZONE_DEFAULT_PAGES_CACHE['collaboration'] = 'start'; // Set this in case collaboration zone removed but still referenced. Performance tweak! foreach ($_zone_default_page as $zone_row) { $ZONE_DEFAULT_PAGES_CACHE[$zone_row['zone_name']] = $zone_row['zone_default_page']; } } return $ZONE_DEFAULT_PAGES_CACHE[$zone_name]; } } /** * Turn a boring codename, into a "pretty" title. * * @param ID_TEXT $boring The codename * @return string The title */ function titleify($boring) { $ret = $boring; if (strpos($ret, '/') !== false || strpos($ret, '\\') !== false) { $ret = preg_replace('#([/\\\\])#', '${1} ', $ret); } $ret = ucwords(trim(str_replace('_', ' ', $boring))); $acronyms = array( 'CMS', 'CNS', 'URL', 'ID', 'UI', 'HTML', 'MSN', 'LDAP', 'SMS', 'SSL', 'XML', 'HPHP', 'CSS', 'SEO', 'JavaScript', ); foreach ($acronyms as $acronym) { if (stripos($ret, $acronym) !== false) { $ret = cms_preg_replace_safe('#(^|\s)' . preg_quote($acronym, '#') . '(\s|$)#i', '$1' . $acronym . '$2', $ret); } } if (strpos($ret, 'Ecommerce') !== false) { $ret = str_replace('Ecommerce', addon_installed('ecommerce') ? do_lang('ecommerce:ECOMMERCE') : 'eCommerce', $ret); } if (strpos($ret, 'Cpfs') !== false) { $ret = str_replace('Cpfs', do_lang('cns:CUSTOM_PROFILE_FIELDS'), $ret); } if (strpos($ret, 'Captcha') !== false) { $ret = str_replace('Captcha', addon_installed('captcha') ? do_lang('captcha:CAPTCHA') : 'CAPTCHA', $ret); } $ret = str_replace('Adminzone', do_lang('ADMIN_ZONE'), $ret); $ret = str_replace('Emails', do_lang('EMAILS'), $ret); $ret = str_replace('Phpinfo', 'PHP-Info', $ret); $ret = str_replace('CNS', 'Conversr', $ret); if (strpos($ret, 'Default Set') !== false) { $ret = str_replace('Default Set/cartoons', do_lang('cns:AVATARS_CARTOONS'), $ret); $ret = str_replace('Default Set/thematic', do_lang('cns:AVATARS_THEMATIC'), $ret); $ret = str_replace('Default Set', do_lang('cns:AVATARS_MISC'), $ret); } if ($GLOBALS['XSS_DETECT'] && ocp_is_escaped($boring)) { ocp_mark_as_escaped($ret); } return $ret; } /** * Propagate Filtercode through links. * * @param ID_TEXT $prefix Prefix for main filter environment variable * @return array Extra URL mappings */ function propagate_filtercode($prefix = '') { $active_filter = either_param_string(($prefix == '') ? 'active_filter' : ($prefix . '_active_filter'), ''); $map = array(); if ($active_filter != '') { $map['active_filter'] = $active_filter; foreach (array_keys($_GET + $_POST) as $key) { if (substr($key, 0, 7) == 'filter_') { $map[$key] = either_param_string($key, ''); } } } return $map; } /** * Propagate Filtercode through page-links. * * @return string Extra page-link mappings */ function propagate_filtercode_page_link() { $map = propagate_filtercode(); $_map = ''; foreach ($map as $key => $val) { $_map .= ':' . $key . '=' . urlencode($val); } return $_map; } /** * Make some text fractionably editable (i.e. inline editable). * * @param ID_TEXT $content_type Content type * @param mixed $id Content ID * @param mixed $title Content title (either unescaped string, or Compiled Comcode [i.e. Tempcode]) * @return Tempcode Inline editable HTML to put into output */ function make_fractionable_editable($content_type, $id, $title) { require_code('content'); $ob = get_content_object($content_type); $info = $ob->info(); $parameters = array( is_object($title) ? $title->evaluate() : $title, array_key_exists('edit_page_link_field', $info) ? $info['edit_page_link_field'] : preg_replace('#^\w\w?_#', '', array_key_exists('title_field_post', $info) ? $info['title_field_post'] : $info['title_field']), array_key_exists('edit_page_link_pattern_post', $info) ? str_replace('_WILD', is_integer($id) ? strval($id) : $id, $info['edit_page_link_pattern_post']) : preg_replace('#:_(.*)#', ':__${1}', str_replace('_WILD', is_integer($id) ? strval($id) : $id, $info['edit_page_link_pattern'])), (array_key_exists('title_field_supports_comcode', $info) && $info['title_field_supports_comcode']) ? '1' : '0', ); return directive_tempcode('FRACTIONAL_EDITABLE', is_object($title) ? $title : escape_html($title), $parameters); } /** * Find whether a fractional edit is underway. * * @return boolean Whether a fractional edit is underway */ function fractional_edit() { return post_param_integer('fractional_edit', 0) == 1; } /** * Convert some HTML to plain text. * * @param string $in HTML * @return string Plain text */ function strip_html($in) { if ((strpos($in, '<') === false) && (strpos($in, '&') === false)) { return $in; // Optimisation } $search = array( '#<script[^>]*?' . '>.*?</script>#si', // Strip out JavaScript '#<style[^>]*?' . '>.*?</style>#siU', // Strip style tags properly '#<![\s\S]*?--[ \t\n\r]*>#', // Strip multi-line comments including CDATA ); $in = preg_replace($search, '', $in); if (get_charset() != 'utf-8') { $in = str_replace(array('–', '—', '·', '“', '”', '‘', '’'), array('-', '-', '|', '"', '"', "'", "'"), $in); } $in = str_replace('><', '> <', $in); $in = strip_tags($in); return @html_entity_decode($in, ENT_QUOTES, get_charset()); } /** * Find the base URL for documentation. * * @return URLPATH The base URL for documentation */ function get_brand_base_url() { $value = function_exists('get_value') ? get_value('rebrand_base_url') : null; if (($value === null) || ($value == '')) { $value = 'http://compo.sr'; } return $value; } /** * Get a URL to a Composr tutorial. * * @param ?ID_TEXT $tutorial Name of a tutorial (null: don't include the page part) * @return URLPATH URL to a tutorial */ function get_tutorial_url($tutorial) { $ret = get_brand_page_url(array('page' => is_null($tutorial) ? 'abcdef' : $tutorial), 'docs' . strval(cms_version())); if (is_null($tutorial)) { $ret = str_replace('abcdef.htm', '', $ret); } return $ret; } /** * Get a URL to a compo.sr page. * * @param array $params URL map * @param ID_TEXT $zone Zone * @return URLPATH URL to page */ function get_brand_page_url($params, $zone) { // Assumes brand site supports .htm URLs, which it should return get_brand_base_url() . (($zone == '') ? '' : '/') . $zone . '/' . urlencode(str_replace('_', '-', $params['page'])) . '.htm'; } /** * Get the brand name. * * @return string The brand name */ function brand_name() { $value = function_exists('get_value') ? get_value('rebrand_name') : null; if ($value === null) { $value = 'Composr'; } return $value; } /** * Find if we're on an Conversr satellite site. * * @return boolean If we are */ function is_cns_satellite_site() { if (get_forum_type() != 'cns') { return false; } return (isset($GLOBALS['FORUM_DB'])) && ((get_db_site() != get_db_forums()) || (get_db_site_host() != get_db_forums_host()) || (get_db_site_user() != get_db_forums_user())); } /** * Convert GUIDs to IDs in some text. * * @param string $text Input text * @return string Output text */ function convert_guids_to_ids($text) { if (!addon_installed('commandr')) { return $text; } $matches = array(); $num_matches = preg_match_all('#^{?([0-9a-fA-F]){8}(-([0-9a-fA-F]){4}){3}-([0-9a-fA-F]){12}}?$#', $text, $matches); if ($num_matches != 0) { require_code('resource_fs'); $guids = array(); for ($i = 0; $i < $num_matches; $i++) { $guids[] = $matches[0][$i]; } $mappings = find_ids_via_guids($guids); foreach ($mappings as $guid => $id) { $text = str_replace($guid, $id, $text); } } return $text; } /** * Set if a mass-import is in progress. * * @param boolean $doing_mass_import If it is */ function set_mass_import_mode($doing_mass_import = true) { global $MASS_IMPORT_HAPPENING; $MASS_IMPORT_HAPPENING = $doing_mass_import; } /** * Find if a mass-import is in progress. * * @return boolean If it is */ function get_mass_import_mode() { global $MASS_IMPORT_HAPPENING; return $MASS_IMPORT_HAPPENING; } /** * Prepare an argument for use literally in a command. Works around common PHP restrictions. * * @param string $arg The argument. * @return string Escaped. */ function escapeshellarg_wrap($arg) { if (php_function_allowed('escapeshellarg')) { return escapeshellarg($arg); } return "'" . addslashes(str_replace(array(chr(0), "'"), array('', "'\"'\"'"), $arg)) . "'"; } /** * Find whether Composr is running on a local network, rather than a live-site. * * @return boolean If it is running locally */ function running_locally() { return (substr(cms_srv('HTTP_HOST'), 0, 8) == '192.168.') || (substr(cms_srv('HTTP_HOST'), 0, 7) == '10.0.0.') || (in_array(cms_srv('HTTP_HOST'), array('localhost'))); } /** * Exit if we are running on a Google App Engine application (live or development). */ function appengine_general_guard() { if (GOOGLE_APPENGINE) { warn_exit(do_lang_tempcode('NOT_ON_GOOGLE_APPENGINE')); } } /** * Exit if we are running on a live Google App Engine application. */ function appengine_live_guard() { if (appengine_is_live()) { warn_exit(do_lang_tempcode('NOT_ON_LIVE_GOOGLE_APPENGINE')); } } /** * Check serialized data for objects, as a security measure. * * @param string $data &$data Serialized data * @param ?mixed $safe_replacement What to substitute if objects are contained (null: substitute null) */ function secure_serialized_data(&$data, $safe_replacement = null) { // Security check, unserialize can result in unchecked magic method invocation on defined objects // Would be a vulnerability if there's a defined class where such method invocation has dangerous side-effects $matches = array(); $num_matches = preg_match_all('#(^|;)O:[\d\+\-\.]+:"([^"]+)"#', $data, $matches); for ($i = 0; $i < $num_matches; $i++) { $harsh = true; // Could be turned into a method parameter later, if needed if ($harsh) { $bad_methods = array( '__.*', 'code_to_preexecute', ); } else { $bad_methods = array( '__sleep', '__wakeup', '__destruct', '__toString', '__set_state', '__isset', '__get', '__set', '__call', '__callStatic', 'code_to_preexecute', ); } $class_name = $matches[2][$i]; $methods = get_class_methods($class_name); foreach ($bad_methods as $bad_method) { foreach ($methods as $method) { if (preg_match('#^' . $bad_method . '$#', $method) != 0) { $data = serialize($safe_replacement); return; } } } } } /** * Creates a PHP value from a stored representation. * Wraps the fact that new versions of PHP have better security, but old ones won't let you pass the extra parameter. * * @param string $data Serialized string. * @return ~mixed What was originally serialised (false: bad data given, or actually false was serialized). */ function cms_unserialize($data) { if (version_compare(PHP_VERSION, '7.0.0') >= 0) { return unserialize($data, array('allowed_classes' => false)); } return unserialize($data); } /** * Update a catalogue content field reference, to a new value. * * @param ID_TEXT $type Content type * @param ID_TEXT $from Old value * @param ID_TEXT $to New value */ function update_catalogue_content_ref($type, $from, $to) { if (strpos(get_db_type(), 'mysql') !== false) { $GLOBALS['SITE_DB']->query_update('catalogue_fields f JOIN ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'catalogue_efv_short v ON v.cf_id=f.id', array('cv_value' => $to), array('cv_value' => $from, 'cf_type' => $type)); } else { $fields = $GLOBALS['SITE_DB']->query_select('catalogue_fields', array('id'), array('cf_type' => $type)); foreach ($fields as $field) { $GLOBALS['SITE_DB']->query_update('catalogue_efv_short', array('cv_value' => $to), array('cv_value' => $from, 'cf_id' => $field['id'])); } } } /** * Start a profiling block, for a specified identifier (of your own choosing). * * @param ID_TEXT $identifier Identifier */ function cms_profile_start_for($identifier) { require_code('profiler'); _cms_profile_start_for($identifier); } /** * End a profiling block, for a specified identifier (of your own choosing - but you must have started it with cms_profile_start_for). * * @param ID_TEXT $identifier Identifier * @param ?string $specifics Longer details of what happened (e.g. a specific SQL query that ran) (null: none provided) */ function cms_profile_end_for($identifier, $specifics = null) { require_code('profiler'); _cms_profile_end_for($identifier, $specifics); } /** * Put out some benign HTTP output. * FastCGI seems to have a weird issue with 'slowish spiky process not continuing with output' - this works around it. Not ideal as would break headers in any subsequent code. */ function send_http_output_ping() { global $DOING_OUTPUT_PINGS; $DOING_OUTPUT_PINGS = true; if (running_script('index')) { if (!headers_sent()) { safe_ini_set('zlib.output_compression', 'Off'); // Otherwise it can compress all the spaces to nothing cms_ob_end_clean(); // Otherwise flushing won't help } echo ' '; flush(); } } /** * Improve security by turning on a strict CSP that only allows stuff from partner sites and disables frames and forms. * Must be called before page output starts. * * @param ?MEMBER $enable_more_open_html_for Allow more open HTML for a particular member ID (null: no member). It still will use the HTML blacklist functionality (unless they have even higher access already), but will remove the more restrictive whitelist functionality. Use of set_high_security_csp here is further decreasing the risk from dangerous HTML, even though the risk should be very low anyway due to the blacklist filter. */ function set_high_security_csp($enable_more_open_html_for = null) { require_code('input_filter'); $_partners = get_allowed_partner_sites(); if ($_partners == array()) { $partners = ''; } else { $partners = ' ' . implode(' ', $_partners); $partners .= ' https://' . implode(' https://', $_partners); $partners .= ' http://' . implode(' http://', $_partners); } $value = ""; $value .= "script-src 'self'{$partners}; "; // browser will check mime-type, so okay for self $value .= "style-src 'self'{$partners}; "; // browser will check mime-type, so okay for self $value .= "object-src 'none'; "; // browser may not check mime-type, so none $value .= "frame-src 'none'; child-src 'none'; "; $value .= "form-action 'self'; "; $value .= "base-uri 'self'; "; $value .= "frame-ancestors 'self'{$partners}; "; header('Content-Security-Policy:' . trim($value)); if ($enable_more_open_html_for !== null) { global $PRIVILEGE_CACHE; has_privilege($enable_more_open_html_for, 'allow_html'); // Force loading, so we can amend the cached value cleanly $PRIVILEGE_CACHE[$enable_more_open_html_for]['allow_html'][''][''][''] = 1; } } /** * Set a CSP header to not allow any frames to include us. */ function set_no_clickjacking_csp() { require_code('input_filter'); $_partners = get_allowed_partner_sites(); if ($_partners == array()) { $partners = ''; } else { $partners = ' ' . implode(' ', $_partners); $partners .= ' https://' . implode(' https://', $_partners); $partners .= ' http://' . implode(' http://', $_partners); } $value = ""; $value .= "frame-ancestors 'self'{$partners}; "; @header('Content-Security-Policy:' . trim($value)); } /** * Stop the web browser trying to save us, and breaking some requests in the process. */ function disable_browser_xss_detection() { @header('X-XSS-Protection: 0'); } /** * Whether smart decaching is enabled. It is slightly inefficient but makes site development easier for people. * * @param boolean $support_temporary_disable Support it being temporarily disabled * @return boolean If smart decaching is enabled */ function support_smart_decaching($support_temporary_disable = false) { if ($support_temporary_disable) { global $DISABLE_SMART_DECACHING_TEMPORARILY; if ($DISABLE_SMART_DECACHING_TEMPORARILY) { return false; } } static $has_in_url = null; if ($has_in_url === null) { $has_in_url = (get_param_integer('keep_smart_decaching', 0) == 1); } if ($has_in_url) { return true; } global $SITE_INFO; if (!empty($SITE_INFO['disable_smart_decaching'])) { if ($SITE_INFO['disable_smart_decaching'] == '1') { return false; } static $has_temporary = null; if ($has_temporary === null) { $has_temporary = false; $matches = array(); if (preg_match('#^(\d+):(.*)$#', $SITE_INFO['disable_smart_decaching'], $matches) != 0) { $time = intval($matches[1]); $path = $matches[2]; if (is_file($path) && filemtime($path) > time() - $time) { $has_temporary = true; } } } return $has_temporary; } return true; // By default it is on } /** * For performance reasons disable smart decaching for cases that allow it to be disabled temporarily (it does a lot of file system checks). */ function disable_smart_decaching_temporarily() { global $DISABLE_SMART_DECACHING_TEMPORARILY; $DISABLE_SMART_DECACHING_TEMPORARILY = true; } /** * Find if the current request has POST fields worth considering/propagating. Very standard framework fields will be ignored. * * @return boolean Whether it does */ function has_interesting_post_fields() { $post = $_POST; $to_ignore = array( 'csrf_token', 'y' . md5(get_site_name() . ': antispam'), 'login_username', 'password', 'remember_me', 'login_invisible', 'redirect', 'redirect_passon', ); foreach ($to_ignore as $field) { unset($post[$field]); } return (count($post) !== 0); } /** * Apply escaping for an HTTP header. * * @param string $str Text to insert into header * @param boolean $within_quotes Text is between quotes * @return string Escaped text */ function escape_header($str, $within_quotes = false) { if ($within_quotes) { $str = addslashes($str); } return str_replace(array("\r", "\n"), array('', ''), $str); } /** * Find if a forum post is a spacer post. * * @param string $post The spacer post * @return array A pair: Whether it is, and the language it is in */ function is_spacer_post($post) { if (substr($post, 0, 10) == '[semihtml]') { $post = substr($post, 10); } $langs = find_all_langs(); foreach (array_keys($langs) as $lang) { $matcher = do_lang('SPACER_POST_MATCHER', null, null, null, $lang); if (substr($post, 0, strlen($matcher)) == $matcher) { return array(true, $lang); } } return array(false, get_site_default_lang()); } /** * Get the Internet host name corresponding to a given IP address. * * @param string $ip_address IP address * @return string Host name OR IP address if failed to look up */ function cms_gethostbyaddr($ip_address) { $hostname = ''; if ((php_function_allowed('shell_exec')) && (function_exists('get_value')) && (get_value('slow_php_dns') === '1')) { $hostname = trim(preg_replace('#^.* #', '', shell_exec('host ' . escapeshellarg_wrap($ip_address)))); } if ($hostname == '') { if (php_function_allowed('gethostbyaddr')) { $hostname = @gethostbyaddr($ip_address); } } if ($hostname == '') { $hostname = $ip_address; } return $hostname; } /** * Get the IP address corresponding to a given Internet host name. * * @param string $hostname Host name * @return string IP address OR host name if failed to look up */ function cms_gethostbyname($hostname) { $ip_address = ''; if ((php_function_allowed('shell_exec')) && (function_exists('get_value')) && (get_value('slow_php_dns') === '1')) { $ip_address = preg_replace('#^.*has address (\d+\.\d+\.\d+).*#s', '$1', shell_exec('host ' . escapeshellarg_wrap($hostname))); } if ($ip_address == '') { if (php_function_allowed('gethostbyaddr')) { $ip_address = @gethostbyaddr($ip_address); } } if ($ip_address == '') { $ip_address = $hostname; } return $ip_address; } /** * Unpack some bytes to an integer, so we can do some bitwise arithmetic on them. * Assumes unsigned, unless you request 4 bytes. * * @param string $str Input string * @param ?integer $bytes How many bytes to read (null: as many as there are in $str) * @set 1 2 4 * @param boolean $little_endian Whether to use little endian (Intel order) as opposed to big endian (network/natural order) * @return integer Read integer */ function cms_unpack_to_uinteger($str, $bytes = null, $little_endian = false) { if ($bytes === null) { $bytes = strlen($str); } switch ($bytes) { case 1: $result = unpack('C', $str); break; case 2: $result = unpack($little_endian ? 'v' : 'n', $str); break; case 4: $result = unpack($little_endian ? 'V' : 'N', $str); break; default: warn_exit(do_lang_tempcode('INTERNAL_ERROR')); } return $result[1]; } /** * Perform a regular expression match. * Automatically applies utf-8 if possible and appropriate. \s is not actually Unicode-safe, for example (as it matches non-breaking-spaces). * * @param string $pattern The pattern. * @param string $subject The subject string. * @param ?array $matches Where matches will be put (note that it is a list of maps, except the arrays are turned inside out) (null: do not store matches). Note that this is actually passed by reference, but is also optional. (null: don't gather) * @param integer $flags Either 0, or PREG_OFFSET_CAPTURE. * @param integer $offset Offset to start from. Usually use with 'A' modifier to anchor it (using '^' in the pattern will not work) * @return ~integer The number of matches (false: error). */ function cms_preg_match_safe($pattern, $subject, &$matches = null, $flags = 0, $offset = 0) { if (get_charset() == 'utf-8') { $result = @preg_match($pattern . 'u', $subject, $matches, $flags, $offset); if ($result !== false) { return $result; } } return preg_match($pattern, $subject, $matches, $flags, $offset); } /** * Array entries that match the pattern. * Automatically applies utf-8 if possible and appropriate. \s is not actually Unicode-safe, for example (as it matches non-breaking-spaces). * * @param string $pattern The pattern. * @param array $subject The subject strings. * @param integer $flags Either 0, or PREG_GREP_INVERT. * @return array Matches. */ function cms_preg_grep_safe($pattern, $subject, $flags = 0) { if (get_charset() == 'utf-8') { $result = @preg_grep($pattern . 'u', $subject, $flags); if ($result !== false) { return $result; } } return preg_grep($pattern, $subject, $flags); } /** * Perform a global regular expression match. * Automatically applies utf-8 if possible and appropriate. \s is not actually Unicode-safe, for example (as it matches non-breaking-spaces). * * @param string $pattern The pattern. * @param string $subject The subject string. * @param ?array $matches Where matches will be put (note that it is a list of maps, except the arrays are turned inside out). Note that this is actually passed by reference, but is also optional. (null: don't gather) * @param integer $flags Either 0, or PREG_OFFSET_CAPTURE. * @return ~integer The number of matches (false: error). */ function cms_preg_match_all_safe($pattern, $subject, &$matches, $flags = 0) { if (get_charset() == 'utf-8') { $result = @preg_match_all($pattern . 'u', $subject, $matches, $flags); if ($result !== false) { return $result; } } return preg_match_all($pattern, $subject, $matches, $flags); } /** * Perform a regular expression search and replace. * Automatically applies utf-8 if possible and appropriate. \s is not actually Unicode-safe, for example (as it matches non-breaking-spaces). * * @param mixed $pattern The pattern (string or array). * @param mixed $replacement The replacement string (string or array). * @param string $subject The subject string. * @param integer $limit The limit of replacements (-1: no limit). * @return ~string The string with replacements made (false: error). */ function cms_preg_replace_safe($pattern, $replacement, $subject, $limit = -1) { if (get_charset() == 'utf-8') { $result = @preg_replace($pattern . 'u', $replacement, $subject, $limit); if ($result !== false) { return $result; } } return preg_replace($pattern, $replacement, $subject, $limit); } /** * Perform a regular expression search and replace using a callback. * Automatically applies utf-8 if possible and appropriate. \s is not actually Unicode-safe, for example (as it matches non-breaking-spaces). * * @param string $pattern The pattern. * @param mixed $callback The callback. * @param string $subject The subject string. * @param integer $limit The limit of replacements (-1: no limit). * @return ~string The string with replacements made (false: error). */ function cms_preg_replace_callback_safe($pattern, $callback, $subject, $limit = -1) { if (get_charset() == 'utf-8') { $result = @preg_replace_callback($pattern . 'u', $callback, $subject, $limit); if ($result !== false) { return $result; } } return preg_replace_callback($pattern, $callback, $subject, $limit); } /** * Split string by a regular expression. * Automatically applies utf-8 if possible and appropriate. \s is not actually Unicode-safe, for example (as it matches non-breaking-spaces). * * @param string $pattern The pattern. * @param string $subject The subject. * @param ?integer $max_splits The maximum number of splits to make (null: no limit). * @param ?integer $mode The special mode (null: none). * @return array The array due to splitting. */ function cms_preg_split_safe($pattern, $subject, $max_splits = null, $mode = null) { if (get_charset() == 'utf-8') { $result = @preg_split($pattern . 'u', $subject, $max_splits, $mode); if ($result !== false) { return $result; } } return preg_split($pattern, $subject, $max_splits, $mode); }