$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. ).
*
* @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 .= '' . (is_integer($key) ? strval($key) : $key) . '=' . $val . '' . "\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(
'##si', // Strip out JavaScript
'##siU', // Strip style tags properly
'##', // 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);
}