time(), ); // #1 latest member ID, #2 the real name for a new registration. if (is_numeric($parameter1)) { $changes['latestMember'] = $parameter1; $changes['latestRealName'] = $parameter2; updateSettings(array('totalMembers' => true), true); } // We need to calculate the totals. else { // Update the latest activated member (highest id_member) and count. $result = $smcFunc['db_query']('', ' SELECT COUNT(*), MAX(id_member) FROM {db_prefix}members WHERE is_activated = {int:is_activated}', array( 'is_activated' => 1, ) ); list ($changes['totalMembers'], $changes['latestMember']) = $smcFunc['db_fetch_row']($result); $smcFunc['db_free_result']($result); // Get the latest activated member's display name. $result = $smcFunc['db_query']('', ' SELECT real_name FROM {db_prefix}members WHERE id_member = {int:id_member} LIMIT 1', array( 'id_member' => (int) $changes['latestMember'], ) ); list ($changes['latestRealName']) = $smcFunc['db_fetch_row']($result); $smcFunc['db_free_result']($result); // Update the amount of members awaiting approval $result = $smcFunc['db_query']('', ' SELECT COUNT(*) FROM {db_prefix}members WHERE is_activated IN ({array_int:activation_status})', array( 'activation_status' => array(3, 4, 5), ) ); list ($changes['unapprovedMembers']) = $smcFunc['db_fetch_row']($result); $smcFunc['db_free_result']($result); } updateSettings($changes); break; case 'message': if ($parameter1 === true && $parameter2 !== null) updateSettings(array('totalMessages' => true, 'maxMsgID' => $parameter2), true); else { // SUM and MAX on a smaller table is better for InnoDB tables. $result = $smcFunc['db_query']('', ' SELECT SUM(num_posts + unapproved_posts) AS total_messages, MAX(id_last_msg) AS max_msg_id FROM {db_prefix}boards WHERE redirect = {string:blank_redirect}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? ' AND id_board != {int:recycle_board}' : ''), array( 'recycle_board' => isset($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0, 'blank_redirect' => '', ) ); $row = $smcFunc['db_fetch_assoc']($result); $smcFunc['db_free_result']($result); updateSettings(array( 'totalMessages' => $row['total_messages'] === null ? 0 : $row['total_messages'], 'maxMsgID' => $row['max_msg_id'] === null ? 0 : $row['max_msg_id'] )); } break; case 'subject': // Remove the previous subject (if any). $smcFunc['db_query']('', ' DELETE FROM {db_prefix}log_search_subjects WHERE id_topic = {int:id_topic}', array( 'id_topic' => (int) $parameter1, ) ); // Insert the new subject. if ($parameter2 !== null) { $parameter1 = (int) $parameter1; $parameter2 = text2words($parameter2); $inserts = array(); foreach ($parameter2 as $word) $inserts[] = array($word, $parameter1); if (!empty($inserts)) $smcFunc['db_insert']('ignore', '{db_prefix}log_search_subjects', array('word' => 'string', 'id_topic' => 'int'), $inserts, array('word', 'id_topic') ); } break; case 'topic': if ($parameter1 === true) updateSettings(array('totalTopics' => true), true); else { // Get the number of topics - a SUM is better for InnoDB tables. // We also ignore the recycle bin here because there will probably be a bunch of one-post topics there. $result = $smcFunc['db_query']('', ' SELECT SUM(num_topics + unapproved_topics) AS total_topics FROM {db_prefix}boards' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? ' WHERE id_board != {int:recycle_board}' : ''), array( 'recycle_board' => !empty($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0, ) ); $row = $smcFunc['db_fetch_assoc']($result); $smcFunc['db_free_result']($result); updateSettings(array('totalTopics' => $row['total_topics'] === null ? 0 : $row['total_topics'])); } break; case 'postgroups': // Parameter two is the updated columns: we should check to see if we base groups off any of these. if ($parameter2 !== null && !in_array('posts', $parameter2)) return; $postgroups = cache_get_data('updateStats:postgroups', 360); if ($postgroups == null || $parameter1 == null) { // Fetch the postgroups! $request = $smcFunc['db_query']('', ' SELECT id_group, min_posts FROM {db_prefix}membergroups WHERE min_posts != {int:min_posts}', array( 'min_posts' => -1, ) ); $postgroups = array(); while ($row = $smcFunc['db_fetch_assoc']($request)) $postgroups[$row['id_group']] = $row['min_posts']; $smcFunc['db_free_result']($request); // Sort them this way because if it's done with MySQL it causes a filesort :(. arsort($postgroups); cache_put_data('updateStats:postgroups', $postgroups, 360); } // Oh great, they've screwed their post groups. if (empty($postgroups)) return; // Set all membergroups from most posts to least posts. $conditions = ''; $lastMin = 0; foreach ($postgroups as $id => $min_posts) { $conditions .= ' WHEN posts >= ' . $min_posts . (!empty($lastMin) ? ' AND posts <= ' . $lastMin : '') . ' THEN ' . $id; $lastMin = $min_posts; } // A big fat CASE WHEN... END is faster than a zillion UPDATE's ;). $smcFunc['db_query']('', ' UPDATE {db_prefix}members SET id_post_group = CASE ' . $conditions . ' ELSE 0 END' . ($parameter1 != null ? ' WHERE ' . (is_array($parameter1) ? 'id_member IN ({array_int:members})' : 'id_member = {int:members}') : ''), array( 'members' => $parameter1, ) ); break; default: loadLanguage('Errors'); trigger_error(sprintf($txt['invalid_statistic_type'], $type), E_USER_NOTICE); } } /** * Updates the columns in the members table. * Assumes the data has been htmlspecialchar'd. * this function should be used whenever member data needs to be * updated in place of an UPDATE query. * * id_member is either an int or an array of ints to be updated. * * data is an associative array of the columns to be updated and their respective values. * any string values updated should be quoted and slashed. * * the value of any column can be '+' or '-', which mean 'increment' * and decrement, respectively. * * if the member's post number is updated, updates their post groups. * * @param mixed $members An array of member IDs, the ID of a single member, or null to update this for all members * @param array $data The info to update for the members */ function updateMemberData($members, $data) { global $modSettings, $user_info, $smcFunc, $sourcedir, $cache_enable; // An empty array means there's nobody to update. if ($members === array()) return; $parameters = array(); if (is_array($members)) { $condition = 'id_member IN ({array_int:members})'; $parameters['members'] = $members; } elseif ($members === null) $condition = '1=1'; else { $condition = 'id_member = {int:member}'; $parameters['member'] = $members; } // Everything is assumed to be a string unless it's in the below. $knownInts = array( 'date_registered', 'posts', 'id_group', 'last_login', 'instant_messages', 'unread_messages', 'new_pm', 'pm_prefs', 'gender', 'show_online', 'pm_receive_from', 'alerts', 'id_theme', 'is_activated', 'id_msg_last_visit', 'id_post_group', 'total_time_logged_in', 'warning', ); $knownFloats = array( 'time_offset', ); if (!empty($modSettings['integrate_change_member_data'])) { // Only a few member variables are really interesting for integration. $integration_vars = array( 'member_name', 'real_name', 'email_address', 'id_group', 'gender', 'birthdate', 'website_title', 'website_url', 'location', 'time_format', 'timezone', 'time_offset', 'avatar', 'lngfile', ); $vars_to_integrate = array_intersect($integration_vars, array_keys($data)); // Only proceed if there are any variables left to call the integration function. if (count($vars_to_integrate) != 0) { // Fetch a list of member_names if necessary if ((!is_array($members) && $members === $user_info['id']) || (is_array($members) && count($members) == 1 && in_array($user_info['id'], $members))) $member_names = array($user_info['username']); else { $member_names = array(); $request = $smcFunc['db_query']('', ' SELECT member_name FROM {db_prefix}members WHERE ' . $condition, $parameters ); while ($row = $smcFunc['db_fetch_assoc']($request)) $member_names[] = $row['member_name']; $smcFunc['db_free_result']($request); } if (!empty($member_names)) foreach ($vars_to_integrate as $var) call_integration_hook('integrate_change_member_data', array($member_names, $var, &$data[$var], &$knownInts, &$knownFloats)); } } $setString = ''; foreach ($data as $var => $val) { switch ($var) { case 'birthdate': $type = 'date'; break; case 'member_ip': case 'member_ip2': $type = 'inet'; break; default: $type = 'string'; } if (in_array($var, $knownInts)) $type = 'int'; elseif (in_array($var, $knownFloats)) $type = 'float'; // Doing an increment? if ($var == 'alerts' && ($val === '+' || $val === '-')) { include_once($sourcedir . '/Profile-Modify.php'); if (is_array($members)) { $val = 'CASE '; foreach ($members as $k => $v) $val .= 'WHEN id_member = ' . $v . ' THEN '. alert_count($v, true) . ' '; $val = $val . ' END'; $type = 'raw'; } else $val = alert_count($members, true); } elseif ($type == 'int' && ($val === '+' || $val === '-')) { $val = $var . ' ' . $val . ' 1'; $type = 'raw'; } // Ensure posts, instant_messages, and unread_messages don't overflow or underflow. if (in_array($var, array('posts', 'instant_messages', 'unread_messages'))) { if (preg_match('~^' . $var . ' (\+ |- |\+ -)([\d]+)~', $val, $match)) { if ($match[1] != '+ ') $val = 'CASE WHEN ' . $var . ' <= ' . abs($match[2]) . ' THEN 0 ELSE ' . $val . ' END'; $type = 'raw'; } } $setString .= ' ' . $var . ' = {' . $type . ':p_' . $var . '},'; $parameters['p_' . $var] = $val; } $smcFunc['db_query']('', ' UPDATE {db_prefix}members SET' . substr($setString, 0, -1) . ' WHERE ' . $condition, $parameters ); updateStats('postgroups', $members, array_keys($data)); // Clear any caching? if (!empty($cache_enable) && $cache_enable >= 2 && !empty($members)) { if (!is_array($members)) $members = array($members); foreach ($members as $member) { if ($cache_enable >= 3) { cache_put_data('member_data-profile-' . $member, null, 120); cache_put_data('member_data-normal-' . $member, null, 120); cache_put_data('member_data-minimal-' . $member, null, 120); } cache_put_data('user_settings-' . $member, null, 60); } } } /** * Updates the settings table as well as $modSettings... only does one at a time if $update is true. * * - updates both the settings table and $modSettings array. * - all of changeArray's indexes and values are assumed to have escaped apostrophes (')! * - if a variable is already set to what you want to change it to, that * variable will be skipped over; it would be unnecessary to reset. * - When use_update is true, UPDATEs will be used instead of REPLACE. * - when use_update is true, the value can be true or false to increment * or decrement it, respectively. * * @param array $changeArray An array of info about what we're changing in 'setting' => 'value' format * @param bool $update Whether to use an UPDATE query instead of a REPLACE query */ function updateSettings($changeArray, $update = false) { global $modSettings, $smcFunc; if (empty($changeArray) || !is_array($changeArray)) return; $toRemove = array(); // Go check if there is any setting to be removed. foreach ($changeArray as $k => $v) if ($v === null) { // Found some, remove them from the original array and add them to ours. unset($changeArray[$k]); $toRemove[] = $k; } // Proceed with the deletion. if (!empty($toRemove)) $smcFunc['db_query']('', ' DELETE FROM {db_prefix}settings WHERE variable IN ({array_string:remove})', array( 'remove' => $toRemove, ) ); // In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs. if ($update) { foreach ($changeArray as $variable => $value) { $smcFunc['db_query']('', ' UPDATE {db_prefix}settings SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value} WHERE variable = {string:variable}', array( 'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value), 'variable' => $variable, ) ); $modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value); } // Clean out the cache and make sure the cobwebs are gone too. cache_put_data('modSettings', null, 90); return; } $replaceArray = array(); foreach ($changeArray as $variable => $value) { // Don't bother if it's already like that ;). if (isset($modSettings[$variable]) && $modSettings[$variable] == $value) continue; // If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it. elseif (!isset($modSettings[$variable]) && empty($value)) continue; $replaceArray[] = array($variable, $value); $modSettings[$variable] = $value; } if (empty($replaceArray)) return; $smcFunc['db_insert']('replace', '{db_prefix}settings', array('variable' => 'string-255', 'value' => 'string-65534'), $replaceArray, array('variable') ); // Kill the cache - it needs redoing now, but we won't bother ourselves with that here. cache_put_data('modSettings', null, 90); } /** * Constructs a page list. * * - builds the page list, e.g. 1 ... 6 7 [8] 9 10 ... 15. * - flexible_start causes it to use "url.page" instead of "url;start=page". * - very importantly, cleans up the start value passed, and forces it to * be a multiple of num_per_page. * - checks that start is not more than max_value. * - base_url should be the URL without any start parameter on it. * - uses the compactTopicPagesEnable and compactTopicPagesContiguous * settings to decide how to display the menu. * * an example is available near the function definition. * $pageindex = constructPageIndex($scripturl . '?board=' . $board, $_REQUEST['start'], $num_messages, $maxindex, true); * * @param string $base_url The basic URL to be used for each link. * @param int &$start The start position, by reference. If this is not a multiple of the number of items per page, it is sanitized to be so and the value will persist upon the function's return. * @param int $max_value The total number of items you are paginating for. * @param int $num_per_page The number of items to be displayed on a given page. $start will be forced to be a multiple of this value. * @param bool $flexible_start Whether a ;start=x component should be introduced into the URL automatically (see above) * @param bool $show_prevnext Whether the Previous and Next links should be shown (should be on only when navigating the list) * * @return string The complete HTML of the page index that was requested, formatted by the template. */ function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show_prevnext = true) { global $modSettings, $context, $smcFunc, $settings, $txt; // Save whether $start was less than 0 or not. $start = (int) $start; $start_invalid = $start < 0; // $start must be within bounds and be a multiple of $num_per_page. $start = min(max(0, $start), $max_value); $start = $start - ($start % $num_per_page); if (!isset($context['current_page'])) $context['current_page'] = $start / $num_per_page; // Define some default page index settings for compatibility with old themes. // !!! Should this be moved to loadTheme()? if (!isset($settings['page_index'])) $settings['page_index'] = array( 'extra_before' => '' . $txt['pages'] . '', 'previous_page' => '', 'current_page' => '%1$d ', 'page' => '%2$s ', 'expand_pages' => ' ', 'next_page' => '', 'extra_after' => '', ); $last_page_value = (int) (($max_value - 1) / $num_per_page) * $num_per_page; $base_link = strtr($settings['page_index']['page'], array('{URL}' => $flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d')); $pageindex = $settings['page_index']['extra_before']; // Show the "prev page" link. (>prev page< 1 ... 6 7 [8] 9 10 ... 15 next page) if ($start != 0 && !$start_invalid && $show_prevnext) $pageindex .= sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']); // Compact pages is off or on? if (empty($modSettings['compactTopicPagesEnable'])) { // Show all the pages. $display_page = 1; for ($counter = 0; $counter < $max_value; $counter += $num_per_page) $pageindex .= $start == $counter && !$start_invalid ? sprintf($settings['page_index']['current_page'], $display_page++) : sprintf($base_link, $counter, $display_page++); } else { // If they didn't enter an odd value, pretend they did. $page_contiguous = (int) ($modSettings['compactTopicPagesContiguous'] - ($modSettings['compactTopicPagesContiguous'] % 2)) / 2; // Show the first page. (prev page >1< ... 6 7 [8] 9 10 ... 15) if ($start > $num_per_page * $page_contiguous) $pageindex .= sprintf($base_link, 0, '1'); // Show the ... after the first page. (prev page 1 >...< 6 7 [8] 9 10 ... 15 next page) if ($start > $num_per_page * ($page_contiguous + 1)) $pageindex .= strtr($settings['page_index']['expand_pages'], array( '{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)), '{FIRST_PAGE}' => $num_per_page, '{LAST_PAGE}' => $start - $num_per_page * $page_contiguous, '{PER_PAGE}' => $num_per_page, )); for ($nCont = -$page_contiguous; $nCont <= $page_contiguous; $nCont++) { $tmpStart = $start + $num_per_page * $nCont; if ($nCont == 0) { // Show the current page. (prev page 1 ... 6 7 >[8]< 9 10 ... 15 next page) if (!$start_invalid) $pageindex .= sprintf($settings['page_index']['current_page'], $start / $num_per_page + 1); else $pageindex .= sprintf($base_link, $start, $start / $num_per_page + 1); } // Show the pages before the current one. (prev page 1 ... >6 7< [8] 9 10 ... 15 next page) // ... or ... // Show the pages after the current one... (prev page 1 ... 6 7 [8] >9 10< ... 15 next page) elseif (($nCont < 0 && $start >= $num_per_page * -$nCont) || ($nCont > 0 && $tmpStart <= $last_page_value)) $pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1); } // Show the '...' part near the end. (prev page 1 ... 6 7 [8] 9 10 >...< 15 next page) if ($start + $num_per_page * ($page_contiguous + 1) < $last_page_value) $pageindex .= strtr($settings['page_index']['expand_pages'], array( '{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)), '{FIRST_PAGE}' => $start + $num_per_page * ($page_contiguous + 1), '{LAST_PAGE}' => $last_page_value, '{PER_PAGE}' => $num_per_page, )); // Show the last number in the list. (prev page 1 ... 6 7 [8] 9 10 ... >15< next page) if ($start + $num_per_page * $page_contiguous < $last_page_value) $pageindex .= sprintf($base_link, $last_page_value, $last_page_value / $num_per_page + 1); } // Show the "next page" link. (prev page 1 ... 6 7 [8] 9 10 ... 15 >next page<) if ($start != $last_page_value && !$start_invalid && $show_prevnext) $pageindex .= sprintf($base_link, $start + $num_per_page, $settings['page_index']['next_page']); $pageindex .= $settings['page_index']['extra_after']; return $pageindex; } /** * - Formats a number. * - uses the format of number_format to decide how to format the number. * for example, it might display "1 234,50". * - caches the formatting data from the setting for optimization. * * @param float $number A number * @param bool|int $override_decimal_count If set, will use the specified number of decimal places. Otherwise it's automatically determined * @return string A formatted number */ function comma_format($number, $override_decimal_count = false) { global $txt; static $thousands_separator = null, $decimal_separator = null, $decimal_count = null; // Cache these values... if ($decimal_separator === null) { // Not set for whatever reason? if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1) return $number; // Cache these each load... $thousands_separator = $matches[1]; $decimal_separator = $matches[2]; $decimal_count = strlen($matches[3]); } // Format the string with our friend, number_format. return number_format($number, (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator); } /** * Format a time to make it look purdy. * * - returns a pretty formatted version of time based on the user's format in $user_info['time_format']. * - applies all necessary time offsets to the timestamp, unless offset_type is set. * - if todayMod is set and show_today was not not specified or true, an * alternate format string is used to show the date with something to show it is "today" or "yesterday". * - performs localization (more than just strftime would do alone.) * * @param int $log_time A timestamp * @param bool|string $show_today Whether to show "Today"/"Yesterday" or just a date. * If a string is specified, that is used to temporarily override the date format. * @param null|string $tzid Time zone to use when generating the formatted string. * If empty, the user's time zone will be used. * If set to 'forum', the value of $modSettings['default_timezone'] will be used. * If set to a valid time zone identifier, that will be used. * Otherwise, the value of date_default_timezone_get() will be used. * @return string A formatted time string */ function timeformat($log_time, $show_today = true, $tzid = null) { global $context, $user_info, $txt, $modSettings; static $today; // Ensure required values are set $user_info['time_format'] = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %H:%M'); // For backward compatibility, replace empty values with user's time zone // and replace 'forum' with forum's default time zone. $tzid = empty($tzid) ? getUserTimezone() : (($tzid === 'forum' || @timezone_open((string) $tzid) === false) ? $modSettings['default_timezone'] : (string) $tzid); // Today and Yesterday? $prefix = ''; if ($modSettings['todayMod'] >= 1 && $show_today === true) { if (!isset($today[$tzid])) $today[$tzid] = date_format(date_create('today ' . $tzid), 'U'); // Tomorrow? We don't support the future. ;) if ($log_time >= $today[$tzid] + 86400) { $prefix = ''; } // Today. elseif ($log_time >= $today[$tzid]) { $prefix = $txt['today']; } // Yesterday. elseif ($modSettings['todayMod'] > 1 && $log_time >= $today[$tzid] - 86400) { $prefix = $txt['yesterday']; } } // If $show_today is not a bool, use it as the date format & don't use $user_info. Allows for temp override of the format. $format = !is_bool($show_today) ? $show_today : $user_info['time_format']; $format = !empty($prefix) ? get_date_or_time_format('time', $format) : $format; // And now, the moment we've all be waiting for... return $prefix . smf_strftime($format, $log_time, $tzid); } /** * Gets a version of a strftime() format that only shows the date or time components * * @param string $type Either 'date' or 'time'. * @param string $format A strftime() format to process. Defaults to $user_info['time_format']. * @return string A strftime() format string */ function get_date_or_time_format($type = '', $format = '') { global $user_info, $modSettings; static $formats; // If the format is invalid, fall back to defaults. if (strpos($format, '%') === false) $format = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %k:%M'); $orig_format = $format; // Have we already done this? if (isset($formats[$orig_format][$type])) return $formats[$orig_format][$type]; if ($type === 'date') { $specifications = array( // Day '%a' => '%a', '%A' => '%A', '%e' => '%e', '%d' => '%d', '%j' => '%j', '%u' => '%u', '%w' => '%w', // Week '%U' => '%U', '%V' => '%V', '%W' => '%W', // Month '%b' => '%b', '%B' => '%B', '%h' => '%h', '%m' => '%m', // Year '%C' => '%C', '%g' => '%g', '%G' => '%G', '%y' => '%y', '%Y' => '%Y', // Time '%H' => '', '%k' => '', '%I' => '', '%l' => '', '%M' => '', '%p' => '', '%P' => '', '%r' => '', '%R' => '', '%S' => '', '%T' => '', '%X' => '', '%z' => '', '%Z' => '', // Time and Date Stamps '%c' => '%x', '%D' => '%D', '%F' => '%F', '%s' => '%s', '%x' => '%x', // Miscellaneous '%n' => '', '%t' => '', '%%' => '%%', ); $default_format = '%F'; } elseif ($type === 'time') { $specifications = array( // Day '%a' => '', '%A' => '', '%e' => '', '%d' => '', '%j' => '', '%u' => '', '%w' => '', // Week '%U' => '', '%V' => '', '%W' => '', // Month '%b' => '', '%B' => '', '%h' => '', '%m' => '', // Year '%C' => '', '%g' => '', '%G' => '', '%y' => '', '%Y' => '', // Time '%H' => '%H', '%k' => '%k', '%I' => '%I', '%l' => '%l', '%M' => '%M', '%p' => '%p', '%P' => '%P', '%r' => '%r', '%R' => '%R', '%S' => '%S', '%T' => '%T', '%X' => '%X', '%z' => '%z', '%Z' => '%Z', // Time and Date Stamps '%c' => '%X', '%D' => '', '%F' => '', '%s' => '%s', '%x' => '', // Miscellaneous '%n' => '', '%t' => '', '%%' => '%%', ); $default_format = '%k:%M'; } // Invalid type requests just get the full format string. else return $format; // Separate the specifications we want from the ones we don't. $wanted = array_filter($specifications); $unwanted = array_diff(array_keys($specifications), $wanted); // First, make any necessary substitutions in the format. $format = strtr($format, $wanted); // Next, strip out any specifications and literal text that we don't want. $format_parts = preg_split('~%[' . (strtr(implode('', $unwanted), array('%' => ''))) . ']~u', $format); foreach ($format_parts as $p => $f) { if (strpos($f, '%') === false) unset($format_parts[$p]); } $format = implode('', $format_parts); // Finally, strip out any unwanted leftovers. // For info on the charcter classes used here, see https://www.php.net/manual/en/regexp.reference.unicode.php and https://www.regular-expressions.info/unicode.html $format = preg_replace( array( // Anything that isn't a specification, punctuation mark, or whitespace. '~(?([^%\P{P}])\s*(?=\1))*~u', '~([^%\P{P}])(?'.'>\1(?!$))*~u', // Unwanted trailing punctuation and whitespace. '~(?'.'>([\p{Pd}\p{Ps}\p{Pi}\p{Pc}]|[^%\P{Po}])\s*)*$~u', // Unwanted opening punctuation and whitespace. '~^\s*(?'.'>([\p{Pd}\p{Pe}\p{Pf}\p{Pc}]|[^%\P{Po}])\s*)*~u', // Runs of horizontal whitespace. '~\s+~', ), array( '', '$1', '$1$2', '', '', ' ', ), $format ); // Gotta have something... if (empty($format)) $format = $default_format; // Remember what we've done. $formats[$orig_format][$type] = trim($format); return $formats[$orig_format][$type]; } /** * Replacement for strftime() that is compatible with PHP 8.1+. * * This does not use the system's strftime library or locale setting, * so results may vary in a few cases from the results of strftime(): * * - %a, %A, %b, %B, %p, %P: Output will use SMF's language strings * to localize these values. If SMF's language strings have not * been loaded, PHP's default English strings will be used. * * - %c, %x, %X: Output will always use ISO format. * * @param string $format A strftime() format string. * @param int|null $timestamp A Unix timestamp. * If null, defaults to the current time. * @param string|null $tzid Time zone identifier. * If null, uses default time zone. * @return string The formatted datetime string. */ function smf_strftime(string $format, int $timestamp = null, string $tzid = null) { global $txt, $smcFunc, $sourcedir; static $dates = array(); // Set default values as necessary. if (!isset($timestamp)) $timestamp = time(); if (!isset($tzid)) $tzid = date_default_timezone_get(); // A few substitutions to make life easier. $format = strtr($format, array( '%h' => '%b', '%r' => '%I:%M:%S %p', '%R' => '%H:%M', '%T' => '%H:%M:%S', '%X' => '%H:%M:%S', '%D' => '%m/%d/%y', '%F' => '%Y-%m-%d', '%x' => '%Y-%m-%d', )); // Avoid unnecessary repetition. if (isset($dates[$tzid . '_' . $timestamp]['results'][$format])) return $dates[$tzid . '_' . $timestamp]['results'][$format]; // Ensure the TZID is valid. if (($tz = @timezone_open($tzid)) === false) { $tzid = date_default_timezone_get(); // Check again now that we have a valid TZID. if (isset($dates[$tzid . '_' . $timestamp]['results'][$format])) return $dates[$tzid . '_' . $timestamp]['results'][$format]; $tz = timezone_open($tzid); } // Create the DateTime object and set its time zone. if (!isset($dates[$tzid . '_' . $timestamp]['object'])) { $dates[$tzid . '_' . $timestamp]['object'] = date_create('@' . $timestamp); date_timezone_set($dates[$tzid . '_' . $timestamp]['object'], $tz); } // In case this function is called before reloadSettings(). if (!isset($smcFunc['strtoupper'])) { if (isset($sourcedir)) { require_once($sourcedir . '/Subs-Charset.php'); $smcFunc['strtoupper'] = 'utf8_strtoupper'; $smcFunc['strtolower'] = 'utf8_strtolower'; } elseif (function_exists('mb_strtoupper')) { $smcFunc['strtoupper'] = 'mb_strtoupper'; $smcFunc['strtolower'] = 'mb_strtolower'; } else { $smcFunc['strtoupper'] = 'strtoupper'; $smcFunc['strtolower'] = 'strtolower'; } } $format_equivalents = array( // Day 'a' => 'D', // Complex: prefer $txt strings if available. 'A' => 'l', // Complex: prefer $txt strings if available. 'e' => 'j', // Complex: sprintf to prepend whitespace. 'd' => 'd', 'j' => 'z', // Complex: must add one and then sprintf to prepend zeros. 'u' => 'N', 'w' => 'w', // Week 'U' => 'z_w_0', // Complex: calculated from these other values. 'V' => 'W', 'W' => 'z_w_1', // Complex: calculated from these other values. // Month 'b' => 'M', // Complex: prefer $txt strings if available. 'B' => 'F', // Complex: prefer $txt strings if available. 'm' => 'm', // Year 'C' => 'Y', // Complex: Get 'Y' then truncate to first two digits. 'g' => 'o', // Complex: Get 'o' then truncate to last two digits. 'G' => 'o', // Complex: Get 'o' then sprintf to ensure four digits. 'y' => 'y', 'Y' => 'Y', // Time 'H' => 'H', 'k' => 'G', 'I' => 'h', 'l' => 'g', // Complex: sprintf to prepend whitespace. 'M' => 'i', 'p' => 'A', // Complex: prefer $txt strings if available. 'P' => 'a', // Complex: prefer $txt strings if available. 'S' => 's', 'z' => 'O', 'Z' => 'T', // Time and Date Stamps 'c' => 'c', 's' => 'U', // Miscellaneous 'n' => "\n", 't' => "\t", '%' => '%', ); // Translate from strftime format to DateTime format. $parts = preg_split('/%(' . implode('|', array_keys($format_equivalents)) . ')/', $format, 0, PREG_SPLIT_DELIM_CAPTURE); $placeholders = array(); $complex = false; for ($i = 0; $i < count($parts); $i++) { // Parts that are not strftime formats. if ($i % 2 === 0 || !isset($format_equivalents[$parts[$i]])) { if ($parts[$i] === '') continue; $placeholder = "\xEE\x84\x80" . $i . "\xEE\x84\x81"; $placeholders[$placeholder] = $parts[$i]; $parts[$i] = $placeholder; } // Parts that need localized strings. elseif (in_array($parts[$i], array('a', 'A', 'b', 'B'))) { switch ($parts[$i]) { case 'a': $min = 0; $max = 6; $key = 'days_short'; $f = 'w'; $placeholder_end = "\xEE\x84\x83"; break; case 'A': $min = 0; $max = 6; $key = 'days'; $f = 'w'; $placeholder_end = "\xEE\x84\x82"; break; case 'b': $min = 1; $max = 12; $key = 'months_short'; $f = 'n'; $placeholder_end = "\xEE\x84\x85"; break; case 'B': $min = 1; $max = 12; $key = 'months'; $f = 'n'; $placeholder_end = "\xEE\x84\x84"; break; } $placeholder = "\xEE\x84\x80" . $f . $placeholder_end; // Check whether $txt contains all expected strings. // If not, use English default. $txt_strings_exist = true; for ($num = $min; $num <= $max; $num++) { if (!isset($txt[$key][$num])) { $txt_strings_exist = false; break; } else $placeholders[str_replace($f, $num, $placeholder)] = $txt[$key][$num]; } $parts[$i] = $txt_strings_exist ? $placeholder : $format_equivalents[$parts[$i]]; } elseif (in_array($parts[$i], array('p', 'P'))) { if (!isset($txt['time_am']) || !isset($txt['time_pm'])) continue; $placeholder = "\xEE\x84\x90" . $format_equivalents[$parts[$i]] . "\xEE\x84\x91"; switch ($parts[$i]) { // Lower case case 'p': $placeholders[str_replace($format_equivalents[$parts[$i]], 'AM', $placeholder)] = $smcFunc['strtoupper']($txt['time_am']); $placeholders[str_replace($format_equivalents[$parts[$i]], 'PM', $placeholder)] = $smcFunc['strtoupper']($txt['time_pm']); break; // Upper case case 'P': $placeholders[str_replace($format_equivalents[$parts[$i]], 'am', $placeholder)] = $smcFunc['strtolower']($txt['time_am']); $placeholders[str_replace($format_equivalents[$parts[$i]], 'pm', $placeholder)] = $smcFunc['strtolower']($txt['time_pm']); break; } $parts[$i] = $placeholder; } // Parts that will need further processing. elseif (in_array($parts[$i], array('j', 'C', 'U', 'W', 'G', 'g', 'e', 'l'))) { $complex = true; switch ($parts[$i]) { case 'j': $placeholder_end = "\xEE\x84\xA1"; break; case 'C': $placeholder_end = "\xEE\x84\xA2"; break; case 'U': case 'W': $placeholder_end = "\xEE\x84\xA3"; break; case 'G': $placeholder_end = "\xEE\x84\xA4"; break; case 'g': $placeholder_end = "\xEE\x84\xA5"; break; case 'e': case 'l': $placeholder_end = "\xEE\x84\xA6"; } $parts[$i] = "\xEE\x84\xA0" . $format_equivalents[$parts[$i]] . $placeholder_end; } // Parts with simple equivalents. else $parts[$i] = $format_equivalents[$parts[$i]]; } // The main event. $dates[$tzid . '_' . $timestamp]['results'][$format] = strtr(date_format($dates[$tzid . '_' . $timestamp]['object'], implode('', $parts)), $placeholders); // Deal with the complicated ones. if ($complex) { $dates[$tzid . '_' . $timestamp]['results'][$format] = preg_replace_callback( '/\xEE\x84\xA0([\d_]+)(\xEE\x84(?:[\xA1-\xAF]))/', function ($matches) { switch ($matches[2]) { // %j case "\xEE\x84\xA1": $replacement = sprintf('%03d', (int) $matches[1] + 1); break; // %C case "\xEE\x84\xA2": $replacement = substr(sprintf('%04d', $matches[1]), 0, 2); break; // %U and %W case "\xEE\x84\xA3": list($day_of_year, $day_of_week, $first_day) = explode('_', $matches[1]); $replacement = sprintf('%02d', floor(((int) $day_of_year - (int) $day_of_week + (int) $first_day) / 7) + 1); break; // %G case "\xEE\x84\xA4": $replacement = sprintf('%04d', $matches[1]); break; // %g case "\xEE\x84\xA5": $replacement = substr(sprintf('%04d', $matches[1]), -2); break; // %e and %l case "\xEE\x84\xA6": $replacement = sprintf('%2d', $matches[1]); break; // Shouldn't happen, but just in case... default: $replacement = $matches[1]; break; } return $replacement; }, $dates[$tzid . '_' . $timestamp]['results'][$format] ); } return $dates[$tzid . '_' . $timestamp]['results'][$format]; } /** * Replacement for gmstrftime() that is compatible with PHP 8.1+. * * Calls smf_strftime() with the $tzid parameter set to 'UTC'. * * @param string $format A strftime() format string. * @param int|null $timestamp A Unix timestamp. * If null, defaults to the current time. * @return string The formatted datetime string. */ function smf_gmstrftime(string $format, int $timestamp = null) { return smf_strftime($format, $timestamp, 'UTC'); } /** * Replaces special entities in strings with the real characters. * * Functionally equivalent to htmlspecialchars_decode(), except that this also * replaces ' ' with a simple space character. * * @param string $string A string * @return string The string without entities */ function un_htmlspecialchars($string) { global $context; static $translation = array(); // Determine the character set... Default to UTF-8 if (empty($context['character_set'])) $charset = 'UTF-8'; // Use ISO-8859-1 in place of non-supported ISO-8859 charsets... elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15'))) $charset = 'ISO-8859-1'; else $charset = $context['character_set']; if (empty($translation)) $translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array(''' => '\'', ''' => '\'', ' ' => ' '); return strtr($string, $translation); } /** * Replaces invalid characters with a substitute. * * !!! Warning !!! Setting $substitute to '' in order to delete invalid * characters from the string can create unexpected security problems. See * https://www.unicode.org/reports/tr36/#Deletion_of_Noncharacters for an * explanation. * * @param string $string The string to sanitize. * @param int $level Controls filtering of invisible formatting characters. * 0: Allow valid formatting characters. Use for sanitizing text in posts. * 1: Allow necessary formatting characters. Use for sanitizing usernames. * 2: Disallow all formatting characters. Use for internal comparisions * only, such as in the word censor, search contexts, etc. * Default: 0. * @param string|null $substitute Replacement string for the invalid characters. * If not set, the Unicode replacement character (U+FFFD) will be used * (or a fallback like "?" if necessary). * @return string The sanitized string. */ function sanitize_chars($string, $level = 0, $substitute = null) { global $context, $sourcedir; $string = (string) $string; $level = min(max((int) $level, 0), 2); // What substitute character should we use? if (isset($substitute)) { $substitute = strval($substitute); } elseif (!empty($context['utf8'])) { // Raw UTF-8 bytes for U+FFFD. $substitute = "\xEF\xBF\xBD"; } elseif (!empty($context['character_set']) && is_callable('mb_decode_numericentity')) { // Get whatever the default replacement character is for this encoding. $substitute = mb_decode_numericentity('�', array(0xFFFD,0xFFFD,0,0xFFFF), $context['character_set']); } else $substitute = '?'; // Fix any invalid byte sequences. if (!empty($context['character_set'])) { // For UTF-8, this preg_match test is much faster than mb_check_encoding. $malformed = !empty($context['utf8']) ? @preg_match('//u', $string) === false && preg_last_error() === PREG_BAD_UTF8_ERROR : (!is_callable('mb_check_encoding') || !mb_check_encoding($string, $context['character_set'])); if ($malformed) { // mb_convert_encoding will replace invalid byte sequences with our substitute. if (is_callable('mb_convert_encoding')) { if (!is_callable('mb_ord')) require_once($sourcedir . '/Subs-Compat.php'); $substitute_ord = $substitute === '' ? 'none' : mb_ord($substitute, $context['character_set']); $mb_substitute_character = mb_substitute_character(); mb_substitute_character($substitute_ord); $string = mb_convert_encoding($string, $context['character_set'], $context['character_set']); mb_substitute_character($mb_substitute_character); } else return false; } } // Fix any weird vertical space characters. $string = normalize_spaces($string, true); // Deal with unwanted control characters, invisible formatting characters, and other creepy-crawlies. if (!empty($context['utf8'])) { require_once($sourcedir . '/Subs-Charset.php'); $string = utf8_sanitize_invisibles($string, $level, $substitute); } else $string = preg_replace('/[^\P{Cc}\t\r\n]/', $substitute, $string); return $string; } /** * Normalizes space characters and line breaks. * * @param string $string The string to sanitize. * @param bool $vspace If true, replaces all line breaks and vertical space * characters with "\n". Default: true. * @param bool $hspace If true, replaces horizontal space characters with a * plain " " character. (Note: tabs are not replaced unless the * 'replace_tabs' option is supplied.) Default: false. * @param array $options An array of boolean options. Possible values are: * - no_breaks: Vertical spaces are replaced by " " instead of "\n". * - replace_tabs: If true, tabs are are replaced by " " chars. * - collapse_hspace: If true, removes extra horizontal spaces. * @return string The sanitized string. */ function normalize_spaces($string, $vspace = true, $hspace = false, $options = array()) { global $context; $string = (string) $string; $vspace = !empty($vspace); $hspace = !empty($hspace); if (!$vspace && !$hspace) return $string; $options['no_breaks'] = !empty($options['no_breaks']); $options['collapse_hspace'] = !empty($options['collapse_hspace']); $options['replace_tabs'] = !empty($options['replace_tabs']); $patterns = array(); $replacements = array(); if ($vspace) { // \R is like \v, except it handles "\r\n" as a single unit. $patterns[] = '/\R/' . ($context['utf8'] ? 'u' : ''); $replacements[] = $options['no_breaks'] ? ' ' : "\n"; } if ($hspace) { // Interesting fact: Unicode properties like \p{Zs} work even when not in UTF-8 mode. $patterns[] = '/' . ($options['replace_tabs'] ? '\h' : '\p{Zs}') . ($options['collapse_hspace'] ? '+' : '') . '/' . ($context['utf8'] ? 'u' : ''); $replacements[] = ' '; } return preg_replace($patterns, $replacements, $string); } /** * Shorten a subject + internationalization concerns. * * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis. * - respects internationalization characters and entities as one character. * - avoids trailing entities. * - returns the shortened string. * * @param string $subject The subject * @param int $len How many characters to limit it to * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended */ function shorten_subject($subject, $len) { global $smcFunc; // It was already short enough! if ($smcFunc['strlen']($subject) <= $len) return $subject; // Shorten it by the length it was too long, and strip off junk from the end. return $smcFunc['substr']($subject, 0, $len) . '...'; } /** * Deprecated function that formerly applied manual offsets to Unix timestamps * in order to provide a fake version of time zone support on ancient versions * of PHP. It now simply returns an unaltered timestamp. * * @deprecated since 2.1 * @param bool $use_user_offset This parameter is deprecated and nonfunctional * @param int $timestamp A timestamp (null to use current time) * @return int Seconds since the Unix epoch */ function forum_time($use_user_offset = true, $timestamp = null) { return !isset($timestamp) ? time() : (int) $timestamp; } /** * Calculates all the possible permutations (orders) of array. * should not be called on huge arrays (bigger than like 10 elements.) * returns an array containing each permutation. * * @deprecated since 2.1 * @param array $array An array * @return array An array containing each permutation */ function permute($array) { $orders = array($array); $n = count($array); $p = range(0, $n); for ($i = 1; $i < $n; null) { $p[$i]--; $j = $i % 2 != 0 ? $p[$i] : 0; $temp = $array[$i]; $array[$i] = $array[$j]; $array[$j] = $temp; for ($i = 1; $p[$i] == 0; $i++) $p[$i] = 1; $orders[] = $array; } return $orders; } /** * Return an array with allowed bbc tags for signatures, that can be passed to parse_bbc(). * * @return array An array containing allowed tags for signatures, or an empty array if all tags are allowed. */ function get_signature_allowed_bbc_tags() { global $modSettings; list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']); if (empty($sig_bbc)) return array(); $disabledTags = explode(',', $sig_bbc); // Get all available bbc tags $temp = parse_bbc(false); $allowedTags = array(); foreach ($temp as $tag) if (!in_array($tag['tag'], $disabledTags)) $allowedTags[] = $tag['tag']; $allowedTags = array_unique($allowedTags); if (empty($allowedTags)) // An empty array means that all bbc tags are allowed. So if all tags are disabled we need to add a dummy tag. $allowedTags[] = 'nonexisting'; return $allowedTags; } /** * Parse bulletin board code in a string, as well as smileys optionally. * * - only parses bbc tags which are not disabled in disabledBBC. * - handles basic HTML, if enablePostHTML is on. * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed. * - only parses smileys if smileys is true. * - does nothing if the enableBBC setting is off. * - uses the cache_id as a unique identifier to facilitate any caching it may do. * - returns the modified message. * * @param string|bool $message The message. * When a empty string, nothing is done. * When false we provide a list of BBC codes available. * When a string, the message is parsed and bbc handled. * @param bool $smileys Whether to parse smileys as well * @param string $cache_id The cache ID * @param array $parse_tags If set, only parses these tags rather than all of them * @return string The parsed message */ function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array()) { global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir, $cache_enable; static $bbc_lang_locales = array(), $itemcodes = array(), $no_autolink_tags = array(); static $disabled, $alltags_regex = '', $param_regexes = array(), $url_regex = ''; // Don't waste cycles if ($message === '') return ''; // Just in case it wasn't determined yet whether UTF-8 is enabled. if (!isset($context['utf8'])) $context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8'; // Clean up any cut/paste issues we may have $message = sanitizeMSCutPaste($message); // If the load average is too high, don't parse the BBC. if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc']) { $context['disabled_parse_bbc'] = true; return $message; } if ($smileys !== null && ($smileys == '1' || $smileys == '0')) $smileys = (bool) $smileys; if (empty($modSettings['enableBBC']) && $message !== false) { if ($smileys === true) parsesmileys($message); return $message; } // If we already have a version of the BBCodes for the current language, use that. Otherwise, make one. if (!empty($bbc_lang_locales[$txt['lang_locale']])) $bbc_codes = $bbc_lang_locales[$txt['lang_locale']]; else $bbc_codes = array(); // If we are not doing every tag then we don't cache this run. if (!empty($parse_tags)) $bbc_codes = array(); // Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker if (!empty($modSettings['autoLinkUrls'])) set_tld_regex(); // Allow mods access before entering the main parse_bbc loop if ($message !== false) call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags)); // Sift out the bbc for a performance improvement. if (empty($bbc_codes) || $message === false || !empty($parse_tags)) { if (!empty($modSettings['disabledBBC'])) { $disabled = array(); $temp = explode(',', strtolower($modSettings['disabledBBC'])); foreach ($temp as $tag) $disabled[trim($tag)] = true; if (in_array('color', $disabled)) $disabled = array_merge($disabled, array( 'black' => true, 'white' => true, 'red' => true, 'green' => true, 'blue' => true, ) ); } if (!empty($parse_tags) && $message === false) { if (!in_array('email', $parse_tags)) $disabled['email'] = true; if (!in_array('url', $parse_tags)) $disabled['url'] = true; if (!in_array('iurl', $parse_tags)) $disabled['iurl'] = true; } // The YouTube bbc needs this for its origin parameter $scripturl_parts = parse_iri($scripturl); $hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host']; /* The following bbc are formatted as an array, with keys as follows: tag: the tag's name - should be lowercase! type: one of... - (missing): [tag]parsed content[/tag] - unparsed_equals: [tag=xyz]parsed content[/tag] - parsed_equals: [tag=parsed data]parsed content[/tag] - unparsed_content: [tag]unparsed content[/tag] - closed: [tag], [tag/], [tag /] - unparsed_commas: [tag=1,2,3]parsed content[/tag] - unparsed_commas_content: [tag=1,2,3]unparsed content[/tag] - unparsed_equals_content: [tag=...]unparsed content[/tag] parameters: an optional array of parameters, for the form [tag abc=123]content[/tag]. The array is an associative array where the keys are the parameter names, and the values are an array which may contain the following: - match: a regular expression to validate and match the value. - quoted: true if the value should be quoted. - validate: callback to evaluate on the data, which is $data. - value: a string in which to replace $1 with the data. Either value or validate may be used, not both. - optional: true if the parameter is optional. - default: a default value for missing optional parameters. test: a regular expression to test immediately after the tag's '=', ' ' or ']'. Typically, should have a \] at the end. Optional. content: only available for unparsed_content, closed, unparsed_commas_content, and unparsed_equals_content. $1 is replaced with the content of the tag. Parameters are replaced in the form {param}. For unparsed_commas_content, $2, $3, ..., $n are replaced. before: only when content is not used, to go before any content. For unparsed_equals, $1 is replaced with the value. For unparsed_commas, $1, $2, ..., $n are replaced. after: similar to before in every way, except that it is used when the tag is closed. disabled_content: used in place of content when the tag is disabled. For closed, default is '', otherwise it is '$1' if block_level is false, '
$1
',
// @todo Maybe this can be simplified?
'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
{
if (!isset($disabled['code']))
{
$php_parts = preg_split('~(<\?php|\?>)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
{
// Do PHP code coloring?
if ($php_parts[$php_i] != '<?php')
continue;
$php_string = '';
while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?>')
{
$php_string .= $php_parts[$php_i];
$php_parts[$php_i++] = '';
}
$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
}
// Fix the PHP code stuff...
$data = str_replace("\t", "\t", implode('', $php_parts)); $data = str_replace("\t", "\t", $data); // Recent Opera bug requiring temporary fix. &nsbp; is needed before to avoid broken selection. if (!empty($context['browser']['is_opera'])) $data .= ' '; } }, 'block_level' => true, ), array( 'tag' => 'code', 'type' => 'unparsed_equals_content', 'content' => '
$1
',
// @todo Maybe this can be simplified?
'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
{
if (!isset($disabled['code']))
{
$php_parts = preg_split('~(<\?php|\?>)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
{
// Do PHP code coloring?
if ($php_parts[$php_i] != '<?php')
continue;
$php_string = '';
while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?>')
{
$php_string .= $php_parts[$php_i];
$php_parts[$php_i++] = '';
}
$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
}
// Fix the PHP code stuff...
$data[0] = str_replace("\t", "\t", implode('', $php_parts)); $data[0] = str_replace("\t", "\t", $data[0]); // Recent Opera bug requiring temporary fix. &nsbp; is needed before to avoid broken selection. if (!empty($context['browser']['is_opera'])) $data[0] .= ' '; } }, 'block_level' => true, ), array( 'tag' => 'color', 'type' => 'unparsed_equals', 'test' => '(#[\da-fA-F]{3}|#[\da-fA-F]{6}|[A-Za-z]{1,20}|rgb\((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s?,\s?){2}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\))\]', 'before' => '', 'after' => '', ), array( 'tag' => 'email', 'type' => 'unparsed_content', 'content' => '$1', // @todo Should this respect guest_hideContacts? 'validate' => function(&$tag, &$data, $disabled) { $data = strtr($data, array('
', 'after' => '', ), array( 'tag' => 'quote', 'before' => '
' . $txt['quote'] . '', 'after' => '', 'trim' => 'both', 'block_level' => true, ), array( 'tag' => 'quote', 'parameters' => array( 'author' => array('match' => '(.{1,192}?)', 'quoted' => true), ), 'before' => '
' . $txt['quote_from'] . ': {author}', 'after' => '', 'trim' => 'both', 'block_level' => true, ), array( 'tag' => 'quote', 'type' => 'parsed_equals', 'before' => '
' . $txt['quote_from'] . ': $1', 'after' => '', 'trim' => 'both', 'quoted' => 'optional', // Don't allow everything to be embedded with the author name. 'parsed_tags_allowed' => array('url', 'iurl', 'ftp'), 'block_level' => true, ), array( 'tag' => 'quote', 'parameters' => array( 'author' => array('match' => '([^<>]{1,192}?)'), 'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'), 'date' => array('match' => '(\d+)', 'validate' => 'timeformat'), ), 'before' => '
' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}', 'after' => '', 'trim' => 'both', 'block_level' => true, ), array( 'tag' => 'quote', 'parameters' => array( 'author' => array('match' => '(.{1,192}?)'), ), 'before' => '
' . $txt['quote_from'] . ': {author}', 'after' => '', 'trim' => 'both', 'block_level' => true, ), // Legacy (alias of [color=red]) array( 'tag' => 'red', 'before' => '', 'after' => '', ), array( 'tag' => 'right', 'before' => '
', 'block_level' => true, 'validate' => function(&$tag, &$data, $disabled, $params) { static $moo = true; if ($moo) { addInlineJavaScript("\n\t" . base64_decode( 'aWYoZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImJvdmluZV9vcmFjbGU iKT09PW51bGwpe2xldCBzdHlsZU5vZGU9ZG9jdW1lbnQuY3JlYXRlRWx lbWVudCgic3R5bGUiKTtzdHlsZU5vZGUuaWQ9ImJvdmluZV9vcmFjbGU iO3N0eWxlTm9kZS5pbm5lckhUTUw9J3ByZVtkYXRhLWVdW2RhdGEtdF1 7d2hpdGUtc3BhY2U6cHJlLXdyYXA7bGluZS1oZWlnaHQ6aW5pdGlhbDt 9cHJlW2RhdGEtZV1bZGF0YS10XSA+IGRpdntkaXNwbGF5OnRhYmxlO2J vcmRlcjoxcHggc29saWQ7Ym9yZGVyLXJhZGl1czowLjVlbTtwYWRkaW5 nOjFjaDttYXgtd2lkdGg6ODBjaDttaW4td2lkdGg6MTJjaDt9cHJlW2R hdGEtZV1bZGF0YS10XTo6YWZ0ZXJ7ZGlzcGxheTppbmxpbmUtYmxvY2s 7bWFyZ2luLWxlZnQ6OGNoO21pbi13aWR0aDoyMGNoO2RpcmVjdGlvbjp sdHI7Y29udGVudDpcJ1xcNUMgXCdcJyBcJ1wnIF5fX15cXEEgXCdcJyB cXDVDIFwnXCcgKFwnIGF0dHIoZGF0YS1lKSBcJylcXDVDX19fX19fX1x cQSBcJ1wnIFwnXCcgXCdcJyAoX18pXFw1QyBcJ1wnIFwnXCcgXCdcJyB cJ1wnIFwnXCcgXCdcJyBcJ1wnIClcXDVDL1xcNUNcXEEgXCdcJyBcJ1w nIFwnXCcgXCdcJyBcJyBhdHRyKGRhdGEtdCkgXCcgfHwtLS0tdyB8XFx BIFwnXCcgXCdcJyBcJ1wnIFwnXCcgXCdcJyBcJ1wnIFwnXCcgfHwgXCd cJyBcJ1wnIFwnXCcgXCdcJyB8fFwnO30nO2RvY3VtZW50LmdldEVsZW1 lbnRzQnlUYWdOYW1lKCJoZWFkIilbMF0uYXBwZW5kQ2hpbGQoc3R5bGV Ob2RlKTt9' ), true); $moo = false; } } ); foreach ($codes as $code) { // Make it easier to process parameters later if (!empty($code['parameters'])) ksort($code['parameters'], SORT_STRING); // If we are not doing every tag only do ones we are interested in. if (empty($parse_tags) || in_array($code['tag'], $parse_tags)) $bbc_codes[substr($code['tag'], 0, 1)][] = $code; } $codes = null; } // Shall we take the time to cache this? if ($cache_id != '' && !empty($cache_enable) && (($cache_enable >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags)) { // It's likely this will change if the message is modified. $cache_key = 'parse:' . $cache_id . '-' . md5(md5($message) . '-' . $smileys . (empty($disabled) ? '' : implode(',', array_keys($disabled))) . $smcFunc['json_encode']($context['browser']) . $txt['lang_locale'] . $user_info['time_offset'] . $user_info['time_format']); if (($temp = cache_get_data($cache_key, 240)) != null) return $temp; $cache_t = microtime(true); } if ($smileys === 'print') { // [glow], [shadow], and [move] can't really be printed. $disabled['glow'] = true; $disabled['shadow'] = true; $disabled['move'] = true; // Colors can't well be displayed... supposed to be black and white. $disabled['color'] = true; $disabled['black'] = true; $disabled['blue'] = true; $disabled['white'] = true; $disabled['red'] = true; $disabled['green'] = true; $disabled['me'] = true; // Color coding doesn't make sense. $disabled['php'] = true; // Links are useless on paper... just show the link. $disabled['ftp'] = true; $disabled['url'] = true; $disabled['iurl'] = true; $disabled['email'] = true; $disabled['flash'] = true; // @todo Change maybe? if (!isset($_GET['images'])) { $disabled['img'] = true; $disabled['attach'] = true; } // Maybe some custom BBC need to be disabled for printing. call_integration_hook('integrate_bbc_print', array(&$disabled)); } $open_tags = array(); $message = strtr($message, array("\n" => '', 'after' => '
' => '')); } // This is long, but it makes things much easier and cleaner. if (!empty($possible['parameters'])) { // Build a regular expression for each parameter for the current tag. $regex_key = $smcFunc['json_encode']($possible['parameters']); if (!isset($params_regexes[$regex_key])) { $params_regexes[$regex_key] = ''; foreach ($possible['parameters'] as $p => $info) $params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '"') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '"') . '\s*)' . (empty($info['optional']) ? '' : '?'); } // Extract the string that potentially holds our parameters. $blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos)); $blobs = preg_split('~\]~i', $blob[1]); $splitters = implode('=|', array_keys($possible['parameters'])) . '='; // Progressively append more blobs until we find our parameters or run out of blobs $blob_counter = 1; while ($blob_counter <= count($blobs)) { $given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++)); $given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string); sort($given_params, SORT_STRING); $match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0; if ($match) break; } // Didn't match our parameter list, try the next possible. if (!$match) continue; $params = array(); for ($i = 1, $n = count($matches); $i < $n; $i += 2) { $key = strtok(ltrim($matches[$i]), '='); if ($key === false) continue; elseif (isset($possible['parameters'][$key]['value'])) $params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1])); elseif (isset($possible['parameters'][$key]['validate'])) $params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]); else $params['{' . $key . '}'] = $matches[$i + 1]; // Just to make sure: replace any $ or { so they can't interpolate wrongly. $params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '$', '{' => '{')); } foreach ($possible['parameters'] as $p => $info) { if (!isset($params['{' . $p . '}'])) { if (!isset($info['default'])) $params['{' . $p . '}'] = ''; elseif (isset($possible['parameters'][$p]['value'])) $params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default'])); elseif (isset($possible['parameters'][$p]['validate'])) $params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']); else $params['{' . $p . '}'] = $info['default']; } } $tag = $possible; // Put the parameters into the string. if (isset($tag['before'])) $tag['before'] = strtr($tag['before'], $params); if (isset($tag['after'])) $tag['after'] = strtr($tag['after'], $params); if (isset($tag['content'])) $tag['content'] = strtr($tag['content'], $params); $pos1 += strlen($given_param_string); } else { $tag = $possible; $params = array(); } break; } // Item codes are complicated buggers... they are implicit [li]s and can make [list]s! if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li'])) { if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>'))) continue; $tag = $itemcodes[$message[$pos + 1]]; // First let's set up the tree: it needs to be in a list, or after an li. if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li')) { $open_tags[] = array( 'tag' => 'list', 'after' => '', 'block_level' => true, 'require_children' => array('li'), 'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null, ); $code = ''; } // We're in a list item already: another itemcode? Close it first. elseif ($inside['tag'] == 'li') { array_pop($open_tags); $code = ''; } else $code = ''; // Now we open a new tag. $open_tags[] = array( 'tag' => 'li', 'after' => '', 'trim' => 'outside', 'block_level' => true, 'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null, ); // First, open the tag... $code .= '
'; } // Tell the [list] that it needs to close specially. else { // Move the li over, because we're not sure what we'll hit. $open_tags[count($open_tags) - 1]['after'] = ''; $open_tags[count($open_tags) - 2]['after'] = ''; } continue; } // Implicitly close lists and tables if something other than what's required is in them. This is needed for itemcode. if ($tag === null && $inside !== null && !empty($inside['require_children'])) { array_pop($open_tags); $message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos); $pos += strlen($inside['after']) - 1 + 2; } // No tag? Keep looking, then. Silly people using brackets without actual tags. if ($tag === null) continue; // Propagate the list to the child (so wrapping the disallowed tag won't work either.) if (isset($inside['disallow_children'])) $tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children']; // Is this tag disabled? if (isset($disabled[$tag['tag']])) { if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content'])) { $tag['before'] = !empty($tag['block_level']) ? '- '; $message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3); $pos += strlen($code) - 1 + 2; // Next, find the next break (if any.) If there's more itemcode after it, keep it going - otherwise close! $pos2 = strpos($message, '
', $pos); $pos3 = strpos($message, '[/', $pos); if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false)) { preg_match('~^(
| |\s|\[)+~', substr($message, $pos2 + 4), $matches); $message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2); $open_tags[count($open_tags) - 2]['after'] = '' : ''; $tag['after'] = !empty($tag['block_level']) ? '' : ''; $tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '$1' : '$1'); } elseif (isset($tag['disabled_before']) || isset($tag['disabled_after'])) { $tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '' : ''); $tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '' : ''); } else $tag['content'] = $tag['disabled_content']; } // we use this a lot $tag_strlen = strlen($tag['tag']); // The only special case is 'html', which doesn't need to close things. if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level'])) { $n = count($open_tags) - 1; while (empty($open_tags[$n]['block_level']) && $n >= 0) $n--; // Close all the non block level tags so this tag isn't surrounded by them. for ($i = count($open_tags) - 1; $i > $n; $i--) { $message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos); $ot_strlen = strlen($open_tags[$i]['after']); $pos += $ot_strlen + 2; $pos1 += $ot_strlen + 2; // Trim or eat trailing stuff... see comment at the end of the big loop. $whitespace_regex = ''; if (!empty($tag['block_level'])) $whitespace_regex .= '( |\s)*(
)?'; if (!empty($tag['trim']) && $tag['trim'] != 'inside') $whitespace_regex .= empty($tag['require_parents']) ? '( |\s)*' : '(
| |\s)*'; if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0) $message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0])); array_pop($open_tags); } } // Can't read past the end of the message $pos1 = min(strlen($message), $pos1); // No type means 'parsed_content'. if (!isset($tag['type'])) { $open_tags[] = $tag; // There's no data to change, but maybe do something based on params? $data = null; if (isset($tag['validate'])) $tag['validate']($tag, $data, $disabled, $params); $message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1); $pos += strlen($tag['before']) - 1 + 2; } // Don't parse the content, just skip it. elseif ($tag['type'] == 'unparsed_content') { $pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1); if ($pos2 === false) continue; $data = substr($message, $pos1, $pos2 - $pos1); if (!empty($tag['block_level']) && substr($data, 0, 4) == '
') $data = substr($data, 4); if (isset($tag['validate'])) $tag['validate']($tag, $data, $disabled, $params); $code = strtr($tag['content'], array('$1' => $data)); $message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen); $pos += strlen($code) - 1 + 2; $last_pos = $pos + 1; } // Don't parse the content, just skip it. elseif ($tag['type'] == 'unparsed_equals_content') { // The value may be quoted for some tags - check. if (isset($tag['quoted'])) { $quoted = substr($message, $pos1, 6) == '"'; if ($tag['quoted'] != 'optional' && !$quoted) continue; if ($quoted) $pos1 += 6; } else $quoted = false; $pos2 = strpos($message, $quoted == false ? ']' : '"]', $pos1); if ($pos2 === false) continue; $pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2); if ($pos3 === false) continue; $data = array( substr($message, $pos2 + ($quoted == false ? 1 : 7), $pos3 - ($pos2 + ($quoted == false ? 1 : 7))), substr($message, $pos1, $pos2 - $pos1) ); if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '
') $data[0] = substr($data[0], 4); // Validation for my parking, please! if (isset($tag['validate'])) $tag['validate']($tag, $data, $disabled, $params); $code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1])); $message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen); $pos += strlen($code) - 1 + 2; } // A closed tag, with no content or value. elseif ($tag['type'] == 'closed') { $pos2 = strpos($message, ']', $pos); // Maybe a custom BBC wants to do something special? $data = null; if (isset($tag['validate'])) $tag['validate']($tag, $data, $disabled, $params); $message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1); $pos += strlen($tag['content']) - 1 + 2; } // This one is sorta ugly... :/. Unfortunately, it's needed for flash. elseif ($tag['type'] == 'unparsed_commas_content') { $pos2 = strpos($message, ']', $pos1); if ($pos2 === false) continue; $pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2); if ($pos3 === false) continue; // We want $1 to be the content, and the rest to be csv. $data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1)); $data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1); if (isset($tag['validate'])) $tag['validate']($tag, $data, $disabled, $params); $code = $tag['content']; foreach ($data as $k => $d) $code = strtr($code, array('$' . ($k + 1) => trim($d))); $message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen); $pos += strlen($code) - 1 + 2; } // This has parsed content, and a csv value which is unparsed. elseif ($tag['type'] == 'unparsed_commas') { $pos2 = strpos($message, ']', $pos1); if ($pos2 === false) continue; $data = explode(',', substr($message, $pos1, $pos2 - $pos1)); if (isset($tag['validate'])) $tag['validate']($tag, $data, $disabled, $params); // Fix after, for disabled code mainly. foreach ($data as $k => $d) $tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d))); $open_tags[] = $tag; // Replace them out, $1, $2, $3, $4, etc. $code = $tag['before']; foreach ($data as $k => $d) $code = strtr($code, array('$' . ($k + 1) => trim($d))); $message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1); $pos += strlen($code) - 1 + 2; } // A tag set to a value, parsed or not. elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals') { // The value may be quoted for some tags - check. if (isset($tag['quoted'])) { $quoted = substr($message, $pos1, 6) == '"'; if ($tag['quoted'] != 'optional' && !$quoted) continue; if ($quoted) $pos1 += 6; } else $quoted = false; if ($quoted) { $end_of_value = strpos($message, '"]', $pos1); $nested_tag = strpos($message, '="', $pos1); // Check so this is not just an quoted url ending with a = if ($nested_tag && substr($message, $nested_tag, 8) == '="]') $nested_tag = false; if ($nested_tag && $nested_tag < $end_of_value) // Nested tag with quoted value detected, use next end tag $nested_tag_pos = strpos($message, $quoted == false ? ']' : '"]', $pos1) + 6; } $pos2 = strpos($message, $quoted == false ? ']' : '"]', isset($nested_tag_pos) ? $nested_tag_pos : $pos1); if ($pos2 === false) continue; $data = substr($message, $pos1, $pos2 - $pos1); // Validation for my parking, please! if (isset($tag['validate'])) $tag['validate']($tag, $data, $disabled, $params); // For parsed content, we must recurse to avoid security problems. if ($tag['type'] != 'unparsed_equals') $data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array()); $tag['after'] = strtr($tag['after'], array('$1' => $data)); $open_tags[] = $tag; $code = strtr($tag['before'], array('$1' => $data)); $message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + ($quoted == false ? 1 : 7)); $pos += strlen($code) - 1 + 2; } // If this is block level, eat any breaks after it. if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '
') $message = substr($message, 0, $pos + 1) . substr($message, $pos + 5); // Are we trimming outside this tag? if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(
| |\s)*~', substr($message, $pos + 1), $matches) != 0) $message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0])); } // Close any remaining tags. while ($tag = array_pop($open_tags)) $message .= "\n" . $tag['after'] . "\n"; // Parse the smileys within the parts where it can be done safely. if ($smileys === true) { $message_parts = explode("\n", $message); for ($i = 0, $n = count($message_parts); $i < $n; $i += 2) parsesmileys($message_parts[$i]); $message = implode('', $message_parts); } // No smileys, just get rid of the markers. else $message = strtr($message, array("\n" => '')); if ($message !== '' && $message[0] === ' ') $message = ' ' . substr($message, 1); // Cleanup whitespace. $message = strtr($message, array(' ' => ' ', "\r" => '', "\n" => '
', '
' => '
', ' ' => "\n")); // Allow mods access to what parse_bbc created call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags)); // Cache the output if it took some time... if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05) cache_put_data($cache_key, $message, 240); // If this was a force parse revert if needed. if (!empty($parse_tags)) { $alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex; unset($real_alltags_regex); } elseif (!empty($bbc_codes)) $bbc_lang_locales[$txt['lang_locale']] = $bbc_codes; return $message; } /** * Parse smileys in the passed message. * * The smiley parsing function which makes pretty faces appear :). * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used. * These are specifically not parsed in code tags [url=mailto:Dad@blah.com] * Caches the smileys from the database or array in memory. * Doesn't return anything, but rather modifies message directly. * * @param string &$message The message to parse smileys in */ function parsesmileys(&$message) { global $modSettings, $txt, $user_info, $context, $smcFunc; static $smileyPregSearch = null, $smileyPregReplacements = array(); // No smiley set at all?! if ($user_info['smiley_set'] == 'none' || trim($message) == '') return; // Maybe a mod wants to implement an alternative method (e.g. emojis instead of images) call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements)); // If smileyPregSearch hasn't been set, do it now. if (empty($smileyPregSearch)) { // Cache for longer when customized smiley codes aren't enabled $cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480; // Load the smileys in reverse order by length so they don't get parsed incorrectly. if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null) { $result = $smcFunc['db_query']('', ' SELECT s.code, f.filename, s.description FROM {db_prefix}smileys AS s JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley) WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? ' AND s.code IN ({array_string:default_codes})' : '') . ' ORDER BY LENGTH(s.code) DESC', array( 'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'), 'smiley_set' => $user_info['smiley_set'], ) ); $smileysfrom = array(); $smileysto = array(); $smileysdescs = array(); while ($row = $smcFunc['db_fetch_assoc']($result)) { $smileysfrom[] = $row['code']; $smileysto[] = $smcFunc['htmlspecialchars']($row['filename']); $smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description']; } $smcFunc['db_free_result']($result); cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time); } else list ($smileysfrom, $smileysto, $smileysdescs) = $temp; // The non-breaking-space is a complex thing... $non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0'; // This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:David@bla.com] doesn't parse the :D smiley) $smileyPregReplacements = array(); $searchParts = array(); $smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/'); for ($i = 0, $n = count($smileysfrom); $i < $n; $i++) { $specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES); $smileyCode = ''; $smileyPregReplacements[$smileysfrom[$i]] = $smileyCode; $searchParts[] = $smileysfrom[$i]; if ($smileysfrom[$i] != $specialChars) { $smileyPregReplacements[$specialChars] = $smileyCode; $searchParts[] = $specialChars; // Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not $specialChars2 = preg_replace('/(\d{2});/', '$1;', $specialChars); if ($specialChars2 != $specialChars) { $smileyPregReplacements[$specialChars2] = $smileyCode; $searchParts[] = $specialChars2; } } } $smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?' => "\n", '
' => "\n", "\t" => 'SMF_TAB();', '[' => '['))); $oldlevel = error_reporting(0); $buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true)); error_reporting($oldlevel); // Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P. $buffer = preg_replace('~SMF_TAB(?:(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '' . "\t" . '', $buffer); return strtr($buffer, array('\'' => ''', '' => '', '
' => '')); } /** * Gets the appropriate URL to use for images (or whatever) when using SSL * * The returned URL may or may not be a proxied URL, depending on the situation. * Mods can implement alternative proxies using the 'integrate_proxy' hook. * * @param string $url The original URL of the requested resource * @return string The URL to use */ function get_proxied_url($url) { global $boardurl, $image_proxy_enabled, $image_proxy_secret, $user_info; // Only use the proxy if enabled, and never for robots if (empty($image_proxy_enabled) || !empty($user_info['possibly_robot'])) return $url; $parsedurl = parse_iri($url); // Don't bother with HTTPS URLs, schemeless URLs, or obviously invalid URLs if (empty($parsedurl['scheme']) || empty($parsedurl['host']) || empty($parsedurl['path']) || $parsedurl['scheme'] === 'https') return $url; // We don't need to proxy our own resources if ($parsedurl['host'] === parse_iri($boardurl, PHP_URL_HOST)) return strtr($url, array('http://' => 'https://')); // By default, use SMF's own image proxy script $proxied_url = strtr($boardurl, array('http://' => 'https://')) . '/proxy.php?request=' . urlencode($url) . '&hash=' . hash_hmac('sha1', $url, $image_proxy_secret); // Allow mods to easily implement an alternative proxy // MOD AUTHORS: To add settings UI for your proxy, use the integrate_general_settings hook. call_integration_hook('integrate_proxy', array($url, &$proxied_url)); return $proxied_url; } /** * Make sure the browser doesn't come back and repost the form data. * Should be used whenever anything is posted. * * @param string $setLocation The URL to redirect them to * @param bool $refresh Whether to use a meta refresh instead * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily */ function redirectexit($setLocation = '', $refresh = false, $permanent = false) { global $scripturl, $context, $modSettings, $db_show_debug, $db_cache; // In case we have mail to send, better do that - as obExit doesn't always quite make it... if (!empty($context['flush_mail'])) // @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\ AddMailQueue(true); $add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:'; if ($add) $setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : ''); // Put the session ID in. if (defined('SID') && SID != '') $setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation); // Keep that debug in their for template debugging! elseif (isset($_GET['debug'])) $setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation); if (!empty($modSettings['queryless_urls']) && (empty($context['server']['is_cgi']) || ini_get('cgi.fix_pathinfo') == 1 || @get_cfg_var('cgi.fix_pathinfo') == 1) && (!empty($context['server']['is_apache']) || !empty($context['server']['is_lighttpd']) || !empty($context['server']['is_litespeed']))) { if (defined('SID') && SID != '') $setLocation = preg_replace_callback( '~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&))((?:board|topic)=[^#]+?)(#[^"]*?)?$~', function($m) use ($scripturl) { return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : ""); }, $setLocation ); else $setLocation = preg_replace_callback( '~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~', function($m) use ($scripturl) { return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : ""); }, $setLocation ); } // Maybe integrations want to change where we are heading? call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent)); // Set the header. header('location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302); // Debugging. if (isset($db_show_debug) && $db_show_debug === true) $_SESSION['debug_redirect'] = $db_cache; obExit(false); } /** * Ends execution. Takes care of template loading and remembering the previous URL. * * @param bool $header Whether to do the header * @param bool $do_footer Whether to do the footer * @param bool $from_index Whether we're coming from the board index * @param bool $from_fatal_error Whether we're coming from a fatal error */ function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false) { global $context, $settings, $modSettings, $txt, $smcFunc, $should_log; static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false; // Attempt to prevent a recursive loop. ++$level; if ($level > 1 && !$from_fatal_error && !$has_fatal_error) exit; if ($from_fatal_error) $has_fatal_error = true; // Clear out the stat cache. if (function_exists('trackStats')) trackStats(); // If we have mail to send, send it. if (function_exists('AddMailQueue') && !empty($context['flush_mail'])) // @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\ AddMailQueue(true); $do_header = $header === null ? !$header_done : $header; if ($do_footer === null) $do_footer = $do_header; // Has the template/header been done yet? if ($do_header) { // Was the page title set last minute? Also update the HTML safe one. if (!empty($context['page_title']) && empty($context['page_title_html_safe'])) $context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](html_entity_decode($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : ''); // Start up the session URL fixer. ob_start('ob_sessrewrite'); if (!empty($settings['output_buffers']) && is_string($settings['output_buffers'])) $buffers = explode(',', $settings['output_buffers']); elseif (!empty($settings['output_buffers'])) $buffers = $settings['output_buffers']; else $buffers = array(); if (isset($modSettings['integrate_buffer'])) $buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers); if (!empty($buffers)) foreach ($buffers as $function) { $call = call_helper($function, true); // Is it valid? if (!empty($call)) ob_start($call); } // Display the screen in the logical order. template_header(); $header_done = true; } if ($do_footer) { loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main'); // Anything special to put out? if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml'])) echo $context['insert_after_template']; // Just so we don't get caught in an endless loop of errors from the footer... if (!$footer_done) { $footer_done = true; template_footer(); // (since this is just debugging... it's okay that it's after