commit 4c75893b86b7580764be3eb14f19a56c20edf5fe Author: Antoine Le Gonidec Date: Mon Jul 22 16:45:07 2024 +0200 Import SMF 2.1.4 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..40aa521 --- /dev/null +++ b/LICENSE @@ -0,0 +1,32 @@ +Copyright © 2023 Simple Machines. All rights reserved. + +Developed by: Simple Machines Forum Project +Simple Machines +https://www.simplemachines.org + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of SMF2.1 nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Packages/.htaccess b/Packages/.htaccess new file mode 100644 index 0000000..91386d1 --- /dev/null +++ b/Packages/.htaccess @@ -0,0 +1,5 @@ + + Order Deny,Allow + Deny from all + Allow from localhost + \ No newline at end of file diff --git a/Packages/backups/.htaccess b/Packages/backups/.htaccess new file mode 100644 index 0000000..91386d1 --- /dev/null +++ b/Packages/backups/.htaccess @@ -0,0 +1,5 @@ + + Order Deny,Allow + Deny from all + Allow from localhost + \ No newline at end of file diff --git a/Packages/backups/index.php b/Packages/backups/index.php new file mode 100644 index 0000000..69278ce --- /dev/null +++ b/Packages/backups/index.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/Packages/index.php b/Packages/index.php new file mode 100644 index 0000000..5345f9c --- /dev/null +++ b/Packages/index.php @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/SSI.php b/SSI.php new file mode 100644 index 0000000..5b18535 --- /dev/null +++ b/SSI.php @@ -0,0 +1,2528 @@ +=')) + require_once($sourcedir . '/Subs-Compat.php'); + +// Create a variable to store some SMF specific functions in. +$smcFunc = array(); + +// Initiate the database connection and define some database functions to use. +loadDatabase(); + +/** + * An autoloader for certain classes. + * + * @param string $class The fully-qualified class name. + */ +spl_autoload_register(function($class) use ($sourcedir) +{ + $classMap = array( + 'ReCaptcha\\' => 'ReCaptcha/', + 'MatthiasMullie\\Minify\\' => 'minify/src/', + 'MatthiasMullie\\PathConverter\\' => 'minify/path-converter/src/', + 'SMF\\Cache\\' => 'Cache/', + ); + + // Do any third-party scripts want in on the fun? + call_integration_hook('integrate_autoload', array(&$classMap)); + + foreach ($classMap as $prefix => $dirName) + { + // does the class use the namespace prefix? + $len = strlen($prefix); + if (strncmp($prefix, $class, $len) !== 0) + { + continue; + } + + // get the relative class name + $relativeClass = substr($class, $len); + + // replace the namespace prefix with the base directory, replace namespace + // separators with directory separators in the relative class name, append + // with .php + $fileName = $dirName . strtr($relativeClass, '\\', '/') . '.php'; + + // if the file exists, require it + if (file_exists($fileName = $sourcedir . '/' . $fileName)) + { + require_once $fileName; + + return; + } + } +}); + +// Load installed 'Mods' settings. +reloadSettings(); +// Clean the request variables. +cleanRequest(); + +// Seed the random generator? +if (empty($modSettings['rand_seed']) || mt_rand(1, 250) == 69) + smf_seed_generator(); + +// Check on any hacking attempts. +if (isset($_REQUEST['GLOBALS']) || isset($_COOKIE['GLOBALS'])) + die('No direct access...'); +elseif (isset($_REQUEST['ssi_theme']) && (int) $_REQUEST['ssi_theme'] == (int) $ssi_theme) + die('No direct access...'); +elseif (isset($_COOKIE['ssi_theme']) && (int) $_COOKIE['ssi_theme'] == (int) $ssi_theme) + die('No direct access...'); +elseif (isset($_REQUEST['ssi_layers'], $ssi_layers) && (@get_magic_quotes_gpc() ? stripslashes($_REQUEST['ssi_layers']) : $_REQUEST['ssi_layers']) == $ssi_layers) + die('No direct access...'); +if (isset($_REQUEST['context'])) + die('No direct access...'); + +// Gzip output? (because it must be boolean and true, this can't be hacked.) +if (isset($ssi_gzip) && $ssi_gzip === true && ini_get('zlib.output_compression') != '1' && ini_get('output_handler') != 'ob_gzhandler' && version_compare(PHP_VERSION, '4.2.0', '>=')) + ob_start('ob_gzhandler'); +else + $modSettings['enableCompressedOutput'] = '0'; + +// Primarily, this is to fix the URLs... +ob_start('ob_sessrewrite'); + +// Start the session... known to scramble SSI includes in cases... +if (!headers_sent()) + loadSession(); +else +{ + if (isset($_COOKIE[session_name()]) || isset($_REQUEST[session_name()])) + { + // Make a stab at it, but ignore the E_WARNINGs generated because we can't send headers. + $temp = error_reporting(error_reporting() & !E_WARNING); + loadSession(); + error_reporting($temp); + } + + if (!isset($_SESSION['session_value'])) + { + $_SESSION['session_var'] = substr(md5($smcFunc['random_int']() . session_id() . $smcFunc['random_int']()), 0, rand(7, 12)); + $_SESSION['session_value'] = md5(session_id() . $smcFunc['random_int']()); + } + $sc = $_SESSION['session_value']; +} + +// Get rid of $board and $topic... do stuff loadBoard would do. +unset($board, $topic); +$user_info['is_mod'] = false; +$context['user']['is_mod'] = &$user_info['is_mod']; +$context['linktree'] = array(); + +// Load the user and their cookie, as well as their settings. +loadUserSettings(); + +// Load the current user's permissions.... +loadPermissions(); + +// Load the current or SSI theme. (just use $ssi_theme = id_theme;) +loadTheme(isset($ssi_theme) ? (int) $ssi_theme : 0); + +// @todo: probably not the best place, but somewhere it should be set... +if (!headers_sent()) + header('content-type: text/html; charset=' . (empty($modSettings['global_character_set']) ? (empty($txt['lang_character_set']) ? 'ISO-8859-1' : $txt['lang_character_set']) : $modSettings['global_character_set'])); + +// Take care of any banning that needs to be done. +if (isset($_REQUEST['ssi_ban']) || (isset($ssi_ban) && $ssi_ban === true)) + is_not_banned(); + +// Do we allow guests in here? +if (empty($ssi_guest_access) && empty($modSettings['allow_guestAccess']) && $user_info['is_guest'] && basename($_SERVER['PHP_SELF']) != 'SSI.php') +{ + require_once($sourcedir . '/Subs-Auth.php'); + KickGuest(); + obExit(null, true); +} + +// Load the stuff like the menu bar, etc. +if (isset($ssi_layers)) +{ + $context['template_layers'] = $ssi_layers; + template_header(); +} +else + setupThemeContext(); + +// Make sure they didn't muss around with the settings... but only if it's not cli. +if (isset($_SERVER['REMOTE_ADDR']) && !isset($_SERVER['is_cli']) && session_id() == '') + trigger_error($txt['ssi_session_broken'], E_USER_NOTICE); + +// Without visiting the forum this session variable might not be set on submit. +if (!isset($_SESSION['USER_AGENT']) && (!isset($_GET['ssi_function']) || $_GET['ssi_function'] !== 'pollVote')) + $_SESSION['USER_AGENT'] = $_SERVER['HTTP_USER_AGENT']; + +// Have the ability to easily add functions to SSI. +call_integration_hook('integrate_SSI'); + +// Ignore a call to ssi_* functions if we are not accessing SSI.php directly. +if (basename($_SERVER['PHP_SELF']) == 'SSI.php') +{ + // You shouldn't just access SSI.php directly by URL!! + if (!isset($_GET['ssi_function'])) + die(sprintf($txt['ssi_not_direct'], $user_info['is_admin'] ? '\'' . addslashes(__FILE__) . '\'' : '\'SSI.php\'')); + // Call a function passed by GET. + if (function_exists('ssi_' . $_GET['ssi_function']) && (!empty($modSettings['allow_guestAccess']) || !$user_info['is_guest'])) + call_user_func('ssi_' . $_GET['ssi_function']); + exit; +} + +// To avoid side effects later on. +unset($_GET['ssi_function']); + +error_reporting($ssi_error_reporting); + +return true; + +/** + * This shuts down the SSI and shows the footer. + * + * @return void + */ +function ssi_shutdown() +{ + if (!isset($_GET['ssi_function']) || $_GET['ssi_function'] != 'shutdown') + template_footer(); +} + +/** + * Show the SMF version. + * + * @param string $output_method If 'echo', displays the version, otherwise returns it + * @return void|string Returns nothing if output_method is 'echo', otherwise returns the version + */ +function ssi_version($output_method = 'echo') +{ + if ($output_method == 'echo') + echo SMF_VERSION; + else + return SMF_VERSION; +} + +/** + * Show the full SMF version string. + * + * @param string $output_method If 'echo', displays the full version string, otherwise returns it + * @return void|string Returns nothing if output_method is 'echo', otherwise returns the version string + */ +function ssi_full_version($output_method = 'echo') +{ + if ($output_method == 'echo') + echo SMF_FULL_VERSION; + else + return SMF_FULL_VERSION; +} + +/** + * Show the SMF software year. + * + * @param string $output_method If 'echo', displays the software year, otherwise returns it + * @return void|string Returns nothing if output_method is 'echo', otherwise returns the software year + */ +function ssi_software_year($output_method = 'echo') +{ + if ($output_method == 'echo') + echo SMF_SOFTWARE_YEAR; + else + return SMF_SOFTWARE_YEAR; +} + +/** + * Show the forum copyright. Only used in our ssi_examples files. + * + * @param string $output_method If 'echo', displays the forum copyright, otherwise returns it + * @return void|string Returns nothing if output_method is 'echo', otherwise returns the copyright string + */ +function ssi_copyright($output_method = 'echo') +{ + global $forum_copyright, $scripturl; + + if ($output_method == 'echo') + printf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR, $scripturl); + else + return sprintf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR, $scripturl); +} + +/** + * Display a welcome message, like: Hey, User, you have 0 messages, 0 are new. + * + * @param string $output_method The output method. If 'echo', will display everything. Otherwise returns an array of user info. + * @return void|array Displays a welcome message or returns an array of user data depending on output_method. + */ +function ssi_welcome($output_method = 'echo') +{ + global $context, $txt, $scripturl; + + if ($output_method == 'echo') + { + if ($context['user']['is_guest']) + echo sprintf($txt[$context['can_register'] ? 'welcome_guest_register' : 'welcome_guest'], $context['forum_name_html_safe'], $scripturl . '?action=login', 'return reqOverlayDiv(this.href, ' . JavaScriptEscape($txt['login']) . ');', $scripturl . '?action=signup'); + else + echo $txt['hello_member'], ' ', $context['user']['name'], '', allowedTo('pm_read') ? ', ' . (empty($context['user']['messages']) ? $txt['msg_alert_no_messages'] : (($context['user']['messages'] == 1 ? sprintf($txt['msg_alert_one_message'], $scripturl . '?action=pm') : sprintf($txt['msg_alert_many_message'], $scripturl . '?action=pm', $context['user']['messages'])) . ', ' . ($context['user']['unread_messages'] == 1 ? $txt['msg_alert_one_new'] : sprintf($txt['msg_alert_many_new'], $context['user']['unread_messages'])))) : ''; + } + // Don't echo... then do what?! + else + return $context['user']; +} + +/** + * Display a menu bar, like is displayed at the top of the forum. + * + * @param string $output_method The output method. If 'echo', will display the menu, otherwise returns an array of menu data. + * @return void|array Displays the menu or returns an array of menu data depending on output_method. + */ +function ssi_menubar($output_method = 'echo') +{ + global $context; + + if ($output_method == 'echo') + template_menu(); + // What else could this do? + else + return $context['menu_buttons']; +} + +/** + * Show a logout link. + * + * @param string $redirect_to A URL to redirect the user to after they log out. + * @param string $output_method The output method. If 'echo', shows a logout link, otherwise returns the HTML for it. + * @return void|string Displays a logout link or returns its HTML depending on output_method. + */ +function ssi_logout($redirect_to = '', $output_method = 'echo') +{ + global $context, $txt, $scripturl; + + if ($redirect_to != '') + $_SESSION['logout_url'] = $redirect_to; + + // Guests can't log out. + if ($context['user']['is_guest']) + return false; + + $link = '' . $txt['logout'] . ''; + + if ($output_method == 'echo') + echo $link; + else + return $link; +} + +/** + * Recent post list: [board] Subject by Poster Date + * + * @param int $num_recent How many recent posts to display + * @param null|array $exclude_boards If set, doesn't show posts from the specified boards + * @param null|array $include_boards If set, only includes posts from the specified boards + * @param string $output_method The output method. If 'echo', displays the posts, otherwise returns an array of information about them. + * @param bool $limit_body Whether or not to only show the first 384 characters of each post + * @return void|array Displays a list of recent posts or returns an array of information about them depending on output_method. + */ +function ssi_recentPosts($num_recent = 8, $exclude_boards = null, $include_boards = null, $output_method = 'echo', $limit_body = true) +{ + global $modSettings, $context; + + // Excluding certain boards... + if ($exclude_boards === null && !empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board'])) + $exclude_boards = array($modSettings['recycle_board']); + else + $exclude_boards = empty($exclude_boards) ? array() : (is_array($exclude_boards) ? $exclude_boards : array($exclude_boards)); + + // What about including certain boards - note we do some protection here as pre-2.0 didn't have this parameter. + if (is_array($include_boards) || (int) $include_boards === $include_boards) + { + $include_boards = is_array($include_boards) ? $include_boards : array($include_boards); + } + elseif ($include_boards != null) + { + $include_boards = array(); + } + + // Let's restrict the query boys (and girls) + $query_where = ' + m.id_msg >= {int:min_message_id} + ' . (empty($exclude_boards) ? '' : ' + AND b.id_board NOT IN ({array_int:exclude_boards})') . ' + ' . ($include_boards === null ? '' : ' + AND b.id_board IN ({array_int:include_boards})') . ' + AND {query_wanna_see_board}' . ($modSettings['postmod_active'] ? ' + AND m.approved = {int:is_approved}' : ''); + + $query_where_params = array( + 'is_approved' => 1, + 'include_boards' => $include_boards === null ? '' : $include_boards, + 'exclude_boards' => empty($exclude_boards) ? '' : $exclude_boards, + 'min_message_id' => $modSettings['maxMsgID'] - (!empty($context['min_message_posts']) ? $context['min_message_posts'] : 25) * min($num_recent, 5), + ); + + // Past to this simpleton of a function... + return ssi_queryPosts($query_where, $query_where_params, $num_recent, 'm.id_msg DESC', $output_method, $limit_body); +} + +/** + * Fetches one or more posts by ID. + * + * @param array $post_ids An array containing the IDs of the posts to show + * @param bool $override_permissions Whether to ignore permissions. If true, will show posts even if the user doesn't have permission to see them. + * @param string $output_method The output method. If 'echo', displays the posts, otherwise returns an array of info about them + * @return void|array Displays the specified posts or returns an array of info about them, depending on output_method. + */ +function ssi_fetchPosts($post_ids = array(), $override_permissions = false, $output_method = 'echo') +{ + global $modSettings; + + if (empty($post_ids)) + return; + + // Allow the user to request more than one - why not? + $post_ids = is_array($post_ids) ? $post_ids : array($post_ids); + + // Restrict the posts required... + $query_where = ' + m.id_msg IN ({array_int:message_list})' . ($override_permissions ? '' : ' + AND {query_wanna_see_board}') . ($modSettings['postmod_active'] ? ' + AND m.approved = {int:is_approved}' : ''); + $query_where_params = array( + 'message_list' => $post_ids, + 'is_approved' => 1, + ); + + // Then make the query and dump the data. + return ssi_queryPosts($query_where, $query_where_params, '', 'm.id_msg DESC', $output_method, false, $override_permissions); +} + +/** + * This handles actually pulling post info. Called from other functions to eliminate duplication. + * + * @param string $query_where The WHERE clause for the query + * @param array $query_where_params An array of parameters for the WHERE clause + * @param int $query_limit The maximum number of rows to return + * @param string $query_order The ORDER BY clause for the query + * @param string $output_method The output method. If 'echo', displays the posts, otherwise returns an array of info about them. + * @param bool $limit_body If true, will only show the first 384 characters of the post rather than all of it + * @param bool|false $override_permissions Whether or not to ignore permissions. If true, will show all posts regardless of whether the user can actually see them + * @return void|array Displays the posts or returns an array of info about them, depending on output_method + */ +function ssi_queryPosts($query_where = '', $query_where_params = array(), $query_limit = 10, $query_order = 'm.id_msg DESC', $output_method = 'echo', $limit_body = false, $override_permissions = false) +{ + global $scripturl, $txt, $user_info; + global $modSettings, $smcFunc, $context; + + if (!empty($modSettings['enable_likes'])) + $context['can_like'] = allowedTo('likes_like'); + + // Find all the posts. Newer ones will have higher IDs. + $request = $smcFunc['db_query']('substring', ' + SELECT + m.poster_time, m.subject, m.id_topic, m.id_member, m.id_msg, m.id_board, m.likes, b.name AS board_name, + COALESCE(mem.real_name, m.poster_name) AS poster_name, ' . ($user_info['is_guest'] ? '1 AS is_read, 0 AS new_from' : ' + COALESCE(lt.id_msg, lmr.id_msg, 0) >= m.id_msg_modified AS is_read, + COALESCE(lt.id_msg, lmr.id_msg, -1) + 1 AS new_from') . ', ' . ($limit_body ? 'SUBSTRING(m.body, 1, 384) AS body' : 'm.body') . ', m.smileys_enabled + FROM {db_prefix}messages AS m + INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)' . ($modSettings['postmod_active'] ? ' + INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)' : '') . ' + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)' . (!$user_info['is_guest'] ? ' + LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = m.id_topic AND lt.id_member = {int:current_member}) + LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = m.id_board AND lmr.id_member = {int:current_member})' : '') . ' + WHERE 1=1 ' . ($override_permissions ? '' : ' + AND {query_wanna_see_board}') . ($modSettings['postmod_active'] ? ' + AND m.approved = {int:is_approved} + AND t.approved = {int:is_approved}' : '') . ' + ' . (empty($query_where) ? '' : 'AND ' . $query_where) . ' + ORDER BY ' . $query_order . ' + ' . ($query_limit == '' ? '' : 'LIMIT ' . $query_limit), + array_merge($query_where_params, array( + 'current_member' => $user_info['id'], + 'is_approved' => 1, + )) + ); + $posts = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $row['body'] = parse_bbc($row['body'], $row['smileys_enabled'], $row['id_msg']); + + // Censor it! + censorText($row['subject']); + censorText($row['body']); + + $preview = strip_tags(strtr($row['body'], array('
' => ' '))); + + // Build the array. + $posts[$row['id_msg']] = array( + 'id' => $row['id_msg'], + 'board' => array( + 'id' => $row['id_board'], + 'name' => $row['board_name'], + 'href' => $scripturl . '?board=' . $row['id_board'] . '.0', + 'link' => '' . $row['board_name'] . '' + ), + 'topic' => $row['id_topic'], + 'poster' => array( + 'id' => $row['id_member'], + 'name' => $row['poster_name'], + 'href' => empty($row['id_member']) ? '' : $scripturl . '?action=profile;u=' . $row['id_member'], + 'link' => empty($row['id_member']) ? $row['poster_name'] : '' . $row['poster_name'] . '' + ), + 'subject' => $row['subject'], + 'short_subject' => shorten_subject($row['subject'], 25), + 'preview' => $smcFunc['strlen']($preview) > 128 ? $smcFunc['substr']($preview, 0, 128) . '...' : $preview, + 'body' => $row['body'], + 'time' => timeformat($row['poster_time']), + 'timestamp' => $row['poster_time'], + 'href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . ';topicseen#new', + 'link' => '' . $row['subject'] . '', + 'new' => !empty($row['is_read']), + 'is_new' => empty($row['is_read']), + 'new_from' => $row['new_from'], + ); + + // Get the likes for each message. + if (!empty($modSettings['enable_likes'])) + $posts[$row['id_msg']]['likes'] = array( + 'count' => $row['likes'], + 'you' => in_array($row['id_msg'], prepareLikesContext($row['id_topic'])), + 'can_like' => !$context['user']['is_guest'] && $row['id_member'] != $context['user']['id'] && !empty($context['can_like']), + ); + } + $smcFunc['db_free_result']($request); + + // If mods want to do something with this list of posts, let them do that now. + call_integration_hook('integrate_ssi_queryPosts', array(&$posts)); + + // Just return it. + if ($output_method != 'echo' || empty($posts)) + return $posts; + + echo ' + '; + foreach ($posts as $post) + echo ' + + + + + '; + echo ' +
+ [', $post['board']['link'], '] + + ', $post['subject'], ' + ', $txt['by'], ' ', $post['poster']['link'], ' + ', $post['is_new'] ? '' . $txt['new'] . '' : '', ' + + ', $post['time'], ' +
'; +} + +/** + * Recent topic list: [board] Subject by Poster Date + * + * @param int $num_recent How many recent topics to show + * @param null|array $exclude_boards If set, exclude topics from the specified board(s) + * @param null|array $include_boards If set, only include topics from the specified board(s) + * @param string $output_method The output method. If 'echo', displays a list of topics, otherwise returns an array of info about them + * @return void|array Either displays a list of topics or returns an array of info about them, depending on output_method. + */ +function ssi_recentTopics($num_recent = 8, $exclude_boards = null, $include_boards = null, $output_method = 'echo') +{ + global $settings, $scripturl, $txt, $user_info; + global $modSettings, $smcFunc, $context; + + if ($exclude_boards === null && !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0) + $exclude_boards = array($modSettings['recycle_board']); + else + $exclude_boards = empty($exclude_boards) ? array() : (is_array($exclude_boards) ? $exclude_boards : array($exclude_boards)); + + // Only some boards?. + if (is_array($include_boards) || (int) $include_boards === $include_boards) + { + $include_boards = is_array($include_boards) ? $include_boards : array($include_boards); + } + elseif ($include_boards != null) + { + $output_method = $include_boards; + $include_boards = array(); + } + + $icon_sources = array(); + foreach ($context['stable_icons'] as $icon) + $icon_sources[$icon] = 'images_url'; + + // Find all the posts in distinct topics. Newer ones will have higher IDs. + $request = $smcFunc['db_query']('substring', ' + SELECT + t.id_topic, b.id_board, b.name AS board_name + FROM {db_prefix}topics AS t + INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg) + LEFT JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board) + WHERE t.id_last_msg >= {int:min_message_id}' . (empty($exclude_boards) ? '' : ' + AND b.id_board NOT IN ({array_int:exclude_boards})') . '' . (empty($include_boards) ? '' : ' + AND b.id_board IN ({array_int:include_boards})') . ' + AND {query_wanna_see_board}' . ($modSettings['postmod_active'] ? ' + AND t.approved = {int:is_approved} + AND ml.approved = {int:is_approved}' : '') . ' + ORDER BY t.id_last_msg DESC + LIMIT ' . $num_recent, + array( + 'include_boards' => empty($include_boards) ? '' : $include_boards, + 'exclude_boards' => empty($exclude_boards) ? '' : $exclude_boards, + 'min_message_id' => $modSettings['maxMsgID'] - (!empty($context['min_message_topics']) ? $context['min_message_topics'] : 35) * min($num_recent, 5), + 'is_approved' => 1, + ) + ); + $topics = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $topics[$row['id_topic']] = $row; + $smcFunc['db_free_result']($request); + + // Did we find anything? If not, bail. + if (empty($topics)) + return array(); + + $recycle_board = !empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board']) ? (int) $modSettings['recycle_board'] : 0; + + // Find all the posts in distinct topics. Newer ones will have higher IDs. + $request = $smcFunc['db_query']('substring', ' + SELECT + ml.poster_time, mf.subject, mf.id_topic, ml.id_member, ml.id_msg, t.num_replies, t.num_views, mg.online_color, t.id_last_msg, + COALESCE(mem.real_name, ml.poster_name) AS poster_name, ' . ($user_info['is_guest'] ? '1 AS is_read, 0 AS new_from' : ' + COALESCE(lt.id_msg, lmr.id_msg, 0) >= ml.id_msg_modified AS is_read, + COALESCE(lt.id_msg, lmr.id_msg, -1) + 1 AS new_from') . ', SUBSTRING(ml.body, 1, 384) AS body, ml.smileys_enabled, ml.icon + FROM {db_prefix}topics AS t + INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg) + INNER JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg) + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ml.id_member)' . (!$user_info['is_guest'] ? ' + LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member}) + LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = t.id_board AND lmr.id_member = {int:current_member})' : '') . ' + LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = mem.id_group) + WHERE t.id_topic IN ({array_int:topic_list}) + ORDER BY t.id_last_msg DESC', + array( + 'current_member' => $user_info['id'], + 'topic_list' => array_keys($topics), + ) + ); + $posts = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $row['body'] = strip_tags(strtr(parse_bbc($row['body'], $row['smileys_enabled'], $row['id_msg']), array('
' => ' '))); + if ($smcFunc['strlen']($row['body']) > 128) + $row['body'] = $smcFunc['substr']($row['body'], 0, 128) . '...'; + + // Censor the subject. + censorText($row['subject']); + censorText($row['body']); + + // Recycled icon + if (!empty($recycle_board) && $topics[$row['id_topic']]['id_board'] == $recycle_board) + $row['icon'] = 'recycled'; + + if (!empty($modSettings['messageIconChecks_enable']) && !isset($icon_sources[$row['icon']])) + $icon_sources[$row['icon']] = file_exists($settings['theme_dir'] . '/images/post/' . $row['icon'] . '.png') ? 'images_url' : 'default_images_url'; + elseif (!isset($icon_sources[$row['icon']])) + $icon_sources[$row['icon']] = 'images_url'; + + // Build the array. + $posts[] = array( + 'board' => array( + 'id' => $topics[$row['id_topic']]['id_board'], + 'name' => $topics[$row['id_topic']]['board_name'], + 'href' => $scripturl . '?board=' . $topics[$row['id_topic']]['id_board'] . '.0', + 'link' => '' . $topics[$row['id_topic']]['board_name'] . '', + ), + 'topic' => $row['id_topic'], + 'poster' => array( + 'id' => $row['id_member'], + 'name' => $row['poster_name'], + 'href' => empty($row['id_member']) ? '' : $scripturl . '?action=profile;u=' . $row['id_member'], + 'link' => empty($row['id_member']) ? $row['poster_name'] : '' . $row['poster_name'] . '' + ), + 'subject' => $row['subject'], + 'replies' => $row['num_replies'], + 'views' => $row['num_views'], + 'short_subject' => shorten_subject($row['subject'], 25), + 'preview' => $row['body'], + 'time' => timeformat($row['poster_time']), + 'timestamp' => $row['poster_time'], + 'href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . ';topicseen#new', + 'link' => '' . $row['subject'] . '', + // Retained for compatibility - is technically incorrect! + 'new' => !empty($row['is_read']), + 'is_new' => empty($row['is_read']), + 'new_from' => $row['new_from'], + 'icon' => '' . $row['icon'] . '', + ); + } + $smcFunc['db_free_result']($request); + + // If mods want to do somthing with this list of topics, let them do that now. + call_integration_hook('integrate_ssi_recentTopics', array(&$posts)); + + // Just return it. + if ($output_method != 'echo' || empty($posts)) + return $posts; + + echo ' + '; + foreach ($posts as $post) + echo ' + + + + + '; + echo ' +
+ [', $post['board']['link'], '] + + ', $post['subject'], ' + ', $txt['by'], ' ', $post['poster']['link'], ' + ', !$post['is_new'] ? '' : '' . $txt['new'] . '', ' + + ', $post['time'], ' +
'; +} + +/** + * Shows a list of top posters + * + * @param int $topNumber How many top posters to list + * @param string $output_method The output method. If 'echo', will display a list of users, otherwise returns an array of info about them. + * @return void|array Either displays a list of users or returns an array of info about them, depending on output_method. + */ +function ssi_topPoster($topNumber = 1, $output_method = 'echo') +{ + global $scripturl, $smcFunc; + + // Find the latest poster. + $request = $smcFunc['db_query']('', ' + SELECT id_member, real_name, posts + FROM {db_prefix}members + ORDER BY posts DESC + LIMIT ' . $topNumber, + array( + ) + ); + $return = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $return[] = array( + 'id' => $row['id_member'], + 'name' => $row['real_name'], + 'href' => $scripturl . '?action=profile;u=' . $row['id_member'], + 'link' => '' . $row['real_name'] . '', + 'posts' => $row['posts'] + ); + $smcFunc['db_free_result']($request); + + // If mods want to do somthing with this list of members, let them do that now. + call_integration_hook('integrate_ssi_topPoster', array(&$return)); + + // Just return all the top posters. + if ($output_method != 'echo') + return $return; + + // Make a quick array to list the links in. + $temp_array = array(); + foreach ($return as $member) + $temp_array[] = $member['link']; + + echo implode(', ', $temp_array); +} + +/** + * Shows a list of top boards based on activity + * + * @param int $num_top How many boards to display + * @param string $output_method The output method. If 'echo', displays a list of boards, otherwise returns an array of info about them. + * @return void|array Displays a list of the top boards or returns an array of info about them, depending on output_method. + */ +function ssi_topBoards($num_top = 10, $output_method = 'echo') +{ + global $txt, $scripturl, $user_info, $modSettings, $smcFunc; + + // Find boards with lots of posts. + $request = $smcFunc['db_query']('', ' + SELECT + b.name, b.num_topics, b.num_posts, b.id_board,' . (!$user_info['is_guest'] ? ' 1 AS is_read' : ' + (COALESCE(lb.id_msg, 0) >= b.id_last_msg) AS is_read') . ' + FROM {db_prefix}boards AS b + LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member}) + WHERE {query_wanna_see_board}' . (!empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board']) ? ' + AND b.id_board != {int:recycle_board}' : '') . ' + ORDER BY b.num_posts DESC + LIMIT ' . $num_top, + array( + 'current_member' => $user_info['id'], + 'recycle_board' => !empty($modSettings['recycle_board']) ? (int) $modSettings['recycle_board'] : null, + ) + ); + $boards = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $boards[] = array( + 'id' => $row['id_board'], + 'num_posts' => $row['num_posts'], + 'num_topics' => $row['num_topics'], + 'name' => $row['name'], + 'new' => empty($row['is_read']), + 'href' => $scripturl . '?board=' . $row['id_board'] . '.0', + 'link' => '' . $row['name'] . '' + ); + $smcFunc['db_free_result']($request); + + // If mods want to do somthing with this list of boards, let them do that now. + call_integration_hook('integrate_ssi_topBoards', array(&$boards)); + + // If we shouldn't output or have nothing to output, just jump out. + if ($output_method != 'echo' || empty($boards)) + return $boards; + + echo ' + + + + + + '; + foreach ($boards as $sBoard) + echo ' + + + + + '; + echo ' +
', $txt['board'], '', $txt['board_topics'], '', $txt['posts'], '
', $sBoard['link'], $sBoard['new'] ? ' ' . $txt['new'] . '' : '', '', comma_format($sBoard['num_topics']), '', comma_format($sBoard['num_posts']), '
'; +} + +// Shows the top topics. +/** + * Shows a list of top topics based on views or replies + * + * @param string $type Can be either replies or views + * @param int $num_topics How many topics to display + * @param string $output_method The output method. If 'echo', displays a list of topics, otherwise returns an array of info about them. + * @return void|array Either displays a list of topics or returns an array of info about them, depending on output_method. + */ +function ssi_topTopics($type = 'replies', $num_topics = 10, $output_method = 'echo') +{ + global $txt, $scripturl, $modSettings, $smcFunc; + + if ($modSettings['totalMessages'] > 100000) + { + // @todo Why don't we use {query(_wanna)_see_board}? + $request = $smcFunc['db_query']('', ' + SELECT id_topic + FROM {db_prefix}topics + WHERE num_' . ($type != 'replies' ? 'views' : 'replies') . ' != 0' . ($modSettings['postmod_active'] ? ' + AND approved = {int:is_approved}' : '') . ' + ORDER BY num_' . ($type != 'replies' ? 'views' : 'replies') . ' DESC + LIMIT {int:limit}', + array( + 'is_approved' => 1, + 'limit' => $num_topics > 100 ? ($num_topics + ($num_topics / 2)) : 100, + ) + ); + $topic_ids = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $topic_ids[] = $row['id_topic']; + $smcFunc['db_free_result']($request); + } + else + $topic_ids = array(); + + $request = $smcFunc['db_query']('', ' + SELECT m.subject, m.id_topic, t.num_views, t.num_replies + FROM {db_prefix}topics AS t + INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg) + INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board) + WHERE {query_wanna_see_board}' . ($modSettings['postmod_active'] ? ' + AND t.approved = {int:is_approved}' : '') . (!empty($topic_ids) ? ' + AND t.id_topic IN ({array_int:topic_list})' : '') . (!empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board']) ? ' + AND b.id_board != {int:recycle_board}' : '') . ' + ORDER BY t.num_' . ($type != 'replies' ? 'views' : 'replies') . ' DESC + LIMIT {int:limit}', + array( + 'topic_list' => $topic_ids, + 'is_approved' => 1, + 'recycle_board' => !empty($modSettings['recycle_board']) ? (int) $modSettings['recycle_board'] : null, + 'limit' => $num_topics, + ) + ); + $topics = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + censorText($row['subject']); + + $topics[] = array( + 'id' => $row['id_topic'], + 'subject' => $row['subject'], + 'num_replies' => $row['num_replies'], + 'num_views' => $row['num_views'], + 'href' => $scripturl . '?topic=' . $row['id_topic'] . '.0', + 'link' => '' . $row['subject'] . '', + ); + } + $smcFunc['db_free_result']($request); + + // If mods want to do somthing with this list of topics, let them do that now. + call_integration_hook('integrate_ssi_topTopics', array(&$topics, $type)); + + if ($output_method != 'echo' || empty($topics)) + return $topics; + + echo ' + + + + + + '; + foreach ($topics as $sTopic) + echo ' + + + + + '; + echo ' +
', $txt['views'], '', $txt['replies'], '
+ ', $sTopic['link'], ' + ', comma_format($sTopic['num_views']), '', comma_format($sTopic['num_replies']), '
'; +} + +/** + * Top topics based on replies + * + * @param int $num_topics How many topics to show + * @param string $output_method The output method. If 'echo', displays a list of topics, otherwise returns an array of info about them + * @return void|array Either displays a list of top topics or returns an array of info about them, depending on output_method. + */ +function ssi_topTopicsReplies($num_topics = 10, $output_method = 'echo') +{ + return ssi_topTopics('replies', $num_topics, $output_method); +} + +/** + * Top topics based on views + * + * @param int $num_topics How many topics to show + * @param string $output_method The output method. If 'echo', displays a list of topics, otherwise returns an array of info about them + * @return void|array Either displays a list of top topics or returns an array of info about them, depending on output_method. + */ +function ssi_topTopicsViews($num_topics = 10, $output_method = 'echo') +{ + return ssi_topTopics('views', $num_topics, $output_method); +} + +/** + * Show a link to the latest member: Please welcome, Someone, our latest member. + * + * @param string $output_method The output method. If 'echo', returns a string with a link to the latest member's profile, otherwise returns an array of info about them. + * @return void|array Displays a "welcome" message for the latest member or returns an array of info about them, depending on output_method. + */ +function ssi_latestMember($output_method = 'echo') +{ + global $txt, $context; + + if ($output_method == 'echo') + echo ' + ', sprintf($txt['welcome_newest_member'], $context['common_stats']['latest_member']['link']), '
'; + else + return $context['common_stats']['latest_member']; +} + +/** + * Fetches a random member. + * + * @param string $random_type If 'day', only fetches a new random member once a day. + * @param string $output_method The output method. If 'echo', displays a link to the member's profile, otherwise returns an array of info about them. + * @return void|array Displays a link to a random member's profile or returns an array of info about them depending on output_method. + */ +function ssi_randomMember($random_type = '', $output_method = 'echo') +{ + global $modSettings; + + // If we're looking for something to stay the same each day then seed the generator. + if ($random_type == 'day') + { + // Set the seed to change only once per day. + mt_srand(floor(time() / 86400)); + } + + // Get the lowest ID we're interested in. + $member_id = mt_rand(1, $modSettings['latestMember']); + + $where_query = ' + id_member >= {int:selected_member} + AND is_activated = {int:is_activated}'; + + $query_where_params = array( + 'selected_member' => $member_id, + 'is_activated' => 1, + ); + + $result = ssi_queryMembers($where_query, $query_where_params, 1, 'id_member ASC', $output_method); + + // If we got nothing do the reverse - in case of unactivated members. + if (empty($result)) + { + $where_query = ' + id_member <= {int:selected_member} + AND is_activated = {int:is_activated}'; + + $query_where_params = array( + 'selected_member' => $member_id, + 'is_activated' => 1, + ); + + $result = ssi_queryMembers($where_query, $query_where_params, 1, 'id_member DESC', $output_method); + } + + // Just to be sure put the random generator back to something... random. + if ($random_type != '') + mt_srand(time()); + + return $result; +} + +/** + * Fetch specific members + * + * @param array $member_ids The IDs of the members to fetch + * @param string $output_method The output method. If 'echo', displays a list of links to the members' profiles, otherwise returns an array of info about them. + * @return void|array Displays links to the specified members' profiles or returns an array of info about them, depending on output_method. + */ +function ssi_fetchMember($member_ids = array(), $output_method = 'echo') +{ + if (empty($member_ids)) + return; + + // Can have more than one member if you really want... + $member_ids = is_array($member_ids) ? $member_ids : array($member_ids); + + // Restrict it right! + $query_where = ' + id_member IN ({array_int:member_list})'; + + $query_where_params = array( + 'member_list' => $member_ids, + ); + + // Then make the query and dump the data. + return ssi_queryMembers($query_where, $query_where_params, '', 'id_member', $output_method); +} + +/** + * Get al members in the specified group + * + * @param int $group_id The ID of the group to get members from + * @param string $output_method The output method. If 'echo', returns a list of group members, otherwise returns an array of info about them. + * @return void|array Displays a list of group members or returns an array of info about them, depending on output_method. + */ +function ssi_fetchGroupMembers($group_id = null, $output_method = 'echo') +{ + if ($group_id === null) + return; + + $query_where = ' + id_group = {int:id_group} + OR id_post_group = {int:id_group} + OR FIND_IN_SET({int:id_group}, additional_groups) != 0'; + + $query_where_params = array( + 'id_group' => $group_id, + ); + + return ssi_queryMembers($query_where, $query_where_params, '', 'real_name', $output_method); +} + +/** + * Pulls info about members based on the specified parameters. Used by other functions to eliminate duplication. + * + * @param string $query_where The info for the WHERE clause of the query + * @param array $query_where_params The parameters for the WHERE clause + * @param string|int $query_limit The number of rows to return or an empty string to return all + * @param string $query_order The info for the ORDER BY clause of the query + * @param string $output_method The output method. If 'echo', displays a list of members, otherwise returns an array of info about them + * @return void|array Displays a list of members or returns an array of info about them, depending on output_method. + */ +function ssi_queryMembers($query_where = null, $query_where_params = array(), $query_limit = '', $query_order = 'id_member DESC', $output_method = 'echo') +{ + global $smcFunc, $memberContext; + + if ($query_where === null) + return; + + // Fetch the members in question. + $request = $smcFunc['db_query']('', ' + SELECT id_member + FROM {db_prefix}members + WHERE ' . $query_where . ' + ORDER BY ' . $query_order . ' + ' . ($query_limit == '' ? '' : 'LIMIT ' . $query_limit), + array_merge($query_where_params, array( + )) + ); + $members = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $members[] = $row['id_member']; + $smcFunc['db_free_result']($request); + + if (empty($members)) + return array(); + + // If mods want to do somthing with this list of members, let them do that now. + call_integration_hook('integrate_ssi_queryMembers', array(&$members)); + + // Load the members. + loadMemberData($members); + + // Draw the table! + if ($output_method == 'echo') + echo ' + '; + + $query_members = array(); + foreach ($members as $member) + { + // Load their context data. + if (!loadMemberContext($member)) + continue; + + // Store this member's information. + $query_members[$member] = $memberContext[$member]; + + // Only do something if we're echo'ing. + if ($output_method == 'echo') + echo ' + + + '; + } + + // End the table if appropriate. + if ($output_method == 'echo') + echo ' +
+ ', $query_members[$member]['link'], ' +
', $query_members[$member]['blurb'], ' +
', $query_members[$member]['avatar']['image'], ' +
'; + + // Send back the data. + return $query_members; +} + +/** + * Show some basic stats: Total This: XXXX, etc. + * + * @param string $output_method The output method. If 'echo', displays the stats, otherwise returns an array of info about them + * @return void|array Doesn't return anything if the user can't view stats. Otherwise either displays the stats or returns an array of info about them, depending on output_method. + */ +function ssi_boardStats($output_method = 'echo') +{ + global $txt, $scripturl, $modSettings, $smcFunc; + + if (!allowedTo('view_stats')) + return; + + $totals = array( + 'members' => $modSettings['totalMembers'], + 'posts' => $modSettings['totalMessages'], + 'topics' => $modSettings['totalTopics'] + ); + + $result = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}boards', + array( + ) + ); + list ($totals['boards']) = $smcFunc['db_fetch_row']($result); + $smcFunc['db_free_result']($result); + + $result = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}categories', + array( + ) + ); + list ($totals['categories']) = $smcFunc['db_fetch_row']($result); + $smcFunc['db_free_result']($result); + + // If mods want to do somthing with the board stats, let them do that now. + call_integration_hook('integrate_ssi_boardStats', array(&$totals)); + + if ($output_method != 'echo') + return $totals; + + echo ' + ', $txt['total_members'], ': ', comma_format($totals['members']), '
+ ', $txt['total_posts'], ': ', comma_format($totals['posts']), '
+ ', $txt['total_topics'], ': ', comma_format($totals['topics']), '
+ ', $txt['total_cats'], ': ', comma_format($totals['categories']), '
+ ', $txt['total_boards'], ': ', comma_format($totals['boards']); +} + +/** + * Shows a list of online users: YY Guests, ZZ Users and then a list... + * + * @param string $output_method The output method. If 'echo', displays a list, otherwise returns an array of info about the online users. + * @return void|array Either displays a list of online users or returns an array of info about them, depending on output_method. + */ +function ssi_whosOnline($output_method = 'echo') +{ + global $user_info, $txt, $sourcedir, $settings; + + require_once($sourcedir . '/Subs-MembersOnline.php'); + $membersOnlineOptions = array( + 'show_hidden' => allowedTo('moderate_forum'), + ); + $return = getMembersOnlineStats($membersOnlineOptions); + + // If mods want to do somthing with the list of who is online, let them do that now. + call_integration_hook('integrate_ssi_whosOnline', array(&$return)); + + // Add some redundancy for backwards compatibility reasons. + if ($output_method != 'echo') + return $return + array( + 'users' => $return['users_online'], + 'guests' => $return['num_guests'], + 'hidden' => $return['num_users_hidden'], + 'buddies' => $return['num_buddies'], + 'num_users' => $return['num_users_online'], + 'total_users' => $return['num_users_online'] + $return['num_guests'], + ); + + echo ' + ', comma_format($return['num_guests']), ' ', $return['num_guests'] == 1 ? $txt['guest'] : $txt['guests'], ', ', comma_format($return['num_users_online']), ' ', $return['num_users_online'] == 1 ? $txt['user'] : $txt['users']; + + $bracketList = array(); + if (!empty($user_info['buddies'])) + $bracketList[] = comma_format($return['num_buddies']) . ' ' . ($return['num_buddies'] == 1 ? $txt['buddy'] : $txt['buddies']); + if (!empty($return['num_spiders'])) + $bracketList[] = comma_format($return['num_spiders']) . ' ' . ($return['num_spiders'] == 1 ? $txt['spider'] : $txt['spiders']); + if (!empty($return['num_users_hidden'])) + $bracketList[] = comma_format($return['num_users_hidden']) . ' ' . $txt['hidden']; + + if (!empty($bracketList)) + echo ' (' . implode(', ', $bracketList) . ')'; + + echo '
+ ', implode(', ', $return['list_users_online']); + + // Showing membergroups? + if (!empty($settings['show_group_key']) && !empty($return['online_groups'])) + { + $membergroups = cache_quick_get('membergroup_list', 'Subs-Membergroups.php', 'cache_getMembergroupList', array()); + + $groups = array(); + foreach ($return['online_groups'] as $group) + { + if (isset($membergroups[$group['id']])) + $groups[] = $membergroups[$group['id']]; + } + + echo '
+ [' . implode(']  [', $groups) . ']'; + } +} + +/** + * Just like whosOnline except it also logs the online presence. + * + * @param string $output_method The output method. If 'echo', displays a list, otherwise returns an array of info about the online users. + * @return void|array Either displays a list of online users or returns an aray of info about them, depending on output_method. + */ +function ssi_logOnline($output_method = 'echo') +{ + writeLog(); + + if ($output_method != 'echo') + return ssi_whosOnline($output_method); + else + ssi_whosOnline($output_method); +} + +// Shows a login box. +/** + * Shows a login box + * + * @param string $redirect_to The URL to redirect the user to after they login + * @param string $output_method The output method. If 'echo' and the user is a guest, displays a login box, otherwise returns whether the user is a guest + * @return void|bool Either displays a login box or returns whether the user is a guest, depending on whether the user is logged in and output_method. + */ +function ssi_login($redirect_to = '', $output_method = 'echo') +{ + global $scripturl, $txt, $user_info, $context; + + if ($redirect_to != '') + $_SESSION['login_url'] = $redirect_to; + + if ($output_method != 'echo' || !$user_info['is_guest']) + return $user_info['is_guest']; + + // Create a login token + createToken('login'); + + echo ' +
+ + + + + + + + + + + + +
 
 
+ + + +
+
'; + +} + +/** + * Show the top poll based on votes + * + * @param string $output_method The output method. If 'echo', displays the poll, otherwise returns an array of info about it + * @return void|array Either shows the top poll or returns an array of info about it, depending on output_method. + */ +function ssi_topPoll($output_method = 'echo') +{ + // Just use recentPoll, no need to duplicate code... + return ssi_recentPoll(true, $output_method); +} + +// Show the most recently posted poll. +/** + * Shows the most recent poll + * + * @param bool $topPollInstead Whether to show the top poll (based on votes) instead of the most recent one + * @param string $output_method The output method. If 'echo', displays the poll, otherwise returns an array of info about it. + * @return void|array Either shows the poll or returns an array of info about it, depending on output_method. + */ +function ssi_recentPoll($topPollInstead = false, $output_method = 'echo') +{ + global $txt, $boardurl, $user_info, $context, $smcFunc, $modSettings; + + $boardsAllowed = array_intersect(boardsAllowedTo('poll_view'), boardsAllowedTo('poll_vote')); + + if (empty($boardsAllowed)) + return array(); + + $request = $smcFunc['db_query']('', ' + SELECT p.id_poll, p.question, t.id_topic, p.max_votes, p.guest_vote, p.hide_results, p.expire_time + FROM {db_prefix}polls AS p + INNER JOIN {db_prefix}topics AS t ON (t.id_poll = p.id_poll' . ($modSettings['postmod_active'] ? ' AND t.approved = {int:is_approved}' : '') . ') + INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)' . ($topPollInstead ? ' + INNER JOIN {db_prefix}poll_choices AS pc ON (pc.id_poll = p.id_poll)' : '') . ' + LEFT JOIN {db_prefix}log_polls AS lp ON (lp.id_poll = p.id_poll AND lp.id_member > {int:no_member} AND lp.id_member = {int:current_member}) + WHERE p.voting_locked = {int:voting_opened} + AND (p.expire_time = {int:no_expiration} OR {int:current_time} < p.expire_time) + AND ' . ($user_info['is_guest'] ? 'p.guest_vote = {int:guest_vote_allowed}' : 'lp.id_choice IS NULL') . ' + AND {query_wanna_see_board}' . (!in_array(0, $boardsAllowed) ? ' + AND b.id_board IN ({array_int:boards_allowed_list})' : '') . (!empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board']) ? ' + AND b.id_board != {int:recycle_board}' : '') . ' + ORDER BY ' . ($topPollInstead ? 'pc.votes' : 'p.id_poll') . ' DESC + LIMIT 1', + array( + 'current_member' => $user_info['id'], + 'boards_allowed_list' => $boardsAllowed, + 'is_approved' => 1, + 'guest_vote_allowed' => 1, + 'no_member' => 0, + 'voting_opened' => 0, + 'no_expiration' => 0, + 'current_time' => time(), + 'recycle_board' => !empty($modSettings['recycle_board']) ? (int) $modSettings['recycle_board'] : null, + ) + ); + $row = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + // This user has voted on all the polls. + if (empty($row) || !is_array($row)) + return array(); + + // If this is a guest who's voted we'll through ourselves to show poll to show the results. + if ($user_info['is_guest'] && (!$row['guest_vote'] || (isset($_COOKIE['guest_poll_vote']) && in_array($row['id_poll'], explode(',', $_COOKIE['guest_poll_vote']))))) + return ssi_showPoll($row['id_topic'], $output_method); + + $request = $smcFunc['db_query']('', ' + SELECT COUNT(DISTINCT id_member) + FROM {db_prefix}log_polls + WHERE id_poll = {int:current_poll}', + array( + 'current_poll' => $row['id_poll'], + ) + ); + list ($total) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + $request = $smcFunc['db_query']('', ' + SELECT id_choice, label, votes + FROM {db_prefix}poll_choices + WHERE id_poll = {int:current_poll}', + array( + 'current_poll' => $row['id_poll'], + ) + ); + $sOptions = array(); + while ($rowChoice = $smcFunc['db_fetch_assoc']($request)) + { + censorText($rowChoice['label']); + + $sOptions[$rowChoice['id_choice']] = array($rowChoice['label'], $rowChoice['votes']); + } + $smcFunc['db_free_result']($request); + + // Can they view it? + $is_expired = !empty($row['expire_time']) && $row['expire_time'] < time(); + $allow_view_results = allowedTo('moderate_board') || $row['hide_results'] == 0 || $is_expired; + + $return = array( + 'id' => $row['id_poll'], + 'image' => 'poll', + 'question' => $row['question'], + 'total_votes' => $total, + 'is_locked' => false, + 'topic' => $row['id_topic'], + 'allow_view_results' => $allow_view_results, + 'options' => array() + ); + + // Calculate the percentages and bar lengths... + $divisor = $return['total_votes'] == 0 ? 1 : $return['total_votes']; + foreach ($sOptions as $i => $option) + { + $bar = floor(($option[1] * 100) / $divisor); + $return['options'][$i] = array( + 'id' => 'options-' . ($topPollInstead ? 'top-' : 'recent-') . $i, + 'percent' => $bar, + 'votes' => $option[1], + 'option' => parse_bbc($option[0]), + 'vote_button' => '' + ); + } + + $return['allowed_warning'] = $row['max_votes'] > 1 ? sprintf($txt['poll_options_limit'], min(count($sOptions), $row['max_votes'])) : ''; + + // If mods want to do somthing with this list of polls, let them do that now. + call_integration_hook('integrate_ssi_recentPoll', array(&$return, $topPollInstead)); + + if ($output_method != 'echo') + return $return; + + if ($allow_view_results) + { + echo ' +
+ ', $return['question'], '
+ ', !empty($return['allowed_warning']) ? $return['allowed_warning'] . '
' : ''; + + foreach ($return['options'] as $option) + echo ' +
'; + + echo ' + + + +
'; + } + else + echo $txt['poll_cannot_see']; +} + +/** + * Shows the poll from the specified topic + * + * @param null|int $topic The topic to show the poll from. If null, $_REQUEST['ssi_topic'] will be used instead. + * @param string $output_method The output method. If 'echo', displays the poll, otherwise returns an array of info about it. + * @return void|array Either displays the poll or returns an array of info about it, depending on output_method. + */ +function ssi_showPoll($topic = null, $output_method = 'echo') +{ + global $txt, $boardurl, $user_info, $context, $smcFunc, $modSettings; + + $boardsAllowed = boardsAllowedTo('poll_view'); + + if (empty($boardsAllowed)) + return array(); + + if ($topic === null && isset($_REQUEST['ssi_topic'])) + $topic = (int) $_REQUEST['ssi_topic']; + else + $topic = (int) $topic; + + $request = $smcFunc['db_query']('', ' + SELECT + p.id_poll, p.question, p.voting_locked, p.hide_results, p.expire_time, p.max_votes, p.guest_vote, b.id_board + FROM {db_prefix}topics AS t + INNER JOIN {db_prefix}polls AS p ON (p.id_poll = t.id_poll) + INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board) + WHERE t.id_topic = {int:current_topic} + AND {query_see_board}' . (!in_array(0, $boardsAllowed) ? ' + AND b.id_board IN ({array_int:boards_allowed_see})' : '') . ($modSettings['postmod_active'] ? ' + AND t.approved = {int:is_approved}' : '') . ' + LIMIT 1', + array( + 'current_topic' => $topic, + 'boards_allowed_see' => $boardsAllowed, + 'is_approved' => 1, + ) + ); + + // Either this topic has no poll, or the user cannot view it. + if ($smcFunc['db_num_rows']($request) == 0) + return array(); + + $row = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + // Check if they can vote. + $already_voted = false; + if (!empty($row['expire_time']) && $row['expire_time'] < time()) + $allow_vote = false; + elseif ($user_info['is_guest']) + { + // There's a difference between "allowed to vote" and "already voted"... + $allow_vote = $row['guest_vote']; + + // Did you already vote? + if (isset($_COOKIE['guest_poll_vote']) && in_array($row['id_poll'], explode(',', $_COOKIE['guest_poll_vote']))) + { + $already_voted = true; + } + } + elseif (!empty($row['voting_locked']) || !allowedTo('poll_vote', $row['id_board'])) + $allow_vote = false; + else + { + $request = $smcFunc['db_query']('', ' + SELECT id_member + FROM {db_prefix}log_polls + WHERE id_poll = {int:current_poll} + AND id_member = {int:current_member} + LIMIT 1', + array( + 'current_member' => $user_info['id'], + 'current_poll' => $row['id_poll'], + ) + ); + $allow_vote = $smcFunc['db_num_rows']($request) == 0; + $already_voted = $allow_vote; + $smcFunc['db_free_result']($request); + } + + // Can they view? + $is_expired = !empty($row['expire_time']) && $row['expire_time'] < time(); + $allow_view_results = allowedTo('moderate_board') || $row['hide_results'] == 0 || ($row['hide_results'] == 1 && $already_voted) || $is_expired; + + $request = $smcFunc['db_query']('', ' + SELECT COUNT(DISTINCT id_member) + FROM {db_prefix}log_polls + WHERE id_poll = {int:current_poll}', + array( + 'current_poll' => $row['id_poll'], + ) + ); + list ($total) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + $request = $smcFunc['db_query']('', ' + SELECT id_choice, label, votes + FROM {db_prefix}poll_choices + WHERE id_poll = {int:current_poll}', + array( + 'current_poll' => $row['id_poll'], + ) + ); + $sOptions = array(); + $total_votes = 0; + while ($rowChoice = $smcFunc['db_fetch_assoc']($request)) + { + censorText($rowChoice['label']); + + $sOptions[$rowChoice['id_choice']] = array($rowChoice['label'], $rowChoice['votes']); + $total_votes += $rowChoice['votes']; + } + $smcFunc['db_free_result']($request); + + $return = array( + 'id' => $row['id_poll'], + 'image' => empty($row['voting_locked']) ? 'poll' : 'locked_poll', + 'question' => $row['question'], + 'total_votes' => $total, + 'is_locked' => !empty($row['voting_locked']), + 'allow_vote' => $allow_vote, + 'allow_view_results' => $allow_view_results, + 'topic' => $topic + ); + + // Calculate the percentages and bar lengths... + $divisor = $total_votes == 0 ? 1 : $total_votes; + foreach ($sOptions as $i => $option) + { + $bar = floor(($option[1] * 100) / $divisor); + $return['options'][$i] = array( + 'id' => 'options-' . $i, + 'percent' => $bar, + 'votes' => $option[1], + 'option' => parse_bbc($option[0]), + 'vote_button' => '' + ); + } + + $return['allowed_warning'] = $row['max_votes'] > 1 ? sprintf($txt['poll_options_limit'], min(count($sOptions), $row['max_votes'])) : ''; + + // If mods want to do somthing with this poll, let them do that now. + call_integration_hook('integrate_ssi_showPoll', array(&$return)); + + if ($output_method != 'echo') + return $return; + + if ($return['allow_vote']) + { + echo ' +
+ ', $return['question'], '
+ ', !empty($return['allowed_warning']) ? $return['allowed_warning'] . '
' : ''; + + foreach ($return['options'] as $option) + echo ' +
'; + + echo ' + + + +
'; + } + else + { + echo ' +
+ ', $return['question'], ' +
'; + + foreach ($return['options'] as $option) + { + echo ' +
', $option['option'], '
+
'; + + if ($return['allow_view_results']) + { + echo ' +
+
+
+
+ ', $option['votes'], ' (', $option['percent'], '%)'; + } + + echo ' +
'; + } + + echo ' +
', ($return['allow_view_results'] ? ' + ' . $txt['poll_total_voters'] . ': ' . $return['total_votes'] . '' : ''), ' +
'; + } +} + +/** + * Handles voting in a poll (done automatically) + */ +function ssi_pollVote() +{ + global $context, $db_prefix, $user_info, $sc, $smcFunc, $sourcedir, $modSettings; + + if (!isset($_POST[$context['session_var']]) || $_POST[$context['session_var']] != $sc || empty($_POST['options']) || !isset($_POST['poll'])) + { + echo ' + + + + +« +'; + return; + } + + // This can cause weird errors! (ie. copyright missing.) + checkSession(); + + $_POST['poll'] = (int) $_POST['poll']; + + // Check if they have already voted, or voting is locked. + $request = $smcFunc['db_query']('', ' + SELECT + p.id_poll, p.voting_locked, p.expire_time, p.max_votes, p.guest_vote, + t.id_topic, + COALESCE(lp.id_choice, -1) AS selected + FROM {db_prefix}polls AS p + INNER JOIN {db_prefix}topics AS t ON (t.id_poll = {int:current_poll}) + INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board) + LEFT JOIN {db_prefix}log_polls AS lp ON (lp.id_poll = p.id_poll AND lp.id_member = {int:current_member}) + WHERE p.id_poll = {int:current_poll} + AND {query_see_board}' . ($modSettings['postmod_active'] ? ' + AND t.approved = {int:is_approved}' : '') . ' + LIMIT 1', + array( + 'current_member' => $user_info['id'], + 'current_poll' => $_POST['poll'], + 'is_approved' => 1, + ) + ); + if ($smcFunc['db_num_rows']($request) == 0) + die; + $row = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + if (!empty($row['voting_locked']) || ($row['selected'] != -1 && !$user_info['is_guest']) || (!empty($row['expire_time']) && time() > $row['expire_time'])) + redirectexit('topic=' . $row['id_topic'] . '.0'); + + // Too many options checked? + if (count($_REQUEST['options']) > $row['max_votes']) + redirectexit('topic=' . $row['id_topic'] . '.0'); + + // It's a guest who has already voted? + if ($user_info['is_guest']) + { + // Guest voting disabled? + if (!$row['guest_vote']) + redirectexit('topic=' . $row['id_topic'] . '.0'); + // Already voted? + elseif (isset($_COOKIE['guest_poll_vote']) && in_array($row['id_poll'], explode(',', $_COOKIE['guest_poll_vote']))) + redirectexit('topic=' . $row['id_topic'] . '.0'); + } + + $sOptions = array(); + $inserts = array(); + foreach ($_REQUEST['options'] as $id) + { + $id = (int) $id; + + $sOptions[] = $id; + $inserts[] = array($_POST['poll'], $user_info['id'], $id); + } + + // Add their vote in to the tally. + $smcFunc['db_insert']('insert', + $db_prefix . 'log_polls', + array('id_poll' => 'int', 'id_member' => 'int', 'id_choice' => 'int'), + $inserts, + array('id_poll', 'id_member', 'id_choice') + ); + $smcFunc['db_query']('', ' + UPDATE {db_prefix}poll_choices + SET votes = votes + 1 + WHERE id_poll = {int:current_poll} + AND id_choice IN ({array_int:option_list})', + array( + 'option_list' => $sOptions, + 'current_poll' => $_POST['poll'], + ) + ); + + // Track the vote if a guest. + if ($user_info['is_guest']) + { + $_COOKIE['guest_poll_vote'] = !empty($_COOKIE['guest_poll_vote']) ? ($_COOKIE['guest_poll_vote'] . ',' . $row['id_poll']) : $row['id_poll']; + + require_once($sourcedir . '/Subs-Auth.php'); + $cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies'])); + smf_setcookie('guest_poll_vote', $_COOKIE['guest_poll_vote'], time() + 2500000, $cookie_url[1], $cookie_url[0], false, false); + } + + redirectexit('topic=' . $row['id_topic'] . '.0'); +} + +// Show a search box. +/** + * Shows a search box + * + * @param string $output_method The output method. If 'echo', displays a search box, otherwise returns the URL of the search page. + * @return void|string Displays a search box or returns the URL to the search page depending on output_method. If you don't have permission to search, the function won't return anything. + */ +function ssi_quickSearch($output_method = 'echo') +{ + global $scripturl, $txt, $context; + + if (!allowedTo('search_posts')) + return; + + if ($output_method != 'echo') + return $scripturl . '?action=search'; + + echo ' +
+ +
'; +} + +/** + * Show a random forum news item + * + * @param string $output_method The output method. If 'echo', shows the news item, otherwise returns it. + * @return void|string Shows or returns a random forum news item, depending on output_method. + */ +function ssi_news($output_method = 'echo') +{ + global $context; + + $context['random_news_line'] = !empty($context['news_lines']) ? $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)] : ''; + + // If mods want to do somthing with the news, let them do that now. Don't need to pass the news line itself, since it is already in $context. + call_integration_hook('integrate_ssi_news'); + + if ($output_method != 'echo') + return $context['random_news_line']; + + echo $context['random_news_line']; +} + +/** + * Show today's birthdays. + * + * @param string $output_method The output method. If 'echo', displays a list of users, otherwise returns an array of info about them. + * @return void|array Displays a list of users or returns an array of info about them depending on output_method. + */ +function ssi_todaysBirthdays($output_method = 'echo') +{ + global $scripturl, $modSettings, $user_info; + + if (empty($modSettings['cal_enabled']) || !allowedTo('calendar_view') || !allowedTo('profile_view')) + return; + + $eventOptions = array( + 'include_birthdays' => true, + 'num_days_shown' => empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index'], + ); + $return = cache_quick_get('calendar_index_offset_' . $user_info['time_offset'], 'Subs-Calendar.php', 'cache_getRecentEvents', array($eventOptions)); + + // The ssi_todaysCalendar variants all use the same hook and just pass on $eventOptions so the hooked code can distinguish different cases if necessary + call_integration_hook('integrate_ssi_calendar', array(&$return, $eventOptions)); + + if ($output_method != 'echo') + return $return['calendar_birthdays']; + + foreach ($return['calendar_birthdays'] as $member) + echo ' + ' . $member['name'] . '' . (isset($member['age']) ? ' (' . $member['age'] . ')' : '') . '' . (!$member['is_last'] ? ', ' : ''); +} + +/** + * Shows today's holidays. + * + * @param string $output_method The output method. If 'echo', displays a list of holidays, otherwise returns an array of info about them. + * @return void|array Displays a list of holidays or returns an array of info about them depending on output_method + */ +function ssi_todaysHolidays($output_method = 'echo') +{ + global $modSettings, $user_info; + + if (empty($modSettings['cal_enabled']) || !allowedTo('calendar_view')) + return; + + $eventOptions = array( + 'include_holidays' => true, + 'num_days_shown' => empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index'], + ); + $return = cache_quick_get('calendar_index_offset_' . $user_info['time_offset'], 'Subs-Calendar.php', 'cache_getRecentEvents', array($eventOptions)); + + // The ssi_todaysCalendar variants all use the same hook and just pass on $eventOptions so the hooked code can distinguish different cases if necessary + call_integration_hook('integrate_ssi_calendar', array(&$return, $eventOptions)); + + if ($output_method != 'echo') + return $return['calendar_holidays']; + + echo ' + ', implode(', ', $return['calendar_holidays']); +} + +/** + * Shows today's events. + * + * @param string $output_method The output method. If 'echo', displays a list of events, otherwise returns an array of info about them. + * @return void|array Displays a list of events or returns an array of info about them depending on output_method + */ +function ssi_todaysEvents($output_method = 'echo') +{ + global $modSettings, $user_info; + + if (empty($modSettings['cal_enabled']) || !allowedTo('calendar_view')) + return; + + $eventOptions = array( + 'include_events' => true, + 'num_days_shown' => empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index'], + ); + $return = cache_quick_get('calendar_index_offset_' . $user_info['time_offset'], 'Subs-Calendar.php', 'cache_getRecentEvents', array($eventOptions)); + + // The ssi_todaysCalendar variants all use the same hook and just pass on $eventOptions so the hooked code can distinguish different cases if necessary + call_integration_hook('integrate_ssi_calendar', array(&$return, $eventOptions)); + + if ($output_method != 'echo') + return $return['calendar_events']; + + foreach ($return['calendar_events'] as $event) + { + if ($event['can_edit']) + echo ' + * '; + echo ' + ' . $event['link'] . (!$event['is_last'] ? ', ' : ''); + } +} + +/** + * Shows today's calendar items (events, birthdays and holidays) + * + * @param string $output_method The output method. If 'echo', displays a list of calendar items, otherwise returns an array of info about them. + * @return void|array Displays a list of calendar items or returns an array of info about them depending on output_method + */ +function ssi_todaysCalendar($output_method = 'echo') +{ + global $modSettings, $txt, $scripturl, $user_info; + + if (empty($modSettings['cal_enabled']) || !allowedTo('calendar_view')) + return; + + $eventOptions = array( + 'include_birthdays' => allowedTo('profile_view'), + 'include_holidays' => true, + 'include_events' => true, + 'num_days_shown' => empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index'], + ); + $return = cache_quick_get('calendar_index_offset_' . $user_info['time_offset'], 'Subs-Calendar.php', 'cache_getRecentEvents', array($eventOptions)); + + // The ssi_todaysCalendar variants all use the same hook and just pass on $eventOptions so the hooked code can distinguish different cases if necessary + call_integration_hook('integrate_ssi_calendar', array(&$return, $eventOptions)); + + if ($output_method != 'echo') + return $return; + + if (!empty($return['calendar_holidays'])) + echo ' + ' . $txt['calendar_prompt'] . ' ' . implode(', ', $return['calendar_holidays']) . '
'; + if (!empty($return['calendar_birthdays'])) + { + echo ' + ' . $txt['birthdays_upcoming'] . ' '; + foreach ($return['calendar_birthdays'] as $member) + echo ' + ', $member['name'], '', isset($member['age']) ? ' (' . $member['age'] . ')' : '', '', !$member['is_last'] ? ', ' : ''; + echo ' +
'; + } + if (!empty($return['calendar_events'])) + { + echo ' + ' . $txt['events_upcoming'] . ' '; + foreach ($return['calendar_events'] as $event) + { + if ($event['can_edit']) + echo ' + * '; + echo ' + ' . $event['link'] . (!$event['is_last'] ? ', ' : ''); + } + } +} + +/** + * Show the latest news, with a template... by board. + * + * @param null|int $board The ID of the board to get the info from. Defaults to $board or $_GET['board'] if not set. + * @param null|int $limit How many items to show. Defaults to $_GET['limit'] or 5 if not set. + * @param null|int $start Start with the specified item. Defaults to $_GET['start'] or 0 if not set. + * @param null|int $length How many characters to show from each post. Defaults to $_GET['length'] or 0 (no limit) if not set. + * @param string $output_method The output method. If 'echo', displays the news items, otherwise returns an array of info about them. + * @return void|array Displays the news items or returns an array of info about them, depending on output_method. + */ +function ssi_boardNews($board = null, $limit = null, $start = null, $length = null, $output_method = 'echo') +{ + global $scripturl, $txt, $settings, $modSettings, $context; + global $smcFunc; + + loadLanguage('Stats'); + + // Must be integers.... + if ($limit === null) + $limit = isset($_GET['limit']) ? (int) $_GET['limit'] : 5; + else + $limit = (int) $limit; + + if ($start === null) + $start = isset($_GET['start']) ? (int) $_GET['start'] : 0; + else + $start = (int) $start; + + if ($board !== null) + $board = (int) $board; + elseif (isset($_GET['board'])) + $board = (int) $_GET['board']; + + if ($length === null) + $length = isset($_GET['length']) ? (int) $_GET['length'] : 0; + else + $length = (int) $length; + + $limit = max(0, $limit); + $start = max(0, $start); + + // Make sure guests can see this board. + $request = $smcFunc['db_query']('', ' + SELECT id_board + FROM {db_prefix}boards + WHERE ' . ($board === null ? '' : 'id_board = {int:current_board} + AND ') . 'FIND_IN_SET(-1, member_groups) != 0 + LIMIT 1', + array( + 'current_board' => $board, + ) + ); + if ($smcFunc['db_num_rows']($request) == 0) + { + if ($output_method == 'echo') + die($txt['ssi_no_guests']); + else + return array(); + } + list ($board) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + $icon_sources = array(); + foreach ($context['stable_icons'] as $icon) + $icon_sources[$icon] = 'images_url'; + + if (!empty($modSettings['enable_likes'])) + { + $context['can_like'] = allowedTo('likes_like'); + } + + // Find the post ids. + $request = $smcFunc['db_query']('', ' + SELECT t.id_first_msg + FROM {db_prefix}topics as t + LEFT JOIN {db_prefix}boards as b ON (b.id_board = t.id_board) + WHERE t.id_board = {int:current_board}' . ($modSettings['postmod_active'] ? ' + AND t.approved = {int:is_approved}' : '') . ' + AND {query_see_board} + ORDER BY t.id_first_msg DESC + LIMIT ' . $start . ', ' . $limit, + array( + 'current_board' => $board, + 'is_approved' => 1, + ) + ); + $posts = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $posts[] = $row['id_first_msg']; + $smcFunc['db_free_result']($request); + + if (empty($posts)) + return array(); + + // Find the posts. + $request = $smcFunc['db_query']('', ' + SELECT + m.icon, m.subject, m.body, COALESCE(mem.real_name, m.poster_name) AS poster_name, m.poster_time, m.likes, + t.num_replies, t.id_topic, m.id_member, m.smileys_enabled, m.id_msg, t.locked, t.id_last_msg, m.id_board + FROM {db_prefix}topics AS t + INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg) + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member) + WHERE t.id_first_msg IN ({array_int:post_list}) + ORDER BY t.id_first_msg DESC + LIMIT ' . count($posts), + array( + 'post_list' => $posts, + ) + ); + $return = array(); + $recycle_board = !empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board']) ? (int) $modSettings['recycle_board'] : 0; + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // If we want to limit the length of the post. + if (!empty($length) && $smcFunc['strlen']($row['body']) > $length) + { + $row['body'] = $smcFunc['substr']($row['body'], 0, $length); + $cutoff = false; + + $last_space = strrpos($row['body'], ' '); + $last_open = strrpos($row['body'], '<'); + $last_close = strrpos($row['body'], '>'); + if (empty($last_space) || ($last_space == $last_open + 3 && (empty($last_close) || (!empty($last_close) && $last_close < $last_open))) || $last_space < $last_open || $last_open == $length - 6) + $cutoff = $last_open; + elseif (empty($last_close) || $last_close < $last_open) + $cutoff = $last_space; + + if ($cutoff !== false) + $row['body'] = $smcFunc['substr']($row['body'], 0, $cutoff); + $row['body'] .= '...'; + } + + $row['body'] = parse_bbc($row['body'], $row['smileys_enabled'], $row['id_msg']); + + if (!empty($recycle_board) && $row['id_board'] == $recycle_board) + $row['icon'] = 'recycled'; + + // Check that this message icon is there... + if (!empty($modSettings['messageIconChecks_enable']) && !isset($icon_sources[$row['icon']])) + $icon_sources[$row['icon']] = file_exists($settings['theme_dir'] . '/images/post/' . $row['icon'] . '.png') ? 'images_url' : 'default_images_url'; + elseif (!isset($icon_sources[$row['icon']])) + $icon_sources[$row['icon']] = 'images_url'; + + censorText($row['subject']); + censorText($row['body']); + + $return[] = array( + 'id' => $row['id_topic'], + 'message_id' => $row['id_msg'], + 'icon' => '' . $row['icon'] . '', + 'subject' => $row['subject'], + 'time' => timeformat($row['poster_time']), + 'timestamp' => $row['poster_time'], + 'body' => $row['body'], + 'href' => $scripturl . '?topic=' . $row['id_topic'] . '.0', + 'link' => '' . $row['num_replies'] . ' ' . ($row['num_replies'] == 1 ? $txt['ssi_comment'] : $txt['ssi_comments']) . '', + 'replies' => $row['num_replies'], + 'comment_href' => !empty($row['locked']) ? '' : $scripturl . '?action=post;topic=' . $row['id_topic'] . '.' . $row['num_replies'] . ';last_msg=' . $row['id_last_msg'], + 'comment_link' => !empty($row['locked']) ? '' : '' . $txt['ssi_write_comment'] . '', + 'new_comment' => !empty($row['locked']) ? '' : '' . $txt['ssi_write_comment'] . '', + 'poster' => array( + 'id' => $row['id_member'], + 'name' => $row['poster_name'], + 'href' => !empty($row['id_member']) ? $scripturl . '?action=profile;u=' . $row['id_member'] : '', + 'link' => !empty($row['id_member']) ? '' . $row['poster_name'] . '' : $row['poster_name'] + ), + 'locked' => !empty($row['locked']), + 'is_last' => false, + // Nasty ternary for likes not messing around the "is_last" check. + 'likes' => !empty($modSettings['enable_likes']) ? array( + 'count' => $row['likes'], + 'you' => in_array($row['id_msg'], prepareLikesContext((int) $row['id_topic'])), + 'can_like' => !$context['user']['is_guest'] && $row['id_member'] != $context['user']['id'] && !empty($context['can_like']), + ) : array(), + ); + } + $smcFunc['db_free_result']($request); + + if (empty($return)) + return $return; + + $return[count($return) - 1]['is_last'] = true; + + // If mods want to do somthing with this list of posts, let them do that now. + call_integration_hook('integrate_ssi_boardNews', array(&$return)); + + if ($output_method != 'echo') + return $return; + + foreach ($return as $news) + { + echo ' +
+

+ ', $news['icon'], ' + ', $news['subject'], ' +

+
', $news['time'], ' ', $txt['by'], ' ', $news['poster']['link'], '
+
', $news['body'], '
+ ', $news['link'], $news['locked'] ? '' : ' | ' . $news['comment_link'], ''; + + // Is there any likes to show? + if (!empty($modSettings['enable_likes'])) + { + echo ' + '; + } + + // Close the main div. + echo ' +
'; + + if (!$news['is_last']) + echo ' +
'; + } +} + +/** + * Show the most recent events + * + * @param int $max_events The maximum number of events to show + * @param string $output_method The output method. If 'echo', displays the events, otherwise returns an array of info about them. + * @return void|array Displays the events or returns an array of info about them, depending on output_method. + */ +function ssi_recentEvents($max_events = 7, $output_method = 'echo') +{ + global $user_info, $scripturl, $modSettings, $txt, $context, $smcFunc; + + if (empty($modSettings['cal_enabled']) || !allowedTo('calendar_view')) + return; + + // Find all events which are happening in the near future that the member can see. + $request = $smcFunc['db_query']('', ' + SELECT + cal.id_event, cal.start_date, cal.end_date, cal.title, cal.id_member, cal.id_topic, + cal.start_time, cal.end_time, cal.timezone, cal.location, + cal.id_board, t.id_first_msg, t.approved + FROM {db_prefix}calendar AS cal + LEFT JOIN {db_prefix}boards AS b ON (b.id_board = cal.id_board) + LEFT JOIN {db_prefix}topics AS t ON (t.id_topic = cal.id_topic) + WHERE cal.start_date <= {date:current_date} + AND cal.end_date >= {date:current_date} + AND (cal.id_board = {int:no_board} OR {query_wanna_see_board}) + ORDER BY cal.start_date DESC + LIMIT ' . $max_events, + array( + 'current_date' => smf_strftime('%Y-%m-%d', time()), + 'no_board' => 0, + ) + ); + $return = array(); + $duplicates = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // Check if we've already come by an event linked to this same topic with the same title... and don't display it if we have. + if (!empty($duplicates[$row['title'] . $row['id_topic']])) + continue; + + // Censor the title. + censorText($row['title']); + + if ($row['start_date'] < smf_strftime('%Y-%m-%d', time())) + $date = smf_strftime('%Y-%m-%d', time()); + else + $date = $row['start_date']; + + // If the topic it is attached to is not approved then don't link it. + if (!empty($row['id_first_msg']) && !$row['approved']) + $row['id_board'] = $row['id_topic'] = $row['id_first_msg'] = 0; + + $allday = (empty($row['start_time']) || empty($row['end_time']) || empty($row['timezone']) || !in_array($row['timezone'], timezone_identifiers_list(DateTimeZone::ALL_WITH_BC))) ? true : false; + + $return[$date][] = array( + 'id' => $row['id_event'], + 'title' => $row['title'], + 'location' => $row['location'], + 'can_edit' => allowedTo('calendar_edit_any') || ($row['id_member'] == $user_info['id'] && allowedTo('calendar_edit_own')), + 'modify_href' => $scripturl . '?action=' . ($row['id_board'] == 0 ? 'calendar;sa=post;' : 'post;msg=' . $row['id_first_msg'] . ';topic=' . $row['id_topic'] . '.0;calendar;') . 'eventid=' . $row['id_event'] . ';' . $context['session_var'] . '=' . $context['session_id'], + 'href' => $row['id_board'] == 0 ? '' : $scripturl . '?topic=' . $row['id_topic'] . '.0', + 'link' => $row['id_board'] == 0 ? $row['title'] : '' . $row['title'] . '', + 'start_date' => $row['start_date'], + 'end_date' => $row['end_date'], + 'start_time' => !$allday ? $row['start_time'] : null, + 'end_time' => !$allday ? $row['end_time'] : null, + 'tz' => !$allday ? $row['timezone'] : null, + 'allday' => $allday, + 'is_last' => false + ); + + // Let's not show this one again, huh? + $duplicates[$row['title'] . $row['id_topic']] = true; + } + $smcFunc['db_free_result']($request); + + foreach ($return as $mday => $array) + $return[$mday][count($array) - 1]['is_last'] = true; + + // If mods want to do somthing with this list of events, let them do that now. + call_integration_hook('integrate_ssi_recentEvents', array(&$return)); + + if ($output_method != 'echo' || empty($return)) + return $return; + + // Well the output method is echo. + echo ' + ' . $txt['events'] . ' '; + foreach ($return as $mday => $array) + foreach ($array as $event) + { + if ($event['can_edit']) + echo ' + * '; + + echo ' + ' . $event['link'] . (!$event['is_last'] ? ', ' : ''); + } +} + +/** + * Checks whether the specified password is correct for the specified user. + * + * @param int|string $id The ID or username of a user + * @param string $password The password to check + * @param bool $is_username If true, treats $id as a username rather than a user ID + * @return bool Whether or not the password is correct. + */ +function ssi_checkPassword($id = null, $password = null, $is_username = false) +{ + global $smcFunc; + + // If $id is null, this was most likely called from a query string and should do nothing. + if ($id === null) + return; + + $request = $smcFunc['db_query']('', ' + SELECT passwd, member_name, is_activated + FROM {db_prefix}members + WHERE ' . ($is_username ? 'member_name' : 'id_member') . ' = {string:id} + LIMIT 1', + array( + 'id' => $id, + ) + ); + list ($pass, $user, $active) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + return hash_verify_password($user, $password, $pass) && $active == 1; +} + +/** + * Shows the most recent attachments that the user can see + * + * @param int $num_attachments How many to show + * @param array $attachment_ext Only shows attachments with the specified extensions ('jpg', 'gif', etc.) if set + * @param string $output_method The output method. If 'echo', displays a table with links/info, otherwise returns an array with information about the attachments + * @return void|array Displays a table of attachment info or returns an array containing info about the attachments, depending on output_method. + */ +function ssi_recentAttachments($num_attachments = 10, $attachment_ext = array(), $output_method = 'echo') +{ + global $smcFunc, $modSettings, $scripturl, $txt, $settings; + + // We want to make sure that we only get attachments for boards that we can see *if* any. + $attachments_boards = boardsAllowedTo('view_attachments'); + + // No boards? Adios amigo. + if (empty($attachments_boards)) + return array(); + + // Is it an array? + $attachment_ext = (array) $attachment_ext; + + // Lets build the query. + $request = $smcFunc['db_query']('', ' + SELECT + att.id_attach, att.id_msg, att.filename, COALESCE(att.size, 0) AS filesize, att.downloads, mem.id_member, + COALESCE(mem.real_name, m.poster_name) AS poster_name, m.id_topic, m.subject, t.id_board, m.poster_time, + att.width, att.height' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : ', COALESCE(thumb.id_attach, 0) AS id_thumb, thumb.width AS thumb_width, thumb.height AS thumb_height') . ' + FROM {db_prefix}attachments AS att + INNER JOIN {db_prefix}messages AS m ON (m.id_msg = att.id_msg) + INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic) + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : ' + LEFT JOIN {db_prefix}attachments AS thumb ON (thumb.id_attach = att.id_thumb)') . ' + WHERE att.attachment_type = 0' . ($attachments_boards === array(0) ? '' : ' + AND m.id_board IN ({array_int:boards_can_see})') . (!empty($attachment_ext) ? ' + AND att.fileext IN ({array_string:attachment_ext})' : '') . + (!$modSettings['postmod_active'] || allowedTo('approve_posts') ? '' : ' + AND t.approved = {int:is_approved} + AND m.approved = {int:is_approved} + AND att.approved = {int:is_approved}') . ' + ORDER BY att.id_attach DESC + LIMIT {int:num_attachments}', + array( + 'boards_can_see' => $attachments_boards, + 'attachment_ext' => $attachment_ext, + 'num_attachments' => $num_attachments, + 'is_approved' => 1, + ) + ); + + // We have something. + $attachments = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $filename = preg_replace('~&#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', htmlspecialchars($row['filename'])); + + // Is it an image? + $attachments[$row['id_attach']] = array( + 'member' => array( + 'id' => $row['id_member'], + 'name' => $row['poster_name'], + 'link' => empty($row['id_member']) ? $row['poster_name'] : '' . $row['poster_name'] . '', + ), + 'file' => array( + 'filename' => $filename, + 'filesize' => round($row['filesize'] / 1024, 2) . $txt['kilobyte'], + 'downloads' => $row['downloads'], + 'href' => $scripturl . '?action=dlattach;topic=' . $row['id_topic'] . '.0;attach=' . $row['id_attach'], + 'link' => ' ' . $filename . '', + 'is_image' => !empty($row['width']) && !empty($row['height']) && !empty($modSettings['attachmentShowImages']), + ), + 'topic' => array( + 'id' => $row['id_topic'], + 'subject' => $row['subject'], + 'href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . '#msg' . $row['id_msg'], + 'link' => '' . $row['subject'] . '', + 'time' => timeformat($row['poster_time']), + ), + ); + + // Images. + if ($attachments[$row['id_attach']]['file']['is_image']) + { + $id_thumb = empty($row['id_thumb']) ? $row['id_attach'] : $row['id_thumb']; + $attachments[$row['id_attach']]['file']['image'] = array( + 'id' => $id_thumb, + 'width' => $row['width'], + 'height' => $row['height'], + 'img' => '' . $filename . '', + 'thumb' => '' . $filename . '', + 'href' => $scripturl . '?action=dlattach;topic=' . $row['id_topic'] . '.0;attach=' . $id_thumb . ';image', + 'link' => '' . $filename . '', + ); + } + } + $smcFunc['db_free_result']($request); + + // If mods want to do somthing with this list of attachments, let them do that now. + call_integration_hook('integrate_ssi_recentAttachments', array(&$attachments)); + + // So you just want an array? Here you can have it. + if ($output_method == 'array' || empty($attachments)) + return $attachments; + + // Give them the default. + echo ' + + + + + + + '; + foreach ($attachments as $attach) + echo ' + + + + + + '; + echo ' +
', $txt['file'], '', $txt['posted_by'], '', $txt['downloads'], '', $txt['filesize'], '
', $attach['file']['link'], '', $attach['member']['link'], '', $attach['file']['downloads'], '', $attach['file']['filesize'], '
'; +} + +?> \ No newline at end of file diff --git a/Settings.php b/Settings.php new file mode 100644 index 0000000..16e2b11 --- /dev/null +++ b/Settings.php @@ -0,0 +1,272 @@ +'); + $db_last_error = 0; +} + +if (file_exists(dirname(__FILE__) . '/install.php')) +{ + $secure = false; + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') + $secure = true; + elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' || !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on') + $secure = true; + + if (basename($_SERVER['PHP_SELF']) != 'install.php') + { + header('location: http' . ($secure ? 's' : '') . '://' . (empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] . (empty($_SERVER['SERVER_PORT']) || $_SERVER['SERVER_PORT'] == '80' ? '' : ':' . $_SERVER['SERVER_PORT']) : $_SERVER['HTTP_HOST']) . (strtr(dirname($_SERVER['PHP_SELF']), '\\', '/') == '/' ? '' : strtr(dirname($_SERVER['PHP_SELF']), '\\', '/')) . '/install.php'); + exit; + } +} + +?> \ No newline at end of file diff --git a/Settings_bak.php b/Settings_bak.php new file mode 100644 index 0000000..d422a05 --- /dev/null +++ b/Settings_bak.php @@ -0,0 +1,271 @@ +'); + $db_last_error = 0; +} + +if (file_exists(dirname(__FILE__) . '/install.php')) +{ + $secure = false; + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') + $secure = true; + elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' || !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on') + $secure = true; + + if (basename($_SERVER['PHP_SELF']) != 'install.php') + { + header('location: http' . ($secure ? 's' : '') . '://' . (empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] . (empty($_SERVER['SERVER_PORT']) || $_SERVER['SERVER_PORT'] == '80' ? '' : ':' . $_SERVER['SERVER_PORT']) : $_SERVER['HTTP_HOST']) . (strtr(dirname($_SERVER['PHP_SELF']), '\\', '/') == '/' ? '' : strtr(dirname($_SERVER['PHP_SELF']), '\\', '/')) . '/install.php'); + exit; + } +} + +?> \ No newline at end of file diff --git a/Smileys/alienine/afro.png b/Smileys/alienine/afro.png new file mode 100644 index 0000000..7108774 Binary files /dev/null and b/Smileys/alienine/afro.png differ diff --git a/Smileys/alienine/angel.png b/Smileys/alienine/angel.png new file mode 100644 index 0000000..6f0ad61 Binary files /dev/null and b/Smileys/alienine/angel.png differ diff --git a/Smileys/alienine/angry.png b/Smileys/alienine/angry.png new file mode 100644 index 0000000..da7a563 Binary files /dev/null and b/Smileys/alienine/angry.png differ diff --git a/Smileys/alienine/azn.png b/Smileys/alienine/azn.png new file mode 100644 index 0000000..1f8bd87 Binary files /dev/null and b/Smileys/alienine/azn.png differ diff --git a/Smileys/alienine/blank.png b/Smileys/alienine/blank.png new file mode 100644 index 0000000..1213db9 Binary files /dev/null and b/Smileys/alienine/blank.png differ diff --git a/Smileys/alienine/cheesy.png b/Smileys/alienine/cheesy.png new file mode 100644 index 0000000..cd2fcd9 Binary files /dev/null and b/Smileys/alienine/cheesy.png differ diff --git a/Smileys/alienine/cool.png b/Smileys/alienine/cool.png new file mode 100644 index 0000000..e457e3e Binary files /dev/null and b/Smileys/alienine/cool.png differ diff --git a/Smileys/alienine/cry.png b/Smileys/alienine/cry.png new file mode 100644 index 0000000..cb33eea Binary files /dev/null and b/Smileys/alienine/cry.png differ diff --git a/Smileys/alienine/embarrassed.png b/Smileys/alienine/embarrassed.png new file mode 100644 index 0000000..4f54ee3 Binary files /dev/null and b/Smileys/alienine/embarrassed.png differ diff --git a/Smileys/alienine/evil.png b/Smileys/alienine/evil.png new file mode 100644 index 0000000..2d6cb6b Binary files /dev/null and b/Smileys/alienine/evil.png differ diff --git a/Smileys/alienine/grin.png b/Smileys/alienine/grin.png new file mode 100644 index 0000000..34d0b57 Binary files /dev/null and b/Smileys/alienine/grin.png differ diff --git a/Smileys/alienine/huh.png b/Smileys/alienine/huh.png new file mode 100644 index 0000000..90d4aba Binary files /dev/null and b/Smileys/alienine/huh.png differ diff --git a/Smileys/alienine/index.php b/Smileys/alienine/index.php new file mode 100644 index 0000000..69278ce --- /dev/null +++ b/Smileys/alienine/index.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/Smileys/alienine/kiss.png b/Smileys/alienine/kiss.png new file mode 100644 index 0000000..54e02c4 Binary files /dev/null and b/Smileys/alienine/kiss.png differ diff --git a/Smileys/alienine/laugh.png b/Smileys/alienine/laugh.png new file mode 100644 index 0000000..9a1cec7 Binary files /dev/null and b/Smileys/alienine/laugh.png differ diff --git a/Smileys/alienine/lipsrsealed.png b/Smileys/alienine/lipsrsealed.png new file mode 100644 index 0000000..95fbdd0 Binary files /dev/null and b/Smileys/alienine/lipsrsealed.png differ diff --git a/Smileys/alienine/police.png b/Smileys/alienine/police.png new file mode 100644 index 0000000..200544b Binary files /dev/null and b/Smileys/alienine/police.png differ diff --git a/Smileys/alienine/rolleyes.png b/Smileys/alienine/rolleyes.png new file mode 100644 index 0000000..c1c6c79 Binary files /dev/null and b/Smileys/alienine/rolleyes.png differ diff --git a/Smileys/alienine/sad.png b/Smileys/alienine/sad.png new file mode 100644 index 0000000..afa55b4 Binary files /dev/null and b/Smileys/alienine/sad.png differ diff --git a/Smileys/alienine/shocked.png b/Smileys/alienine/shocked.png new file mode 100644 index 0000000..5c16861 Binary files /dev/null and b/Smileys/alienine/shocked.png differ diff --git a/Smileys/alienine/smiley.png b/Smileys/alienine/smiley.png new file mode 100644 index 0000000..75dc02d Binary files /dev/null and b/Smileys/alienine/smiley.png differ diff --git a/Smileys/alienine/tongue.png b/Smileys/alienine/tongue.png new file mode 100644 index 0000000..b78d0e9 Binary files /dev/null and b/Smileys/alienine/tongue.png differ diff --git a/Smileys/alienine/undecided.png b/Smileys/alienine/undecided.png new file mode 100644 index 0000000..c60f976 Binary files /dev/null and b/Smileys/alienine/undecided.png differ diff --git a/Smileys/alienine/wink.png b/Smileys/alienine/wink.png new file mode 100644 index 0000000..c85449e Binary files /dev/null and b/Smileys/alienine/wink.png differ diff --git a/Smileys/fugue/afro.png b/Smileys/fugue/afro.png new file mode 100644 index 0000000..e73416d Binary files /dev/null and b/Smileys/fugue/afro.png differ diff --git a/Smileys/fugue/angel.png b/Smileys/fugue/angel.png new file mode 100644 index 0000000..b80f5c6 Binary files /dev/null and b/Smileys/fugue/angel.png differ diff --git a/Smileys/fugue/angry.png b/Smileys/fugue/angry.png new file mode 100644 index 0000000..fe81ce7 Binary files /dev/null and b/Smileys/fugue/angry.png differ diff --git a/Smileys/fugue/azn.png b/Smileys/fugue/azn.png new file mode 100644 index 0000000..0f1f536 Binary files /dev/null and b/Smileys/fugue/azn.png differ diff --git a/Smileys/fugue/blank.png b/Smileys/fugue/blank.png new file mode 100644 index 0000000..317fb28 Binary files /dev/null and b/Smileys/fugue/blank.png differ diff --git a/Smileys/fugue/cheesy.png b/Smileys/fugue/cheesy.png new file mode 100644 index 0000000..7a50ed7 Binary files /dev/null and b/Smileys/fugue/cheesy.png differ diff --git a/Smileys/fugue/cool.png b/Smileys/fugue/cool.png new file mode 100644 index 0000000..8c958d8 Binary files /dev/null and b/Smileys/fugue/cool.png differ diff --git a/Smileys/fugue/cry.png b/Smileys/fugue/cry.png new file mode 100644 index 0000000..c8d5138 Binary files /dev/null and b/Smileys/fugue/cry.png differ diff --git a/Smileys/fugue/embarrassed.png b/Smileys/fugue/embarrassed.png new file mode 100644 index 0000000..7e8288e Binary files /dev/null and b/Smileys/fugue/embarrassed.png differ diff --git a/Smileys/fugue/evil.png b/Smileys/fugue/evil.png new file mode 100644 index 0000000..42855df Binary files /dev/null and b/Smileys/fugue/evil.png differ diff --git a/Smileys/fugue/grin.png b/Smileys/fugue/grin.png new file mode 100644 index 0000000..81c1415 Binary files /dev/null and b/Smileys/fugue/grin.png differ diff --git a/Smileys/fugue/huh.png b/Smileys/fugue/huh.png new file mode 100644 index 0000000..75236b1 Binary files /dev/null and b/Smileys/fugue/huh.png differ diff --git a/Smileys/fugue/index.php b/Smileys/fugue/index.php new file mode 100644 index 0000000..69278ce --- /dev/null +++ b/Smileys/fugue/index.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/Smileys/fugue/kiss.png b/Smileys/fugue/kiss.png new file mode 100644 index 0000000..da06331 Binary files /dev/null and b/Smileys/fugue/kiss.png differ diff --git a/Smileys/fugue/laugh.png b/Smileys/fugue/laugh.png new file mode 100644 index 0000000..0c68d52 Binary files /dev/null and b/Smileys/fugue/laugh.png differ diff --git a/Smileys/fugue/lipsrsealed.png b/Smileys/fugue/lipsrsealed.png new file mode 100644 index 0000000..7792aba Binary files /dev/null and b/Smileys/fugue/lipsrsealed.png differ diff --git a/Smileys/fugue/police.png b/Smileys/fugue/police.png new file mode 100644 index 0000000..0ec4928 Binary files /dev/null and b/Smileys/fugue/police.png differ diff --git a/Smileys/fugue/rolleyes.png b/Smileys/fugue/rolleyes.png new file mode 100644 index 0000000..decfb05 Binary files /dev/null and b/Smileys/fugue/rolleyes.png differ diff --git a/Smileys/fugue/sad.png b/Smileys/fugue/sad.png new file mode 100644 index 0000000..56f58c5 Binary files /dev/null and b/Smileys/fugue/sad.png differ diff --git a/Smileys/fugue/shocked.png b/Smileys/fugue/shocked.png new file mode 100644 index 0000000..41a32f8 Binary files /dev/null and b/Smileys/fugue/shocked.png differ diff --git a/Smileys/fugue/smiley.png b/Smileys/fugue/smiley.png new file mode 100644 index 0000000..26d6a89 Binary files /dev/null and b/Smileys/fugue/smiley.png differ diff --git a/Smileys/fugue/tongue.png b/Smileys/fugue/tongue.png new file mode 100644 index 0000000..083444e Binary files /dev/null and b/Smileys/fugue/tongue.png differ diff --git a/Smileys/fugue/undecided.png b/Smileys/fugue/undecided.png new file mode 100644 index 0000000..f6227bf Binary files /dev/null and b/Smileys/fugue/undecided.png differ diff --git a/Smileys/fugue/wink.png b/Smileys/fugue/wink.png new file mode 100644 index 0000000..9c233e2 Binary files /dev/null and b/Smileys/fugue/wink.png differ diff --git a/Smileys/index.php b/Smileys/index.php new file mode 100644 index 0000000..46a9a13 --- /dev/null +++ b/Smileys/index.php @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/Sources/Admin.php b/Sources/Admin.php new file mode 100644 index 0000000..65b4b88 --- /dev/null +++ b/Sources/Admin.php @@ -0,0 +1,971 @@ + + * It initialises all the basic context required for the admin center.
+ * It passes execution onto the relevant admin section.
+ * If the passed section is not found it shows the admin home page. + */ +function AdminMain() +{ + global $txt, $context, $scripturl, $modSettings, $settings; + global $smcFunc, $sourcedir, $options, $boarddir; + + // Load the language and templates.... + loadLanguage('Admin'); + loadTemplate('Admin'); + loadJavaScriptFile('admin.js', array('minimize' => true), 'smf_admin'); + loadCSSFile('admin.css', array(), 'smf_admin'); + + // No indexing evil stuff. + $context['robot_no_index'] = true; + + require_once($sourcedir . '/Subs-Menu.php'); + + // Some preferences. + $context['admin_preferences'] = !empty($options['admin_preferences']) ? $smcFunc['json_decode']($options['admin_preferences'], true) : array(); + + /** @var array $admin_areas Defines the menu structure for the admin center. See {@link Subs-Menu.php Subs-Menu.php} for details! */ + $admin_areas = array( + 'forum' => array( + 'title' => $txt['admin_main'], + 'permission' => array('admin_forum', 'manage_permissions', 'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news', 'manage_boards', 'manage_smileys', 'manage_attachments'), + 'areas' => array( + 'index' => array( + 'label' => $txt['admin_center'], + 'function' => 'AdminHome', + 'icon' => 'administration', + ), + 'credits' => array( + 'label' => $txt['support_credits_title'], + 'function' => 'AdminHome', + 'icon' => 'support', + ), + 'news' => array( + 'label' => $txt['news_title'], + 'file' => 'ManageNews.php', + 'function' => 'ManageNews', + 'icon' => 'news', + 'permission' => array('edit_news', 'send_mail', 'admin_forum'), + 'subsections' => array( + 'editnews' => array($txt['admin_edit_news'], 'edit_news'), + 'mailingmembers' => array($txt['admin_newsletters'], 'send_mail'), + 'settings' => array($txt['settings'], 'admin_forum'), + ), + ), + 'packages' => array( + 'label' => $txt['package'], + 'file' => 'Packages.php', + 'function' => 'Packages', + 'permission' => array('admin_forum'), + 'icon' => 'packages', + 'subsections' => array( + 'browse' => array($txt['browse_packages']), + 'packageget' => array($txt['download_packages'], 'url' => $scripturl . '?action=admin;area=packages;sa=packageget;get'), + 'perms' => array($txt['package_file_perms']), + 'options' => array($txt['package_settings']), + ), + ), + 'search' => array( + 'function' => 'AdminSearch', + 'permission' => array('admin_forum'), + 'select' => 'index' + ), + 'adminlogoff' => array( + 'label' => $txt['admin_logoff'], + 'function' => 'AdminEndSession', + 'enabled' => empty($modSettings['securityDisable']), + 'icon' => 'exit', + ), + + ), + ), + 'config' => array( + 'title' => $txt['admin_config'], + 'permission' => array('admin_forum'), + 'areas' => array( + 'featuresettings' => array( + 'label' => $txt['modSettings_title'], + 'file' => 'ManageSettings.php', + 'function' => 'ModifyFeatureSettings', + 'icon' => 'features', + 'subsections' => array( + 'basic' => array($txt['mods_cat_features']), + 'bbc' => array($txt['manageposts_bbc_settings']), + 'layout' => array($txt['mods_cat_layout']), + 'sig' => array($txt['signature_settings_short']), + 'profile' => array($txt['custom_profile_shorttitle']), + 'likes' => array($txt['likes']), + 'mentions' => array($txt['mentions']), + 'alerts' => array($txt['notifications']), + ), + ), + 'antispam' => array( + 'label' => $txt['antispam_title'], + 'file' => 'ManageSettings.php', + 'function' => 'ModifyAntispamSettings', + 'icon' => 'security', + ), + 'languages' => array( + 'label' => $txt['language_configuration'], + 'file' => 'ManageLanguages.php', + 'function' => 'ManageLanguages', + 'icon' => 'languages', + 'subsections' => array( + 'edit' => array($txt['language_edit']), + 'add' => array($txt['language_add']), + 'settings' => array($txt['language_settings']), + ), + ), + 'current_theme' => array( + 'label' => $txt['theme_current_settings'], + 'file' => 'Themes.php', + 'function' => 'ThemesMain', + 'custom_url' => $scripturl . '?action=admin;area=theme;sa=list;th=' . $settings['theme_id'], + 'icon' => 'current_theme', + ), + 'theme' => array( + 'label' => $txt['theme_admin'], + 'file' => 'Themes.php', + 'function' => 'ThemesMain', + 'custom_url' => $scripturl . '?action=admin;area=theme', + 'icon' => 'themes', + 'subsections' => array( + 'admin' => array($txt['themeadmin_admin_title']), + 'list' => array($txt['themeadmin_list_title']), + 'reset' => array($txt['themeadmin_reset_title']), + 'edit' => array($txt['themeadmin_edit_title']), + ), + ), + 'modsettings' => array( + 'label' => $txt['admin_modifications'], + 'file' => 'ManageSettings.php', + 'function' => 'ModifyModSettings', + 'icon' => 'modifications', + 'subsections' => array( + 'general' => array($txt['mods_cat_modifications_misc']), + // Mod Authors for a "ADD AFTER" on this line. Ensure you end your change with a comma. For example: + // 'shout' => array($txt['shout']), + // Note the comma!! The setting with automatically appear with the first mod to be added. + ), + ), + ), + ), + 'layout' => array( + 'title' => $txt['layout_controls'], + 'permission' => array('manage_boards', 'admin_forum', 'manage_smileys', 'manage_attachments', 'moderate_forum'), + 'areas' => array( + 'manageboards' => array( + 'label' => $txt['admin_boards'], + 'file' => 'ManageBoards.php', + 'function' => 'ManageBoards', + 'icon' => 'boards', + 'permission' => array('manage_boards'), + 'subsections' => array( + 'main' => array($txt['boards_edit']), + 'newcat' => array($txt['mboards_new_cat']), + 'settings' => array($txt['settings'], 'admin_forum'), + ), + ), + 'postsettings' => array( + 'label' => $txt['manageposts'], + 'file' => 'ManagePosts.php', + 'function' => 'ManagePostSettings', + 'permission' => array('admin_forum'), + 'icon' => 'posts', + 'subsections' => array( + 'posts' => array($txt['manageposts_settings']), + 'censor' => array($txt['admin_censored_words']), + 'topics' => array($txt['manageposts_topic_settings']), + 'drafts' => array($txt['manage_drafts']), + ), + ), + 'managecalendar' => array( + 'label' => $txt['manage_calendar'], + 'file' => 'ManageCalendar.php', + 'function' => 'ManageCalendar', + 'icon' => 'calendar', + 'permission' => array('admin_forum'), + 'inactive' => empty($modSettings['cal_enabled']), + 'subsections' => empty($modSettings['cal_enabled']) ? array() : array( + 'holidays' => array($txt['manage_holidays'], 'admin_forum'), + 'settings' => array($txt['calendar_settings'], 'admin_forum'), + ), + ), + 'managesearch' => array( + 'label' => $txt['manage_search'], + 'file' => 'ManageSearch.php', + 'function' => 'ManageSearch', + 'icon' => 'search', + 'permission' => array('admin_forum'), + 'subsections' => array( + 'weights' => array($txt['search_weights']), + 'method' => array($txt['search_method']), + 'settings' => array($txt['settings']), + ), + ), + 'smileys' => array( + 'label' => $txt['smileys_manage'], + 'file' => 'ManageSmileys.php', + 'function' => 'ManageSmileys', + 'icon' => 'smiley', + 'permission' => array('manage_smileys'), + 'subsections' => array( + 'editsets' => array($txt['smiley_sets']), + 'addsmiley' => array($txt['smileys_add'], 'enabled' => !empty($modSettings['smiley_enable'])), + 'editsmileys' => array($txt['smileys_edit'], 'enabled' => !empty($modSettings['smiley_enable'])), + 'setorder' => array($txt['smileys_set_order'], 'enabled' => !empty($modSettings['smiley_enable'])), + 'editicons' => array($txt['icons_edit_message_icons'], 'enabled' => !empty($modSettings['messageIcons_enable'])), + 'settings' => array($txt['settings']), + ), + ), + 'manageattachments' => array( + 'label' => $txt['attachments_avatars'], + 'file' => 'ManageAttachments.php', + 'function' => 'ManageAttachments', + 'icon' => 'attachment', + 'permission' => array('manage_attachments'), + 'subsections' => array( + 'browse' => array($txt['attachment_manager_browse']), + 'attachments' => array($txt['attachment_manager_settings']), + 'avatars' => array($txt['attachment_manager_avatar_settings']), + 'attachpaths' => array($txt['attach_directories']), + 'maintenance' => array($txt['attachment_manager_maintenance']), + ), + ), + 'sengines' => array( + 'label' => $txt['search_engines'], + 'inactive' => empty($modSettings['spider_mode']), + 'file' => 'ManageSearchEngines.php', + 'icon' => 'engines', + 'function' => 'SearchEngines', + 'permission' => 'admin_forum', + 'subsections' => empty($modSettings['spider_mode']) ? array() : array( + 'stats' => array($txt['spider_stats']), + 'logs' => array($txt['spider_logs']), + 'spiders' => array($txt['spiders']), + 'settings' => array($txt['settings']), + ), + ), + ), + ), + 'members' => array( + 'title' => $txt['admin_manage_members'], + 'permission' => array('moderate_forum', 'manage_membergroups', 'manage_bans', 'manage_permissions', 'admin_forum'), + 'areas' => array( + 'viewmembers' => array( + 'label' => $txt['admin_users'], + 'file' => 'ManageMembers.php', + 'function' => 'ViewMembers', + 'icon' => 'members', + 'permission' => array('moderate_forum'), + 'subsections' => array( + 'all' => array($txt['view_all_members']), + 'search' => array($txt['mlist_search']), + ), + ), + 'membergroups' => array( + 'label' => $txt['admin_groups'], + 'file' => 'ManageMembergroups.php', + 'function' => 'ModifyMembergroups', + 'icon' => 'membergroups', + 'permission' => array('manage_membergroups'), + 'subsections' => array( + 'index' => array($txt['membergroups_edit_groups'], 'manage_membergroups'), + 'add' => array($txt['membergroups_new_group'], 'manage_membergroups'), + 'settings' => array($txt['settings'], 'admin_forum'), + ), + ), + 'permissions' => array( + 'label' => $txt['edit_permissions'], + 'file' => 'ManagePermissions.php', + 'function' => 'ModifyPermissions', + 'icon' => 'permissions', + 'permission' => array('manage_permissions'), + 'subsections' => array( + 'index' => array($txt['permissions_groups'], 'manage_permissions'), + 'board' => array($txt['permissions_boards'], 'manage_permissions'), + 'profiles' => array($txt['permissions_profiles'], 'manage_permissions'), + 'postmod' => array($txt['permissions_post_moderation'], 'manage_permissions'), + 'settings' => array($txt['settings'], 'admin_forum'), + ), + ), + 'regcenter' => array( + 'label' => $txt['registration_center'], + 'file' => 'ManageRegistration.php', + 'function' => 'RegCenter', + 'icon' => 'regcenter', + 'permission' => array('admin_forum', 'moderate_forum'), + 'subsections' => array( + 'register' => array($txt['admin_browse_register_new'], 'moderate_forum'), + 'agreement' => array($txt['registration_agreement'], 'admin_forum'), + 'policy' => array($txt['privacy_policy'], 'admin_forum'), + 'reservednames' => array($txt['admin_reserved_set'], 'admin_forum'), + 'settings' => array($txt['settings'], 'admin_forum'), + ), + ), + 'warnings' => array( + 'label' => $txt['warnings'], + 'file' => 'ManageSettings.php', + 'function' => 'ModifyWarningSettings', + 'icon' => 'warning', + 'inactive' => $modSettings['warning_settings'][0] == 0, + 'permission' => array('admin_forum'), + ), + 'ban' => array( + 'label' => $txt['ban_title'], + 'file' => 'ManageBans.php', + 'function' => 'Ban', + 'icon' => 'ban', + 'permission' => 'manage_bans', + 'subsections' => array( + 'list' => array($txt['ban_edit_list']), + 'add' => array($txt['ban_add_new']), + 'browse' => array($txt['ban_trigger_browse']), + 'log' => array($txt['ban_log']), + ), + ), + 'paidsubscribe' => array( + 'label' => $txt['paid_subscriptions'], + 'inactive' => empty($modSettings['paid_enabled']), + 'file' => 'ManagePaid.php', + 'icon' => 'paid', + 'function' => 'ManagePaidSubscriptions', + 'permission' => 'admin_forum', + 'subsections' => empty($modSettings['paid_enabled']) ? array() : array( + 'view' => array($txt['paid_subs_view']), + 'settings' => array($txt['settings']), + ), + ), + ), + ), + 'maintenance' => array( + 'title' => $txt['admin_maintenance'], + 'permission' => array('admin_forum'), + 'areas' => array( + 'serversettings' => array( + 'label' => $txt['admin_server_settings'], + 'file' => 'ManageServer.php', + 'function' => 'ModifySettings', + 'icon' => 'server', + 'subsections' => array( + 'general' => array($txt['general_settings']), + 'database' => array($txt['database_settings']), + 'cookie' => array($txt['cookies_sessions_settings']), + 'security' => array($txt['security_settings']), + 'cache' => array($txt['caching_settings']), + 'export' => array($txt['export_settings']), + 'loads' => array($txt['load_balancing_settings']), + 'phpinfo' => array($txt['phpinfo_settings']), + ), + ), + 'maintain' => array( + 'label' => $txt['maintain_title'], + 'file' => 'ManageMaintenance.php', + 'icon' => 'maintain', + 'function' => 'ManageMaintenance', + 'subsections' => array( + 'routine' => array($txt['maintain_sub_routine'], 'admin_forum'), + 'database' => array($txt['maintain_sub_database'], 'admin_forum'), + 'members' => array($txt['maintain_sub_members'], 'admin_forum'), + 'topics' => array($txt['maintain_sub_topics'], 'admin_forum'), + 'hooks' => array($txt['hooks_title_list'], 'admin_forum'), + ), + ), + 'scheduledtasks' => array( + 'label' => $txt['maintain_tasks'], + 'file' => 'ManageScheduledTasks.php', + 'icon' => 'scheduled', + 'function' => 'ManageScheduledTasks', + 'subsections' => array( + 'tasks' => array($txt['maintain_tasks'], 'admin_forum'), + 'tasklog' => array($txt['scheduled_log'], 'admin_forum'), + 'settings' => array($txt['scheduled_tasks_settings'], 'admin_forum'), + ), + ), + 'mailqueue' => array( + 'label' => $txt['mailqueue_title'], + 'file' => 'ManageMail.php', + 'function' => 'ManageMail', + 'icon' => 'mail', + 'subsections' => array( + 'browse' => array($txt['mailqueue_browse'], 'admin_forum'), + 'settings' => array($txt['mailqueue_settings'], 'admin_forum'), + 'test' => array($txt['mailqueue_test'], 'admin_forum'), + ), + ), + 'reports' => array( + 'label' => $txt['generate_reports'], + 'file' => 'Reports.php', + 'function' => 'ReportsMain', + 'icon' => 'reports', + ), + 'logs' => array( + 'label' => $txt['logs'], + 'function' => 'AdminLogs', + 'icon' => 'logs', + 'subsections' => array( + 'errorlog' => array($txt['errorlog'], 'admin_forum', 'enabled' => !empty($modSettings['enableErrorLogging']), 'url' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc'), + 'adminlog' => array($txt['admin_log'], 'admin_forum', 'enabled' => !empty($modSettings['adminlog_enabled'])), + 'modlog' => array($txt['moderation_log'], 'admin_forum', 'enabled' => !empty($modSettings['modlog_enabled'])), + 'banlog' => array($txt['ban_log'], 'manage_bans'), + 'spiderlog' => array($txt['spider_logs'], 'admin_forum', 'enabled' => !empty($modSettings['spider_mode'])), + 'tasklog' => array($txt['scheduled_log'], 'admin_forum'), + 'settings' => array($txt['log_settings'], 'admin_forum'), + ), + ), + 'repairboards' => array( + 'label' => $txt['admin_repair'], + 'file' => 'RepairBoards.php', + 'function' => 'RepairBoards', + 'select' => 'maintain', + 'hidden' => true, + ), + ), + ), + ); + + // Any files to include for administration? + if (!empty($modSettings['integrate_admin_include'])) + { + $admin_includes = explode(',', $modSettings['integrate_admin_include']); + foreach ($admin_includes as $include) + { + $include = strtr(trim($include), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir'])); + if (file_exists($include)) + require_once($include); + } + } + + // Make sure the administrator has a valid session... + validateSession(); + + // Actually create the menu! + $admin_include_data = createMenu($admin_areas, array('do_big_icons' => true)); + unset($admin_areas); + + // Nothing valid? + if ($admin_include_data == false) + fatal_lang_error('no_access', false); + + // Build the link tree. + $context['linktree'][] = array( + 'url' => $scripturl . '?action=admin', + 'name' => $txt['admin_center'], + ); + if (isset($admin_include_data['current_area']) && $admin_include_data['current_area'] != 'index') + $context['linktree'][] = array( + 'url' => $scripturl . '?action=admin;area=' . $admin_include_data['current_area'] . ';' . $context['session_var'] . '=' . $context['session_id'], + 'name' => $admin_include_data['label'], + ); + if (!empty($admin_include_data['current_subsection']) && $admin_include_data['subsections'][$admin_include_data['current_subsection']][0] != $admin_include_data['label']) + $context['linktree'][] = array( + 'url' => $scripturl . '?action=admin;area=' . $admin_include_data['current_area'] . ';sa=' . $admin_include_data['current_subsection'] . ';' . $context['session_var'] . '=' . $context['session_id'], + 'name' => $admin_include_data['subsections'][$admin_include_data['current_subsection']][0], + ); + + // Make a note of the Unique ID for this menu. + $context['admin_menu_id'] = $context['max_menu_id']; + $context['admin_menu_name'] = 'menu_data_' . $context['admin_menu_id']; + + // Where in the admin are we? + $context['admin_area'] = $admin_include_data['current_area']; + + // Now - finally - call the right place! + if (isset($admin_include_data['file'])) + require_once($sourcedir . '/' . $admin_include_data['file']); + + // Get the right callable. + $call = call_helper($admin_include_data['function'], true); + + // Is it valid? + if (!empty($call)) + call_user_func($call); +} + +/** + * The main administration section. + * It prepares all the data necessary for the administration front page. + * It uses the Admin template along with the admin sub template. + * It requires the moderate_forum, manage_membergroups, manage_bans, + * admin_forum, manage_permissions, manage_attachments, manage_smileys, + * manage_boards, edit_news, or send_mail permission. + * It uses the index administrative area. + * It can be found by going to ?action=admin. + */ +function AdminHome() +{ + global $sourcedir, $txt, $scripturl, $context, $user_info; + + // You have to be able to do at least one of the below to see this page. + isAllowedTo(array('admin_forum', 'manage_permissions', 'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news', 'manage_boards', 'manage_smileys', 'manage_attachments')); + + // Find all of this forum's administrators... + require_once($sourcedir . '/Subs-Membergroups.php'); + if (listMembergroupMembers_Href($context['administrators'], 1, 32) && allowedTo('manage_membergroups')) + { + // Add a 'more'-link if there are more than 32. + $context['more_admins_link'] = '' . $txt['more'] . ''; + } + + // Load the credits stuff. + require_once($sourcedir . '/Who.php'); + Credits(true); + + // This makes it easier to get the latest news with your time format. + $context['time_format'] = urlencode($user_info['time_format']); + $context['forum_version'] = SMF_FULL_VERSION; + + // Get a list of current server versions. + require_once($sourcedir . '/Subs-Admin.php'); + $checkFor = array( + 'gd', + 'imagemagick', + 'db_server', + 'apcu', + 'memcacheimplementation', + 'memcachedimplementation', + 'postgres', + 'sqlite', + 'zend', + 'filebased', + 'php', + 'server', + ); + $context['current_versions'] = getServerVersions($checkFor); + + $context['can_admin'] = allowedTo('admin_forum'); + + $context['sub_template'] = $context['admin_area'] == 'credits' ? 'credits' : 'admin'; + $context['page_title'] = $context['admin_area'] == 'credits' ? $txt['support_credits_title'] : $txt['admin_center']; + if ($context['admin_area'] != 'credits') + $context[$context['admin_menu_name']]['tab_data'] = array( + 'title' => $txt['admin_center'], + 'help' => '', + 'description' => '' . $txt['hello_guest'] . ' ' . $context['user']['name'] . '! + ' . sprintf($txt['admin_main_welcome'], $txt['admin_center'], $txt['help'], $txt['help']), + ); + + // Lastly, fill in the blanks in the support resources paragraphs. + $txt['support_resources_p1'] = sprintf($txt['support_resources_p1'], + 'https://wiki.simplemachines.org/', + 'https://wiki.simplemachines.org/smf/features2', + 'https://wiki.simplemachines.org/smf/options2', + 'https://wiki.simplemachines.org/smf/themes2', + 'https://wiki.simplemachines.org/smf/packages2' + ); + $txt['support_resources_p2'] = sprintf($txt['support_resources_p2'], + 'https://www.simplemachines.org/community/', + 'https://www.simplemachines.org/redirect/english_support', + 'https://www.simplemachines.org/redirect/international_support_boards', + 'https://www.simplemachines.org/redirect/smf_support', + 'https://www.simplemachines.org/redirect/customize_support' + ); + + if ($context['admin_area'] == 'admin') + loadJavaScriptFile('admin.js', array('defer' => false, 'minimize' => true), 'smf_admin'); +} + +/** + * Get one of the admin information files from Simple Machines. + */ +function DisplayAdminFile() +{ + global $context, $modSettings, $smcFunc; + + setMemoryLimit('32M'); + + if (empty($_REQUEST['filename']) || !is_string($_REQUEST['filename'])) + fatal_lang_error('no_access', false); + + // Strip off the forum cache part or we won't find it... + $_REQUEST['filename'] = str_replace($context['browser_cache'], '', $_REQUEST['filename']); + + $request = $smcFunc['db_query']('', ' + SELECT data, filetype + FROM {db_prefix}admin_info_files + WHERE filename = {string:current_filename} + LIMIT 1', + array( + 'current_filename' => $_REQUEST['filename'], + ) + ); + + if ($smcFunc['db_num_rows']($request) == 0) + fatal_lang_error('admin_file_not_found', true, array($_REQUEST['filename']), 404); + + list ($file_data, $filetype) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // @todo Temp + // Figure out if sesc is still being used. + if (strpos($file_data, ';sesc=') !== false && $filetype == 'text/javascript') + $file_data = ' +if (!(\'smfForum_sessionvar\' in window)) + window.smfForum_sessionvar = \'sesc\'; +' . strtr($file_data, array(';sesc=' => ';\' + window.smfForum_sessionvar + \'=')); + + $context['template_layers'] = array(); + // Lets make sure we aren't going to output anything nasty. + @ob_end_clean(); + if (!empty($modSettings['enableCompressedOutput'])) + @ob_start('ob_gzhandler'); + else + @ob_start(); + + // Make sure they know what type of file we are. + header('content-type: ' . $filetype); + echo $file_data; + obExit(false); +} + +/** + * This function allocates out all the search stuff. + */ +function AdminSearch() +{ + global $txt, $context, $smcFunc, $sourcedir; + + isAllowedTo('admin_forum'); + + // What can we search for? + $subActions = array( + 'internal' => 'AdminSearchInternal', + 'online' => 'AdminSearchOM', + 'member' => 'AdminSearchMember', + ); + + $context['search_type'] = !isset($_REQUEST['search_type']) || !isset($subActions[$_REQUEST['search_type']]) ? 'internal' : $_REQUEST['search_type']; + $context['search_term'] = isset($_REQUEST['search_term']) ? $smcFunc['htmlspecialchars']($_REQUEST['search_term'], ENT_QUOTES) : ''; + + $context['sub_template'] = 'admin_search_results'; + $context['page_title'] = $txt['admin_search_results']; + + // Keep track of what the admin wants. + if (empty($context['admin_preferences']['sb']) || $context['admin_preferences']['sb'] != $context['search_type']) + { + $context['admin_preferences']['sb'] = $context['search_type']; + + // Update the preferences. + require_once($sourcedir . '/Subs-Admin.php'); + updateAdminPreferences(); + } + + if (trim($context['search_term']) == '') + $context['search_results'] = array(); + else + call_helper($subActions[$context['search_type']]); +} + +/** + * A complicated but relatively quick internal search. + */ +function AdminSearchInternal() +{ + global $context, $txt, $helptxt, $scripturl, $sourcedir; + + // Try to get some more memory. + setMemoryLimit('128M'); + + // Load a lot of language files. + $language_files = array( + 'Help', 'ManageMail', 'ManageSettings', 'ManageCalendar', 'ManageBoards', 'ManagePaid', 'ManagePermissions', 'Search', + 'Login', 'ManageSmileys', 'Drafts', + ); + + // All the files we need to include. + $include_files = array( + 'ManageSettings', 'ManageBoards', 'ManageNews', 'ManageAttachments', 'ManageCalendar', 'ManageMail', 'ManagePaid', 'ManagePermissions', + 'ManagePosts', 'ManageRegistration', 'ManageSearch', 'ManageSearchEngines', 'ManageServer', 'ManageSmileys', 'ManageLanguages', + ); + + // This is a special array of functions that contain setting data - we query all these to simply pull all setting bits! + $settings_search = array( + array('ModifyBasicSettings', 'area=featuresettings;sa=basic'), + array('ModifyBBCSettings', 'area=featuresettings;sa=bbc'), + array('ModifyLayoutSettings', 'area=featuresettings;sa=layout'), + array('ModifyLikesSettings', 'area=featuresettings;sa=likes'), + array('ModifyMentionsSettings', 'area=featuresettings;sa=mentions'), + array('ModifySignatureSettings', 'area=featuresettings;sa=sig'), + array('ModifyAntispamSettings', 'area=antispam'), + array('ModifyWarningSettings', 'area=warnings'), + array('ModifyGeneralModSettings', 'area=modsettings;sa=general'), + // Mod authors if you want to be "real freaking good" then add any setting pages for your mod BELOW this line! + array('ManageAttachmentSettings', 'area=manageattachments;sa=attachments'), + array('ManageAvatarSettings', 'area=manageattachments;sa=avatars'), + array('ModifyCalendarSettings', 'area=managecalendar;sa=settings'), + array('EditBoardSettings', 'area=manageboards;sa=settings'), + array('ModifyMailSettings', 'area=mailqueue;sa=settings'), + array('ModifyNewsSettings', 'area=news;sa=settings'), + array('GeneralPermissionSettings', 'area=permissions;sa=settings'), + array('ModifyPostSettings', 'area=postsettings;sa=posts'), + array('ModifyTopicSettings', 'area=postsettings;sa=topics'), + array('ModifyDraftSettings', 'area=postsettings;sa=drafts'), + array('EditSearchSettings', 'area=managesearch;sa=settings'), + array('EditSmileySettings', 'area=smileys;sa=settings'), + array('ModifyGeneralSettings', 'area=serversettings;sa=general'), + array('ModifyDatabaseSettings', 'area=serversettings;sa=database'), + array('ModifyCookieSettings', 'area=serversettings;sa=cookie'), + array('ModifyGeneralSecuritySettings', 'area=serversettings;sa=security'), + array('ModifyCacheSettings', 'area=serversettings;sa=cache'), + array('ModifyLanguageSettings', 'area=languages;sa=settings'), + array('ModifyRegistrationSettings', 'area=regcenter;sa=settings'), + array('ManageSearchEngineSettings', 'area=sengines;sa=settings'), + array('ModifySubscriptionSettings', 'area=paidsubscribe;sa=settings'), + array('ModifyLogSettings', 'area=logs;sa=settings'), + ); + + call_integration_hook('integrate_admin_search', array(&$language_files, &$include_files, &$settings_search)); + + loadLanguage(implode('+', $language_files)); + + foreach ($include_files as $file) + require_once($sourcedir . '/' . $file . '.php'); + + /* This is the huge array that defines everything... it's a huge array of items formatted as follows: + 0 = Language index (Can be array of indexes) to search through for this setting. + 1 = URL for this indexes page. + 2 = Help index for help associated with this item (If different from 0) + */ + + $search_data = array( + // All the major sections of the forum. + 'sections' => array( + ), + 'settings' => array( + array('COPPA', 'area=regcenter;sa=settings'), + array('CAPTCHA', 'area=antispam'), + ), + ); + + // Go through the admin menu structure trying to find suitably named areas! + foreach ($context[$context['admin_menu_name']]['sections'] as $section) + { + foreach ($section['areas'] as $menu_key => $menu_item) + { + $search_data['sections'][] = array($menu_item['label'], 'area=' . $menu_key); + if (!empty($menu_item['subsections'])) + foreach ($menu_item['subsections'] as $key => $sublabel) + { + if (isset($sublabel['label'])) + $search_data['sections'][] = array($sublabel['label'], 'area=' . $menu_key . ';sa=' . $key); + } + } + } + + foreach ($settings_search as $setting_area) + { + // Get a list of their variables. + $config_vars = call_user_func($setting_area[0], true); + + foreach ($config_vars as $var) + if (!empty($var[1]) && !in_array($var[0], array('permissions', 'switch', 'desc'))) + $search_data['settings'][] = array($var[(isset($var[2]) && in_array($var[2], array('file', 'db'))) ? 0 : 1], $setting_area[1], 'alttxt' => (isset($var[2]) && in_array($var[2], array('file', 'db'))) || isset($var[3]) ? (in_array($var[2], array('file', 'db')) ? $var[1] : $var[3]) : ''); + } + + $context['page_title'] = $txt['admin_search_results']; + $context['search_results'] = array(); + + $search_term = strtolower(un_htmlspecialchars($context['search_term'])); + // Go through all the search data trying to find this text! + foreach ($search_data as $section => $data) + { + foreach ($data as $item) + { + $found = false; + if (!is_array($item[0])) + $item[0] = array($item[0]); + foreach ($item[0] as $term) + { + if (stripos($term, $search_term) !== false || (isset($txt[$term]) && stripos($txt[$term], $search_term) !== false) || (isset($txt['setting_' . $term]) && stripos($txt['setting_' . $term], $search_term) !== false)) + { + $found = $term; + break; + } + } + + if ($found) + { + // Format the name - and remove any descriptions the entry may have. + $name = isset($txt[$found]) ? $txt[$found] : (isset($txt['setting_' . $found]) ? $txt['setting_' . $found] : (!empty($item['alttxt']) ? $item['alttxt'] : $found)); + $name = preg_replace('~<(?:div|span)\sclass="smalltext">.+?~', '', $name); + + $context['search_results'][] = array( + 'url' => (substr($item[1], 0, 4) == 'area' ? $scripturl . '?action=admin;' . $item[1] : $item[1]) . ';' . $context['session_var'] . '=' . $context['session_id'] . ((substr($item[1], 0, 4) == 'area' && $section == 'settings' ? '#' . $item[0][0] : '')), + 'name' => $name, + 'type' => $section, + 'help' => shorten_subject(isset($item[2]) ? strip_tags($helptxt[$item[2]]) : (isset($helptxt[$found]) ? strip_tags($helptxt[$found]) : ''), 255), + ); + } + } + } +} + +/** + * All this does is pass through to manage members. + * {@see ViewMembers()} + */ +function AdminSearchMember() +{ + global $context, $sourcedir; + + require_once($sourcedir . '/ManageMembers.php'); + $_REQUEST['sa'] = 'query'; + + $_POST['membername'] = un_htmlspecialchars($context['search_term']); + $_POST['types'] = ''; + + ViewMembers(); +} + +/** + * This file allows the user to search the SM online manual for a little of help. + */ +function AdminSearchOM() +{ + global $context, $sourcedir; + + $context['doc_apiurl'] = 'https://wiki.simplemachines.org/api.php'; + $context['doc_scripturl'] = 'https://wiki.simplemachines.org/smf/'; + + // Set all the parameters search might expect. + $postVars = explode(' ', $context['search_term']); + + // Encode the search data. + foreach ($postVars as $k => $v) + $postVars[$k] = urlencode($v); + + // This is what we will send. + $postVars = implode('+', $postVars); + + // Get the results from the doc site. + // Demo URL: + // https://wiki.simplemachines.org/api.php?action=query&list=search&srprop=timestamp|snippet&format=xml&srwhat=text&srsearch=template+eval + $search_results = fetch_web_data($context['doc_apiurl'] . '?action=query&list=search&srprop=timestamp|snippet&format=xml&srwhat=text&srsearch=' . $postVars); + + // If we didn't get any xml back we are in trouble - perhaps the doc site is overloaded? + if (!$search_results || preg_match('~<' . '\?xml\sversion="\d+\.\d+"\?' . '>\s*(]*>.+?)~is', $search_results, $matches) != true) + fatal_lang_error('cannot_connect_doc_site'); + + $search_results = $matches[1]; + + // Otherwise we simply walk through the XML and stick it in context for display. + $context['search_results'] = array(); + require_once($sourcedir . '/Class-Package.php'); + + // Get the results loaded into an array for processing! + $results = new xmlArray($search_results, false); + + // Move through the api layer. + if (!$results->exists('api')) + fatal_lang_error('cannot_connect_doc_site'); + + // Are there actually some results? + if ($results->exists('api/query/search/p')) + { + $relevance = 0; + foreach ($results->set('api/query/search/p') as $result) + { + $context['search_results'][$result->fetch('@title')] = array( + 'title' => $result->fetch('@title'), + 'relevance' => $relevance++, + 'snippet' => str_replace('class=\'searchmatch\'', 'class="highlight"', un_htmlspecialchars($result->fetch('@snippet'))), + ); + } + } +} + +/** + * This function decides which log to load. + */ +function AdminLogs() +{ + global $sourcedir, $context, $txt, $scripturl, $modSettings; + + // These are the logs they can load. + $log_functions = array( + 'errorlog' => array('ManageErrors.php', 'ViewErrorLog'), + 'adminlog' => array('Modlog.php', 'ViewModlog', 'disabled' => empty($modSettings['adminlog_enabled'])), + 'modlog' => array('Modlog.php', 'ViewModlog', 'disabled' => empty($modSettings['modlog_enabled'])), + 'banlog' => array('ManageBans.php', 'BanLog'), + 'spiderlog' => array('ManageSearchEngines.php', 'SpiderLogs'), + 'tasklog' => array('ManageScheduledTasks.php', 'TaskLog'), + 'settings' => array('ManageSettings.php', 'ModifyLogSettings'), + ); + + // If it's not got a sa set it must have come here for first time, pretend error log should be reversed. + if (!isset($_REQUEST['sa'])) + $_REQUEST['desc'] = true; + + // Setup some tab stuff. + $context[$context['admin_menu_name']]['tab_data'] = array( + 'title' => $txt['logs'], + 'help' => '', + 'description' => $txt['maintain_info'], + 'tabs' => array( + 'errorlog' => array( + 'url' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc', + 'description' => sprintf($txt['errorlog_desc'], $txt['remove']), + ), + 'adminlog' => array( + 'description' => $txt['admin_log_desc'], + ), + 'modlog' => array( + 'description' => $txt['moderation_log_desc'], + ), + 'banlog' => array( + 'description' => $txt['ban_log_description'], + ), + 'spiderlog' => array( + 'description' => $txt['spider_log_desc'], + ), + 'tasklog' => array( + 'description' => $txt['scheduled_log_desc'], + ), + 'settings' => array( + 'description' => $txt['log_settings_desc'], + ), + ), + ); + + call_integration_hook('integrate_manage_logs', array(&$log_functions)); + + $subAction = isset($_REQUEST['sa']) && isset($log_functions[$_REQUEST['sa']]) && empty($log_functions[$_REQUEST['sa']]['disabled']) ? $_REQUEST['sa'] : 'errorlog'; + + require_once($sourcedir . '/' . $log_functions[$subAction][0]); + call_helper($log_functions[$subAction][1]); +} + +/** + * This ends a admin session, requiring authentication to access the ACP again. + */ +function AdminEndSession() +{ + // This is so easy! + unset($_SESSION['admin_time']); + + // Clean any admin tokens as well. + foreach ($_SESSION['token'] as $key => $token) + if (strpos($key, '-admin') !== false) + unset($_SESSION['token'][$key]); + + redirectexit(); +} + +?> \ No newline at end of file diff --git a/Sources/Agreement.php b/Sources/Agreement.php new file mode 100644 index 0000000..66a29e1 --- /dev/null +++ b/Sources/Agreement.php @@ -0,0 +1,184 @@ + '', '.txt' => '', '.' => '_')); + $context['agreement'] = parse_bbc(file_get_contents($context['agreement_file']), true, $cache_id); + } + elseif ($context['can_accept_agreement']) + fatal_lang_error('error_no_agreement', false); + } + + if (!$context['accept_doc'] || $context['can_accept_privacy_policy']) + { + // Have we got a localized policy? + if (!empty($modSettings['policy_' . $user_info['language']])) + $context['privacy_policy'] = parse_bbc($modSettings['policy_' . $user_info['language']]); + elseif (!empty($modSettings['policy_' . $language])) + $context['privacy_policy'] = parse_bbc($modSettings['policy_' . $language]); + // Then I guess we've got nothing + elseif ($context['can_accept_privacy_policy']) + fatal_lang_error('error_no_privacy_policy', false); + } +} + +function canRequireAgreement() +{ + global $boarddir, $context, $modSettings, $options, $user_info; + + // Guests can't agree + if (!empty($user_info['is_guest']) || empty($modSettings['requireAgreement'])) + return false; + + $agreement_lang = file_exists($boarddir . '/agreement.' . $user_info['language'] . '.txt') ? $user_info['language'] : 'default'; + + if (empty($modSettings['agreement_updated_' . $agreement_lang])) + return false; + + $context['agreement_accepted_date'] = empty($options['agreement_accepted']) ? 0 : $options['agreement_accepted']; + + // A new timestamp means that there are new changes to the registration agreement and must therefore be shown. + return empty($options['agreement_accepted']) || $modSettings['agreement_updated_' . $agreement_lang] > $options['agreement_accepted']; +} + +function canRequirePrivacyPolicy() +{ + global $modSettings, $options, $user_info, $language, $context; + + if (!empty($user_info['is_guest']) || empty($modSettings['requirePolicyAgreement'])) + return false; + + $policy_lang = !empty($modSettings['policy_' . $user_info['language']]) ? $user_info['language'] : $language; + + if (empty($modSettings['policy_updated_' . $policy_lang])) + return false; + + $context['privacy_policy_accepted_date'] = empty($options['policy_accepted']) ? 0 : $options['policy_accepted']; + + return empty($options['policy_accepted']) || $modSettings['policy_updated_' . $policy_lang] > $options['policy_accepted']; +} + +// Let's tell them there's a new agreement +function Agreement() +{ + global $context, $modSettings, $scripturl, $smcFunc, $txt; + + prepareAgreementContext(); + + loadLanguage('Agreement'); + loadTemplate('Agreement'); + + $page_title = ''; + if (!empty($context['agreement']) && !empty($context['privacy_policy'])) + $page_title = $txt['agreement_and_privacy_policy']; + elseif (!empty($context['agreement'])) + $page_title = $txt['agreement']; + elseif (!empty($context['privacy_policy'])) + $page_title = $txt['privacy_policy']; + + $context['page_title'] = $page_title; + $context['linktree'][] = array( + 'url' => $scripturl . '?action=agreement', + 'name' => $context['page_title'], + ); + + if (isset($_SESSION['old_url'])) + $_SESSION['redirect_url'] = $_SESSION['old_url']; + +} + +// I solemly swear to no longer chase squirrels. +function AcceptAgreement() +{ + global $context, $modSettings, $smcFunc, $user_info; + + $can_accept_agreement = !empty($modSettings['requireAgreement']) && canRequireAgreement(); + $can_accept_privacy_policy = !empty($modSettings['requirePolicyAgreement']) && canRequirePrivacyPolicy(); + + if ($can_accept_agreement || $can_accept_privacy_policy) + { + checkSession(); + + if ($can_accept_agreement) + { + $smcFunc['db_insert']('replace', + '{db_prefix}themes', + array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string', 'value' => 'string'), + array($user_info['id'], 1, 'agreement_accepted', time()), + array('id_member', 'id_theme', 'variable') + ); + logAction('agreement_accepted', array('applicator' => $user_info['id']), 'user'); + } + + if ($can_accept_privacy_policy) + { + $smcFunc['db_insert']('replace', + '{db_prefix}themes', + array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string', 'value' => 'string'), + array($user_info['id'], 1, 'policy_accepted', time()), + array('id_member', 'id_theme', 'variable') + ); + logAction('policy_accepted', array('applicator' => $user_info['id']), 'user'); + } + } + + // Redirect back to chasing those squirrels, er, viewing those memes. + redirectexit(!empty($_SESSION['redirect_url']) ? $_SESSION['redirect_url'] : ''); +} + +?> \ No newline at end of file diff --git a/Sources/Attachments.php b/Sources/Attachments.php new file mode 100644 index 0000000..3564f71 --- /dev/null +++ b/Sources/Attachments.php @@ -0,0 +1,538 @@ + true, + 'data' => array(), + 'extra' => '', + ); + + /** + * @var array $_subActions An array of all valid sub-actions + */ + protected $_subActions = array( + 'add', + 'delete', + ); + + /** + * @var string|bool $_sa The current sub-action, or false if there isn't one + */ + protected $_sa = false; + + /** + * Attachments constructor. + * + * Sets up some initial information - the message ID, board, current attachment upload dir, etc. + */ + public function __construct() + { + global $modSettings, $context; + + $this->_msg = (int) !empty($_REQUEST['msg']) ? $_REQUEST['msg'] : 0; + $this->_board = (int) !empty($_REQUEST['board']) ? $_REQUEST['board'] : null; + + $this->_currentAttachmentUploadDir = $modSettings['currentAttachmentUploadDir']; + + $this->_attachmentUploadDir = $modSettings['attachmentUploadDir']; + + $this->_attchDir = $context['attach_dir'] = $this->_attachmentUploadDir[$modSettings['currentAttachmentUploadDir']]; + + $this->_canPostAttachment = $context['can_post_attachment'] = !empty($modSettings['attachmentEnable']) && $modSettings['attachmentEnable'] == 1 && (allowedTo('post_attachment', $this->_board) || ($modSettings['postmod_active'] && allowedTo('post_unapproved_attachments', $this->_board))); + } + + /** + * Handles calling the appropriate function based on the sub-action + */ + public function call() + { + global $smcFunc, $sourcedir; + + require_once($sourcedir . '/Subs-Attachments.php'); + + // Need this. For reasons... + loadLanguage('Post'); + + $this->_sa = !empty($_REQUEST['sa']) ? $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($_REQUEST['sa'])) : false; + + if ($this->_canPostAttachment && $this->_sa && in_array($this->_sa, $this->_subActions)) + $this->{$this->_sa}(); + + // Just send a generic message. + else + $this->setResponse(array( + 'text' => $this->_sa == 'add' ? 'attach_error_title' : 'attached_file_deleted_error', + 'type' => 'error', + 'data' => false, + )); + + // Back to the future, oh, to the browser! + $this->sendResponse(); + } + + /** + * Handles deleting the attachment + */ + public function delete() + { + global $sourcedir; + + // Need this, don't ask why just nod your head. + require_once($sourcedir . '/ManageAttachments.php'); + + $attachID = !empty($_REQUEST['attach']) && is_numeric($_REQUEST['attach']) ? (int) $_REQUEST['attach'] : 0; + + // Need something to work with. + if (!$attachID || (!empty($_SESSION['already_attached']) && !isset($_SESSION['already_attached'][$attachID]))) + return $this->setResponse(array( + 'text' => 'attached_file_deleted_error', + 'type' => 'error', + 'data' => false, + )); + + // Lets pass some params and see what happens :P + $affectedMessage = removeAttachments(array('id_attach' => $attachID), '', true, true); + + // Gotta also remove the attachment from the session var. + unset($_SESSION['already_attached'][$attachID]); + + // $affectedMessage returns an empty array array(0) which php treats as non empty... awesome... + $this->setResponse(array( + 'text' => !empty($affectedMessage) ? 'attached_file_deleted' : 'attached_file_deleted_error', + 'type' => !empty($affectedMessage) ? 'info' : 'warning', + 'data' => $affectedMessage, + )); + } + + /** + * Handles adding an attachment + */ + public function add() + { + // You gotta be able to post attachments. + if (!$this->_canPostAttachment) + return $this->setResponse(array( + 'text' => 'attached_file_cannot', + 'type' => 'error', + 'data' => false, + )); + + // Process them at once! + $this->processAttachments(); + + // The attachments was created and moved the the right folder, time to update the DB. + if (!empty($_SESSION['temp_attachments'])) + $this->createAttach(); + + // Set the response. + $this->setResponse(); + } + + /** + * Moves an attachment to the proper directory and set the relevant data into $_SESSION['temp_attachments'] + */ + protected function processAttachments() + { + global $context, $modSettings, $smcFunc, $user_info, $txt; + + if (!isset($_FILES['attachment']['name'])) + $_FILES['attachment']['tmp_name'] = array(); + + // If there are attachments, calculate the total size and how many. + $context['attachments']['total_size'] = 0; + $context['attachments']['quantity'] = 0; + + // If this isn't a new post, check the current attachments. + if (isset($_REQUEST['msg'])) + { + $context['attachments']['quantity'] = count($context['current_attachments']); + foreach ($context['current_attachments'] as $attachment) + $context['attachments']['total_size'] += $attachment['size']; + } + + // A bit of house keeping first. + if (!empty($_SESSION['temp_attachments']) && count($_SESSION['temp_attachments']) == 1) + unset($_SESSION['temp_attachments']); + + // Our infamous SESSION var, we are gonna have soo much fun with it! + if (!isset($_SESSION['temp_attachments'])) + $_SESSION['temp_attachments'] = array(); + + // Make sure we're uploading to the right place. + if (!empty($modSettings['automanage_attachments'])) + automanage_attachments_check_directory(); + + // Is the attachments folder actually there? + if (!empty($context['dir_creation_error'])) + $this->_generalErrors[] = $context['dir_creation_error']; + + // The current attach folder ha some issues... + elseif (!is_dir($this->_attchDir)) + { + $this->_generalErrors[] = 'attach_folder_warning'; + log_error(sprintf($txt['attach_folder_admin_warning'], $this->_attchDir), 'critical'); + } + + // If this isn't a new post, check the current attachments. + if (empty($this->_generalErrors) && $this->_msg) + { + $context['attachments'] = array(); + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*), SUM(size) + FROM {db_prefix}attachments + WHERE id_msg = {int:id_msg} + AND attachment_type = {int:attachment_type}', + array( + 'id_msg' => (int) $this->_msg, + 'attachment_type' => 0, + ) + ); + list ($context['attachments']['quantity'], $context['attachments']['total_size']) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + } + + else + $context['attachments'] = array( + 'quantity' => 0, + 'total_size' => 0, + ); + + // Check for other general errors here. + + // If we have an initial error, delete the files. + if (!empty($this->_generalErrors)) + { + // And delete the files 'cos they ain't going nowhere. + foreach ($_FILES['attachment']['tmp_name'] as $n => $dummy) + if (file_exists($_FILES['attachment']['tmp_name'][$n])) + unlink($_FILES['attachment']['tmp_name'][$n]); + + $_FILES['attachment']['tmp_name'] = array(); + + // No point in going further with this. + return; + } + + // Loop through $_FILES['attachment'] array and move each file to the current attachments folder. + foreach ($_FILES['attachment']['tmp_name'] as $n => $dummy) + { + if ($_FILES['attachment']['name'][$n] == '') + continue; + + // First, let's first check for PHP upload errors. + $errors = array(); + if (!empty($_FILES['attachment']['error'][$n])) + { + if ($_FILES['attachment']['error'][$n] == 2) + $errors[] = array('file_too_big', array($modSettings['attachmentSizeLimit'])); + + else + log_error($_FILES['attachment']['name'][$n] . ': ' . $txt['php_upload_error_' . $_FILES['attachment']['error'][$n]]); + + // Log this one, because... + if ($_FILES['attachment']['error'][$n] == 6) + log_error($_FILES['attachment']['name'][$n] . ': ' . $txt['php_upload_error_6'], 'critical'); + + // Weird, no errors were cached, still fill out a generic one. + if (empty($errors)) + $errors[] = 'attach_php_error'; + } + + // Try to move and rename the file before doing any more checks on it. + $attachID = 'post_tmp_' . $user_info['id'] . '_' . md5(mt_rand()); + $destName = $this->_attchDir . '/' . $attachID; + + // No errors, YAY! + if (empty($errors)) + { + // The reported MIME type of the attachment might not be reliable. + $detected_mime_type = get_mime_type($_FILES['attachment']['tmp_name'][$n], true); + if ($detected_mime_type !== false) + $_FILES['attachment']['type'][$n] = $detected_mime_type; + + $_SESSION['temp_attachments'][$attachID] = array( + 'name' => $smcFunc['htmlspecialchars'](basename($_FILES['attachment']['name'][$n])), + 'tmp_name' => $destName, + 'size' => $_FILES['attachment']['size'][$n], + 'type' => $_FILES['attachment']['type'][$n], + 'id_folder' => $modSettings['currentAttachmentUploadDir'], + 'errors' => array(), + ); + + // Move the file to the attachments folder with a temp name for now. + if (@move_uploaded_file($_FILES['attachment']['tmp_name'][$n], $destName)) + smf_chmod($destName, 0644); + + // This is madness!! + else + { + // File couldn't be moved. + $_SESSION['temp_attachments'][$attachID]['errors'][] = 'attach_timeout'; + if (file_exists($_FILES['attachment']['tmp_name'][$n])) + unlink($_FILES['attachment']['tmp_name'][$n]); + } + } + + // Fill up a nice array with some data from the file and the errors encountered so far. + else + { + $_SESSION['temp_attachments'][$attachID] = array( + 'name' => $smcFunc['htmlspecialchars'](basename($_FILES['attachment']['name'][$n])), + 'tmp_name' => $destName, + 'errors' => $errors, + ); + + if (file_exists($_FILES['attachment']['tmp_name'][$n])) + unlink($_FILES['attachment']['tmp_name'][$n]); + } + + // If there's no errors to this point. We still do need to apply some additional checks before we are finished. + if (empty($_SESSION['temp_attachments'][$attachID]['errors'])) + attachmentChecks($attachID); + } + + // Mod authors, finally a hook to hang an alternate attachment upload system upon + // Upload to the current attachment folder with the file name $attachID or 'post_tmp_' . $user_info['id'] . '_' . md5(mt_rand()) + // Populate $_SESSION['temp_attachments'][$attachID] with the following: + // name => The file name + // tmp_name => Path to the temp file ($this->_attchDir . '/' . $attachID). + // size => File size (required). + // type => MIME type (optional if not available on upload). + // id_folder => $modSettings['currentAttachmentUploadDir'] + // errors => An array of errors (use the index of the $txt variable for that error). + // Template changes can be done using "integrate_upload_template". + call_integration_hook('integrate_attachment_upload', array()); + } + + /** + * Actually attaches the file + */ + protected function createAttach() + { + global $txt, $user_info, $modSettings; + + // Create an empty session var to keep track of all the files we attached. + if (!isset($_SESSION['already_attached'])) + $_SESSION['already_attached'] = array(); + + foreach ($_SESSION['temp_attachments'] as $attachID => $attachment) + { + $attachmentOptions = array( + 'post' => $this->_msg, + 'poster' => $user_info['id'], + 'name' => $attachment['name'], + 'tmp_name' => $attachment['tmp_name'], + 'size' => isset($attachment['size']) ? $attachment['size'] : 0, + 'mime_type' => isset($attachment['type']) ? $attachment['type'] : '', + 'id_folder' => isset($attachment['id_folder']) ? $attachment['id_folder'] : $modSettings['currentAttachmentUploadDir'], + 'approved' => !$modSettings['postmod_active'] || allowedTo('post_attachment'), + 'errors' => array(), + ); + + if (empty($attachment['errors'])) + { + if (createAttachment($attachmentOptions)) + { + // Avoid JS getting confused. + $attachmentOptions['attachID'] = $attachmentOptions['id']; + unset($attachmentOptions['id']); + + $_SESSION['already_attached'][$attachmentOptions['attachID']] = $attachmentOptions['attachID']; + + if (!empty($attachmentOptions['thumb'])) + $_SESSION['already_attached'][$attachmentOptions['thumb']] = $attachmentOptions['thumb']; + + if ($this->_msg) + assignAttachments($_SESSION['already_attached'], $this->_msg); + } + } + else + { + // Sort out the errors for display and delete any associated files. + $log_these = array('attachments_no_create', 'attachments_no_write', 'attach_timeout', 'ran_out_of_space', 'cant_access_upload_path', 'attach_0_byte_file'); + + foreach ($attachment['errors'] as $error) + { + $attachmentOptions['errors'][] = sprintf($txt['attach_warning'], $attachment['name']); + + if (!is_array($error)) + { + $attachmentOptions['errors'][] = $txt[$error]; + if (in_array($error, $log_these)) + log_error($attachment['name'] . ': ' . $txt[$error], 'critical'); + } + else + $attachmentOptions['errors'][] = vsprintf($txt[$error[0]], (array) $error[1]); + } + if (file_exists($attachment['tmp_name'])) + unlink($attachment['tmp_name']); + } + + // You don't need to know. + unset($attachmentOptions['tmp_name']); + unset($attachmentOptions['destination']); + + // Regardless of errors, pass the results. + $this->_attachResults[] = $attachmentOptions; + } + + // Temp save this on the db. + if (!empty($_SESSION['already_attached'])) + $this->_attachSuccess = $_SESSION['already_attached']; + + unset($_SESSION['temp_attachments']); + + // Allow user to see previews for all of this post's attachments, even if the post hasn't been submitted yet. + if (!isset($_SESSION['attachments_can_preview'])) + $_SESSION['attachments_can_preview'] = array(); + if (!empty($_SESSION['already_attached'])) + $_SESSION['attachments_can_preview'] += array_fill_keys(array_keys($_SESSION['already_attached']), true); + } + + /** + * Sets up the response information + * + * @param array $data Data for the response if we're not adding an attachment + */ + protected function setResponse($data = array()) + { + global $txt; + + // Some default values in case something is missed or neglected :P + $this->_response = array( + 'text' => 'attach_php_error', + 'type' => 'error', + 'data' => false, + ); + + // Adding needs some VIP treatment. + if ($this->_sa == 'add') + { + // Is there any generic errors? made some sense out of them! + if ($this->_generalErrors) + foreach ($this->_generalErrors as $k => $v) + $this->_generalErrors[$k] = (is_array($v) ? vsprintf($txt[$v[0]], (array) $v[1]) : $txt[$v]); + + // Gotta urlencode the filename. + if ($this->_attachResults) + foreach ($this->_attachResults as $k => $v) + $this->_attachResults[$k]['name'] = urlencode($this->_attachResults[$k]['name']); + + $this->_response = array( + 'files' => $this->_attachResults ? $this->_attachResults : false, + 'generalErrors' => $this->_generalErrors ? $this->_generalErrors : false, + ); + } + + // Rest of us mere mortals gets no special treatment... + elseif (!empty($data)) + if (!empty($data['text']) && !empty($txt[$data['text']])) + $this->_response['text'] = $txt[$data['text']]; + } + + /** + * Sends the response data + */ + protected function sendResponse() + { + global $smcFunc, $modSettings, $context; + + ob_end_clean(); + + if (!empty($modSettings['enableCompressedOutput'])) + @ob_start('ob_gzhandler'); + else + ob_start(); + + // Set the header. + header('content-type: application/json; charset=' . $context['character_set'] . ''); + + echo $smcFunc['json_encode']($this->_response ? $this->_response : array()); + + // Done. + obExit(false); + die; + } +} + +?> \ No newline at end of file diff --git a/Sources/BoardIndex.php b/Sources/BoardIndex.php new file mode 100644 index 0000000..1aacce3 --- /dev/null +++ b/Sources/BoardIndex.php @@ -0,0 +1,150 @@ + true, + 'base_level' => 0, + 'parent_id' => 0, + 'set_latest_post' => true, + 'countChildPosts' => !empty($modSettings['countChildPosts']), + ); + $context['categories'] = getBoardIndex($boardIndexOptions); + + // Now set up for the info center. + $context['info_center'] = array(); + + // Retrieve the latest posts if the theme settings require it. + if (!empty($settings['number_recent_posts'])) + { + if ($settings['number_recent_posts'] > 1) + { + $latestPostOptions = array( + 'number_posts' => $settings['number_recent_posts'], + ); + $context['latest_posts'] = cache_quick_get('boardindex-latest_posts:' . md5($user_info['query_wanna_see_board'] . $user_info['language']), 'Subs-Recent.php', 'cache_getLastPosts', array($latestPostOptions)); + } + + if (!empty($context['latest_posts']) || !empty($context['latest_post'])) + $context['info_center'][] = array( + 'tpl' => 'recent', + 'txt' => 'recent_posts', + ); + } + + // Load the calendar? + if (!empty($modSettings['cal_enabled']) && allowedTo('calendar_view')) + { + // Retrieve the calendar data (events, birthdays, holidays). + $eventOptions = array( + 'include_holidays' => $modSettings['cal_showholidays'] > 1, + 'include_birthdays' => $modSettings['cal_showbdays'] > 1, + 'include_events' => $modSettings['cal_showevents'] > 1, + 'num_days_shown' => empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index'], + ); + $context += cache_quick_get('calendar_index_offset_' . $user_info['time_offset'], 'Subs-Calendar.php', 'cache_getRecentEvents', array($eventOptions)); + + // Whether one or multiple days are shown on the board index. + $context['calendar_only_today'] = $modSettings['cal_days_for_index'] == 1; + + // This is used to show the "how-do-I-edit" help. + $context['calendar_can_edit'] = allowedTo('calendar_edit_any'); + + if (!empty($context['show_calendar'])) + $context['info_center'][] = array( + 'tpl' => 'calendar', + 'txt' => $context['calendar_only_today'] ? 'calendar_today' : 'calendar_upcoming', + ); + } + + // And stats. + $context['show_stats'] = allowedTo('view_stats') && !empty($modSettings['trackStats']); + if ($settings['show_stats_index']) + $context['info_center'][] = array( + 'tpl' => 'stats', + 'txt' => 'forum_stats', + ); + + // Now the online stuff + require_once($sourcedir . '/Subs-MembersOnline.php'); + $membersOnlineOptions = array( + 'show_hidden' => allowedTo('moderate_forum'), + 'sort' => 'log_time', + 'reverse_sort' => true, + ); + $context += getMembersOnlineStats($membersOnlineOptions); + $context['show_buddies'] = !empty($user_info['buddies']); + $context['show_who'] = allowedTo('who_view') && !empty($modSettings['who_enabled']); + $context['info_center'][] = array( + 'tpl' => 'online', + 'txt' => 'online_users', + ); + + // Track most online statistics? (Subs-MembersOnline.php) + if (!empty($modSettings['trackStats'])) + trackStatsUsersOnline($context['num_guests'] + $context['num_users_online']); + + // Are we showing all membergroups on the board index? + if (!empty($settings['show_group_key'])) + $context['membergroups'] = cache_quick_get('membergroup_list', 'Subs-Membergroups.php', 'cache_getMembergroupList', array()); + + // And back to normality. + $context['page_title'] = sprintf($txt['forum_index'], $context['forum_name']); + + // Mark read button + $context['mark_read_button'] = array( + 'markread' => array('text' => 'mark_as_read', 'image' => 'markread.png', 'custom' => 'data-confirm="' . $txt['are_sure_mark_read'] . '"', 'class' => 'you_sure', 'url' => $scripturl . '?action=markasread;sa=all;' . $context['session_var'] . '=' . $context['session_id']), + ); + + // Replace the collapse and expand default alts. + addJavaScriptVar('smf_expandAlt', $txt['show_category'], true); + addJavaScriptVar('smf_collapseAlt', $txt['hide_category'], true); + + // Allow mods to add additional buttons here + call_integration_hook('integrate_mark_read_button'); + + if (!empty($settings['show_newsfader'])) + { + loadJavaScriptFile('slippry.min.js', array(), 'smf_jquery_slippry'); + loadCSSFile('slider.min.css', array(), 'smf_jquery_slider'); + } +} + +?> \ No newline at end of file diff --git a/Sources/Cache/APIs/Apcu.php b/Sources/Cache/APIs/Apcu.php new file mode 100644 index 0000000..1c73583 --- /dev/null +++ b/Sources/Cache/APIs/Apcu.php @@ -0,0 +1,96 @@ +prefix . strtr($key, ':/', '-_'); + + $value = apcu_fetch($key . 'smf'); + + return !empty($value) ? $value : null; + } + + /** + * {@inheritDoc} + */ + public function putData($key, $value, $ttl = null) + { + $key = $this->prefix . strtr($key, ':/', '-_'); + + // An extended key is needed to counteract a bug in APC. + if ($value === null) + return apcu_delete($key . 'smf'); + + else + return apcu_store($key . 'smf', $value, $ttl !== null ? $ttl : $this->ttl); + } + + /** + * {@inheritDoc} + */ + public function cleanCache($type = '') + { + $this->invalidateCache(); + + return apcu_clear_cache(); + } + + /** + * {@inheritDoc} + */ + public function getVersion() + { + return phpversion('apcu'); + } +} + +?> \ No newline at end of file diff --git a/Sources/Cache/APIs/FileBased.php b/Sources/Cache/APIs/FileBased.php new file mode 100644 index 0000000..6078f8c --- /dev/null +++ b/Sources/Cache/APIs/FileBased.php @@ -0,0 +1,274 @@ +setCachedir(); + } + + /** + * {@inheritDoc} + */ + public function isSupported($test = false) + { + $supported = is_writable($this->cachedir); + + if ($test) + return $supported; + + return parent::isSupported() && $supported; + } + + private function readFile($file) + { + if (($fp = @fopen($file, 'rb')) !== false) + { + if (!flock($fp, LOCK_SH)) + { + fclose($fp); + return false; + } + $string = ''; + while (!feof($fp)) + $string .= fread($fp, 8192); + + flock($fp, LOCK_UN); + fclose($fp); + + return $string; + } + + return false; + } + + private function writeFile($file, $string) + { + if (($fp = fopen($file, 'cb')) !== false) + { + if (!flock($fp, LOCK_EX)) + { + fclose($fp); + return false; + } + ftruncate($fp, 0); + $bytes = 0; + $pieces = str_split($string, 8192); + foreach ($pieces as $piece) + { + if (($val = fwrite($fp, $piece, 8192)) !== false) + $bytes += $val; + else + return false; + } + fflush($fp); + flock($fp, LOCK_UN); + fclose($fp); + + return $bytes; + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function connect() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getData($key, $ttl = null) + { + $file = sprintf('%s/data_%s.cache', + $this->cachedir, + $this->prefix . strtr($key, ':/', '-_') + ); + + // SMF Data returns $value and $expired. $expired has a unix timestamp of when this expires. + if (file_exists($file) && ($raw = $this->readFile($file)) !== false) + { + if (($value = smf_json_decode($raw, true, false)) !== array() && isset($value['expiration']) && $value['expiration'] >= time()) + return $value['value']; + else + @unlink($file); + } + + return null; + } + + /** + * {@inheritDoc} + */ + public function putData($key, $value, $ttl = null) + { + $file = sprintf('%s/data_%s.cache', + $this->cachedir, + $this->prefix . strtr($key, ':/', '-_') + ); + $ttl = $ttl !== null ? $ttl : $this->ttl; + + if ($value === null) + @unlink($file); + else + { + $cache_data = json_encode( + array( + 'expiration' => time() + $ttl, + 'value' => $value + ), + JSON_NUMERIC_CHECK + ); + + // Write out the cache file, check that the cache write was successful; all the data must be written + // If it fails due to low diskspace, or other, remove the cache file + if ($this->writeFile($file, $cache_data) !== strlen($cache_data)) + { + @unlink($file); + return false; + } + else + return true; + } + } + + /** + * {@inheritDoc} + */ + public function cleanCache($type = '') + { + // No directory = no game. + if (!is_dir($this->cachedir)) + return; + + // Remove the files in SMF's own disk cache, if any + $files = new GlobIterator($this->cachedir . '/' . $type . '*.cache', FilesystemIterator::NEW_CURRENT_AND_KEY); + + foreach ($files as $file => $info) + unlink($this->cachedir . '/' . $file); + + // Make this invalid. + $this->invalidateCache(); + + return true; + } + + /** + * {@inheritDoc} + */ + public function invalidateCache() + { + // We don't worry about $cachedir here, since the key is based on the real $cachedir. + parent::invalidateCache(); + + // Since SMF is file based, be sure to clear the statcache. + clearstatcache(); + + return true; + } + + /** + * {@inheritDoc} + */ + public function cacheSettings(array &$config_vars) + { + global $context, $txt; + + $class_name = $this->getImplementationClassKeyName(); + $class_name_txt_key = strtolower($class_name); + + $config_vars[] = $txt['cache_'. $class_name_txt_key .'_settings']; + $config_vars[] = array('cachedir', $txt['cachedir'], 'file', 'text', 36, 'cache_cachedir'); + + if (!isset($context['settings_post_javascript'])) + $context['settings_post_javascript'] = ''; + + if (empty($context['settings_not_writable'])) + $context['settings_post_javascript'] .= ' + $("#cache_accelerator").change(function (e) { + var cache_type = e.currentTarget.value; + $("#cachedir").prop("disabled", cache_type != "'. $class_name .'"); + });'; + } + + /** + * Sets the $cachedir or uses the SMF default $cachedir.. + * + * @access public + * @param string $dir A valid path + * @return boolean If this was successful or not. + */ + public function setCachedir($dir = null) + { + global $cachedir; + + // If its invalid, use SMF's. + if (is_null($dir) || !is_writable($dir)) + $this->cachedir = $cachedir; + + else + $this->cachedir = $dir; + } + + /** + * Gets the current $cachedir. + * + * @access public + * @return string the value of $ttl. + */ + public function getCachedir() + { + return $this->cachedir; + } + + /** + * {@inheritDoc} + */ + public function getVersion() + { + return SMF_VERSION; + } +} + +?> \ No newline at end of file diff --git a/Sources/Cache/APIs/MemcacheImplementation.php b/Sources/Cache/APIs/MemcacheImplementation.php new file mode 100644 index 0000000..b9a6024 --- /dev/null +++ b/Sources/Cache/APIs/MemcacheImplementation.php @@ -0,0 +1,193 @@ +memcache = new Memcache(); + + $servers = explode(',', $cache_memcached); + $port = 0; + + // Don't try more times than we have servers! + $connected = false; + $level = 0; + + // We should keep trying if a server times out, but only for the amount of servers we have. + while (!$connected && $level < count($servers)) + { + ++$level; + + $server = trim($servers[array_rand($servers)]); + + // No server, can't connect to this. + if (empty($server)) + continue; + + // Normal host names do not contain slashes, while e.g. unix sockets do. Assume alternative transport pipe with port 0. + if (strpos($server, '/') !== false) + $host = $server; + + else + { + $server = explode(':', $server); + $host = $server[0]; + $port = isset($server[1]) ? $server[1] : 11211; + } + + // Don't wait too long: yes, we want the server, but we might be able to run the query faster! + if (empty($db_persist)) + $connected = $this->memcache->connect($host, $port); + + else + $connected = $this->memcache->pconnect($host, $port); + } + + return $connected; + } + + /** + * {@inheritDoc} + */ + public function getData($key, $ttl = null) + { + $key = $this->prefix . strtr($key, ':/', '-_'); + + $value = $this->memcache->get($key); + + // $value should return either data or false (from failure, key not found or empty array). + if ($value === false) + return null; + + return $value; + } + + /** + * {@inheritDoc} + */ + public function putData($key, $value, $ttl = null) + { + $key = $this->prefix . strtr($key, ':/', '-_'); + + return $this->memcache->set($key, $value, 0, $ttl !== null ? $ttl : $this->ttl); + } + + /** + * {@inheritDoc} + */ + public function quit() + { + return $this->memcache->close(); + } + + /** + * {@inheritDoc} + */ + public function cleanCache($type = '') + { + $this->invalidateCache(); + + return $this->memcache->flush(); + } + + /** + * {@inheritDoc} + */ + public function cacheSettings(array &$config_vars) + { + global $context, $txt; + + if (!in_array($txt[self::CLASS_KEY .'_settings'], $config_vars)) + { + $config_vars[] = $txt[self::CLASS_KEY .'_settings']; + $config_vars[] = array( + self::CLASS_KEY, + $txt[self::CLASS_KEY .'_servers'], + 'file', + 'text', + 0, + 'subtext' => $txt[self::CLASS_KEY .'_servers_subtext']); + } + + if (!isset($context['settings_post_javascript'])) + $context['settings_post_javascript'] = ''; + + if (empty($context['settings_not_writable'])) + $context['settings_post_javascript'] .= ' + $("#cache_accelerator").change(function (e) { + var cache_type = e.currentTarget.value; + $("#'. self::CLASS_KEY .'").prop("disabled", cache_type != "MemcacheImplementation" && cache_type != "MemcachedImplementation"); + });'; + } + + /** + * {@inheritDoc} + */ + public function getVersion() + { + if (!is_object($this->memcache)) + return false; + + // This gets called in Subs-Admin getServerVersions when loading up support information. If we can't get a connection, return nothing. + $result = $this->memcache->getVersion(); + + if (!empty($result)) + return $result; + + return false; + } +} + +?> \ No newline at end of file diff --git a/Sources/Cache/APIs/MemcachedImplementation.php b/Sources/Cache/APIs/MemcachedImplementation.php new file mode 100644 index 0000000..234c12d --- /dev/null +++ b/Sources/Cache/APIs/MemcachedImplementation.php @@ -0,0 +1,213 @@ +servers = array_map( + function($server) + { + if (strpos($server, '/') !== false) + return array($server, 0); + + else + { + $server = explode(':', $server); + return array($server[0], isset($server[1]) ? (int) $server[1] : 11211); + } + }, + explode(',', $cache_memcached) + ); + + parent::__construct(); + } + + /** + * {@inheritDoc} + */ + public function isSupported($test = false) + { + global $cache_memcached; + + $supported = class_exists('Memcached'); + + if ($test) + return $supported; + + return parent::isSupported() && $supported && !empty($cache_memcached); + } + + /** + * {@inheritDoc} + */ + public function connect() + { + $this->memcached = new Memcached; + + return $this->addServers(); + } + + /** + * Add memcached servers. + * + * Don't add servers if they already exist. Ideal for persistent connections. + * + * @return bool True if there are servers in the daemon, false if not. + */ + protected function addServers() + { + $currentServers = $this->memcached->getServerList(); + $retVal = !empty($currentServers); + foreach ($this->servers as $server) + { + // Figure out if we have this server or not + $foundServer = false; + foreach ($currentServers as $currentServer) + { + if ($server[0] == $currentServer['host'] && $server[1] == $currentServer['port']) + { + $foundServer = true; + break; + } + } + + // Found it? + if (empty($foundServer)) + $retVal |= $this->memcached->addServer($server[0], $server[1]); + } + + return $retVal; + } + + /** + * {@inheritDoc} + */ + public function getData($key, $ttl = null) + { + $key = $this->prefix . strtr($key, ':/', '-_'); + + $value = $this->memcached->get($key); + + // $value should return either data or false (from failure, key not found or empty array). + if ($value === false) + return null; + + return $value; + } + + /** + * {@inheritDoc} + */ + public function putData($key, $value, $ttl = null) + { + $key = $this->prefix . strtr($key, ':/', '-_'); + + return $this->memcached->set($key, $value, $ttl !== null ? $ttl : $this->ttl); + } + + /** + * {@inheritDoc} + */ + public function cleanCache($type = '') + { + $this->invalidateCache(); + + // Memcached accepts a delay parameter, always use 0 (instant). + return $this->memcached->flush(0); + } + + /** + * {@inheritDoc} + */ + public function quit() + { + return $this->memcached->quit(); + } + + /** + * {@inheritDoc} + */ + public function cacheSettings(array &$config_vars) + { + global $context, $txt; + + if (!in_array($txt[self::CLASS_KEY .'_settings'], $config_vars)) + { + $config_vars[] = $txt[self::CLASS_KEY .'_settings']; + $config_vars[] = array( + self::CLASS_KEY, + $txt[self::CLASS_KEY .'_servers'], + 'file', + 'text', + 0, + 'subtext' => $txt[self::CLASS_KEY .'_servers_subtext']); + } + + if (!isset($context['settings_post_javascript'])) + $context['settings_post_javascript'] = ''; + + if (empty($context['settings_not_writable'])) + $context['settings_post_javascript'] .= ' + $("#cache_accelerator").change(function (e) { + var cache_type = e.currentTarget.value; + $("#'. self::CLASS_KEY .'").prop("disabled", cache_type != "MemcacheImplementation" && cache_type != "MemcachedImplementation"); + });'; + } + + /** + * {@inheritDoc} + */ + public function getVersion() + { + if (!is_object($this->memcached)) + return false; + + // This gets called in Subs-Admin getServerVersions when loading up support information. If we can't get a connection, return nothing. + $result = $this->memcached->getVersion(); + + if (!empty($result)) + return current($result); + + return false; + } +} + +?> \ No newline at end of file diff --git a/Sources/Cache/APIs/Postgres.php b/Sources/Cache/APIs/Postgres.php new file mode 100644 index 0000000..16de7ff --- /dev/null +++ b/Sources/Cache/APIs/Postgres.php @@ -0,0 +1,220 @@ +db_prefix = $db_prefix; + $this->db_connection = $db_connection; + + parent::__construct(); + } + + /** + * {@inheritDoc} + */ + public function connect() + { + $result = pg_query_params($this->db_connection, 'SELECT 1 + FROM pg_tables + WHERE schemaname = $1 + AND tablename = $2', + array( + 'public', + $this->db_prefix . 'cache', + ) + ); + + if (pg_affected_rows($result) === 0) + pg_query($this->db_connection, 'CREATE UNLOGGED TABLE ' . $this->db_prefix . 'cache (key text, value text, ttl bigint, PRIMARY KEY (key))'); + + $this->prepareQueries( + array( + 'smf_cache_get_data', + 'smf_cache_put_data', + 'smf_cache_delete_data', + ), + array( + 'SELECT value FROM ' . $this->db_prefix . 'cache WHERE key = $1 AND ttl >= $2 LIMIT 1', + 'INSERT INTO ' . $this->db_prefix . 'cache(key,value,ttl) VALUES($1,$2,$3) + ON CONFLICT(key) DO UPDATE SET value = $2, ttl = $3', + 'DELETE FROM ' . $this->db_prefix . 'cache WHERE key = $1', + ) + ); + + return true; + } + + /** + * Stores a prepared SQL statement, ensuring that it's not done twice. + * + * @param array $stmtnames + * @param array $queries + */ + private function prepareQueries(array $stmtnames, array $queries) + { + $result = pg_query_params( + $this->db_connection, + 'SELECT name FROM pg_prepared_statements WHERE name = ANY ($1)', + array('{' . implode(', ', $stmtnames) . '}') + ); + + $arr = pg_num_rows($result) == 0 ? array() : array_map( + function($el) + { + return $el['name']; + }, + pg_fetch_all($result) + ); + foreach ($stmtnames as $idx => $stmtname) + if (!in_array($stmtname, $arr)) + pg_prepare($this->db_connection, $stmtname, $queries[$idx]); + } + + /** + * {@inheritDoc} + */ + public function isSupported($test = false) + { + global $smcFunc; + + if ($smcFunc['db_title'] !== POSTGRE_TITLE) + return false; + + $result = pg_query($this->db_connection, 'SHOW server_version_num'); + $res = pg_fetch_assoc($result); + + if ($res['server_version_num'] < 90500) + return false; + + return $test ? true : parent::isSupported(); + } + + /** + * {@inheritDoc} + */ + public function getData($key, $ttl = null) + { + $result = pg_execute($this->db_connection, 'smf_cache_get_data', array($key, time())); + + if (pg_affected_rows($result) === 0) + return null; + + $res = pg_fetch_assoc($result); + + return $res['value']; + } + + /** + * {@inheritDoc} + */ + public function putData($key, $value, $ttl = null) + { + $ttl = time() + (int) ($ttl !== null ? $ttl : $this->ttl); + + if ($value === null) + $result = pg_execute($this->db_connection, 'smf_cache_delete_data', array($key)); + else + $result = pg_execute($this->db_connection, 'smf_cache_put_data', array($key, $value, $ttl)); + + return pg_affected_rows($result) > 0; + } + + /** + * {@inheritDoc} + */ + public function cleanCache($type = '') + { + if ($type == 'expired') + pg_query($this->db_connection, 'DELETE FROM ' . $this->db_prefix . 'cache WHERE ttl < ' . time() . ';'); + else + pg_query($this->db_connection, 'TRUNCATE ' . $this->db_prefix . 'cache'); + + $this->invalidateCache(); + + return true; + } + + /** + * {@inheritDoc} + */ + public function getVersion() + { + return pg_version($this->db_connection)['server']; + } + + /** + * {@inheritDoc} + */ + public function housekeeping() + { + $this->createTempTable(); + $this->cleanCache(); + $this->retrieveData(); + $this->deleteTempTable(); + } + + /** + * Create the temp table of valid data. + * + * @return void + */ + private function createTempTable() + { + pg_query($this->db_connection, 'CREATE LOCAL TEMP TABLE IF NOT EXISTS ' . $this->db_prefix . 'cache_tmp AS SELECT * FROM ' . $this->db_prefix . 'cache WHERE ttl >= ' . time()); + } + + /** + * Delete the temp table. + * + * @return void + */ + private function deleteTempTable() + { + pg_query($this->db_connection, 'DROP TABLE IF EXISTS ' . $this->db_prefix . 'cache_tmp'); + } + + /** + * Retrieve the valid data from temp table. + * + * @return void + */ + private function retrieveData() + { + pg_query($this->db_connection, 'INSERT INTO ' . $this->db_prefix . 'cache SELECT * FROM ' . $this->db_prefix . 'cache_tmp ON CONFLICT DO NOTHING'); + } +} + +?> \ No newline at end of file diff --git a/Sources/Cache/APIs/Sqlite.php b/Sources/Cache/APIs/Sqlite.php new file mode 100755 index 0000000..06b9f22 --- /dev/null +++ b/Sources/Cache/APIs/Sqlite.php @@ -0,0 +1,207 @@ +setCachedir(); + } + + /** + * {@inheritDoc} + */ + public function connect() + { + $database = $this->cachedir . '/' . 'SQLite3Cache.db3'; + $this->cacheDB = new SQLite3($database); + $this->cacheDB->busyTimeout(1000); + if (filesize($database) == 0) + { + $this->cacheDB->exec('CREATE TABLE cache (key text unique, value blob, ttl int);'); + $this->cacheDB->exec('CREATE INDEX ttls ON cache(ttl);'); + } + } + + /** + * {@inheritDoc} + */ + public function isSupported($test = false) + { + $supported = class_exists("SQLite3") && is_writable($this->cachedir); + + if ($test) + return $supported; + + return parent::isSupported() && $supported; + } + + /** + * {@inheritDoc} + */ + public function getData($key, $ttl = null) + { + $query = 'SELECT value FROM cache WHERE key = \'' . $this->cacheDB->escapeString($key) . '\' AND ttl >= ' . time() . ' LIMIT 1'; + $result = $this->cacheDB->query($query); + + $value = null; + while ($res = $result->fetchArray(SQLITE3_ASSOC)) + $value = $res['value']; + + return !empty($value) ? $value : null; + } + + /** + * {@inheritDoc} + */ + public function putData($key, $value, $ttl = null) + { + $ttl = time() + (int) ($ttl !== null ? $ttl : $this->ttl); + if ($value === null) + $query = 'DELETE FROM cache WHERE key = \'' . $this->cacheDB->escapeString($key) . '\';'; + else + $query = 'REPLACE INTO cache VALUES (\'' . $this->cacheDB->escapeString($key) . '\', \'' . $this->cacheDB->escapeString($value) . '\', ' . $ttl . ');'; + $result = $this->cacheDB->exec($query); + + return $result; + } + + /** + * {@inheritDoc} + */ + public function cleanCache($type = '') + { + if ($type == 'expired') + $query = 'DELETE FROM cache WHERE ttl < ' . time() . ';'; + else + $query = 'DELETE FROM cache;'; + + $result = $this->cacheDB->exec($query); + + $query = 'VACUUM;'; + $this->cacheDB->exec($query); + + $this->invalidateCache(); + + return $result; + } + + /** + * {@inheritDoc} + */ + public function cacheSettings(array &$config_vars) + { + global $context, $txt; + + $class_name = $this->getImplementationClassKeyName(); + $class_name_txt_key = strtolower($class_name); + + $config_vars[] = $txt['cache_'. $class_name_txt_key .'_settings']; + $config_vars[] = array( + 'cachedir_'. $class_name_txt_key, + $txt['cachedir_'. $class_name_txt_key], + 'file', + 'text', + 36, + 'cache_'. $class_name_txt_key .'_cachedir', + ); + + if (!isset($context['settings_post_javascript'])) + $context['settings_post_javascript'] = ''; + + if (empty($context['settings_not_writable'])) + $context['settings_post_javascript'] .= ' + $("#cache_accelerator").change(function (e) { + var cache_type = e.currentTarget.value; + $("#cachedir_'. $class_name_txt_key .'").prop("disabled", cache_type != "'. $class_name .'"); + });'; + } + + /** + * Sets the $cachedir or uses the SMF default $cachedir.. + * + * @access public + * + * @param string $dir A valid path + * + * @return boolean If this was successful or not. + */ + public function setCachedir($dir = null) + { + global $cachedir, $cachedir_sqlite, $sourcedir; + + // If its invalid, use SMF's. + if (!isset($dir) || !is_writable($dir)) + { + if (!isset($cachedir_sqlite) || !is_writable($cachedir_sqlite)) + { + $cachedir_sqlite = $cachedir; + + require_once($sourcedir . '/Subs-Admin.php'); + updateSettingsFile(array('cachedir_sqlite' => $cachedir_sqlite)); + } + + $this->cachedir = $cachedir_sqlite; + } + else + $this->cachedir = $dir; + } + + /** + * {@inheritDoc} + */ + public function getVersion() + { + if (null == $this->cacheDB) + $this->connect(); + + return $this->cacheDB->version()['versionString']; + } + + /** + * {@inheritDoc} + */ + public function housekeeping() + { + $this->cleanCache('expired'); + } +} + +?> \ No newline at end of file diff --git a/Sources/Cache/APIs/Zend.php b/Sources/Cache/APIs/Zend.php new file mode 100644 index 0000000..eb63872 --- /dev/null +++ b/Sources/Cache/APIs/Zend.php @@ -0,0 +1,95 @@ +prefix . strtr($key, ':/', '-_'); + + // Zend's pricey stuff. + if (function_exists('zend_shm_cache_fetch')) + return zend_shm_cache_fetch('SMF::' . $key); + + elseif (function_exists('output_cache_get')) + return output_cache_get($key, $ttl); + } + + /** + * {@inheritDoc} + */ + public function putData($key, $value, $ttl = null) + { + $key = $this->prefix . strtr($key, ':/', '-_'); + + if (function_exists('zend_shm_cache_store')) + return zend_shm_cache_store('SMF::' . $key, $value, $ttl); + + elseif (function_exists('output_cache_put')) + return output_cache_put($key, $value); + } + + /** + * {@inheritDoc} + */ + public function cleanCache($type = '') + { + $this->invalidateCache(); + + return zend_shm_cache_clear('SMF'); + } + + /** + * {@inheritDoc} + */ + public function getVersion() + { + return zend_version(); + } +} + +?> \ No newline at end of file diff --git a/Sources/Cache/CacheApi.php b/Sources/Cache/CacheApi.php new file mode 100644 index 0000000..0700abf --- /dev/null +++ b/Sources/Cache/CacheApi.php @@ -0,0 +1,252 @@ +setPrefix(); + } + + /** + * Checks whether we can use the cache method performed by this API. + * + * @access public + * @param bool $test Test if this is supported or enabled. + * @return bool Whether or not the cache is supported + */ + public function isSupported($test = false) + { + global $cache_enable; + + if ($test) + return true; + + return !empty($cache_enable); + } + + /** + * Sets the cache prefix. + * + * @access public + * @param string $prefix The prefix to use. + * If empty, the prefix will be generated automatically. + * @return bool If this was successful or not. + */ + public function setPrefix($prefix = '') + { + global $boardurl, $cachedir, $boarddir; + + if (!is_string($prefix)) + $prefix = ''; + + // Use the supplied prefix, if there is one. + if (!empty($prefix)) + { + $this->prefix = $prefix; + + return true; + } + + // Ideally the prefix should reflect the last time the cache was reset. + if (!empty($cachedir) && file_exists($cachedir . '/index.php')) + { + $mtime = filemtime($cachedir . '/index.php'); + } + // Fall back to the last time that Settings.php was updated. + elseif (!empty($boarddir) && file_exists($boarddir . '/Settings.php')) + { + $mtime = filemtime($boarddir . '/Settings.php'); + } + // This should never happen, but just in case... + else + { + $mtime = filemtime(realpath($_SERVER['SCRIPT_FILENAME'])); + } + + $this->prefix = md5($boardurl . $mtime) . '-SMF-'; + + return true; + } + + /** + * Gets the prefix as defined from set or the default. + * + * @access public + * @return string the value of $key. + */ + public function getPrefix() + { + return $this->prefix; + } + + /** + * Sets a default Time To Live, if this isn't specified we let the class define it. + * + * @access public + * @param int $ttl The default TTL + * @return bool If this was successful or not. + */ + public function setDefaultTTL($ttl = 120) + { + $this->ttl = $ttl; + + return true; + } + + /** + * Gets the TTL as defined from set or the default. + * + * @access public + * @return int the value of $ttl. + */ + public function getDefaultTTL() + { + return $this->ttl; + } + + /** + * Invalidate all cached data. + * + * @return bool Whether or not we could invalidate the cache. + */ + public function invalidateCache() + { + global $cachedir; + + // Invalidate cache, to be sure! + // ... as long as index.php can be modified, anyway. + if (is_writable($cachedir . '/' . 'index.php')) + @touch($cachedir . '/' . 'index.php'); + + return true; + } + + /** + * Closes connections to the cache method. + * + * @access public + * @return bool Whether the connections were closed. + */ + public function quit() + { + return true; + } + + /** + * Specify custom settings that the cache API supports. + * + * @access public + * @param array $config_vars Additional config_vars, see ManageSettings.php for usage. + */ + public function cacheSettings(array &$config_vars) + { + } + + /** + * Gets the latest version of SMF this is compatible with. + * + * @access public + * @return string the value of $key. + */ + public function getCompatibleVersion() + { + return $this->version_compatible; + } + + /** + * Gets the min version that we support. + * + * @access public + * @return string the value of $key. + */ + public function getMinimumVersion() + { + return $this->min_smf_version; + } + + /** + * Gets the Version of the Caching API. + * + * @access public + * @return string the value of $key. + */ + public function getVersion() + { + return $this->min_smf_version; + } + + /** + * Run housekeeping of this cache + * exp. clean up old data or do optimization + * + * @access public + * @return void + */ + public function housekeeping() + { + } + + /** + * Gets the class identifier of the current caching API implementation. + * + * @access public + * @return string the unique identifier for the current class implementation. + */ + public function getImplementationClassKeyName() + { + $class_name = get_class($this); + + if ($position = strrpos($class_name, '\\')) + return substr($class_name, $position + 1); + + else + return get_class($this); + } +} + +?> \ No newline at end of file diff --git a/Sources/Cache/CacheApiInterface.php b/Sources/Cache/CacheApiInterface.php new file mode 100644 index 0000000..14d7840 --- /dev/null +++ b/Sources/Cache/CacheApiInterface.php @@ -0,0 +1,82 @@ + \ No newline at end of file diff --git a/Sources/Calendar.php b/Sources/Calendar.php new file mode 100644 index 0000000..139b835 --- /dev/null +++ b/Sources/Calendar.php @@ -0,0 +1,736 @@ + $modSettings['cal_minyear'], + 'max_year' => $modSettings['cal_maxyear'], + ); + + // Doing something other than calendar viewing? + $subActions = array( + 'ical' => 'iCalDownload', + 'post' => 'CalendarPost', + ); + + if (isset($_GET['sa']) && isset($subActions[$_GET['sa']])) + return call_helper($subActions[$_GET['sa']]); + + // You can't do anything if the calendar is off. + if (empty($modSettings['cal_enabled'])) + fatal_lang_error('calendar_off', false); + + // This is gonna be needed... + loadTemplate('Calendar'); + loadCSSFile('calendar.css', array('force_current' => false, 'validate' => true, 'rtl' => 'calendar.rtl.css'), 'smf_calendar'); + + // Did the specify an individual event ID? If so, let's splice the year/month in to what we would otherwise be doing. + if (isset($_GET['event'])) + { + $evid = (int) $_GET['event']; + if ($evid > 0) + { + $request = $smcFunc['db_query']('', ' + SELECT start_date + FROM {db_prefix}calendar + WHERE id_event = {int:event_id}', + array( + 'event_id' => $evid, + ) + ); + if ($row = $smcFunc['db_fetch_assoc']($request)) + { + $_REQUEST['start_date'] = $row['start_date']; + + // We might use this later. + $context['selected_event'] = $evid; + } + $smcFunc['db_free_result']($request); + } + unset ($_GET['event']); + } + + // Set the page title to mention the calendar ;). + $context['page_title'] = $txt['calendar']; + + // Ensure a default view is defined + if (empty($options['calendar_default_view'])) + $options['calendar_default_view'] = 'viewlist'; + + // What view do we want? + if (isset($_GET['viewweek'])) + $context['calendar_view'] = 'viewweek'; + elseif (isset($_GET['viewmonth'])) + $context['calendar_view'] = 'viewmonth'; + elseif (isset($_GET['viewlist'])) + $context['calendar_view'] = 'viewlist'; + else + $context['calendar_view'] = $options['calendar_default_view']; + + // Don't let search engines index the non-default calendar pages + if ($context['calendar_view'] !== $options['calendar_default_view']) + $context['robot_no_index'] = true; + + // Get the current day of month... + require_once($sourcedir . '/Subs-Calendar.php'); + $today = getTodayInfo(); + + // Need a start date for all views + if (!empty($_REQUEST['start_date'])) + { + $start_parsed = date_parse(str_replace(',', '', convertDateToEnglish($_REQUEST['start_date']))); + if (empty($start_parsed['error_count']) && empty($start_parsed['warning_count'])) + { + $_REQUEST['year'] = $start_parsed['year']; + $_REQUEST['month'] = $start_parsed['month']; + $_REQUEST['day'] = $start_parsed['day']; + } + } + $year = !empty($_REQUEST['year']) ? (int) $_REQUEST['year'] : $today['year']; + $month = !empty($_REQUEST['month']) ? (int) $_REQUEST['month'] : $today['month']; + $day = !empty($_REQUEST['day']) ? (int) $_REQUEST['day'] : (!empty($_REQUEST['month']) ? 1 : $today['day']); + + $start_object = checkdate($month, $day, $year) === true ? date_create(implode('-', array($year, $month, $day)) . ' ' . getUserTimezone()) : date_create(implode('-', array($today['year'], $today['month'], $today['day'])) . ' ' . getUserTimezone()); + + // Need an end date for the list view + if (!empty($_REQUEST['end_date'])) + { + $end_parsed = date_parse(str_replace(',', '', convertDateToEnglish($_REQUEST['end_date']))); + if (empty($end_parsed['error_count']) && empty($end_parsed['warning_count'])) + { + $_REQUEST['end_year'] = $end_parsed['year']; + $_REQUEST['end_month'] = $end_parsed['month']; + $_REQUEST['end_day'] = $end_parsed['day']; + } + } + $end_year = !empty($_REQUEST['end_year']) ? (int) $_REQUEST['end_year'] : null; + $end_month = !empty($_REQUEST['end_month']) ? (int) $_REQUEST['end_month'] : null; + $end_day = !empty($_REQUEST['end_day']) ? (int) $_REQUEST['end_day'] : null; + + $end_object = null; + + if (isset($end_month, $end_day, $end_year) && checkdate($end_month, $end_day, $end_year)) + { + $end_object = date_create(implode('-', array($end_year, $end_month, $end_day)) . ' ' . getUserTimezone()); + } + + if (empty($end_object) || $start_object >= $end_object) + { + $num_days_shown = empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index']; + + $end_object = date_create(date_format($start_object, 'Y-m-d') . ' ' . getUserTimezone()); + + date_add($end_object, date_interval_create_from_date_string($num_days_shown . ' days')); + } + + $curPage = array( + 'year' => date_format($start_object, 'Y'), + 'month' => date_format($start_object, 'n'), + 'day' => date_format($start_object, 'j'), + 'start_date' => date_format($start_object, 'Y-m-d'), + 'end_year' => date_format($end_object, 'Y'), + 'end_month' => date_format($end_object, 'n'), + 'end_day' => date_format($end_object, 'j'), + 'end_date' => date_format($end_object, 'Y-m-d'), + ); + + // Make sure the year and month are in valid ranges. + if ($curPage['month'] < 1 || $curPage['month'] > 12) + fatal_lang_error('invalid_month', false); + if ($curPage['year'] < $modSettings['cal_minyear'] || $curPage['year'] > $modSettings['cal_maxyear']) + fatal_lang_error('invalid_year', false); + // If we have a day clean that too. + if ($context['calendar_view'] != 'viewmonth') + { + $isValid = checkdate($curPage['month'], $curPage['day'], $curPage['year']); + if (!$isValid) + fatal_lang_error('invalid_day', false); + } + + // Load all the context information needed to show the calendar grid. + $calendarOptions = array( + 'start_day' => !empty($options['calendar_start_day']) ? $options['calendar_start_day'] : 0, + 'show_birthdays' => in_array($modSettings['cal_showbdays'], array(1, 2)), + 'show_events' => in_array($modSettings['cal_showevents'], array(1, 2)), + 'show_holidays' => in_array($modSettings['cal_showholidays'], array(1, 2)), + 'show_week_num' => true, + 'short_day_titles' => !empty($modSettings['cal_short_days']), + 'short_month_titles' => !empty($modSettings['cal_short_months']), + 'show_next_prev' => !empty($modSettings['cal_prev_next_links']), + 'show_week_links' => isset($modSettings['cal_week_links']) ? $modSettings['cal_week_links'] : 0, + ); + + // Load up the main view. + if ($context['calendar_view'] == 'viewlist') + $context['calendar_grid_main'] = getCalendarList($curPage['start_date'], $curPage['end_date'], $calendarOptions); + elseif ($context['calendar_view'] == 'viewweek') + $context['calendar_grid_main'] = getCalendarWeek($curPage['start_date'], $calendarOptions); + else + $context['calendar_grid_main'] = getCalendarGrid($curPage['start_date'], $calendarOptions); + + // Load up the previous and next months. + $context['calendar_grid_current'] = getCalendarGrid($curPage['start_date'], $calendarOptions, false, false); + + // Only show previous month if it isn't pre-January of the min-year + if ($context['calendar_grid_current']['previous_calendar']['year'] > $modSettings['cal_minyear'] || $curPage['month'] != 1) + $context['calendar_grid_prev'] = getCalendarGrid($context['calendar_grid_current']['previous_calendar']['start_date'], $calendarOptions, true, false); + + // Only show next month if it isn't post-December of the max-year + if ($context['calendar_grid_current']['next_calendar']['year'] < $modSettings['cal_maxyear'] || $curPage['month'] != 12) + $context['calendar_grid_next'] = getCalendarGrid($context['calendar_grid_current']['next_calendar']['start_date'], $calendarOptions, false, false); + + // Basic template stuff. + $context['allow_calendar_event'] = allowedTo('calendar_post'); + + // If you don't allow events not linked to posts and you're not an admin, we have more work to do... + if ($context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin']) + { + $boards_can_post = boardsAllowedTo('post_new'); + $context['allow_calendar_event'] &= !empty($boards_can_post); + } + + $context['can_post'] = $context['allow_calendar_event']; + $context['current_day'] = $curPage['day']; + $context['current_month'] = $curPage['month']; + $context['current_year'] = $curPage['year']; + $context['show_all_birthdays'] = isset($_GET['showbd']); + $context['blocks_disabled'] = !empty($modSettings['cal_disable_prev_next']) ? 1 : 0; + + // Set the page title to mention the month or week, too + if ($context['calendar_view'] != 'viewlist') + $context['page_title'] .= ' - ' . ($context['calendar_view'] == 'viewweek' ? $context['calendar_grid_main']['week_title'] : $txt['months_titles'][$context['current_month']] . ' ' . $context['current_year']); + + // Load up the linktree! + $context['linktree'][] = array( + 'url' => $scripturl . '?action=calendar', + 'name' => $txt['calendar'] + ); + // Add the current month to the linktree. + $context['linktree'][] = array( + 'url' => $scripturl . '?action=calendar;year=' . $context['current_year'] . ';month=' . $context['current_month'], + 'name' => $txt['months_titles'][$context['current_month']] . ' ' . $context['current_year'] + ); + // If applicable, add the current week to the linktree. + if ($context['calendar_view'] == 'viewweek') + $context['linktree'][] = array( + 'url' => $scripturl . '?action=calendar;viewweek;year=' . $context['current_year'] . ';month=' . $context['current_month'] . ';day=' . $context['current_day'], + 'name' => $context['calendar_grid_main']['week_title'], + ); + + // Build the calendar button array. + $context['calendar_buttons'] = array(); + + if ($context['can_post']) + $context['calendar_buttons']['post_event'] = array('text' => 'calendar_post_event', 'image' => 'calendarpe.png', 'url' => $scripturl . '?action=calendar;sa=post;month=' . $context['current_month'] . ';year=' . $context['current_year'] . ';' . $context['session_var'] . '=' . $context['session_id']); + + // Allow mods to add additional buttons here + call_integration_hook('integrate_calendar_buttons'); +} + +/** + * This function processes posting/editing/deleting a calendar event. + * + * - calls {@link Post.php|Post() Post()} function if event is linked to a post. + * - calls {@link Subs-Calendar.php|insertEvent() insertEvent()} to insert the event if not linked to post. + * + * It requires the calendar_post permission to use. + * It uses the event_post sub template in the Calendar template. + * It is accessed with ?action=calendar;sa=post. + */ +function CalendarPost() +{ + global $context, $txt, $user_info, $sourcedir, $scripturl; + global $modSettings, $topic, $smcFunc; + + // Well - can they? + isAllowedTo('calendar_post'); + + // We need these for all kinds of useful functions. + require_once($sourcedir . '/Subs-Calendar.php'); + require_once($sourcedir . '/Subs.php'); + + // Cast this for safety... + if (isset($_REQUEST['eventid'])) + $_REQUEST['eventid'] = (int) $_REQUEST['eventid']; + + // We want a fairly compact version of the time, but as close as possible to the user's settings. + $time_string = strtr(get_date_or_time_format('time'), array( + '%I' => '%l', + '%H' => '%k', + '%S' => '', + '%r' => '%l:%M %p', + '%R' => '%k:%M', + '%T' => '%l:%M', + )); + + $time_string = preg_replace('~:(?=\s|$|%[pPzZ])~', '', $time_string); + + // Submitting? + if (isset($_POST[$context['session_var']], $_REQUEST['eventid'])) + { + checkSession(); + + // Validate the post... + if (!isset($_POST['link_to_board'])) + validateEventPost(); + + // If you're not allowed to edit any events, you have to be the poster. + if ($_REQUEST['eventid'] > 0 && !allowedTo('calendar_edit_any')) + isAllowedTo('calendar_edit_' . (!empty($user_info['id']) && getEventPoster($_REQUEST['eventid']) == $user_info['id'] ? 'own' : 'any')); + + // New - and directing? + if (isset($_POST['link_to_board']) || empty($modSettings['cal_allow_unlinked'])) + { + $_REQUEST['calendar'] = 1; + require_once($sourcedir . '/Post.php'); + return Post(); + } + // New... + elseif ($_REQUEST['eventid'] == -1) + { + $eventOptions = array( + 'board' => 0, + 'topic' => 0, + 'title' => $smcFunc['substr']($_REQUEST['evtitle'], 0, 100), + 'location' => $smcFunc['substr']($_REQUEST['event_location'], 0, 255), + 'member' => $user_info['id'], + ); + insertEvent($eventOptions); + } + + // Deleting... + elseif (isset($_REQUEST['deleteevent'])) + removeEvent($_REQUEST['eventid']); + + // ... or just update it? + else + { + $eventOptions = array( + 'title' => $smcFunc['substr']($_REQUEST['evtitle'], 0, 100), + 'location' => $smcFunc['substr']($_REQUEST['event_location'], 0, 255), + ); + modifyEvent($_REQUEST['eventid'], $eventOptions); + } + + updateSettings(array( + 'calendar_updated' => time(), + )); + + // No point hanging around here now... + if (isset($_POST['start_date'])) + { + $d = date_parse($_POST['start_date']); + $year = $d['year']; + $month = $d['month']; + $day = $d['day']; + } + elseif (isset($_POST['start_datetime'])) + { + $d = date_parse($_POST['start_datetime']); + $year = $d['year']; + $month = $d['month']; + $day = $d['day']; + } + else + { + $today = getdate(); + $year = isset($_POST['year']) ? $_POST['year'] : $today['year']; + $month = isset($_POST['month']) ? $_POST['month'] : $today['mon']; + $day = isset($_POST['day']) ? $_POST['day'] : $today['mday']; + } + redirectexit($scripturl . '?action=calendar;month=' . $month . ';year=' . $year . ';day=' . $day); + } + + // If we are not enabled... we are not enabled. + if (empty($modSettings['cal_allow_unlinked']) && empty($_REQUEST['eventid'])) + { + $_REQUEST['calendar'] = 1; + require_once($sourcedir . '/Post.php'); + return Post(); + } + + // New? + if (!isset($_REQUEST['eventid'])) + { + $context['event'] = array( + 'boards' => array(), + 'board' => 0, + 'new' => 1, + 'eventid' => -1, + 'title' => '', + 'location' => '', + ); + + $eventDatetimes = getNewEventDatetimes(); + $context['event'] = array_merge($context['event'], $eventDatetimes); + + $context['event']['last_day'] = (int) smf_strftime('%d', mktime(0, 0, 0, $context['event']['month'] == 12 ? 1 : $context['event']['month'] + 1, 0, $context['event']['month'] == 12 ? $context['event']['year'] + 1 : $context['event']['year'])); + } + else + { + $context['event'] = getEventProperties($_REQUEST['eventid']); + + if ($context['event'] === false) + fatal_lang_error('no_access', false); + + // If it has a board, then they should be editing it within the topic. + if (!empty($context['event']['topic']['id']) && !empty($context['event']['topic']['first_msg'])) + { + // We load the board up, for a check on the board access rights... + $topic = $context['event']['topic']['id']; + loadBoard(); + } + + // Make sure the user is allowed to edit this event. + if ($context['event']['member'] != $user_info['id']) + isAllowedTo('calendar_edit_any'); + elseif (!allowedTo('calendar_edit_any')) + isAllowedTo('calendar_edit_own'); + } + + // An all day event? Set up some nice defaults in case the user wants to change that + if ($context['event']['allday'] == true) + { + $context['event']['tz'] = getUserTimezone(); + $context['event']['start_time'] = timeformat(time(), $time_string); + $context['event']['end_time'] = timeformat(time() + 3600, $time_string); + } + // Otherwise, just adjust these to look nice on the input form + else + { + $context['event']['start_time'] = $context['event']['start_time_orig']; + $context['event']['end_time'] = $context['event']['end_time_orig']; + } + + // Need this so the user can select a timezone for the event. + $context['all_timezones'] = smf_list_timezones($context['event']['start_date']); + + // If the event's timezone is not in SMF's standard list of time zones, try to fix it. + if (!isset($context['all_timezones'][$context['event']['tz']])) + { + $later = strtotime('@' . $context['event']['start_timestamp'] . ' + 1 year'); + $tzinfo = timezone_transitions_get(timezone_open($context['event']['tz']), $context['event']['start_timestamp'], $later); + + $found = false; + foreach ($context['all_timezones'] as $possible_tzid => $dummy) + { + // Ignore the "-----" option + if (empty($possible_tzid)) + continue; + + $possible_tzinfo = timezone_transitions_get(timezone_open($possible_tzid), $context['event']['start_timestamp'], $later); + + if ($tzinfo === $possible_tzinfo) + { + $context['event']['tz'] = $possible_tzid; + $found = true; + break; + } + } + + // Hm. That's weird. Well, just prepend it to the list and let the user deal with it. + if (!$found) + { + $d = date_create($context['event']['start_datetime'] . ' ' . $context['event']['tz']); + $context['all_timezones'] = array($context['event']['tz'] => '[UTC' . date_format($d, 'P') . '] - ' . $context['event']['tz']) + $context['all_timezones']; + } + } + + // Get list of boards that can be posted in. + $boards = boardsAllowedTo('post_new'); + if (empty($boards)) + { + // You can post new events but can't link them to anything... + $context['event']['categories'] = array(); + } + else + { + // Load the list of boards and categories in the context. + require_once($sourcedir . '/Subs-MessageIndex.php'); + $boardListOptions = array( + 'included_boards' => in_array(0, $boards) ? null : $boards, + 'not_redirection' => true, + 'use_permissions' => true, + 'selected_board' => $modSettings['cal_defaultboard'], + ); + $context['event']['categories'] = getBoardList($boardListOptions); + } + + // Template, sub template, etc. + loadTemplate('Calendar'); + $context['sub_template'] = 'event_post'; + + $context['page_title'] = isset($_REQUEST['eventid']) ? $txt['calendar_edit'] : $txt['calendar_post_event']; + $context['linktree'][] = array( + 'name' => $context['page_title'], + ); + + loadDatePicker('#event_time_input .date_input'); + loadTimePicker('#event_time_input .time_input', $time_string); + loadDatePair('#event_time_input', 'date_input', 'time_input'); + addInlineJavaScript(' + $("#allday").click(function(){ + $("#start_time").attr("disabled", this.checked); + $("#end_time").attr("disabled", this.checked); + $("#tz").attr("disabled", this.checked); + });', true); +} + +/** + * This function offers up a download of an event in iCal 2.0 format. + * + * Follows the conventions in {@link https://tools.ietf.org/html/rfc5546 RFC5546} + * Sets events as all day events since we don't have hourly events + * Will honor and set multi day events + * Sets a sequence number if the event has been modified + * + * @todo .... allow for week or month export files as well? + */ +function iCalDownload() +{ + global $smcFunc, $sourcedir, $modSettings, $webmaster_email, $mbname; + + // You can't export if the calendar export feature is off. + if (empty($modSettings['cal_export'])) + fatal_lang_error('calendar_export_off', false); + + // Goes without saying that this is required. + if (!isset($_REQUEST['eventid'])) + fatal_lang_error('no_access', false); + + // This is kinda wanted. + require_once($sourcedir . '/Subs-Calendar.php'); + + // Load up the event in question and check it exists. + $event = getEventProperties($_REQUEST['eventid']); + + if ($event === false) + fatal_lang_error('no_access', false); + + // Check the title isn't too long - iCal requires some formatting if so. + $title = str_split($event['title'], 30); + foreach ($title as $id => $line) + { + if ($id != 0) + $title[$id] = ' ' . $title[$id]; + $title[$id] .= "\n"; + } + + // Format the dates. + $datestamp = date('Ymd\THis\Z', time()); + $start_date = date_create($event['start_date'] . (isset($event['start_time']) ? ' ' . $event['start_time'] : '') . (isset($event['tz']) ? ' ' . $event['tz'] : '')); + $end_date = date_create($event['end_date'] . (isset($event['end_time']) ? ' ' . $event['end_time'] : '') . (isset($event['tz']) ? ' ' . $event['tz'] : '')); + + if (!empty($event['start_time'])) + { + $datestart = date_format($start_date, 'Ymd\THis'); + $dateend = date_format($end_date, 'Ymd\THis'); + } + else + { + $datestart = date_format($start_date, 'Ymd'); + + date_add($end_date, date_interval_create_from_date_string('1 day')); + $dateend = date_format($end_date, 'Ymd'); + } + + // This is what we will be sending later + $filecontents = ''; + $filecontents .= 'BEGIN:VCALENDAR' . "\n"; + $filecontents .= 'METHOD:PUBLISH' . "\n"; + $filecontents .= 'PRODID:-//SimpleMachines//' . SMF_FULL_VERSION . '//EN' . "\n"; + $filecontents .= 'VERSION:2.0' . "\n"; + $filecontents .= 'BEGIN:VEVENT' . "\n"; + // @TODO - Should be the members email who created the event rather than $webmaster_email. + $filecontents .= 'ORGANIZER;CN="' . $event['realname'] . '":MAILTO:' . $webmaster_email . "\n"; + $filecontents .= 'DTSTAMP:' . $datestamp . "\n"; + $filecontents .= 'DTSTART' . (!empty($event['start_time']) ? ';TZID=' . $event['tz'] : ';VALUE=DATE') . ':' . $datestart . "\n"; + + // event has a duration + if ($event['start_iso_gmdate'] != $event['end_iso_gmdate']) + $filecontents .= 'DTEND' . (!empty($event['end_time']) ? ';TZID=' . $event['tz'] : ';VALUE=DATE') . ':' . $dateend . "\n"; + + // event has changed? advance the sequence for this UID + if ($event['sequence'] > 0) + $filecontents .= 'SEQUENCE:' . $event['sequence'] . "\n"; + + if (!empty($event['location'])) + $filecontents .= 'LOCATION:' . str_replace(',', '\,', $event['location']) . "\n"; + + $filecontents .= 'SUMMARY:' . implode('', $title); + $filecontents .= 'UID:' . $event['eventid'] . '@' . str_replace(' ', '-', $mbname) . "\n"; + $filecontents .= 'END:VEVENT' . "\n"; + $filecontents .= 'END:VCALENDAR'; + + // Send some standard headers. + ob_end_clean(); + if (!empty($modSettings['enableCompressedOutput'])) + @ob_start('ob_gzhandler'); + else + ob_start(); + + // Send the file headers + header('pragma: '); + header('cache-control: no-cache'); + if (!isBrowser('gecko')) + header('content-transfer-encoding: binary'); + header('expires: ' . gmdate('D, d M Y H:i:s', time() + 525600 * 60) . ' GMT'); + header('last-modified: ' . gmdate('D, d M Y H:i:s', time()) . 'GMT'); + header('accept-ranges: bytes'); + header('connection: close'); + header('content-disposition: attachment; filename="' . $event['title'] . '.ics"'); + if (empty($modSettings['enableCompressedOutput'])) + header('content-length: ' . $smcFunc['strlen']($filecontents)); + + // This is a calendar item! + header('content-type: text/calendar'); + + // Chuck out the card. + echo $filecontents; + + // Off we pop - lovely! + obExit(false); +} + +/** + * Nothing to see here. Move along. + */ +function clock() +{ + global $smcFunc, $settings, $context, $scripturl; + + $context['onimg'] = $settings['images_url'] . '/bbc/bbc_hoverbg.png'; + $context['offimg'] = $settings['images_url'] . '/bbc/bbc_bg.png'; + + $context['page_title'] = 'Anyone know what time it is?'; + $context['linktree'][] = array( + 'url' => $scripturl . '?action=clock', + 'name' => 'Clock', + ); + $context['robot_no_index'] = true; + + $omfg = isset($_REQUEST['omfg']); + $bcd = !isset($_REQUEST['rb']) && !isset($_REQUEST['omfg']) && !isset($_REQUEST['time']); + + loadTemplate('Calendar'); + + if ($bcd) + { + $context['sub_template'] = 'bcd'; + $context['linktree'][] = array('url' => $scripturl . '?action=clock;bcd', 'name' => 'BCD'); + $context['clockicons'] = $smcFunc['json_decode'](base64_decode('eyJoMSI6WzIsMV0sImgyIjpbOCw0LDIsMV0sIm0xIjpbNCwyLDFdLCJtMiI6WzgsNCwyLDFdLCJzMSI6WzQsMiwxXSwiczIiOls4LDQsMiwxXX0='), true); + } + elseif (!$omfg && !isset($_REQUEST['time'])) + { + $context['sub_template'] = 'hms'; + $context['linktree'][] = array('url' => $scripturl . '?action=clock', 'name' => 'Binary'); + $context['clockicons'] = $smcFunc['json_decode'](base64_decode('eyJoIjpbMTYsOCw0LDIsMV0sIm0iOlszMiwxNiw4LDQsMiwxXSwicyI6WzMyLDE2LDgsNCwyLDFdfQ'), true); + } + elseif ($omfg) + { + $context['sub_template'] = 'omfg'; + $context['linktree'][] = array('url' => $scripturl . '?action=clock;omfg', 'name' => 'OMFG'); + $context['clockicons'] = $smcFunc['json_decode'](base64_decode('eyJ5ZWFyIjpbNjQsMzIsMTYsOCw0LDIsMV0sIm1vbnRoIjpbOCw0LDIsMV0sImRheSI6WzE2LDgsNCwyLDFdLCJob3VyIjpbMTYsOCw0LDIsMV0sIm1pbiI6WzMyLDE2LDgsNCwyLDFdLCJzZWMiOlszMiwxNiw4LDQsMiwxXX0='), true); + } + elseif (isset($_REQUEST['time'])) + { + $context['sub_template'] = 'thetime'; + $time = getdate($_REQUEST['time'] == 'now' ? time() : (int) $_REQUEST['time']); + $context['linktree'][] = array('url' => $scripturl . '?action=clock;time=' . $_REQUEST['time'], 'name' => 'Requested Time'); + $context['clockicons'] = array( + 'year' => array( + 64 => false, + 32 => false, + 16 => false, + 8 => false, + 4 => false, + 2 => false, + 1 => false + ), + 'month' => array( + 8 => false, + 4 => false, + 2 => false, + 1 => false + ), + 'day' => array( + 16 => false, + 4 => false, + 8 => false, + 2 => false, + 1 => false + ), + 'hour' => array( + 32 => false, + 16 => false, + 8 => false, + 4 => false, + 2 => false, + 1 => false + ), + 'min' => array( + 32 => false, + 16 => false, + 8 => false, + 4 => false, + 2 => false, + 1 => false + ), + 'sec' => array( + 32 => false, + 16 => false, + 8 => false, + 4 => false, + 2 => false, + 1 => false + ), + ); + + foreach ($context['clockicons'] as $t => $vs) + foreach ($vs as $v => $dumb) + { + if ($$t >= $v) + { + $$t -= $v; + $context['clockicons'][$t][$v] = true; + } + } + } +} + +?> \ No newline at end of file diff --git a/Sources/Class-BrowserDetect.php b/Sources/Class-BrowserDetect.php new file mode 100644 index 0000000..bf4f4b0 --- /dev/null +++ b/Sources/Class-BrowserDetect.php @@ -0,0 +1,462 @@ +_browsers = array(); + $this->_is_mobile = false; + + // Initialize some values we'll set differently if necessary... + $this->_browsers['needs_size_fix'] = false; + + // One at a time, one at a time, and in this order too + if ($this->isOpera()) + $this->setupOpera(); + // Meh... + elseif ($this->isEdge()) + $this->setupEdge(); + // Them webkits need to be set up too + elseif ($this->isWebkit()) + $this->setupWebkit(); + // We may have work to do on Firefox... + elseif ($this->isFirefox()) + $this->setupFirefox(); + // Old friend, old frenemy + elseif ($this->isIe()) + $this->setupIe(); + + // Just a few mobile checks + $this->isOperaMini(); + $this->isOperaMobi(); + + // IE11 seems to be fine by itself without being lumped into the "is_ie" category + $this->isIe11(); + + // Be you robot or human? + if ($user_info['possibly_robot']) + { + // This isn't meant to be reliable, it's just meant to catch most bots to prevent PHPSESSID from showing up. + $this->_browsers['possibly_robot'] = !empty($user_info['possibly_robot']); + + // Robots shouldn't be logging in or registering. So, they aren't a bot. Better to be wrong than sorry (or people won't be able to log in!), anyway. + if ((isset($_REQUEST['action']) && in_array($_REQUEST['action'], array('login', 'login2', 'register', 'signup'))) || !$user_info['is_guest']) + $this->_browsers['possibly_robot'] = false; + } + else + $this->_browsers['possibly_robot'] = false; + + // Fill out the historical array as needed to support old mods that don't use isBrowser + $this->fillInformation(); + + // Make it easy to check if the browser is on a mobile device. + $this->_browsers['is_mobile'] = $this->_is_mobile; + + // Last step ... + $this->setupBrowserPriority(); + + // Now see what you've done! + $context['browser'] = $this->_browsers; + } + + /** + * Determine if the browser is Opera or not + * + * @return boolean Whether or not this is Opera + */ + function isOpera() + { + if (!isset($this->_browsers['is_opera'])) + $this->_browsers['is_opera'] = strpos($_SERVER['HTTP_USER_AGENT'], 'Opera') !== false; + return $this->_browsers['is_opera']; + } + + /** + * Determine if the browser is IE or not + * + * @return boolean true Whether or not the browser is IE + */ + function isIe() + { + // I'm IE, Yes I'm the real IE; All you other IEs are just imitating. + if (!isset($this->_browsers['is_ie'])) + $this->_browsers['is_ie'] = !$this->isOpera() && !$this->isGecko() && !$this->isWebTv() && preg_match('~MSIE \d+~', $_SERVER['HTTP_USER_AGENT']) === 1; + return $this->_browsers['is_ie']; + } + + /** + * Determine if the browser is IE11 or not + * + * @return boolean Whether or not the browser is IE11 + */ + function isIe11() + { + // IE11 is a bit different than earlier versions + // The isGecko() part is to ensure we get this right... + if (!isset($this->_browsers['is_ie11'])) + $this->_browsers['is_ie11'] = strpos($_SERVER['HTTP_USER_AGENT'], 'Trident') !== false && $this->isGecko(); + return $this->_browsers['is_ie11']; + } + + /** + * Determine if the browser is Edge or not + * + * @return boolean Whether or not the browser is Edge + */ + function isEdge() + { + if (!isset($this->_browsers['is_edge'])) + $this->_browsers['is_edge'] = strpos($_SERVER['HTTP_USER_AGENT'], 'Edge') !== false; + return $this->_browsers['is_edge']; + } + + /** + * Determine if the browser is a Webkit based one or not + * + * @return boolean Whether or not this is a Webkit-based browser + */ + function isWebkit() + { + if (!isset($this->_browsers['is_webkit'])) + $this->_browsers['is_webkit'] = strpos($_SERVER['HTTP_USER_AGENT'], 'AppleWebKit') !== false; + return $this->_browsers['is_webkit']; + } + + /** + * Determine if the browser is Firefox or one of its variants + * + * @return boolean Whether or not this is Firefox (or one of its variants) + */ + function isFirefox() + { + if (!isset($this->_browsers['is_firefox'])) + $this->_browsers['is_firefox'] = preg_match('~(?:Firefox|Ice[wW]easel|IceCat|Shiretoko|Minefield)/~', $_SERVER['HTTP_USER_AGENT']) === 1 && $this->isGecko(); + return $this->_browsers['is_firefox']; + } + + /** + * Determine if the browser is WebTv or not + * + * @return boolean Whether or not this is WebTV + */ + function isWebTv() + { + if (!isset($this->_browsers['is_web_tv'])) + $this->_browsers['is_web_tv'] = strpos($_SERVER['HTTP_USER_AGENT'], 'WebTV') !== false; + return $this->_browsers['is_web_tv']; + } + + /** + * Determine if the browser is konqueror or not + * + * @return boolean Whether or not this is Konqueror + */ + function isKonqueror() + { + if (!isset($this->_browsers['is_konqueror'])) + $this->_browsers['is_konqueror'] = strpos($_SERVER['HTTP_USER_AGENT'], 'Konqueror') !== false; + return $this->_browsers['is_konqueror']; + } + + /** + * Determine if the browser is Gecko or not + * + * @return boolean Whether or not this is a Gecko-based browser + */ + function isGecko() + { + if (!isset($this->_browsers['is_gecko'])) + $this->_browsers['is_gecko'] = strpos($_SERVER['HTTP_USER_AGENT'], 'Gecko') !== false && !$this->isWebkit() && !$this->isKonqueror(); + return $this->_browsers['is_gecko']; + } + + /** + * Determine if the browser is Opera Mini or not + * + * @return boolean Whether or not this is Opera Mini + */ + function isOperaMini() + { + if (!isset($this->_browsers['is_opera_mini'])) + $this->_browsers['is_opera_mini'] = (isset($_SERVER['HTTP_X_OPERAMINI_PHONE_UA']) || stripos($_SERVER['HTTP_USER_AGENT'], 'opera mini') !== false); + if ($this->_browsers['is_opera_mini']) + $this->_is_mobile = true; + return $this->_browsers['is_opera_mini']; + } + + /** + * Determine if the browser is Opera Mobile or not + * + * @return boolean Whether or not this is Opera Mobile + */ + function isOperaMobi() + { + if (!isset($this->_browsers['is_opera_mobi'])) + $this->_browsers['is_opera_mobi'] = stripos($_SERVER['HTTP_USER_AGENT'], 'opera mobi') !== false; + if ($this->_browsers['is_opera_mobi']) + $this->_is_mobile = true; + return $this->_browsers['is_opera_mini']; + } + + /** + * Detect Safari / Chrome / iP[ao]d / iPhone / Android / Blackberry from webkit. + * - set the browser version for Safari and Chrome + * - set the mobile flag for mobile based useragents + */ + private function setupWebkit() + { + $this->_browsers += array( + 'is_chrome' => strpos($_SERVER['HTTP_USER_AGENT'], 'Chrome') !== false, + 'is_iphone' => (strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false || strpos($_SERVER['HTTP_USER_AGENT'], 'iPod') !== false) && strpos($_SERVER['HTTP_USER_AGENT'], 'iPad') === false, + 'is_blackberry' => stripos($_SERVER['HTTP_USER_AGENT'], 'BlackBerry') !== false || strpos($_SERVER['HTTP_USER_AGENT'], 'PlayBook') !== false, + 'is_android' => strpos($_SERVER['HTTP_USER_AGENT'], 'Android') !== false, + 'is_nokia' => strpos($_SERVER['HTTP_USER_AGENT'], 'SymbianOS') !== false, + ); + + // blackberry, playbook, iphone, nokia, android and ipods set a mobile flag + if ($this->_browsers['is_iphone'] || $this->_browsers['is_blackberry'] || $this->_browsers['is_android'] || $this->_browsers['is_nokia']) + $this->_is_mobile = true; + + // @todo what to do with the blaPad? ... for now leave it detected as Safari ... + $this->_browsers['is_safari'] = strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') !== false && !$this->_browsers['is_chrome'] && !$this->_browsers['is_iphone']; + $this->_browsers['is_ipad'] = strpos($_SERVER['HTTP_USER_AGENT'], 'iPad') !== false; + + // if Chrome, get the major version + if ($this->_browsers['is_chrome']) + { + if (preg_match('~chrome[/]([0-9][0-9]?[.])~i', $_SERVER['HTTP_USER_AGENT'], $match) === 1) + $this->_browsers['is_chrome' . (int) $match[1]] = true; + } + + // or if Safari get its major version + if ($this->_browsers['is_safari']) + { + if (preg_match('~version/?(.*)safari.*~i', $_SERVER['HTTP_USER_AGENT'], $match) === 1) + $this->_browsers['is_safari' . (int) trim($match[1])] = true; + } + } + + /** + * Additional IE checks and settings. + * - determines the version of the IE browser in use + * - detects ie4 onward + * - attempts to distinguish between IE and IE in compatibility view + * - checks for old IE on macs as well, since we can + */ + private function setupIe() + { + $this->_browsers['is_ie_compat_view'] = false; + + // get the version of the browser from the msie tag + if (preg_match('~MSIE\s?([0-9][0-9]?.[0-9])~i', $_SERVER['HTTP_USER_AGENT'], $msie_match) === 1) + { + $msie_match[1] = trim($msie_match[1]); + $msie_match[1] = (($msie_match[1] - (int) $msie_match[1]) == 0) ? (int) $msie_match[1] : $msie_match[1]; + $this->_browsers['is_ie' . $msie_match[1]] = true; + } + + // "modern" ie uses trident 4=ie8, 5=ie9, 6=ie10, 7=ie11 even in compatibility view + if (preg_match('~Trident/([0-9.])~i', $_SERVER['HTTP_USER_AGENT'], $trident_match) === 1) + { + $this->_browsers['is_ie' . ((int) $trident_match[1] + 4)] = true; + + // If trident is set, see the (if any) msie tag in the user agent matches ... if not its in some compatibility view + if (isset($msie_match[1]) && ($msie_match[1] < $trident_match[1] + 4)) + $this->_browsers['is_ie_compat_view'] = true; + } + + // Detect true IE6 and IE7 and not IE in compat mode. + $this->_browsers['is_ie7'] = !empty($this->_browsers['is_ie7']) && ($this->_browsers['is_ie_compat_view'] === false); + $this->_browsers['is_ie6'] = !empty($this->_browsers['is_ie6']) && ($this->_browsers['is_ie_compat_view'] === false); + + // IE mobile 7 or 9, ... shucks why not + if ((!empty($this->_browsers['is_ie7']) && strpos($_SERVER['HTTP_USER_AGENT'], 'IEMobile/7') !== false) || (!empty($this->_browsers['is_ie9']) && strpos($_SERVER['HTTP_USER_AGENT'], 'IEMobile/9') !== false)) + { + $this->_browsers['is_ie_mobi'] = true; + $this->_is_mobile = true; + } + + // And some throwbacks to a bygone era, deposited here like cholesterol in your arteries + $this->_browsers += array( + 'is_ie4' => !empty($this->_browsers['is_ie4']) && !$this->_browsers['is_web_tv'], + 'is_mac_ie' => strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE 5.') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') !== false + ); + + // Before IE8 we need to fix IE... lots! + $this->_browsers['ie_standards_fix'] = (($this->_browsers['is_ie6'] === true) || ($this->_browsers['is_ie7'] === true)) ? true : false; + + // We may even need a size fix... + $this->_browsers['needs_size_fix'] = (!empty($this->_browsers['is_ie5']) || !empty($this->_browsers['is_ie5.5']) || !empty($this->_browsers['is_ie4'])) && !$this->_browsers['is_mac_ie']; + } + + /** + * Additional firefox checks. + * - Gets the version of the FF browser in use + * - Considers all FF variants as FF including IceWeasel, IceCat, Shiretoko and Minefiled + */ + private function setupFirefox() + { + if (preg_match('~(?:Firefox|Ice[wW]easel|IceCat|Shiretoko|Minefield)[\/ \(]([^ ;\)]+)~', $_SERVER['HTTP_USER_AGENT'], $match) === 1) + $this->_browsers['is_firefox' . (int) $match[1]] = true; + } + + /** + * More Opera checks if we are opera. + * - checks for the version of Opera in use + * - uses checks for 10 first and falls through to <9 + */ + private function setupOpera() + { + // Opera 10+ uses the version tag at the end of the string + if (preg_match('~\sVersion/([0-9]+)\.[0-9]+(?:\s*|$)~', $_SERVER['HTTP_USER_AGENT'], $match)) + $this->_browsers['is_opera' . (int) $match[1]] = true; + // Opera pre 10 is supposed to uses the Opera tag alone, as do some spoofers + elseif (preg_match('~Opera[ /]([0-9]+)(?!\\.[89])~', $_SERVER['HTTP_USER_AGENT'], $match)) + $this->_browsers['is_opera' . (int) $match[1]] = true; + + // Needs size fix? + $this->_browsers['needs_size_fix'] = !empty($this->_browsers['is_opera6']); + } + + /** + * Sets the version number for MS edge. + */ + private function setupEdge() + { + if (preg_match('~Edge[\/]([0-9][0-9]?[\.][0-9][0-9])~i', $_SERVER['HTTP_USER_AGENT'], $match) === 1) + $this->_browsers['is_edge' . (int) $match[1]] = true; + } + + /** + * Get the browser name that we will use in the + * - The order of each browser in $browser_priority is important + * - if you want to have id='ie6' and not id='ie' then it must appear first in the list of ie browsers + * - only sets browsers that may need some help via css for compatibility + */ + private function setupBrowserPriority() + { + global $context; + + if ($this->_is_mobile) + $context['browser_body_id'] = 'mobile'; + else + { + // add in any specific detection conversions here if you want a special body id e.g. 'is_opera9' => 'opera9' + $browser_priority = array( + 'is_ie6' => 'ie6', + 'is_ie7' => 'ie7', + 'is_ie8' => 'ie8', + 'is_ie9' => 'ie9', + 'is_ie10' => 'ie10', + 'is_ie11' => 'ie11', + 'is_ie' => 'ie', + 'is_edge' => 'edge', + 'is_firefox' => 'firefox', + 'is_chrome' => 'chrome', + 'is_safari' => 'safari', + 'is_opera10' => 'opera10', + 'is_opera11' => 'opera11', + 'is_opera12' => 'opera12', + 'is_opera' => 'opera', + 'is_konqueror' => 'konqueror', + ); + + $context['browser_body_id'] = 'smf'; + $active = array_reverse(array_keys($this->_browsers, true)); + foreach ($active as $browser) + { + if (array_key_exists($browser, $browser_priority)) + { + $context['browser_body_id'] = $browser_priority[$browser]; + break; + } + } + } + } + + /** + * Fill out the historical array + * - needed to support old mods that don't use isBrowser + */ + function fillInformation() + { + $this->_browsers += array( + 'is_opera' => false, + 'is_opera6' => false, + 'is_opera7' => false, + 'is_opera8' => false, + 'is_opera9' => false, + 'is_opera10' => false, + 'is_webkit' => false, + 'is_mac_ie' => false, + 'is_web_tv' => false, + 'is_konqueror' => false, + 'is_firefox' => false, + 'is_firefox1' => false, + 'is_firefox2' => false, + 'is_firefox3' => false, + 'is_iphone' => false, + 'is_android' => false, + 'is_chrome' => false, + 'is_safari' => false, + 'is_gecko' => false, + 'is_edge' => false, + 'is_ie8' => false, + 'is_ie7' => false, + 'is_ie6' => false, + 'is_ie5.5' => false, + 'is_ie5' => false, + 'is_ie' => false, + 'is_ie4' => false, + 'ie_standards_fix' => false, + 'needs_size_fix' => false, + 'possibly_robot' => false, + ); + } +} + +?> \ No newline at end of file diff --git a/Sources/Class-CurlFetchWeb.php b/Sources/Class-CurlFetchWeb.php new file mode 100644 index 0000000..f596e3f --- /dev/null +++ b/Sources/Class-CurlFetchWeb.php @@ -0,0 +1,371 @@ + 1), 5); + * ``` + * + * ### Make the call + * Fetch a page + * ``` + * $fetch_data->get_url_data('https://www.simplemachines.org'); + * ``` + * Post to a page providing an array + * ``` + * $fetch_data->get_url_data('https://www.simplemachines.org', array('user' => 'name', 'password' => 'password')); + * ``` + * Post to a page providing a string + * ``` + * $fetch_data->get_url_data('https://www.simplemachines.org', parameter1¶meter2¶meter3); + * ``` + * + * ### Get the data + * Just the page content + * ``` + * $fetch_data->result('body'); + * ``` + * An array of results, body, header, http result codes + * ``` + * $fetch_data->result(); + * ``` + * Show all results of all calls (in the event of a redirect) + * ``` + * $fetch_data->result_raw(); + * ``` + * Show the results of a specific call (in the event of a redirect) + * ``` + * $fetch_data->result_raw(0); + * ``` + */ +class curl_fetch_web_data +{ + /** + * Set the default items for this class + * + * @var array $default_options + */ + private $default_options = array( + CURLOPT_RETURNTRANSFER => 1, // Get returned value as a string (don't output it) + CURLOPT_HEADER => 1, // We need the headers to do our own redirect + CURLOPT_FOLLOWLOCATION => 0, // Don't follow, we will do it ourselves so safe mode and open_basedir will dig it + CURLOPT_USERAGENT => SMF_USER_AGENT, // set a normal looking useragent + CURLOPT_CONNECTTIMEOUT => 15, // Don't wait forever on a connection + CURLOPT_TIMEOUT => 90, // A page should load in this amount of time + CURLOPT_MAXREDIRS => 5, // stop after this many redirects + CURLOPT_ENCODING => 'gzip,deflate', // accept gzip and decode it + CURLOPT_SSL_VERIFYPEER => 0, // stop cURL from verifying the peer's certificate + CURLOPT_SSL_VERIFYHOST => 0, // stop cURL from verifying the peer's host + CURLOPT_POST => 0, // no post data unless its passed + ); + + /** + * @var int Maximum number of redirects + */ + public $max_redirect; + + /** + * @var array An array of cURL options + */ + public $user_options = array(); + + /** + * @var string Any post data as form name => value + */ + public $post_data; + + /** + * @var array An array of cURL options + */ + public $options; + + /** + * @var int ??? + */ + public $current_redirect; + + /** + * @var array Stores responses (url, code, error, headers, body) in the response array + */ + public $response = array(); + + /** + * @var string The header + */ + public $headers; + + /** + * Start the curl object + * - allow for user override values + * + * @param array $options An array of cURL options + * @param int $max_redirect Maximum number of redirects + */ + public function __construct($options = array(), $max_redirect = 3) + { + // Initialize class variables + $this->max_redirect = intval($max_redirect); + $this->user_options = $options; + } + + /** + * Main calling function, + * - will request the page data from a given $url + * - optionally will post data to the page form if post data is supplied + * - passed arrays will be converted to a post string joined with &'s + * - calls set_options to set the curl opts array values based on the defaults and user input + * + * @param string $url the site we are going to fetch + * @param array $post_data any post data as form name => value + * @return object An instance of the curl_fetch_web_data class + */ + public function get_url_data($url, $post_data = array()) + { + // POSTing some data perhaps? + if (!empty($post_data) && is_array($post_data)) + $this->post_data = $this->build_post_data($post_data); + elseif (!empty($post_data)) + $this->post_data = trim($post_data); + + // set the options and get it + $this->set_options(); + $this->curl_request(str_replace(' ', '%20', $url)); + + return $this; + } + + /** + * Makes the actual cURL call + * - stores responses (url, code, error, headers, body) in the response array + * - detects 301, 302, 307 codes and will redirect to the given response header location + * + * @param string $url The site to fetch + * @param bool $redirect Whether or not this was a redirect request + * @return void|bool Sets various properties of the class or returns false if the URL isn't specified + */ + private function curl_request($url, $redirect = false) + { + // we do have a url I hope + if ($url == '') + return false; + else + $this->options[CURLOPT_URL] = $url; + + // if we have not already been redirected, set it up so we can if needed + if (!$redirect) + { + $this->current_redirect = 1; + $this->response = array(); + } + + // Initialize the curl object and make the call + $cr = curl_init(); + curl_setopt_array($cr, $this->options); + curl_exec($cr); + + // Get what was returned + $curl_info = curl_getinfo($cr); + $curl_content = curl_multi_getcontent($cr); + $url = $curl_info['url']; // Last effective URL + $http_code = $curl_info['http_code']; // Last HTTP code + $body = (!curl_error($cr)) ? substr($curl_content, $curl_info['header_size']) : false; + $error = (curl_error($cr)) ? curl_error($cr) : false; + + // close this request + curl_close($cr); + + // store this 'loops' data, someone may want all of these :O + $this->response[] = array( + 'url' => $url, + 'code' => $http_code, + 'error' => $error, + 'headers' => isset($this->headers) ? $this->headers : false, + 'body' => $body, + 'size' => $curl_info['download_content_length'], + ); + + // If this a redirect with a location header and we have not given up, then do it again + if (preg_match('~30[127]~i', $http_code) === 1 && $this->headers['location'] != '' && $this->current_redirect <= $this->max_redirect) + { + $this->current_redirect++; + $header_location = $this->get_redirect_url($url, $this->headers['location']); + $this->redirect($header_location, $url); + } + } + + /** + * Used if being redirected to ensure we have a fully qualified address + * + * @param string $last_url The URL we went to + * @param string $new_url The URL we were redirected to + * @return string The new URL that was in the HTTP header + */ + private function get_redirect_url($last_url = '', $new_url = '') + { + // Get the elements for these urls + $last_url_parse = parse_url($last_url); + $new_url_parse = parse_url($new_url); + + // redirect headers are often incomplete or relative so we need to make sure they are fully qualified + $new_url_parse['scheme'] = isset($new_url_parse['scheme']) ? $new_url_parse['scheme'] : $last_url_parse['scheme']; + $new_url_parse['host'] = isset($new_url_parse['host']) ? $new_url_parse['host'] : $last_url_parse['host']; + $new_url_parse['path'] = isset($new_url_parse['path']) ? $new_url_parse['path'] : $last_url_parse['path']; + $new_url_parse['query'] = isset($new_url_parse['query']) ? $new_url_parse['query'] : ''; + + // Build the new URL that was in the http header + return $new_url_parse['scheme'] . '://' . $new_url_parse['host'] . $new_url_parse['path'] . (!empty($new_url_parse['query']) ? '?' . $new_url_parse['query'] : ''); + } + + /** + * Used to return the results to the calling program + * - called as ->result() will return the full final array + * - called as ->result('body') to just return the page source of the result + * + * @param string $area Used to return an area such as body, header, error + * @return string The response + */ + public function result($area = '') + { + $max_result = count($this->response) - 1; + + // just return a specifed area or the entire result? + if ($area == '') + return $this->response[$max_result]; + else + return isset($this->response[$max_result][$area]) ? $this->response[$max_result][$area] : $this->response[$max_result]; + } + + /** + * Will return all results from all loops (redirects) + * - Can be called as ->result_raw(x) where x is a specific loop results. + * - Call as ->result_raw() for everything. + * + * @param string $response_number Which response we want to get + * @return array|string The entire response array or just the specified response + */ + public function result_raw($response_number = '') + { + if (!is_numeric($response_number)) + return $this->response; + else + { + $response_number = min($response_number, count($this->response) - 1); + return $this->response[$response_number]; + } + } + + /** + * Takes supplied POST data and url encodes it + * - forms the date (for post) in to a string var=xyz&var2=abc&var3=123 + * - drops vars with @ since we don't support sending files (uploading) + * + * @param array|string $post_data The raw POST data + * @return string A string of post data + */ + private function build_post_data($post_data) + { + if (is_array($post_data)) + { + $postvars = array(); + + // build the post data, drop ones with leading @'s since those can be used to send files, we don't support that. + foreach ($post_data as $name => $value) + $postvars[] = $name . '=' . urlencode($value[0] == '@' ? '' : $value); + + return implode('&', $postvars); + } + else + return $post_data; + } + + /** + * Sets the final cURL options for the current call + * - overwrites our default values with user supplied ones or appends new user ones to what we have + * - sets the callback function now that $this is existing + * + * @return void + */ + private function set_options() + { + // Callback to parse the returned headers, if any + $this->default_options[CURLOPT_HEADERFUNCTION] = array($this, 'header_callback'); + + // Any user options to account for + if (is_array($this->user_options)) + { + $keys = array_merge(array_keys($this->default_options), array_keys($this->user_options)); + $vals = array_merge($this->default_options, $this->user_options); + $this->options = array_combine($keys, $vals); + } + else + $this->options = $this->default_options; + + // POST data options, here we don't allow any overide + if (isset($this->post_data)) + { + $this->options[CURLOPT_POST] = 1; + $this->options[CURLOPT_POSTFIELDS] = $this->post_data; + } + } + + /** + * Called to initiate a redirect from a 301, 302 or 307 header + * - resets the cURL options for the loop, sets the referrer flag + * + * @param string $target_url The URL we want to redirect to + * @param string $referer_url The URL that we're redirecting from + */ + private function redirect($target_url, $referer_url) + { + // no no I last saw that over there ... really, 301, 302, 307 + $this->set_options(); + $this->options[CURLOPT_REFERER] = $referer_url; + $this->curl_request($target_url, true); + } + + /** + * Callback function to parse returned headers + * - lowercases everything to make it consistent + * + * @param curl_fetch_web_data $cr The curl request + * @param string $header The header + * @return int The length of the header + */ + private function header_callback($cr, $header) + { + $_header = trim($header); + $temp = explode(': ', $_header, 2); + + // set proper headers only + if (isset($temp[0]) && isset($temp[1])) + $this->headers[strtolower($temp[0])] = strtolower(trim($temp[1])); + + // return the length of what was passed unless you want a Failed writing header error ;) + return strlen($header); + } +} + +?> \ No newline at end of file diff --git a/Sources/Class-Graphics.php b/Sources/Class-Graphics.php new file mode 100644 index 0000000..f2f7021 --- /dev/null +++ b/Sources/Class-Graphics.php @@ -0,0 +1,705 @@ +MAX_LZW_BITS = 12; + unset($this->Next, $this->Vals, $this->Stack, $this->Buf); + + $this->Next = range(0, (1 << $this->MAX_LZW_BITS) - 1); + $this->Vals = range(0, (1 << $this->MAX_LZW_BITS) - 1); + $this->Stack = range(0, (1 << ($this->MAX_LZW_BITS + 1)) - 1); + $this->Buf = range(0, 279); + } + + public function decompress($data, &$datLen) + { + $stLen = strlen($data); + $datLen = 0; + $ret = ''; + + $this->LZWCommand($data, true); + + while (($iIndex = $this->LZWCommand($data, false)) >= 0) + $ret .= chr($iIndex); + + $datLen = $stLen - strlen($data); + + if ($iIndex != -2) + return false; + + return $ret; + } + + public function LZWCommand(&$data, $bInit) + { + if ($bInit) + { + $this->SetCodeSize = ord($data[0]); + $data = substr($data, 1); + + $this->CodeSize = $this->SetCodeSize + 1; + $this->ClearCode = 1 << $this->SetCodeSize; + $this->EndCode = $this->ClearCode + 1; + $this->MaxCode = $this->ClearCode + 2; + $this->MaxCodeSize = $this->ClearCode << 1; + + $this->GetCode($data, $bInit); + + $this->Fresh = 1; + for ($i = 0; $i < $this->ClearCode; $i++) + { + $this->Next[$i] = 0; + $this->Vals[$i] = $i; + } + + for (; $i < (1 << $this->MAX_LZW_BITS); $i++) + { + $this->Next[$i] = 0; + $this->Vals[$i] = 0; + } + + $this->sp = 0; + return 1; + } + + if ($this->Fresh) + { + $this->Fresh = 0; + do + { + $this->FirstCode = $this->GetCode($data, $bInit); + $this->OldCode = $this->FirstCode; + } + while ($this->FirstCode == $this->ClearCode); + + return $this->FirstCode; + } + + if ($this->sp > 0) + { + $this->sp--; + return $this->Stack[$this->sp]; + } + + while (($Code = $this->GetCode($data, $bInit)) >= 0) + { + if ($Code == $this->ClearCode) + { + for ($i = 0; $i < $this->ClearCode; $i++) + { + $this->Next[$i] = 0; + $this->Vals[$i] = $i; + } + + for (; $i < (1 << $this->MAX_LZW_BITS); $i++) + { + $this->Next[$i] = 0; + $this->Vals[$i] = 0; + } + + $this->CodeSize = $this->SetCodeSize + 1; + $this->MaxCodeSize = $this->ClearCode << 1; + $this->MaxCode = $this->ClearCode + 2; + $this->sp = 0; + $this->FirstCode = $this->GetCode($data, $bInit); + $this->OldCode = $this->FirstCode; + + return $this->FirstCode; + } + + if ($Code == $this->EndCode) + return -2; + + $InCode = $Code; + if ($Code >= $this->MaxCode) + { + $this->Stack[$this->sp] = $this->FirstCode; + $this->sp++; + $Code = $this->OldCode; + } + + while ($Code >= $this->ClearCode) + { + $this->Stack[$this->sp] = $this->Vals[$Code]; + $this->sp++; + + if ($Code == $this->Next[$Code]) // Circular table entry, big GIF Error! + return -1; + + $Code = $this->Next[$Code]; + } + + $this->FirstCode = $this->Vals[$Code]; + $this->Stack[$this->sp] = $this->FirstCode; + $this->sp++; + + if (($Code = $this->MaxCode) < (1 << $this->MAX_LZW_BITS)) + { + $this->Next[$Code] = $this->OldCode; + $this->Vals[$Code] = $this->FirstCode; + $this->MaxCode++; + + if (($this->MaxCode >= $this->MaxCodeSize) && ($this->MaxCodeSize < (1 << $this->MAX_LZW_BITS))) + { + $this->MaxCodeSize *= 2; + $this->CodeSize++; + } + } + + $this->OldCode = $InCode; + if ($this->sp > 0) + { + $this->sp--; + return $this->Stack[$this->sp]; + } + } + + return $Code; + } + + public function GetCode(&$data, $bInit) + { + if ($bInit) + { + $this->CurBit = 0; + $this->LastBit = 0; + $this->Done = 0; + $this->LastByte = 2; + + return 1; + } + + if (($this->CurBit + $this->CodeSize) >= $this->LastBit) + { + if ($this->Done) + { + // Ran off the end of my bits... + if ($this->CurBit >= $this->LastBit) + return 0; + + return -1; + } + + $this->Buf[0] = $this->Buf[$this->LastByte - 2]; + $this->Buf[1] = $this->Buf[$this->LastByte - 1]; + + $count = ord($data[0]); + $data = substr($data, 1); + + if ($count) + { + for ($i = 0; $i < $count; $i++) + $this->Buf[2 + $i] = ord($data[$i]); + + $data = substr($data, $count); + } + else + $this->Done = 1; + + $this->LastByte = 2 + $count; + $this->CurBit = ($this->CurBit - $this->LastBit) + 16; + $this->LastBit = (2 + $count) << 3; + } + + $iRet = 0; + for ($i = $this->CurBit, $j = 0; $j < $this->CodeSize; $i++, $j++) + $iRet |= (($this->Buf[intval($i / 8)] & (1 << ($i % 8))) != 0) << $j; + + $this->CurBit += $this->CodeSize; + return $iRet; + } +} + +class gif_color_table +{ + public $m_nColors; + public $m_arColors; + + public function __construct() + { + unset($this->m_nColors, $this->m_arColors); + } + + public function load($lpData, $num) + { + $this->m_nColors = 0; + $this->m_arColors = array(); + + for ($i = 0; $i < $num; $i++) + { + $rgb = substr($lpData, $i * 3, 3); + if (strlen($rgb) < 3) + return false; + + $this->m_arColors[] = (ord($rgb[2]) << 16) + (ord($rgb[1]) << 8) + ord($rgb[0]); + $this->m_nColors++; + } + + return true; + } + + public function toString() + { + $ret = ''; + + for ($i = 0; $i < $this->m_nColors; $i++) + { + $ret .= + chr(($this->m_arColors[$i] & 0x000000FF)) . // R + chr(($this->m_arColors[$i] & 0x0000FF00) >> 8) . // G + chr(($this->m_arColors[$i] & 0x00FF0000) >> 16); // B + } + + return $ret; + } + + public function colorIndex($rgb) + { + $dif = 0; + $rgb = intval($rgb) & 0xFFFFFF; + $r1 = ($rgb & 0x0000FF); + $g1 = ($rgb & 0x00FF00) >> 8; + $b1 = ($rgb & 0xFF0000) >> 16; + $idx = -1; + + for ($i = 0; $i < $this->m_nColors; $i++) + { + $r2 = ($this->m_arColors[$i] & 0x000000FF); + $g2 = ($this->m_arColors[$i] & 0x0000FF00) >> 8; + $b2 = ($this->m_arColors[$i] & 0x00FF0000) >> 16; + $d = abs($r2 - $r1) + abs($g2 - $g1) + abs($b2 - $b1); + + if (($idx == -1) || ($d < $dif)) + { + $idx = $i; + $dif = $d; + } + } + + return $idx; + } +} + +class gif_file_header +{ + public $m_lpVer, $m_nWidth, $m_nHeight, $m_bGlobalClr, $m_nColorRes; + public $m_bSorted, $m_nTableSize, $m_nBgColor, $m_nPixelRatio; + public $m_colorTable; + + public function __construct() + { + unset($this->m_lpVer, $this->m_nWidth, $this->m_nHeight, $this->m_bGlobalClr, $this->m_nColorRes); + unset($this->m_bSorted, $this->m_nTableSize, $this->m_nBgColor, $this->m_nPixelRatio, $this->m_colorTable); + } + + public function load($lpData, &$hdrLen) + { + $hdrLen = 0; + + $this->m_lpVer = substr($lpData, 0, 6); + if (($this->m_lpVer != 'GIF87a') && ($this->m_lpVer != 'GIF89a')) + return false; + + list ($this->m_nWidth, $this->m_nHeight) = array_values(unpack('v2', substr($lpData, 6, 4))); + + if (!$this->m_nWidth || !$this->m_nHeight) + return false; + + $b = ord(substr($lpData, 10, 1)); + $this->m_bGlobalClr = ($b & 0x80) ? true : false; + $this->m_nColorRes = ($b & 0x70) >> 4; + $this->m_bSorted = ($b & 0x08) ? true : false; + $this->m_nTableSize = 2 << ($b & 0x07); + $this->m_nBgColor = ord(substr($lpData, 11, 1)); + $this->m_nPixelRatio = ord(substr($lpData, 12, 1)); + $hdrLen = 13; + + if ($this->m_bGlobalClr) + { + $this->m_colorTable = new gif_color_table(); + if (!$this->m_colorTable->load(substr($lpData, $hdrLen), $this->m_nTableSize)) + return false; + + $hdrLen += 3 * $this->m_nTableSize; + } + + return true; + } +} + +class gif_image_header +{ + public $m_nLeft, $m_nTop, $m_nWidth, $m_nHeight, $m_bLocalClr; + public $m_bInterlace, $m_bSorted, $m_nTableSize, $m_colorTable; + + public function __construct() + { + unset($this->m_nLeft, $this->m_nTop, $this->m_nWidth, $this->m_nHeight, $this->m_bLocalClr); + unset($this->m_bInterlace, $this->m_bSorted, $this->m_nTableSize, $this->m_colorTable); + } + + public function load($lpData, &$hdrLen) + { + $hdrLen = 0; + + // Get the width/height/etc. from the header. + list ($this->m_nLeft, $this->m_nTop, $this->m_nWidth, $this->m_nHeight) = array_values(unpack('v4', substr($lpData, 0, 8))); + + if (!$this->m_nWidth || !$this->m_nHeight) + return false; + + $b = ord($lpData[8]); + $this->m_bLocalClr = ($b & 0x80) ? true : false; + $this->m_bInterlace = ($b & 0x40) ? true : false; + $this->m_bSorted = ($b & 0x20) ? true : false; + $this->m_nTableSize = 2 << ($b & 0x07); + $hdrLen = 9; + + if ($this->m_bLocalClr) + { + $this->m_colorTable = new gif_color_table(); + if (!$this->m_colorTable->load(substr($lpData, $hdrLen), $this->m_nTableSize)) + return false; + + $hdrLen += 3 * $this->m_nTableSize; + } + + return true; + } +} + +class gif_image +{ + public $m_disp, $m_bUser, $m_bTrans, $m_nDelay, $m_nTrans, $m_lpComm; + public $m_gih, $m_data, $m_lzw; + + public function __construct() + { + unset($this->m_disp, $this->m_bUser, $this->m_nDelay, $this->m_nTrans, $this->m_lpComm, $this->m_data); + $this->m_gih = new gif_image_header(); + $this->m_lzw = new gif_lzw_compression(); + } + + public function load($data, &$datLen) + { + $datLen = 0; + + while (true) + { + $b = ord($data[0]); + $data = substr($data, 1); + $datLen++; + + switch ($b) + { + // Extension... + case 0x21: + $len = 0; + if (!$this->skipExt($data, $len)) + return false; + + $datLen += $len; + break; + + // Image... + case 0x2C: + // Load the header and color table. + $len = 0; + if (!$this->m_gih->load($data, $len)) + return false; + + $data = substr($data, $len); + $datLen += $len; + + // Decompress the data, and ride on home ;). + $len = 0; + if (!($this->m_data = $this->m_lzw->decompress($data, $len))) + return false; + + $datLen += $len; + + if ($this->m_gih->m_bInterlace) + $this->deInterlace(); + + return true; + + case 0x3B: // EOF + default: + return false; + } + } + return false; + } + + public function skipExt(&$data, &$extLen) + { + $extLen = 0; + + $b = ord($data[0]); + $data = substr($data, 1); + $extLen++; + + switch ($b) + { + // Graphic Control... + case 0xF9: + $b = ord($data[1]); + $this->m_disp = ($b & 0x1C) >> 2; + $this->m_bUser = ($b & 0x02) ? true : false; + $this->m_bTrans = ($b & 0x01) ? true : false; + list ($this->m_nDelay) = array_values(unpack('v', substr($data, 2, 2))); + $this->m_nTrans = ord($data[4]); + break; + + // Comment... + case 0xFE: + $this->m_lpComm = substr($data, 1, ord($data[0])); + break; + + // Plain text... + case 0x01: + break; + + // Application... + case 0xFF: + break; + } + + // Skip default as defs may change. + $b = ord($data[0]); + $data = substr($data, 1); + $extLen++; + while ($b > 0) + { + $data = substr($data, $b); + $extLen += $b; + $b = ord($data[0]); + $data = substr($data, 1); + $extLen++; + } + return true; + } + + public function deInterlace() + { + $data = $this->m_data; + + for ($i = 0; $i < 4; $i++) + { + switch ($i) + { + case 0: + $s = 8; + $y = 0; + break; + + case 1: + $s = 8; + $y = 4; + break; + + case 2: + $s = 4; + $y = 2; + break; + + case 3: + $s = 2; + $y = 1; + break; + } + + for (; $y < $this->m_gih->m_nHeight; $y += $s) + { + $lne = substr($this->m_data, 0, $this->m_gih->m_nWidth); + $this->m_data = substr($this->m_data, $this->m_gih->m_nWidth); + + $data = + substr($data, 0, $y * $this->m_gih->m_nWidth) . + $lne . + substr($data, ($y + 1) * $this->m_gih->m_nWidth); + } + } + + $this->m_data = $data; + } +} + +class gif_file +{ + public $header, $image, $data, $loaded; + + public function __construct() + { + $this->data = ''; + $this->loaded = false; + $this->header = new gif_file_header(); + $this->image = new gif_image(); + } + + public function loadFile($filename, $iIndex) + { + if ($iIndex < 0) + return false; + + $this->data = @file_get_contents($filename); + if ($this->data === false) + return false; + + // Tell the header to load up.... + $len = 0; + if (!$this->header->load($this->data, $len)) + return false; + + $this->data = substr($this->data, $len); + + // Keep reading (at least once) so we get to the actual image we're looking for. + for ($j = 0; $j <= $iIndex; $j++) + { + $imgLen = 0; + if (!$this->image->load($this->data, $imgLen)) + return false; + + $this->data = substr($this->data, $imgLen); + } + + $this->loaded = true; + return true; + } + + public function get_png_data($background_color) + { + if (!$this->loaded) + return false; + + // Prepare the color table. + if ($this->image->m_gih->m_bLocalClr) + { + $colors = $this->image->m_gih->m_nTableSize; + $pal = $this->image->m_gih->m_colorTable->toString(); + + if ($background_color != -1) + $background_color = $this->image->m_gih->m_colorTable->colorIndex($background_color); + } + elseif ($this->header->m_bGlobalClr) + { + $colors = $this->header->m_nTableSize; + $pal = $this->header->m_colorTable->toString(); + + if ($background_color != -1) + $background_color = $this->header->m_colorTable->colorIndex($background_color); + } + else + { + $colors = 0; + $background_color = -1; + } + + if ($background_color == -1) + $background_color = $this->header->m_nBgColor; + + $data = &$this->image->m_data; + $header = &$this->image->m_gih; + + $i = 0; + $bmp = ''; + + // Prepare the bitmap itself. + for ($y = 0; $y < $this->header->m_nHeight; $y++) + { + $bmp .= "\x00"; + + for ($x = 0; $x < $this->header->m_nWidth; $x++, $i++) + { + // Is this in the proper range? If so, get the specific pixel data... + if ($x >= $header->m_nLeft && $y >= $header->m_nTop && $x < ($header->m_nLeft + $header->m_nWidth) && $y < ($header->m_nTop + $header->m_nHeight)) + $bmp .= $data[$i]; + // Otherwise, this is background... + else + $bmp .= chr($background_color); + } + } + + $bmp = gzcompress($bmp, 9); + + // Output the basic signature first of all. + $out = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"; + + // Now, we want the header... + $out .= "\x00\x00\x00\x0D"; + $tmp = 'IHDR' . pack('N', (int) $this->header->m_nWidth) . pack('N', (int) $this->header->m_nHeight) . "\x08\x03\x00\x00\x00"; + $out .= $tmp . pack('N', smf_crc32($tmp)); + + // The palette, assuming we have one to speak of... + if ($colors > 0) + { + $out .= pack('N', (int) $colors * 3); + $tmp = 'PLTE' . $pal; + $out .= $tmp . pack('N', smf_crc32($tmp)); + } + + // Do we have any transparency we want to make available? + if ($this->image->m_bTrans && $colors > 0) + { + $out .= pack('N', (int) $colors); + $tmp = 'tRNS'; + + // Stick each color on - full transparency or none. + for ($i = 0; $i < $colors; $i++) + $tmp .= $i == $this->image->m_nTrans ? "\x00" : "\xFF"; + + $out .= $tmp . pack('N', smf_crc32($tmp)); + } + + // Here's the data itself! + $out .= pack('N', strlen($bmp)); + $tmp = 'IDAT' . $bmp; + $out .= $tmp . pack('N', smf_crc32($tmp)); + + // EOF marker... + $out .= "\x00\x00\x00\x00" . 'IEND' . "\xAE\x42\x60\x82"; + + return $out; + } +} + +// 64-bit only functions? +if (!function_exists('smf_crc32')) +{ + require_once $sourcedir . '/Subs-Compat.php'; +} + +?> \ No newline at end of file diff --git a/Sources/Class-Package.php b/Sources/Class-Package.php new file mode 100644 index 0000000..65f4c95 --- /dev/null +++ b/Sources/Class-Package.php @@ -0,0 +1,1229 @@ +debug_level = $level !== null ? $level : error_reporting(); + $this->trim = $auto_trim; + + // Is the data already parsed? + if ($is_clone) + { + $this->array = $data; + return; + } + + // Is the input an array? (ie. passed from file()?) + if (is_array($data)) + $data = implode('', $data); + + // Remove any xml declaration or doctype, and parse out comments and CDATA. + $data = preg_replace('//s', '', $this->_to_cdata(preg_replace(array('/^<\?xml.+?\?' . '>/is', '/]+?' . '>/s'), '', $data))); + + // Now parse the xml! + $this->array = $this->_parse($data); + } + + /** + * Get the root element's name. + * Example use: + * echo $element->name(); + * + * @return string The root element's name + */ + public function name() + { + return isset($this->array['name']) ? $this->array['name'] : ''; + } + + /** + * Get a specified element's value or attribute by path. + * Children are parsed for text, but only textual data is returned + * unless get_elements is true. + * Example use: + * $data = $xml->fetch('html/head/title'); + * + * @param string $path The path to the element to fetch + * @param bool $get_elements Whether to include elements + * @return string The value or attribute of the specified element + */ + public function fetch($path, $get_elements = false) + { + // Get the element, in array form. + $array = $this->path($path); + + if ($array === false) + return false; + + // Getting elements into this is a bit complicated... + if ($get_elements && !is_string($array)) + { + $temp = ''; + + // Use the _xml() function to get the xml data. + foreach ($array->array as $val) + { + // Skip the name and any attributes. + if (is_array($val)) + $temp .= $this->_xml($val, null); + } + + // Just get the XML data and then take out the CDATAs. + return $this->_to_cdata($temp); + } + + // Return the value - taking care to pick out all the text values. + return is_string($array) ? $array : $this->_fetch($array->array); + } + + /** Get an element, returns a new xmlArray. + * It finds any elements that match the path specified. + * It will always return a set if there is more than one of the element + * or return_set is true. + * Example use: + * $element = $xml->path('html/body'); + * + * @param $path string The path to the element to get + * @param $return_full bool Whether to return the full result set + * @return xmlArray a new xmlArray. + */ + public function path($path, $return_full = false) + { + global $txt; + + // Split up the path. + $path = explode('/', $path); + + // Start with a base array. + $array = $this->array; + + // For each element in the path. + foreach ($path as $el) + { + // Deal with sets.... + if (strpos($el, '[') !== false) + { + $lvl = (int) substr($el, strpos($el, '[') + 1); + $el = substr($el, 0, strpos($el, '[')); + } + // Find an attribute. + elseif (substr($el, 0, 1) == '@') + { + // It simplifies things if the attribute is already there ;). + if (isset($array[$el])) + return $array[$el]; + else + { + $trace = debug_backtrace(); + $i = 0; + while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] == get_class($this)) + $i++; + $debug = ' (from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line'] . ')'; + + // Cause an error. + if ($this->debug_level & E_NOTICE) + { + loadLanguage('Errors'); + trigger_error(sprintf($txt['undefined_xml_attribute'], substr($el, 1) . $debug), E_USER_NOTICE); + } + return false; + } + } + else + $lvl = null; + + // Find this element. + $array = $this->_path($array, $el, $lvl); + } + + // Clean up after $lvl, for $return_full. + if ($return_full && (!isset($array['name']) || substr($array['name'], -1) != ']')) + $array = array('name' => $el . '[]', $array); + + // Create the right type of class... + $newClass = get_class($this); + + // Return a new xmlArray for the result. + return $array === false ? false : new $newClass($array, $this->trim, $this->debug_level, true); + } + + /** + * Check if an element exists. + * Example use, + * echo $xml->exists('html/body') ? 'y' : 'n'; + * + * @param string $path The path to the element to get. + * @return boolean Whether the specified path exists + */ + public function exists($path) + { + // Split up the path. + $path = explode('/', $path); + + // Start with a base array. + $array = $this->array; + + // For each element in the path. + foreach ($path as $el) + { + // Deal with sets.... + if (strpos($el, '[') !== false) + { + $lvl = (int) substr($el, strpos($el, '[') + 1); + $el = substr($el, 0, strpos($el, '[')); + } + // Find an attribute. + elseif (substr($el, 0, 1) == '@') + return isset($array[$el]); + else + $lvl = null; + + // Find this element. + $array = $this->_path($array, $el, $lvl, true); + } + + return $array !== false; + } + + /** + * Count the number of occurrences of a path. + * Example use: + * echo $xml->count('html/head/meta'); + * + * @param string $path The path to search for. + * @return int The number of elements the path matches. + */ + public function count($path) + { + // Get the element, always returning a full set. + $temp = $this->path($path, true); + + // Start at zero, then count up all the numeric keys. + $i = 0; + foreach ($temp->array as $item) + { + if (is_array($item)) + $i++; + } + + return $i; + } + + /** + * Get an array of xmlArray's matching the specified path. + * This differs from ->path(path, true) in that instead of an xmlArray + * of elements, an array of xmlArray's is returned for use with foreach. + * Example use: + * foreach ($xml->set('html/body/p') as $p) + * + * @param $path string The path to search for. + * @return xmlArray[] An array of xmlArray objects + */ + public function set($path) + { + // None as yet, just get the path. + $array = array(); + $xml = $this->path($path, true); + + foreach ($xml->array as $val) + { + // Skip these, they aren't elements. + if (!is_array($val) || $val['name'] == '!') + continue; + + // Create the right type of class... + $newClass = get_class($this); + + // Create a new xmlArray and stick it in the array. + $array[] = new $newClass($val, $this->trim, $this->debug_level, true); + } + + return $array; + } + + /** + * Create an xml file from an xmlArray, the specified path if any. + * Example use: + * echo $this->create_xml(); + * + * @param string $path The path to the element. (optional) + * @return string Xml-formatted string. + */ + public function create_xml($path = null) + { + // Was a path specified? If so, use that array. + if ($path !== null) + { + $path = $this->path($path); + + // The path was not found + if ($path === false) + return false; + + $path = $path->array; + } + // Just use the current array. + else + $path = $this->array; + + // Add the xml declaration to the front. + return '' . $this->_xml($path, 0); + } + + /** + * Output the xml in an array form. + * Example use: + * print_r($xml->to_array()); + * + * @param string $path The path to output. + * @return array An array of XML data + */ + public function to_array($path = null) + { + // Are we doing a specific path? + if ($path !== null) + { + $path = $this->path($path); + + // The path was not found + if ($path === false) + return false; + + $path = $path->array; + } + // No, so just use the current array. + else + $path = $this->array; + + return $this->_array($path); + } + + /** + * Parse data into an array. (privately used...) + * + * @param string $data The data to parse + * @return array The parsed array + */ + protected function _parse($data) + { + // Start with an 'empty' array with no data. + $current = array( + ); + + // Loop until we're out of data. + while ($data != '') + { + // Find and remove the next tag. + preg_match('/\A<([\w\-:]+)((?:\s+.+?)?)([\s]?\/)?' . '>/', $data, $match); + if (isset($match[0])) + $data = preg_replace('/' . preg_quote($match[0], '/') . '/s', '', $data, 1); + + // Didn't find a tag? Keep looping.... + if (!isset($match[1]) || $match[1] == '') + { + // If there's no <, the rest is data. + if (strpos($data, '<') === false) + { + $text_value = $this->_from_cdata($data); + $data = ''; + + if ($text_value != '') + $current[] = array( + 'name' => '!', + 'value' => $text_value + ); + } + // If the < isn't immediately next to the current position... more data. + elseif (strpos($data, '<') > 0) + { + $text_value = $this->_from_cdata(substr($data, 0, strpos($data, '<'))); + $data = substr($data, strpos($data, '<')); + + if ($text_value != '') + $current[] = array( + 'name' => '!', + 'value' => $text_value + ); + } + // If we're looking at a with no start, kill it. + elseif (strpos($data, '<') !== false && strpos($data, '<') == 0) + { + if (strpos($data, '<', 1) !== false) + { + $text_value = $this->_from_cdata(substr($data, 0, strpos($data, '<', 1))); + $data = substr($data, strpos($data, '<', 1)); + + if ($text_value != '') + $current[] = array( + 'name' => '!', + 'value' => $text_value + ); + } + else + { + $text_value = $this->_from_cdata($data); + $data = ''; + + if ($text_value != '') + $current[] = array( + 'name' => '!', + 'value' => $text_value + ); + } + } + + // Wait for an actual occurance of an element. + continue; + } + + // Create a new element in the array. + $el = &$current[]; + $el['name'] = $match[1]; + + // If this ISN'T empty, remove the close tag and parse the inner data. + if ((!isset($match[3]) || trim($match[3]) != '/') && (!isset($match[2]) || trim($match[2]) != '/')) + { + // Because PHP 5.2.0+ seems to croak using regex, we'll have to do this the less fun way. + $last_tag_end = strpos($data, ''); + if ($last_tag_end === false) + continue; + + $offset = 0; + while (1 == 1) + { + // Where is the next start tag? + $next_tag_start = strpos($data, '<' . $match[1], $offset); + // If the next start tag is after the last end tag then we've found the right close. + if ($next_tag_start === false || $next_tag_start > $last_tag_end) + break; + + // If not then find the next ending tag. + $next_tag_end = strpos($data, '', $offset); + + // Didn't find one? Then just use the last and sod it. + if ($next_tag_end === false) + break; + else + { + $last_tag_end = $next_tag_end; + $offset = $next_tag_start + 1; + } + } + // Parse the insides. + $inner_match = substr($data, 0, $last_tag_end); + // Data now starts from where this section ends. + $data = substr($data, $last_tag_end + strlen('')); + + if (!empty($inner_match)) + { + // Parse the inner data. + if (strpos($inner_match, '<') !== false) + $el += $this->_parse($inner_match); + elseif (trim($inner_match) != '') + { + $text_value = $this->_from_cdata($inner_match); + if ($text_value != '') + $el[] = array( + 'name' => '!', + 'value' => $text_value + ); + } + } + } + + // If we're dealing with attributes as well, parse them out. + if (isset($match[2]) && $match[2] != '') + { + // Find all the attribute pairs in the string. + preg_match_all('/([\w:]+)="(.+?)"/', $match[2], $attr, PREG_SET_ORDER); + + // Set them as @attribute-name. + foreach ($attr as $match_attr) + $el['@' . $match_attr[1]] = $match_attr[2]; + } + } + + // Return the parsed array. + return $current; + } + + /** + * Get a specific element's xml. (privately used...) + * + * @param array $array An array of element data + * @param null|int $indent How many levels to indent the elements (null = no indent) + * @return string The formatted XML + */ + protected function _xml($array, $indent) + { + $indentation = $indent !== null ? ' +' . str_repeat(' ', $indent) : ''; + + // This is a set of elements, with no name... + if (is_array($array) && !isset($array['name'])) + { + $temp = ''; + foreach ($array as $val) + $temp .= $this->_xml($val, $indent); + return $temp; + } + + // This is just text! + if ($array['name'] == '!') + return $indentation . ''; + elseif (substr($array['name'], -2) == '[]') + $array['name'] = substr($array['name'], 0, -2); + + // Start the element. + $output = $indentation . '<' . $array['name']; + + $inside_elements = false; + $output_el = ''; + + // Run through and recursively output all the elements or attrbutes inside this. + foreach ($array as $k => $v) + { + if (substr($k, 0, 1) == '@') + $output .= ' ' . substr($k, 1) . '="' . $v . '"'; + elseif (is_array($v)) + { + $output_el .= $this->_xml($v, $indent === null ? null : $indent + 1); + $inside_elements = true; + } + } + + // Indent, if necessary.... then close the tag. + if ($inside_elements) + $output .= '>' . $output_el . $indentation . ''; + else + $output .= ' />'; + + return $output; + } + + /** + * Return an element as an array + * + * @param array $array An array of data + * @return string|array A string with the element's value or an array of element data + */ + protected function _array($array) + { + $return = array(); + $text = ''; + foreach ($array as $value) + { + if (!is_array($value) || !isset($value['name'])) + continue; + + if ($value['name'] == '!') + $text .= $value['value']; + else + $return[$value['name']] = $this->_array($value); + } + + if (empty($return)) + return $text; + else + return $return; + } + + /** + * Parse out CDATA tags. (htmlspecialchars them...) + * + * @param string $data The data with CDATA tags included + * @return string The data contained within CDATA tags + */ + function _to_cdata($data) + { + $inCdata = $inComment = false; + $output = ''; + + $parts = preg_split('~(|)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE); + foreach ($parts as $part) + { + // Handle XML comments. + if (!$inCdata && $part === '') + $inComment = false; + elseif ($inComment) + continue; + + // Handle Cdata blocks. + elseif (!$inComment && $part === '') + $inCdata = false; + elseif ($inCdata) + $output .= htmlentities($part, ENT_QUOTES); + + // Everything else is kept as is. + else + $output .= $part; + } + + return $output; + } + + /** + * Turn the CDATAs back to normal text. + * + * @param string $data The data with CDATA tags + * @return string The transformed data + */ + protected function _from_cdata($data) + { + // Get the HTML translation table and reverse it. + $trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES)); + + // Translate all the entities out. + $data = strtr( + preg_replace_callback( + '~&#(\d{1,4});~', + function($m) + { + return chr("$m[1]"); + }, + $data + ), + $trans_tbl + ); + + return $this->trim ? trim($data) : $data; + } + + /** + * Given an array, return the text from that array. (recursive and privately used.) + * + * @param array $array An aray of data + * @return string The text from the array + */ + protected function _fetch($array) + { + // Don't return anything if this is just a string. + if (is_string($array)) + return ''; + + $temp = ''; + foreach ($array as $text) + { + // This means it's most likely an attribute or the name itself. + if (!isset($text['name'])) + continue; + + // This is text! + if ($text['name'] == '!') + $temp .= $text['value']; + // Another element - dive in ;). + else + $temp .= $this->_fetch($text); + } + + // Return all the bits and pieces we've put together. + return $temp; + } + + /** + * Get a specific array by path, one level down. (privately used...) + * + * @param array $array An array of data + * @param string $path The path + * @param int $level How far deep into the array we should go + * @param bool $no_error Whether or not to ignore errors + * @return string|array The specified array (or the contents of said array if there's only one result) + */ + protected function _path($array, $path, $level, $no_error = false) + { + global $txt; + + // Is $array even an array? It might be false! + if (!is_array($array)) + return false; + + // Asking for *no* path? + if ($path == '' || $path == '.') + return $array; + $paths = explode('|', $path); + + // A * means all elements of any name. + $show_all = in_array('*', $paths); + + $results = array(); + + // Check each element. + foreach ($array as $value) + { + if (!is_array($value) || $value['name'] === '!') + continue; + + if ($show_all || in_array($value['name'], $paths)) + { + // Skip elements before "the one". + if ($level !== null && $level > 0) + $level--; + else + $results[] = $value; + } + } + + // No results found... + if (empty($results)) + { + $trace = debug_backtrace(); + $i = 0; + while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] == get_class($this)) + $i++; + $debug = ' from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line']; + + // Cause an error. + if ($this->debug_level & E_NOTICE && !$no_error) + { + loadLanguage('Errors'); + trigger_error(sprintf($txt['undefined_xml_element'], $path . $debug), E_USER_NOTICE); + } + return false; + } + // Only one result. + elseif (count($results) == 1 || $level !== null) + return $results[0]; + // Return the result set. + else + return $results + array('name' => $path . '[]'); + } +} + +/** + * Class ftp_connection + * Simple FTP protocol implementation. + * + * @see https://tools.ietf.org/html/rfc959 + */ +class ftp_connection +{ + /** + * @var string Holds the connection response + */ + public $connection; + + /** + * @var string Holds any errors + */ + public $error; + + /** + * @var string Holds the last message from the server + */ + public $last_message; + + /** + * @var boolean Whether or not this is a passive connection + */ + public $pasv; + + /** + * Create a new FTP connection... + * + * @param string $ftp_server The server to connect to + * @param int $ftp_port The port to connect to + * @param string $ftp_user The username + * @param string $ftp_pass The password + */ + public function __construct($ftp_server, $ftp_port = 21, $ftp_user = 'anonymous', $ftp_pass = 'ftpclient@simplemachines.org') + { + // Initialize variables. + $this->connection = 'no_connection'; + $this->error = false; + $this->pasv = array(); + + if ($ftp_server !== null) + $this->connect($ftp_server, $ftp_port, $ftp_user, $ftp_pass); + } + + /** + * Connects to a server + * + * @param string $ftp_server The address of the server + * @param int $ftp_port The port + * @param string $ftp_user The username + * @param string $ftp_pass The password + */ + public function connect($ftp_server, $ftp_port = 21, $ftp_user = 'anonymous', $ftp_pass = 'ftpclient@simplemachines.org') + { + if (strpos($ftp_server, 'ftp://') === 0) + $ftp_server = substr($ftp_server, 6); + elseif (strpos($ftp_server, 'ftps://') === 0) + $ftp_server = 'ssl://' . substr($ftp_server, 7); + if (strpos($ftp_server, 'http://') === 0) + $ftp_server = substr($ftp_server, 7); + elseif (strpos($ftp_server, 'https://') === 0) + $ftp_server = substr($ftp_server, 8); + $ftp_server = strtr($ftp_server, array('/' => '', ':' => '', '@' => '')); + + // Connect to the FTP server. + $this->connection = @fsockopen($ftp_server, $ftp_port, $err, $err, 5); + if (!$this->connection) + { + $this->error = 'bad_server'; + $this->last_message = 'Invalid Server'; + return; + } + + // Get the welcome message... + if (!$this->check_response(220)) + { + $this->error = 'bad_response'; + $this->last_message = 'Bad Response'; + return; + } + + // Send the username, it should ask for a password. + fwrite($this->connection, 'USER ' . $ftp_user . "\r\n"); + + if (!$this->check_response(331)) + { + $this->error = 'bad_username'; + $this->last_message = 'Invalid Username'; + return; + } + + // Now send the password... and hope it goes okay. + + fwrite($this->connection, 'PASS ' . $ftp_pass . "\r\n"); + if (!$this->check_response(230)) + { + $this->error = 'bad_password'; + $this->last_message = 'Invalid Password'; + return; + } + } + + /** + * Changes to a directory (chdir) via the ftp connection + * + * @param string $ftp_path The path to the directory we want to change to + * @return boolean Whether or not the operation was successful + */ + public function chdir($ftp_path) + { + if (!is_resource($this->connection)) + return false; + + // No slash on the end, please... + if ($ftp_path !== '/' && substr($ftp_path, -1) === '/') + $ftp_path = substr($ftp_path, 0, -1); + + fwrite($this->connection, 'CWD ' . $ftp_path . "\r\n"); + if (!$this->check_response(250)) + { + $this->error = 'bad_path'; + return false; + } + + return true; + } + + /** + * Changes a files atrributes (chmod) + * + * @param string $ftp_file The file to CHMOD + * @param int|string $chmod The value for the CHMOD operation + * @return boolean Whether or not the operation was successful + */ + public function chmod($ftp_file, $chmod) + { + if (!is_resource($this->connection)) + return false; + + if ($ftp_file == '') + $ftp_file = '.'; + + // Do we have a file or a dir? + $is_dir = is_dir($ftp_file); + $is_writable = false; + + // Set different modes. + $chmod_values = $is_dir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666); + + foreach ($chmod_values as $val) + { + // If it's writable, break out of the loop. + if (is_writable($ftp_file)) + { + $is_writable = true; + break; + } + + else + { + // Convert the chmod value from octal (0777) to text ("777"). + fwrite($this->connection, 'SITE CHMOD ' . decoct($val) . ' ' . $ftp_file . "\r\n"); + if (!$this->check_response(200)) + { + $this->error = 'bad_file'; + break; + } + } + } + return $is_writable; + } + + /** + * Deletes a file + * + * @param string $ftp_file The file to delete + * @return boolean Whether or not the operation was successful + */ + public function unlink($ftp_file) + { + // We are actually connected, right? + if (!is_resource($this->connection)) + return false; + + // Delete file X. + fwrite($this->connection, 'DELE ' . $ftp_file . "\r\n"); + if (!$this->check_response(250)) + { + fwrite($this->connection, 'RMD ' . $ftp_file . "\r\n"); + + // Still no love? + if (!$this->check_response(250)) + { + $this->error = 'bad_file'; + return false; + } + } + + return true; + } + + /** + * Reads the response to the command from the server + * + * @param string $desired The desired response + * @return boolean Whether or not we got the desired response + */ + public function check_response($desired) + { + // Wait for a response that isn't continued with -, but don't wait too long. + $time = time(); + do + $this->last_message = fgets($this->connection, 1024); + while ((strlen($this->last_message) < 4 || strpos($this->last_message, ' ') === 0 || strpos($this->last_message, ' ', 3) !== 3) && time() - $time < 5); + + // Was the desired response returned? + return is_array($desired) ? in_array(substr($this->last_message, 0, 3), $desired) : substr($this->last_message, 0, 3) == $desired; + } + + /** + * Used to create a passive connection + * + * @return boolean Whether the passive connection was created successfully + */ + public function passive() + { + // We can't create a passive data connection without a primary one first being there. + if (!is_resource($this->connection)) + return false; + + // Request a passive connection - this means, we'll talk to you, you don't talk to us. + @fwrite($this->connection, 'PASV' . "\r\n"); + $time = time(); + do + $response = fgets($this->connection, 1024); + while (strpos($response, ' ', 3) !== 3 && time() - $time < 5); + + // If it's not 227, we weren't given an IP and port, which means it failed. + if (strpos($response, '227 ') !== 0) + { + $this->error = 'bad_response'; + return false; + } + + // Snatch the IP and port information, or die horribly trying... + if (preg_match('~\((\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))\)~', $response, $match) == 0) + { + $this->error = 'bad_response'; + return false; + } + + // This is pretty simple - store it for later use ;). + $this->pasv = array('ip' => $match[1] . '.' . $match[2] . '.' . $match[3] . '.' . $match[4], 'port' => $match[5] * 256 + $match[6]); + + return true; + } + + /** + * Creates a new file on the server + * + * @param string $ftp_file The file to create + * @return boolean Whether or not the file was created successfully + */ + public function create_file($ftp_file) + { + // First, we have to be connected... very important. + if (!is_resource($this->connection)) + return false; + + // I'd like one passive mode, please! + if (!$this->passive()) + return false; + + // Seems logical enough, so far... + fwrite($this->connection, 'STOR ' . $ftp_file . "\r\n"); + + // Okay, now we connect to the data port. If it doesn't work out, it's probably "file already exists", etc. + $fp = @fsockopen($this->pasv['ip'], $this->pasv['port'], $err, $err, 5); + if (!$fp || !$this->check_response(150)) + { + $this->error = 'bad_file'; + @fclose($fp); + return false; + } + + // This may look strange, but we're just closing it to indicate a zero-byte upload. + fclose($fp); + if (!$this->check_response(226)) + { + $this->error = 'bad_response'; + return false; + } + + return true; + } + + /** + * Generates a directory listing for the current directory + * + * @param string $ftp_path The path to the directory + * @param bool $search Whether or not to get a recursive directory listing + * @return string|boolean The results of the command or false if unsuccessful + */ + public function list_dir($ftp_path = '', $search = false) + { + // Are we even connected...? + if (!is_resource($this->connection)) + return false; + + // Passive... non-agressive... + if (!$this->passive()) + return false; + + // Get the listing! + fwrite($this->connection, 'LIST -1' . ($search ? 'R' : '') . ($ftp_path == '' ? '' : ' ' . $ftp_path) . "\r\n"); + + // Connect, assuming we've got a connection. + $fp = @fsockopen($this->pasv['ip'], $this->pasv['port'], $err, $err, 5); + if (!$fp || !$this->check_response(array(150, 125))) + { + $this->error = 'bad_response'; + @fclose($fp); + return false; + } + + // Read in the file listing. + $data = ''; + while (!feof($fp)) + $data .= fread($fp, 4096); + fclose($fp); + + // Everything go okay? + if (!$this->check_response(226)) + { + $this->error = 'bad_response'; + return false; + } + + return $data; + } + + /** + * Determines the current directory we are in + * + * @param string $file The name of a file + * @param string $listing A directory listing or null to generate one + * @return string|boolean The name of the file or false if it wasn't found + */ + public function locate($file, $listing = null) + { + if ($listing === null) + $listing = $this->list_dir('', true); + $listing = explode("\n", $listing); + + @fwrite($this->connection, 'PWD' . "\r\n"); + $time = time(); + do + $response = fgets($this->connection, 1024); + while ($response[3] != ' ' && time() - $time < 5); + + // Check for 257! + if (preg_match('~^257 "(.+?)" ~', $response, $match) != 0) + $current_dir = strtr($match[1], array('""' => '"')); + else + $current_dir = ''; + + for ($i = 0, $n = count($listing); $i < $n; $i++) + { + if (trim($listing[$i]) == '' && isset($listing[$i + 1])) + { + $current_dir = substr(trim($listing[++$i]), 0, -1); + $i++; + } + + // Okay, this file's name is: + $listing[$i] = $current_dir . '/' . trim(strlen($listing[$i]) > 30 ? strrchr($listing[$i], ' ') : $listing[$i]); + + if ($file[0] == '*' && substr($listing[$i], -(strlen($file) - 1)) == substr($file, 1)) + return $listing[$i]; + if (substr($file, -1) == '*' && substr($listing[$i], 0, strlen($file) - 1) == substr($file, 0, -1)) + return $listing[$i]; + if (basename($listing[$i]) == $file || $listing[$i] == $file) + return $listing[$i]; + } + + return false; + } + + /** + * Creates a new directory on the server + * + * @param string $ftp_dir The name of the directory to create + * @return boolean Whether or not the operation was successful + */ + public function create_dir($ftp_dir) + { + // We must be connected to the server to do something. + if (!is_resource($this->connection)) + return false; + + // Make this new beautiful directory! + fwrite($this->connection, 'MKD ' . $ftp_dir . "\r\n"); + if (!$this->check_response(257)) + { + $this->error = 'bad_file'; + return false; + } + + return true; + } + + /** + * Detects the current path + * + * @param string $filesystem_path The full path from the filesystem + * @param string $lookup_file The name of a file in the specified path + * @return array An array of detected info - username, path from FTP root and whether or not the current path was found + */ + public function detect_path($filesystem_path, $lookup_file = null) + { + $username = ''; + + if (isset($_SERVER['DOCUMENT_ROOT'])) + { + if (preg_match('~^/home[2]?/([^/]+?)/public_html~', $_SERVER['DOCUMENT_ROOT'], $match)) + { + $username = $match[1]; + + $path = strtr($_SERVER['DOCUMENT_ROOT'], array('/home/' . $match[1] . '/' => '', '/home2/' . $match[1] . '/' => '')); + + if (substr($path, -1) == '/') + $path = substr($path, 0, -1); + + if (strlen(dirname($_SERVER['PHP_SELF'])) > 1) + $path .= dirname($_SERVER['PHP_SELF']); + } + elseif (strpos($filesystem_path, '/var/www/') === 0) + $path = substr($filesystem_path, 8); + else + $path = strtr(strtr($filesystem_path, array('\\' => '/')), array($_SERVER['DOCUMENT_ROOT'] => '')); + } + else + $path = ''; + + if (is_resource($this->connection) && $this->list_dir($path) == '') + { + $data = $this->list_dir('', true); + + if ($lookup_file === null) + $lookup_file = $_SERVER['PHP_SELF']; + + $found_path = dirname($this->locate('*' . basename(dirname($lookup_file)) . '/' . basename($lookup_file), $data)); + if ($found_path == false) + $found_path = dirname($this->locate(basename($lookup_file))); + if ($found_path != false) + $path = $found_path; + } + elseif (is_resource($this->connection)) + $found_path = true; + + return array($username, $path, isset($found_path)); + } + + /** + * Close the ftp connection + * + * @return boolean Always returns true + */ + public function close() + { + // Goodbye! + fwrite($this->connection, 'QUIT' . "\r\n"); + fclose($this->connection); + + return true; + } +} + +?> \ No newline at end of file diff --git a/Sources/Class-Punycode.php b/Sources/Class-Punycode.php new file mode 100755 index 0000000..4352476 --- /dev/null +++ b/Sources/Class-Punycode.php @@ -0,0 +1,611 @@ + + * @package php-punycode + * @license MIT + * + * Simple Machines Forum (SMF) + * + * @package SMF + * @author Simple Machines https://www.simplemachines.org + * @copyright 2022 Simple Machines and individual contributors + * @license https://www.simplemachines.org/about/smf/license.php BSD + * + * @version 2.1.3 + */ + +if (!defined('SMF')) + die('No direct access...'); + +/** + * Punycode implementation as described in RFC 3492 + * + * @link http://tools.ietf.org/html/rfc3492 + */ +class Punycode +{ + /** + * Bootstring parameter values + * + */ + const BASE = 36; + const TMIN = 1; + const TMAX = 26; + const SKEW = 38; + const DAMP = 700; + const INITIAL_BIAS = 72; + const INITIAL_N = 128; + const PREFIX = 'xn--'; + const DELIMITER = '-'; + + /** + * IDNA Error constants + */ + const IDNA_ERROR_EMPTY_LABEL = 1; + const IDNA_ERROR_LABEL_TOO_LONG = 2; + const IDNA_ERROR_DOMAIN_NAME_TOO_LONG = 4; + const IDNA_ERROR_LEADING_HYPHEN = 8; + const IDNA_ERROR_TRAILING_HYPHEN = 16; + const IDNA_ERROR_HYPHEN_3_4 = 32; + const IDNA_ERROR_LEADING_COMBINING_MARK = 64; + const IDNA_ERROR_DISALLOWED = 128; + const IDNA_ERROR_PUNYCODE = 256; + const IDNA_ERROR_LABEL_HAS_DOT = 512; + const IDNA_ERROR_INVALID_ACE_LABEL = 1024; + const IDNA_ERROR_BIDI = 2048; + const IDNA_ERROR_CONTEXTJ = 4096; + + /** + * Encode table + * + * @param array + */ + protected static $encodeTable = array( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', + 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + ); + + /** + * Decode table + * + * @param array + */ + protected static $decodeTable = array( + 'a' => 0, 'b' => 1, 'c' => 2, 'd' => 3, 'e' => 4, 'f' => 5, + 'g' => 6, 'h' => 7, 'i' => 8, 'j' => 9, 'k' => 10, 'l' => 11, + 'm' => 12, 'n' => 13, 'o' => 14, 'p' => 15, 'q' => 16, 'r' => 17, + 's' => 18, 't' => 19, 'u' => 20, 'v' => 21, 'w' => 22, 'x' => 23, + 'y' => 24, 'z' => 25, '0' => 26, '1' => 27, '2' => 28, '3' => 29, + '4' => 30, '5' => 31, '6' => 32, '7' => 33, '8' => 34, '9' => 35 + ); + + /** + * Character encoding + * + * @param string + */ + protected $encoding; + + /** + * Whether to use Non-Transitional Processing. + * Setting this to true breaks backward compatibility with IDNA2003. + * + * @param bool + */ + protected $nonTransitional = false; + + /** + * Whether to use STD3 ASCII rules. + * + * @param bool + */ + protected $std3 = false; + + /** + * Constructor + * + * @param string $encoding Character encoding + */ + public function __construct($encoding = 'UTF-8') + { + $this->encoding = $encoding; + } + + /** + * Enable/disable Non-Transitional Processing + * + * @param bool $nonTransitional Whether to use Non-Transitional Processing + */ + public function useNonTransitional(bool $nonTransitional) + { + $this->nonTransitional = $nonTransitional; + } + + /** + * Enable/disable STD3 ASCII rules + * + * @param bool $std3 Whether to use STD3 ASCII rules + */ + public function useStd3(bool $std3) + { + $this->std3 = $std3; + } + + /** + * Encode a domain to its Punycode version + * + * @param string $input Domain name in Unicode to be encoded + * @return string Punycode representation in ASCII + */ + public function encode($input) + { + // For compatibility with idn_to_* functions + if ($this->decode($input) === false) + return false; + + $errors = array(); + $preprocessed = $this->preprocess($input, $errors); + + if (!empty($errors)) + { + return false; + } + + $parts = explode('.', $preprocessed); + foreach ($parts as $p => &$part) { + $part = $this->encodePart($part); + + $validation_status = $this->validateLabel($part, true); + + switch ($validation_status) { + case self::IDNA_ERROR_LABEL_TOO_LONG: + case self::IDNA_ERROR_LEADING_HYPHEN: + case self::IDNA_ERROR_TRAILING_HYPHEN: + case self::IDNA_ERROR_LEADING_COMBINING_MARK: + case self::IDNA_ERROR_DISALLOWED: + case self::IDNA_ERROR_PUNYCODE: + case self::IDNA_ERROR_LABEL_HAS_DOT: + case self::IDNA_ERROR_INVALID_ACE_LABEL: + case self::IDNA_ERROR_BIDI: + case self::IDNA_ERROR_CONTEXTJ: + return false; + break; + + case self::IDNA_ERROR_HYPHEN_3_4: + $part = $parts[$p]; + break; + + case self::IDNA_ERROR_EMPTY_LABEL: + $parts_count = count($parts); + if ($parts_count === 1 || $p !== $parts_count - 1) + return false; + break; + + default: + break; + } + } + $output = implode('.', $parts); + + // IDNA_ERROR_DOMAIN_NAME_TOO_LONG + if (strlen(rtrim($output, '.')) > 253) + return false; + + return $output; + } + + /** + * Encode a part of a domain name, such as tld, to its Punycode version + * + * @param string $input Part of a domain name + * @return string Punycode representation of a domain part + */ + protected function encodePart($input) + { + $codePoints = $this->listCodePoints($input); + + $n = static::INITIAL_N; + $bias = static::INITIAL_BIAS; + $delta = 0; + $h = $b = count($codePoints['basic']); + + $output = ''; + foreach ($codePoints['basic'] as $code) { + $output .= $this->codePointToChar($code); + } + if ($input === $output) { + return $output; + } + if ($b > 0) { + $output .= static::DELIMITER; + } + + $codePoints['nonBasic'] = array_unique($codePoints['nonBasic']); + sort($codePoints['nonBasic']); + + $i = 0; + $length = mb_strlen($input, $this->encoding); + while ($h < $length) { + $m = $codePoints['nonBasic'][$i++]; + $delta = $delta + ($m - $n) * ($h + 1); + $n = $m; + + foreach ($codePoints['all'] as $c) { + if ($c < $n || $c < static::INITIAL_N) { + $delta++; + } + if ($c === $n) { + $q = $delta; + for ($k = static::BASE;; $k += static::BASE) { + $t = $this->calculateThreshold($k, $bias); + if ($q < $t) { + break; + } + + $code = $t + (((int) $q - $t) % (static::BASE - $t)); + $output .= static::$encodeTable[$code]; + + $q = ($q - $t) / (static::BASE - $t); + } + + $output .= static::$encodeTable[(int) $q]; + $bias = $this->adapt($delta, $h + 1, ($h === $b)); + $delta = 0; + $h++; + } + } + + $delta++; + $n++; + } + $out = static::PREFIX . $output; + + return $out; + } + + /** + * Decode a Punycode domain name to its Unicode counterpart + * + * @param string $input Domain name in Punycode + * @return string Unicode domain name + */ + public function decode($input) + { + $errors = array(); + $preprocessed = $this->preprocess($input, $errors); + + if (!empty($errors)) + { + return false; + } + + $parts = explode('.', $preprocessed); + foreach ($parts as $p => &$part) + { + if (strpos($part, static::PREFIX) === 0) + { + $part = substr($part, strlen(static::PREFIX)); + $part = $this->decodePart($part); + + if ($part === false) + return false; + } + + if ($this->validateLabel($part, false) !== 0) + { + if ($part === '') + { + $parts_count = count($parts); + + if ($parts_count === 1 || $p !== $parts_count - 1) + return false; + } + else + return false; + } + } + $output = implode('.', $parts); + + return $output; + } + + /** + * Decode a part of domain name, such as tld + * + * @param string $input Part of a domain name + * @return string Unicode domain part + */ + protected function decodePart($input) + { + $n = static::INITIAL_N; + $i = 0; + $bias = static::INITIAL_BIAS; + $output = ''; + + $pos = strrpos($input, static::DELIMITER); + if ($pos !== false) + { + $output = substr($input, 0, $pos++); + } + else + { + $pos = 0; + } + + $outputLength = strlen($output); + $inputLength = strlen($input); + while ($pos < $inputLength) + { + $oldi = $i; + $w = 1; + + for ($k = static::BASE;; $k += static::BASE) + { + if (!isset($input[$pos]) || !isset(static::$decodeTable[$input[$pos]])) + return false; + + $digit = static::$decodeTable[$input[$pos++]]; + $i = $i + ($digit * $w); + $t = $this->calculateThreshold($k, $bias); + + if ($digit < $t) + { + break; + } + + $w = $w * (static::BASE - $t); + } + + $bias = $this->adapt($i - $oldi, ++$outputLength, ($oldi === 0)); + $n = $n + (int) ($i / $outputLength); + $i = $i % ($outputLength); + $output = mb_substr($output, 0, $i, $this->encoding) . $this->codePointToChar($n) . mb_substr($output, $i, $outputLength - 1, $this->encoding); + + $i++; + } + + return $output; + } + + /** + * Calculate the bias threshold to fall between TMIN and TMAX + * + * @param integer $k + * @param integer $bias + * @return integer + */ + protected function calculateThreshold($k, $bias) + { + if ($k <= $bias + static::TMIN) + { + return static::TMIN; + } + elseif ($k >= $bias + static::TMAX) + { + return static::TMAX; + } + return $k - $bias; + } + + /** + * Bias adaptation + * + * @param integer $delta + * @param integer $numPoints + * @param boolean $firstTime + * @return integer + */ + protected function adapt($delta, $numPoints, $firstTime) + { + $delta = (int) ( + ($firstTime) + ? $delta / static::DAMP + : $delta / 2 + ); + $delta += (int) ($delta / $numPoints); + + $k = 0; + while ($delta > ((static::BASE - static::TMIN) * static::TMAX) / 2) + { + $delta = (int) ($delta / (static::BASE - static::TMIN)); + $k = $k + static::BASE; + } + $k = $k + (int) (((static::BASE - static::TMIN + 1) * $delta) / ($delta + static::SKEW)); + + return $k; + } + + /** + * List code points for a given input + * + * @param string $input + * @return array Multi-dimension array with basic, non-basic and aggregated code points + */ + protected function listCodePoints($input) + { + $codePoints = array( + 'all' => array(), + 'basic' => array(), + 'nonBasic' => array(), + ); + + $length = mb_strlen($input, $this->encoding); + for ($i = 0; $i < $length; $i++) + { + $char = mb_substr($input, $i, 1, $this->encoding); + $code = $this->charToCodePoint($char); + if ($code < 128) + { + $codePoints['all'][] = $codePoints['basic'][] = $code; + } + else + { + $codePoints['all'][] = $codePoints['nonBasic'][] = $code; + } + } + + return $codePoints; + } + + /** + * Convert a single or multi-byte character to its code point + * + * @param string $char + * @return integer + */ + protected function charToCodePoint($char) + { + $code = ord($char[0]); + if ($code < 128) + { + return $code; + } + elseif ($code < 224) + { + return (($code - 192) * 64) + (ord($char[1]) - 128); + } + elseif ($code < 240) + { + return (($code - 224) * 4096) + ((ord($char[1]) - 128) * 64) + (ord($char[2]) - 128); + } + else + { + return (($code - 240) * 262144) + ((ord($char[1]) - 128) * 4096) + ((ord($char[2]) - 128) * 64) + (ord($char[3]) - 128); + } + } + + /** + * Convert a code point to its single or multi-byte character + * + * @param integer $code + * @return string + */ + protected function codePointToChar($code) + { + if ($code <= 0x7F) + { + return chr($code); + } + elseif ($code <= 0x7FF) + { + return chr(($code >> 6) + 192) . chr(($code & 63) + 128); + } + elseif ($code <= 0xFFFF) + { + return chr(($code >> 12) + 224) . chr((($code >> 6) & 63) + 128) . chr(($code & 63) + 128); + } + else + { + return chr(($code >> 18) + 240) . chr((($code >> 12) & 63) + 128) . chr((($code >> 6) & 63) + 128) . chr(($code & 63) + 128); + } + } + + /** + * Prepare domain name string for Punycode processing. + * See https://www.unicode.org/reports/tr46/#Processing + * + * @param string $domain A domain name + * @param array $errors Will record any errors encountered during preprocessing + */ + protected function preprocess(string $domain, array &$errors = array()) + { + global $sourcedir; + + require_once($sourcedir . '/Unicode/Idna.php'); + require_once($sourcedir . '/Subs-Charset.php'); + + $regexes = idna_regex(); + + if (preg_match('/[' . $regexes['disallowed'] . ($this->std3 ? $regexes['disallowed_std3'] : '') . ']/u', $domain)) + $errors[] = 'disallowed'; + + $domain = preg_replace('/[' . $regexes['ignored'] . ']/u', '', $domain); + + unset($regexes); + + $maps = idna_maps(); + + if (!$this->nonTransitional) + $maps = array_merge($maps, idna_maps_deviation()); + + if (!$this->std3) + $maps = array_merge($maps, idna_maps_not_std3()); + + return utf8_normalize_c(strtr($domain, $maps)); + } + + /** + * Validates an individual part of a domain name. + * + * @param string $label Individual part of a domain name. + * @param bool $toPunycode True for encoding to Punycode, false for decoding. + */ + protected function validateLabel(string $label, bool $toPunycode = true) + { + global $sourcedir; + + $length = strlen($label); + + if ($length === 0) + { + return self::IDNA_ERROR_EMPTY_LABEL; + } + + if ($toPunycode) + { + if ($length > 63) + { + return self::IDNA_ERROR_LABEL_TOO_LONG; + } + + if ($this->std3 && $length !== strspn($label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-')) + { + return self::IDNA_ERROR_PUNYCODE; + } + } + + if (strpos($label, '-') === 0) + { + return self::IDNA_ERROR_LEADING_HYPHEN; + } + + if (strrpos($label, '-') === $length - 1) + { + return self::IDNA_ERROR_TRAILING_HYPHEN; + } + + if (substr($label, 2, 2) === '--') + { + return self::IDNA_ERROR_HYPHEN_3_4; + } + + if (preg_match('/^\p{M}/u', $label)) + { + return self::IDNA_ERROR_LEADING_COMBINING_MARK; + } + + require_once($sourcedir . '/Unicode/Idna.php'); + require_once($sourcedir . '/Subs-Charset.php'); + + $regexes = idna_regex(); + + if (preg_match('/[' . $regexes['disallowed'] . ($this->std3 ? $regexes['disallowed_std3'] : '') . ']/u', $label)) + { + return self::IDNA_ERROR_INVALID_ACE_LABEL; + } + + if (!$toPunycode && $label !== utf8_normalize_kc($label)) + { + return self::IDNA_ERROR_INVALID_ACE_LABEL; + } + + return 0; + } +} + +?> \ No newline at end of file diff --git a/Sources/Class-SearchAPI.php b/Sources/Class-SearchAPI.php new file mode 100644 index 0000000..b11b4da --- /dev/null +++ b/Sources/Class-SearchAPI.php @@ -0,0 +1,291 @@ + $id_msg, + ) + ); + + $id_searchs = array(); + while ($row = $smcFunc['db_fetch_assoc']($result)) + $id_searchs[] = $row['id_search']; + + if (count($id_searchs) < 1) + return; + + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_search_results + WHERE id_search in ({array_int:id_searchs})', + array( + 'id_searchs' => $id_searchs, + ) + ); + + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_search_topics + WHERE id_search in ({array_int:id_searchs})', + array( + 'id_searchs' => $id_searchs, + ) + ); + + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_search_messages + WHERE id_search in ({array_int:id_searchs})', + array( + 'id_searchs' => $id_searchs, + ) + ); + } + + /** + * {@inheritDoc} + */ + public function topicsRemoved(array $topics) + { + } + + /** + * {@inheritDoc} + */ + public function topicsMoved(array $topics, $board_to) + { + } + + /** + * {@inheritDoc} + */ + public function searchQuery(array $query_params, array $searchWords, array $excludedIndexWords, array &$participants, array &$searchArray) + { + } +} + +?> \ No newline at end of file diff --git a/Sources/Class-TOTP.php b/Sources/Class-TOTP.php new file mode 100644 index 0000000..bde68ca --- /dev/null +++ b/Sources/Class-TOTP.php @@ -0,0 +1,371 @@ + + * @package GAuth + * @license MIT + * + * Simple Machines Forum (SMF) + * + * @package SMF + * @author Simple Machines https://www.simplemachines.org + * @copyright 2022 Simple Machines and individual contributors + * @license https://www.simplemachines.org/about/smf/license.php BSD + * + * @version 2.1.0 + */ + +namespace TOTP; + +/** + * Class Auth + * + * @package TOTP + */ +class Auth +{ + /** + * @var array Internal lookup table + */ + private $lookup = array(); + + /** + * @var string Initialization key + */ + private $initKey = null; + + /** + * @var integer Seconds between key refreshes + */ + private $refreshSeconds = 30; + + /** + * @var integer The length of codes to generate + */ + private $codeLength = 6; + + /** + * @var integer Range plus/minus for "window of opportunity" on allowed codes + */ + private $range = 2; + + /** + * Initialize the object and set up the lookup table + * Optionally the Initialization key + * + * @param string $initKey Initialization key + */ + public function __construct($initKey = null) + { + $this->buildLookup(); + + if ($initKey !== null) + { + $this->setInitKey($initKey); + } + } + + /** + * Build the base32 lookup table + */ + public function buildLookup() + { + $lookup = array_combine( + array_merge(range('A', 'Z'), range(2, 7)), + range(0, 31) + ); + $this->setLookup($lookup); + } + + /** + * Get the current "range" value + * + * @return integer Range value + */ + public function getRange() + { + return $this->range; + } + + /** + * Set the "range" value + * + * @param integer $range Range value + * @return \TOTP\Auth instance + */ + public function setRange($range) + { + if (!is_numeric($range)) + { + throw new \InvalidArgumentException('Invalid window range'); + } + $this->range = $range; + return $this; + } + + /** + * Set the initialization key for the object + * + * @param string $key Initialization key + * @throws \InvalidArgumentException If hash is not valid base32 + * @return \TOTP\Auth instance + */ + public function setInitKey($key) + { + if (preg_match('/^[' . implode('', array_keys($this->getLookup())) . ']+$/', $key) == false) + { + throw new \InvalidArgumentException('Invalid base32 hash!'); + } + $this->initKey = $key; + return $this; + } + + /** + * Get the current Initialization key + * + * @return string Initialization key + */ + public function getInitKey() + { + return $this->initKey; + } + + /** + * Set the contents of the internal lookup table + * + * @param array $lookup Lookup data set + * @throws \InvalidArgumentException If lookup given is not an array + * @return \TOTP\Auth instance + */ + public function setLookup($lookup) + { + if (!is_array($lookup)) + { + throw new \InvalidArgumentException('Lookup value must be an array'); + } + $this->lookup = $lookup; + return $this; + } + + /** + * Get the current lookup data set + * + * @return array Lookup data + */ + public function getLookup() + { + return $this->lookup; + } + + /** + * Get the number of seconds for code refresh currently set + * + * @return integer Refresh in seconds + */ + public function getRefresh() + { + return $this->refreshSeconds; + } + + /** + * Set the number of seconds to refresh codes + * + * @param integer $seconds Seconds to refresh + * @throws \InvalidArgumentException If seconds value is not numeric + * @return \TOTP\Auth instance + */ + public function setRefresh($seconds) + { + if (!is_numeric($seconds)) + { + throw new \InvalidArgumentException('Seconds must be numeric'); + } + $this->refreshSeconds = $seconds; + return $this; + } + + /** + * Get the current length for generated codes + * + * @return integer Code length + */ + public function getCodeLength() + { + return $this->codeLength; + } + + /** + * Set the length of the generated codes + * + * @param integer $length Code length + * @return \TOTP\Auth instance + */ + public function setCodeLength($length) + { + $this->codeLength = $length; + return $this; + } + + /** + * Validate the given code + * + * @param string $code Code entered by user + * @param string $initKey Initialization key + * @param string $timestamp Timestamp for calculation + * @param integer $range Seconds before/after to validate hash against + * @throws \InvalidArgumentException If incorrect code length + * @return boolean Pass/fail of validation + */ + public function validateCode($code, $initKey = null, $timestamp = null, $range = null) + { + if (strlen($code) !== $this->getCodeLength()) + { + throw new \InvalidArgumentException('Incorrect code length'); + } + + $range = ($range == null) ? $this->getRange() : $range; + $timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp; + $initKey = ($initKey == null) ? $this->getInitKey() : $initKey; + + $binary = $this->base32_decode($initKey); + + for ($time = ($timestamp - $range); $time <= ($timestamp + $range); $time++) + { + if ($this->generateOneTime($binary, $time) == $code) + { + return true; + } + } + return false; + } + + /** + * Generate a one-time code + * + * @param string $initKey Initialization key [optional] + * @param string $timestamp Timestamp for calculation [optional] + * @return string Generated code/hash + */ + public function generateOneTime($initKey = null, $timestamp = null) + { + $initKey = ($initKey == null) ? $this->getInitKey() : $initKey; + $timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp; + + $hash = hash_hmac( + 'sha1', + pack('N*', 0) . pack('N*', $timestamp), + $initKey, + true + ); + + return str_pad($this->truncateHash($hash), $this->getCodeLength(), '0', STR_PAD_LEFT); + } + + /** + * Generate a code/hash + * Useful for making Initialization codes + * + * @param integer $length Length for the generated code + * @return string Generated code + */ + public function generateCode($length = 16) + { + global $smcFunc; + + $lookup = implode('', array_keys($this->getLookup())); + $code = ''; + + for ($i = 0; $i < $length; $i++) + { + $code .= $lookup[$smcFunc['random_int'](0, strlen($lookup) - 1)]; + } + + return $code; + } + + /** + * Generate the timestamp for the calculation + * + * @return integer Timestamp + */ + public function generateTimestamp() + { + return floor(microtime(true) / $this->getRefresh()); + } + + /** + * Truncate the given hash down to just what we need + * + * @param string $hash Hash to truncate + * @return string Truncated hash value + */ + public function truncateHash($hash) + { + $offset = ord($hash[19]) & 0xf; + + return ( + ((ord($hash[$offset + 0]) & 0x7f) << 24) | + ((ord($hash[$offset + 1]) & 0xff) << 16) | + ((ord($hash[$offset + 2]) & 0xff) << 8) | + (ord($hash[$offset + 3]) & 0xff) + ) % pow(10, $this->getCodeLength()); + } + + /** + * Base32 decoding function + * + * @param string $hash The base32-encoded hash + * @throws \InvalidArgumentException When hash is not valid + * @return string Binary value of hash + */ + public function base32_decode($hash) + { + $lookup = $this->getLookup(); + + if (preg_match('/^[' . implode('', array_keys($lookup)) . ']+$/', $hash) == false) + { + throw new \InvalidArgumentException('Invalid base32 hash!'); + } + + $hash = strtoupper($hash); + $buffer = 0; + $length = 0; + $binary = ''; + + for ($i = 0; $i < strlen($hash); $i++) + { + $buffer = $buffer << 5; + $buffer += $lookup[$hash[$i]]; + $length += 5; + + if ($length >= 8) + { + $length -= 8; + $binary .= chr(($buffer & (0xFF << $length)) >> $length); + } + } + + return $binary; + } + + /** + * Returns a URL to QR code for embedding the QR code + * + * @param string $name The name + * @param string $code The generated code + * @return string The URL to the QR code + */ + public function getQrCodeUrl($name, $code) + { + $url = 'otpauth://totp/' . urlencode($name) . '?secret=' . $code; + return $url; + } +} + +?> \ No newline at end of file diff --git a/Sources/DbExtra-mysql.php b/Sources/DbExtra-mysql.php new file mode 100644 index 0000000..c97641c --- /dev/null +++ b/Sources/DbExtra-mysql.php @@ -0,0 +1,436 @@ + 'smf_db_backup_table', + 'db_optimize_table' => 'smf_db_optimize_table', + 'db_table_sql' => 'smf_db_table_sql', + 'db_list_tables' => 'smf_db_list_tables', + 'db_get_version' => 'smf_db_get_version', + 'db_get_vendor' => 'smf_db_get_vendor', + 'db_allow_persistent' => 'smf_db_allow_persistent', + ); +} + +/** + * Backup $table to $backup_table. + * + * @param string $table The name of the table to backup + * @param string $backup_table The name of the backup table for this table + * @return resource -the request handle to the table creation query + */ +function smf_db_backup_table($table, $backup_table) +{ + global $smcFunc, $db_prefix; + + $table = str_replace('{db_prefix}', $db_prefix, $table); + + // First, get rid of the old table. + $smcFunc['db_query']('', ' + DROP TABLE IF EXISTS {raw:backup_table}', + array( + 'backup_table' => $backup_table, + ) + ); + + // Can we do this the quick way? + $result = $smcFunc['db_query']('', ' + CREATE TABLE {raw:backup_table} LIKE {raw:table}', + array( + 'backup_table' => $backup_table, + 'table' => $table + ) + ); + // If this failed, we go old school. + if ($result) + { + $request = $smcFunc['db_query']('', ' + INSERT INTO {raw:backup_table} + SELECT * + FROM {raw:table}', + array( + 'backup_table' => $backup_table, + 'table' => $table + ) + ); + + // Old school or no school? + if ($request) + return $request; + } + + // At this point, the quick method failed. + $result = $smcFunc['db_query']('', ' + SHOW CREATE TABLE {raw:table}', + array( + 'table' => $table, + ) + ); + list (, $create) = $smcFunc['db_fetch_row']($result); + $smcFunc['db_free_result']($result); + + $create = preg_split('/[\n\r]/', $create); + + $auto_inc = ''; + // Default engine type. + $engine = 'MyISAM'; + $charset = ''; + $collate = ''; + + foreach ($create as $k => $l) + { + // Get the name of the auto_increment column. + if (strpos($l, 'auto_increment')) + $auto_inc = trim($l); + + // For the engine type, see if we can work out what it is. + if (strpos($l, 'ENGINE') !== false || strpos($l, 'TYPE') !== false) + { + // Extract the engine type. + preg_match('~(ENGINE|TYPE)=(\w+)(\sDEFAULT)?(\sCHARSET=(\w+))?(\sCOLLATE=(\w+))?~', $l, $match); + + if (!empty($match[1])) + $engine = $match[1]; + + if (!empty($match[2])) + $engine = $match[2]; + + if (!empty($match[5])) + $charset = $match[5]; + + if (!empty($match[7])) + $collate = $match[7]; + } + + // Skip everything but keys... + if (strpos($l, 'KEY') === false) + unset($create[$k]); + } + + if (!empty($create)) + $create = '( + ' . implode(' + ', $create) . ')'; + else + $create = ''; + + $request = $smcFunc['db_query']('', ' + CREATE TABLE {raw:backup_table} {raw:create} + ENGINE={raw:engine}' . (empty($charset) ? '' : ' CHARACTER SET {raw:charset}' . (empty($collate) ? '' : ' COLLATE {raw:collate}')) . ' + SELECT * + FROM {raw:table}', + array( + 'backup_table' => $backup_table, + 'table' => $table, + 'create' => $create, + 'engine' => $engine, + 'charset' => empty($charset) ? '' : $charset, + 'collate' => empty($collate) ? '' : $collate, + ) + ); + + if ($auto_inc != '') + { + if (preg_match('~\`(.+?)\`\s~', $auto_inc, $match) != 0 && substr($auto_inc, -1, 1) == ',') + $auto_inc = substr($auto_inc, 0, -1); + + $smcFunc['db_query']('', ' + ALTER TABLE {raw:backup_table} + CHANGE COLUMN {raw:column_detail} {raw:auto_inc}', + array( + 'backup_table' => $backup_table, + 'column_detail' => $match[1], + 'auto_inc' => $auto_inc, + ) + ); + } + + return $request; +} + +/** + * This function optimizes a table. + * + * @param string $table The table to be optimized + * @return int How much space was gained + */ +function smf_db_optimize_table($table) +{ + global $smcFunc, $db_prefix; + + $table = str_replace('{db_prefix}', $db_prefix, $table); + + // Get how much overhead there is. + $request = $smcFunc['db_query']('', ' + SHOW TABLE STATUS LIKE {string:table_name}', + array( + 'table_name' => str_replace('_', '\_', $table), + ) + ); + $row = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + $data_before = isset($row['Data_free']) ? $row['Data_free'] : 0; + $request = $smcFunc['db_query']('', ' + OPTIMIZE TABLE `{raw:table}`', + array( + 'table' => $table, + ) + ); + if (!$request) + return -1; + + // How much left? + $request = $smcFunc['db_query']('', ' + SHOW TABLE STATUS LIKE {string:table}', + array( + 'table' => str_replace('_', '\_', $table), + ) + ); + $row = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + $total_change = isset($row['Data_free']) && $data_before > $row['Data_free'] ? $data_before / 1024 : 0; + + return $total_change; +} + +/** + * This function lists all tables in the database. + * The listing could be filtered according to $filter. + * + * @param string|boolean $db string The database name or false to use the current DB + * @param string|boolean $filter String to filter by or false to list all tables + * @return array An array of table names + */ +function smf_db_list_tables($db = false, $filter = false) +{ + global $db_name, $smcFunc; + + $db = $db == false ? $db_name : $db; + $db = trim($db); + $filter = $filter == false ? '' : ' LIKE \'' . $filter . '\''; + + $request = $smcFunc['db_query']('', ' + SHOW TABLES + FROM `{raw:db}` + {raw:filter}', + array( + 'db' => $db[0] == '`' ? strtr($db, array('`' => '')) : $db, + 'filter' => $filter, + ) + ); + $tables = array(); + while ($row = $smcFunc['db_fetch_row']($request)) + $tables[] = $row[0]; + $smcFunc['db_free_result']($request); + + return $tables; +} + +/** + * Dumps the schema (CREATE) for a table. + * + * @todo why is this needed for? + * @param string $tableName The name of the table + * @return string The "CREATE TABLE" SQL string for this table + */ +function smf_db_table_sql($tableName) +{ + global $smcFunc, $db_prefix; + + $tableName = str_replace('{db_prefix}', $db_prefix, $tableName); + + // This will be needed... + $crlf = "\r\n"; + + // Drop it if it exists. + $schema_create = 'DROP TABLE IF EXISTS `' . $tableName . '`;' . $crlf . $crlf; + + // Start the create table... + $schema_create .= 'CREATE TABLE `' . $tableName . '` (' . $crlf; + + // Find all the fields. + $result = $smcFunc['db_query']('', ' + SHOW FIELDS + FROM `{raw:table}`', + array( + 'table' => $tableName, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + // Make the CREATE for this column. + $schema_create .= ' `' . $row['Field'] . '` ' . $row['Type'] . ($row['Null'] != 'YES' ? ' NOT NULL' : ''); + + // Add a default...? + if (!empty($row['Default']) || $row['Null'] !== 'YES') + { + // Make a special case of auto-timestamp. + if ($row['Default'] == 'CURRENT_TIMESTAMP') + $schema_create .= ' /*!40102 NOT NULL default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP */'; + // Text shouldn't have a default. + elseif ($row['Default'] !== null) + { + // If this field is numeric the default needs no escaping. + $type = strtolower($row['Type']); + $isNumericColumn = strpos($type, 'int') !== false || strpos($type, 'bool') !== false || strpos($type, 'bit') !== false || strpos($type, 'float') !== false || strpos($type, 'double') !== false || strpos($type, 'decimal') !== false; + + $schema_create .= ' default ' . ($isNumericColumn ? $row['Default'] : '\'' . $smcFunc['db_escape_string']($row['Default']) . '\''); + } + } + + // And now any extra information. (such as auto_increment.) + $schema_create .= ($row['Extra'] != '' ? ' ' . $row['Extra'] : '') . ',' . $crlf; + } + $smcFunc['db_free_result']($result); + + // Take off the last comma. + $schema_create = substr($schema_create, 0, -strlen($crlf) - 1); + + // Find the keys. + $result = $smcFunc['db_query']('', ' + SHOW KEYS + FROM `{raw:table}`', + array( + 'table' => $tableName, + ) + ); + $indexes = array(); + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + // IS this a primary key, unique index, or regular index? + $row['Key_name'] = $row['Key_name'] == 'PRIMARY' ? 'PRIMARY KEY' : (empty($row['Non_unique']) ? 'UNIQUE ' : ($row['Comment'] == 'FULLTEXT' || (isset($row['Index_type']) && $row['Index_type'] == 'FULLTEXT') ? 'FULLTEXT ' : 'KEY ')) . '`' . $row['Key_name'] . '`'; + + // Is this the first column in the index? + if (empty($indexes[$row['Key_name']])) + $indexes[$row['Key_name']] = array(); + + // A sub part, like only indexing 15 characters of a varchar. + if (!empty($row['Sub_part'])) + $indexes[$row['Key_name']][$row['Seq_in_index']] = '`' . $row['Column_name'] . '`(' . $row['Sub_part'] . ')'; + else + $indexes[$row['Key_name']][$row['Seq_in_index']] = '`' . $row['Column_name'] . '`'; + } + $smcFunc['db_free_result']($result); + + // Build the CREATEs for the keys. + foreach ($indexes as $keyname => $columns) + { + // Ensure the columns are in proper order. + ksort($columns); + + $schema_create .= ',' . $crlf . ' ' . $keyname . ' (' . implode(', ', $columns) . ')'; + } + + // Now just get the comment and engine... (MyISAM, etc.) + $result = $smcFunc['db_query']('', ' + SHOW TABLE STATUS + LIKE {string:table}', + array( + 'table' => strtr($tableName, array('_' => '\\_', '%' => '\\%')), + ) + ); + $row = $smcFunc['db_fetch_assoc']($result); + $smcFunc['db_free_result']($result); + + // Probably MyISAM.... and it might have a comment. + $schema_create .= $crlf . ') ENGINE=' . $row['Engine'] . ($row['Comment'] != '' ? ' COMMENT="' . $row['Comment'] . '"' : ''); + + return $schema_create; +} + +/** + * Get the version number. + * + * @return string The version + */ +function smf_db_get_version() +{ + static $ver; + + if (!empty($ver)) + return $ver; + + global $smcFunc; + + $request = $smcFunc['db_query']('', ' + SELECT VERSION()', + array( + ) + ); + list ($ver) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + return $ver; +} + +/** + * Figures out if we are using MySQL, Percona or MariaDB + * + * @return string The database engine we are using + */ +function smf_db_get_vendor() +{ + global $smcFunc; + static $db_type; + + if (!empty($db_type)) + return $db_type; + + $request = $smcFunc['db_query']('', 'SELECT @@version_comment'); + list ($comment) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // Skip these if we don't have a comment. + if (!empty($comment)) + { + if (stripos($comment, 'percona') !== false) + return 'Percona'; + if (stripos($comment, 'mariadb') !== false) + return 'MariaDB'; + } + else + return 'fail'; + + return 'MySQL'; +} + +/** + * Figures out if persistent connection is allowed + * + * @return boolean + */ +function smf_db_allow_persistent() +{ + $value = ini_get('mysqli.allow_persistent'); + if (strtolower($value) == 'on' || strtolower($value) == 'true' || $value == '1') + return true; + else + return false; +} + +?> \ No newline at end of file diff --git a/Sources/DbExtra-postgresql.php b/Sources/DbExtra-postgresql.php new file mode 100644 index 0000000..99c63b0 --- /dev/null +++ b/Sources/DbExtra-postgresql.php @@ -0,0 +1,341 @@ + 'smf_db_backup_table', + 'db_optimize_table' => 'smf_db_optimize_table', + 'db_table_sql' => 'smf_db_table_sql', + 'db_list_tables' => 'smf_db_list_tables', + 'db_get_version' => 'smf_db_get_version', + 'db_get_vendor' => 'smf_db_get_vendor', + 'db_allow_persistent' => 'smf_db_allow_persistent', + ); +} + +/** + * Backup $table to $backup_table. + * + * @param string $table The name of the table to backup + * @param string $backup_table The name of the backup table for this table + * @return resource -the request handle to the table creation query + */ +function smf_db_backup_table($table, $backup_table) +{ + global $smcFunc, $db_prefix; + + $table = str_replace('{db_prefix}', $db_prefix, $table); + + // Do we need to drop it first? + $tables = smf_db_list_tables(false, $backup_table); + if (!empty($tables)) + $smcFunc['db_query']('', ' + DROP TABLE {raw:backup_table}', + array( + 'backup_table' => $backup_table, + ) + ); + + /** + * @todo Should we create backups of sequences as well? + */ + $smcFunc['db_query']('', ' + CREATE TABLE {raw:backup_table} + ( + LIKE {raw:table} + INCLUDING DEFAULTS + )', + array( + 'backup_table' => $backup_table, + 'table' => $table, + ) + ); + $smcFunc['db_query']('', ' + INSERT INTO {raw:backup_table} + SELECT * FROM {raw:table}', + array( + 'backup_table' => $backup_table, + 'table' => $table, + ) + ); +} + +/** + * This function optimizes a table. + * + * @param string $table The table to be optimized + * @return int How much space was gained + */ +function smf_db_optimize_table($table) +{ + global $smcFunc, $db_prefix; + + $table = str_replace('{db_prefix}', $db_prefix, $table); + + $pg_tables = array('pg_catalog', 'information_schema'); + + $request = $smcFunc['db_query']('', ' + SELECT pg_relation_size(C.oid) AS "size" + FROM pg_class C + LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) + WHERE nspname NOT IN ({array_string:pg_tables}) + AND relname = {string:table}', + array( + 'table' => $table, + 'pg_tables' => $pg_tables, + ) + ); + + $row = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + $old_size = $row['size']; + + $request = $smcFunc['db_query']('', ' + VACUUM FULL ANALYZE {raw:table}', + array( + 'table' => $table, + ) + ); + + if (!$request) + return -1; + + $request = $smcFunc['db_query']('', ' + SELECT pg_relation_size(C.oid) AS "size" + FROM pg_class C + LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) + WHERE nspname NOT IN ({array_string:pg_tables}) + AND relname = {string:table}', + array( + 'table' => $table, + 'pg_tables' => $pg_tables, + ) + ); + + $row = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + if (isset($row['size'])) + return ($old_size - $row['size']) / 1024; + else + return 0; +} + +/** + * This function lists all tables in the database. + * The listing could be filtered according to $filter. + * + * @param string|boolean $db string The database name or false to use the current DB + * @param string|boolean $filter String to filter by or false to list all tables + * @return array An array of table names + */ +function smf_db_list_tables($db = false, $filter = false) +{ + global $smcFunc; + + $request = $smcFunc['db_query']('', ' + SELECT tablename + FROM pg_tables + WHERE schemaname = {string:schema_public}' . ($filter == false ? '' : ' + AND tablename LIKE {string:filter}') . ' + ORDER BY tablename', + array( + 'schema_public' => 'public', + 'filter' => $filter, + ) + ); + + $tables = array(); + while ($row = $smcFunc['db_fetch_row']($request)) + $tables[] = $row[0]; + $smcFunc['db_free_result']($request); + + return $tables; +} + +/** + * Dumps the schema (CREATE) for a table. + * + * @todo why is this needed for? + * @param string $tableName The name of the table + * @return string The "CREATE TABLE" SQL string for this table + */ +function smf_db_table_sql($tableName) +{ + global $smcFunc, $db_prefix; + + $tableName = str_replace('{db_prefix}', $db_prefix, $tableName); + + // This will be needed... + $crlf = "\r\n"; + + // Drop it if it exists. + $schema_create = 'DROP TABLE IF EXISTS ' . $tableName . ';' . $crlf . $crlf; + + // Start the create table... + $schema_create .= 'CREATE TABLE ' . $tableName . ' (' . $crlf; + $index_create = ''; + $seq_create = ''; + + // Find all the fields. + $result = $smcFunc['db_query']('', ' + SELECT column_name, column_default, is_nullable, data_type, character_maximum_length + FROM information_schema.columns + WHERE table_name = {string:table} + ORDER BY ordinal_position', + array( + 'table' => $tableName, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + if ($row['data_type'] == 'character varying') + $row['data_type'] = 'varchar'; + elseif ($row['data_type'] == 'character') + $row['data_type'] = 'char'; + if ($row['character_maximum_length']) + $row['data_type'] .= '(' . $row['character_maximum_length'] . ')'; + + // Make the CREATE for this column. + $schema_create .= ' "' . $row['column_name'] . '" ' . $row['data_type'] . ($row['is_nullable'] != 'YES' ? ' NOT NULL' : ''); + + // Add a default...? + if (trim($row['column_default']) != '') + { + $schema_create .= ' default ' . $row['column_default'] . ''; + + // Auto increment? + if (preg_match('~nextval\(\'(.+?)\'(.+?)*\)~i', $row['column_default'], $matches) != 0) + { + // Get to find the next variable first! + $count_req = $smcFunc['db_query']('', ' + SELECT MAX("{raw:column}") + FROM {raw:table}', + array( + 'column' => $row['column_name'], + 'table' => $tableName, + ) + ); + list ($max_ind) = $smcFunc['db_fetch_row']($count_req); + $smcFunc['db_free_result']($count_req); + // Get the right bloody start! + $seq_create .= 'CREATE SEQUENCE ' . $matches[1] . ' START WITH ' . ($max_ind + 1) . ';' . $crlf . $crlf; + } + } + + $schema_create .= ',' . $crlf; + } + $smcFunc['db_free_result']($result); + + // Take off the last comma. + $schema_create = substr($schema_create, 0, -strlen($crlf) - 1); + + $result = $smcFunc['db_query']('', ' + SELECT pg_get_indexdef(i.indexrelid) AS inddef + FROM pg_class AS c + INNER JOIN pg_index AS i ON (i.indrelid = c.oid) + INNER JOIN pg_class AS c2 ON (c2.oid = i.indexrelid) + WHERE c.relname = {string:table} AND i.indisprimary is {raw:pk}', + array( + 'table' => $tableName, + 'pk' => 'false', + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + $index_create .= $crlf . $row['inddef'] . ';'; + } + + $smcFunc['db_free_result']($result); + + $result = $smcFunc['db_query']('', ' + SELECT pg_get_constraintdef(c.oid) as pkdef + FROM pg_constraint as c + WHERE c.conrelid::regclass::text = {string:table} AND + c.contype = {string:constraintType}', + array( + 'table' => $tableName, + 'constraintType' => 'p', + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + $index_create .= $crlf . 'ALTER TABLE ' . $tableName . ' ADD ' . $row['pkdef'] . ';'; + } + + $smcFunc['db_free_result']($result); + + // Finish it off! + $schema_create .= $crlf . ');'; + + return $seq_create . $schema_create . $index_create; +} + +/** + * Get the version number. + * + * @return string The version + */ +function smf_db_get_version() +{ + global $db_connection; + static $ver; + + if (!empty($ver)) + return $ver; + + $ver = pg_version($db_connection)['server']; + + return $ver; +} + +/** + * Return PostgreSQL + * + * @return string The database engine we are using + */ +function smf_db_get_vendor() +{ + return 'PostgreSQL'; +} + +/** + * Figures out if persistent connection is allowed + * + * @return boolean + */ +function smf_db_allow_persistent() +{ + $value = ini_get('pgsql.allow_persistent'); + if (strtolower($value) == 'on' || strtolower($value) == 'true' || $value == '1') + return true; + else + return false; +} + +?> \ No newline at end of file diff --git a/Sources/DbPackages-mysql.php b/Sources/DbPackages-mysql.php new file mode 100644 index 0000000..b35db47 --- /dev/null +++ b/Sources/DbPackages-mysql.php @@ -0,0 +1,927 @@ + 'smf_db_add_column', + 'db_add_index' => 'smf_db_add_index', + 'db_calculate_type' => 'smf_db_calculate_type', + 'db_change_column' => 'smf_db_change_column', + 'db_create_table' => 'smf_db_create_table', + 'db_drop_table' => 'smf_db_drop_table', + 'db_table_structure' => 'smf_db_table_structure', + 'db_list_columns' => 'smf_db_list_columns', + 'db_list_indexes' => 'smf_db_list_indexes', + 'db_remove_column' => 'smf_db_remove_column', + 'db_remove_index' => 'smf_db_remove_index', + ); + $db_package_log = array(); + } + + // We setup an array of SMF tables we can't do auto-remove on - in case a mod writer cocks it up! + $reservedTables = array( + 'admin_info_files', 'approval_queue', 'attachments', + 'background_tasks', 'ban_groups', 'ban_items', 'board_permissions', + 'board_permissions_view', 'boards', 'calendar', 'calendar_holidays', + 'categories', 'custom_fields', 'group_moderators', 'log_actions', + 'log_activity', 'log_banned', 'log_boards', 'log_comments', + 'log_digest', 'log_errors', 'log_floodcontrol', 'log_group_requests', + 'log_mark_read', 'log_member_notices', 'log_notify', 'log_online', + 'log_packages', 'log_polls', 'log_reported', 'log_reported_comments', + 'log_scheduled_tasks', 'log_search_messages', 'log_search_results', + 'log_search_subjects', 'log_search_topics', 'log_spider_hits', + 'log_spider_stats', 'log_subscribed', 'log_topics', 'mail_queue', + 'member_logins', 'membergroups', 'members', 'mentions', + 'message_icons', 'messages', 'moderator_groups', 'moderators', + 'package_servers', 'permission_profiles', 'permissions', + 'personal_messages', 'pm_labeled_messages', 'pm_labels', + 'pm_recipients', 'pm_rules', 'poll_choices', 'polls', 'qanda', + 'scheduled_tasks', 'sessions', 'settings', 'smiley_files', 'smileys', + 'spiders', 'subscriptions', 'themes', 'topics', 'user_alerts', + 'user_alerts_prefs', 'user_drafts', 'user_likes', + ); + foreach ($reservedTables as $k => $table_name) + $reservedTables[$k] = strtolower($db_prefix . $table_name); + + // We in turn may need the extra stuff. + db_extend('extra'); +} + +/** + * This function can be used to create a table without worrying about schema + * compatibilities across supported database systems. + * - If the table exists will, by default, do nothing. + * - Builds table with columns as passed to it - at least one column must be sent. + * The columns array should have one sub-array for each column - these sub arrays contain: + * 'name' = Column name + * 'type' = Type of column - values from (smallint, mediumint, int, text, varchar, char, tinytext, mediumtext, largetext) + * 'size' => Size of column (If applicable) - for example 255 for a large varchar, 10 for an int etc. + * If not set SMF will pick a size. + * - 'default' = Default value - do not set if no default required. + * - 'not_null' => Can it be null (true or false) - if not set default will be false. + * - 'auto' => Set to true to make it an auto incrementing column. Set to a numerical value to set from what + * it should begin counting. + * - Adds indexes as specified within indexes parameter. Each index should be a member of $indexes. Values are: + * - 'name' => Index name (If left empty SMF will generate). + * - 'type' => Type of index. Choose from 'primary', 'unique' or 'index'. If not set will default to 'index'. + * - 'columns' => Array containing columns that form part of key - in the order the index is to be created. + * - parameters: (None yet) + * - if_exists values: + * - 'ignore' will do nothing if the table exists. (And will return true) + * - 'overwrite' will drop any existing table of the same name. + * - 'error' will return false if the table already exists. + * - 'update' will update the table if the table already exists (no change of ai field and only colums with the same name keep the data) + * + * @param string $table_name The name of the table to create + * @param array $columns An array of column info in the specified format + * @param array $indexes An array of index info in the specified format + * @param array $parameters Extra parameters. Currently only 'engine', the desired MySQL storage engine, is used. + * @param string $if_exists What to do if the table exists. + * @param string $error + * @return boolean Whether or not the operation was successful + */ +function smf_db_create_table($table_name, $columns, $indexes = array(), $parameters = array(), $if_exists = 'ignore', $error = 'fatal') +{ + global $reservedTables, $smcFunc, $db_package_log, $db_prefix, $db_character_set, $db_name; + + static $engines = array(); + + $old_table_exists = false; + + // Strip out the table name, we might not need it in some cases + $real_prefix = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix; + $database = !empty($match[2]) ? $match[2] : $db_name; + + // With or without the database name, the fullname looks like this. + $full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name); + // Do not overwrite $table_name, this causes issues if we pass it onto a helper function. + $short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + + // First - no way do we touch SMF tables. + if (in_array(strtolower($short_table_name), $reservedTables)) + return false; + + // Log that we'll want to remove this on uninstall. + $db_package_log[] = array('remove_table', $short_table_name); + + // Slightly easier on MySQL than the others... + $tables = $smcFunc['db_list_tables']($database); + + if (in_array($full_table_name, $tables)) + { + // This is a sad day... drop the table? If not, return false (error) by default. + if ($if_exists == 'overwrite') + $smcFunc['db_drop_table']($table_name); + elseif ($if_exists == 'update') + { + $smcFunc['db_transaction']('begin'); + $db_trans = true; + $smcFunc['db_drop_table']($short_table_name . '_old'); + $smcFunc['db_query']('', ' + RENAME TABLE ' . $short_table_name . ' TO ' . $short_table_name . '_old', + array( + 'security_override' => true, + ) + ); + $old_table_exists = true; + } + else + return $if_exists == 'ignore'; + } + + // Righty - let's do the damn thing! + $table_query = 'CREATE TABLE ' . $short_table_name . "\n" . '('; + foreach ($columns as $column) + $table_query .= "\n\t" . smf_db_create_query_column($column) . ','; + + // Loop through the indexes next... + foreach ($indexes as $index) + { + // MySQL If its a text column, we need to add a size. + foreach ($index['columns'] as &$c) + { + $c = trim($c); + + // If a size was already specified, we won't be able to match it anyways. + $key = array_search($c, array_column($columns, 'name')); + $columns[$key]['size'] = isset($columns[$key]['size']) && is_numeric($columns[$key]['size']) ? $columns[$key]['size'] : null; + list ($type, $size) = $smcFunc['db_calculate_type']($columns[$key]['type'], $columns[$key]['size']); + if ( + $key === false + || !isset($columns[$key]) + || !in_array($columns[$key]['type'], array('text', 'mediumntext', 'largetext', 'varchar', 'char')) + || ( + isset($size) + && $size <= 191 + ) + ) + continue; + + $c .= '(191)'; + } + + $idx_columns = implode(',', $index['columns']); + + // Is it the primary? + if (isset($index['type']) && $index['type'] == 'primary') + $table_query .= "\n\t" . 'PRIMARY KEY (' . implode(',', $index['columns']) . '),'; + else + { + if (empty($index['name'])) + $index['name'] = trim(implode('_', preg_replace('~(\(\d+\))~', '', $index['columns']))); + + $table_query .= "\n\t" . (isset($index['type']) && $index['type'] == 'unique' ? 'UNIQUE' : 'KEY') . ' ' . $index['name'] . ' (' . $idx_columns . '),'; + } + } + + // No trailing commas! + if (substr($table_query, -1) == ',') + $table_query = substr($table_query, 0, -1); + + // Which engine do we want here? + if (empty($engines)) + { + // Figure out which engines we have + $get_engines = $smcFunc['db_query']('', 'SHOW ENGINES', array()); + + while ($row = $smcFunc['db_fetch_assoc']($get_engines)) + { + if ($row['Support'] == 'YES' || $row['Support'] == 'DEFAULT') + $engines[] = $row['Engine']; + } + + $smcFunc['db_free_result']($get_engines); + } + + // If we don't have this engine, or didn't specify one, default to InnoDB or MyISAM + // depending on which one is available + if (!isset($parameters['engine']) || !in_array($parameters['engine'], $engines)) + { + $parameters['engine'] = in_array('InnoDB', $engines) ? 'InnoDB' : 'MyISAM'; + } + + $table_query .= ') ENGINE=' . $parameters['engine']; + if (!empty($db_character_set) && $db_character_set == 'utf8') + $table_query .= ' DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci'; + + // Create the table! + $smcFunc['db_query']('', $table_query, + array( + 'security_override' => true, + ) + ); + + // Fill the old data + if ($old_table_exists) + { + $same_col = array(); + + $request = $smcFunc['db_query']('', ' + SELECT count(*), column_name + FROM information_schema.columns + WHERE table_name in ({string:table1},{string:table2}) AND table_schema = {string:schema} + GROUP BY column_name + HAVING count(*) > 1', + array( + 'table1' => $short_table_name, + 'table2' => $short_table_name . '_old', + 'schema' => $db_name, + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $same_col[] = $row['column_name']; + } + + $smcFunc['db_query']('', ' + INSERT INTO ' . $short_table_name . '(' + . implode(',', $same_col) . + ') + SELECT ' . implode(',', $same_col) . ' + FROM ' . $short_table_name . '_old', + array() + ); + + $smcFunc['db_drop_table']($short_table_name . '_old'); + } + + return true; +} + +/** + * Drop a table. + * + * @param string $table_name The name of the table to drop + * @param array $parameters Not used at the moment + * @param string $error + * @return boolean Whether or not the operation was successful + */ +function smf_db_drop_table($table_name, $parameters = array(), $error = 'fatal') +{ + global $reservedTables, $smcFunc, $db_prefix, $db_name; + + // After stripping away the database name, this is what's left. + $real_prefix = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix; + $database = !empty($match[2]) ? $match[2] : $db_name; + + // Get some aliases. + $full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name); + // Do not overwrite $table_name, this causes issues if we pass it onto a helper function. + $short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + + // God no - dropping one of these = bad. + if (in_array(strtolower($short_table_name), $reservedTables)) + return false; + + // Does it exist? + $tables = $smcFunc['db_list_tables']($database); + if (in_array($full_table_name, $tables)) + { + $query = 'DROP TABLE ' . $short_table_name; + $smcFunc['db_query']('', + $query, + array( + 'security_override' => true, + ) + ); + + return true; + } + + // Otherwise do 'nout. + return false; +} + +/** + * This function adds a column. + * + * @param string $table_name The name of the table to add the column to + * @param array $column_info An array of column info ({@see smf_db_create_table}) + * @param array $parameters Not used? + * @param string $if_exists What to do if the column exists. If 'update', column is updated. + * @param string $error + * @return boolean Whether or not the operation was successful + */ +function smf_db_add_column($table_name, $column_info, $parameters = array(), $if_exists = 'update', $error = 'fatal') +{ + global $smcFunc, $db_package_log, $db_prefix; + + $short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + $column_info = array_change_key_case($column_info); + + // Log that we will want to uninstall this! + $db_package_log[] = array('remove_column', $short_table_name, $column_info['name']); + + // Does it exist - if so don't add it again! + $columns = $smcFunc['db_list_columns']($table_name, false); + foreach ($columns as $column) + if ($column == $column_info['name']) + { + // If we're going to overwrite then use change column. + if ($if_exists == 'update') + return $smcFunc['db_change_column']($table_name, $column_info['name'], $column_info); + else + return false; + } + + // Get the specifics... + $column_info['size'] = isset($column_info['size']) && is_numeric($column_info['size']) ? $column_info['size'] : null; + + // Now add the thing! + $query = ' + ALTER TABLE ' . $short_table_name . ' + ADD ' . smf_db_create_query_column($column_info) . (empty($column_info['auto']) ? '' : ' primary key' + ); + $smcFunc['db_query']('', $query, + array( + 'security_override' => true, + ) + ); + + return true; +} + +/** + * Removes a column. + * + * @param string $table_name The name of the table to drop the column from + * @param string $column_name The name of the column to drop + * @param array $parameters Not used? + * @param string $error + * @return boolean Whether or not the operation was successful + */ +function smf_db_remove_column($table_name, $column_name, $parameters = array(), $error = 'fatal') +{ + global $smcFunc, $db_prefix; + + $short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + + // Does it exist? + $columns = $smcFunc['db_list_columns']($table_name, true); + + foreach ($columns as $column) + if ($column['name'] == $column_name) + { + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + DROP COLUMN ' . $column_name, + array( + 'security_override' => true, + ) + ); + + return true; + } + + // If here we didn't have to work - joy! + return false; +} + +/** + * Change a column. You only need to specify the column attributes that are changing. + * + * @param string $table_name The name of the table this column is in + * @param string $old_column The name of the column we want to change + * @param array $column_info An array of info about the "new" column definition (see {@link smf_db_create_table()}) + * Note that $column_info also supports two additional parameters that only make sense when changing columns: + * - drop_default - to drop a default that was previously specified + * @return bool + */ +function smf_db_change_column($table_name, $old_column, $column_info) +{ + global $smcFunc, $db_prefix; + + $short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + $column_info = array_change_key_case($column_info); + + // Check it does exist! + $columns = $smcFunc['db_list_columns']($table_name, true); + $old_info = null; + foreach ($columns as $column) + if ($column['name'] == $old_column) + $old_info = $column; + + // Nothing? + if ($old_info == null) + return false; + + // backward compatibility + if (isset($column_info['null']) && !isset($column_info['not_null'])) + $column_info['not_null'] = !$column_info['null']; + + // Get the right bits. + if (isset($column_info['drop_default']) && !empty($column_info['drop_default'])) + $column_info['drop_default'] = true; + else + $column_info['drop_default'] = false; + if (!isset($column_info['name'])) + $column_info['name'] = $old_column; + if (!array_key_exists('default', $column_info) && array_key_exists('default', $old_info) && empty($column_info['drop_default'])) + $column_info['default'] = $old_info['default']; + if (!isset($column_info['not_null'])) + $column_info['not_null'] = $old_info['not_null']; + if (!isset($column_info['auto'])) + $column_info['auto'] = $old_info['auto']; + if (!isset($column_info['type'])) + $column_info['type'] = $old_info['type']; + if (!isset($column_info['size']) || !is_numeric($column_info['size'])) + $column_info['size'] = $old_info['size']; + if (!isset($column_info['unsigned']) || !in_array($column_info['type'], array('int', 'tinyint', 'smallint', 'mediumint', 'bigint'))) + $column_info['unsigned'] = ''; + + // If truly unspecified, make that clear, otherwise, might be confused with NULL... + // (Unspecified = no default whatsoever = column is not nullable with a value of null...) + if (($column_info['not_null'] === true) && !$column_info['drop_default'] && array_key_exists('default', $column_info) && is_null($column_info['default'])) + unset($column_info['default']); + + list ($type, $size) = $smcFunc['db_calculate_type']($column_info['type'], $column_info['size']); + + // Allow for unsigned integers (mysql only) + $unsigned = in_array($type, array('int', 'tinyint', 'smallint', 'mediumint', 'bigint')) && !empty($column_info['unsigned']) ? 'unsigned ' : ''; + + // If you need to drop the default, that needs it's own thing... + // Must be done first, in case the default type is inconsistent with the other changes. + if ($column_info['drop_default']) + { + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + ALTER COLUMN `' . $old_column . '` DROP DEFAULT', + array( + 'security_override' => true, + ) + ); + } + + // Set the default clause. + $default_clause = ''; + if (!$column_info['drop_default'] && array_key_exists('default', $column_info)) + { + if (is_null($column_info['default'])) + $default_clause = 'DEFAULT NULL'; + elseif (is_numeric($column_info['default'])) + $default_clause = 'DEFAULT ' . (strpos($column_info['default'], '.') ? floatval($column_info['default']) : intval($column_info['default'])); + elseif (is_string($column_info['default'])) + $default_clause = 'DEFAULT \'' . $smcFunc['db_escape_string']($column_info['default']) . '\''; + } + + if ($size !== null) + $type = $type . '(' . $size . ')'; + + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + CHANGE COLUMN `' . $old_column . '` `' . $column_info['name'] . '` ' . $type . ' ' . + (!empty($unsigned) ? $unsigned : '') . (!empty($column_info['not_null']) ? 'NOT NULL' : '') . ' ' . + $default_clause . ' ' . + (empty($column_info['auto']) ? '' : 'auto_increment') . ' ', + array( + 'security_override' => true, + ) + ); +} + +/** + * Add an index. + * + * @param string $table_name The name of the table to add the index to + * @param array $index_info An array of index info (see {@link smf_db_create_table()}) + * @param array $parameters Not used? + * @param string $if_exists What to do if the index exists. If 'update', the definition will be updated. + * @param string $error + * @return boolean Whether or not the operation was successful + */ +function smf_db_add_index($table_name, $index_info, $parameters = array(), $if_exists = 'update', $error = 'fatal') +{ + global $smcFunc, $db_package_log, $db_prefix; + + $short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + + // No columns = no index. + if (empty($index_info['columns'])) + return false; + + // MySQL If its a text column, we need to add a size. + $cols = $smcFunc['db_list_columns']($table_name, true); + foreach ($index_info['columns'] as &$c) + { + $c = trim($c); + $cols[$c]['size'] = isset($cols[$c]['size']) && is_numeric($cols[$c]['size']) ? $cols[$c]['size'] : null; + list ($type, $size) = $smcFunc['db_calculate_type']($cols[$c]['type'], $cols[$c]['size']); + + // If a size was already specified, we won't be able to match it anyways. + if ( + !isset($cols[$c]) + || !in_array($cols[$c]['type'], array('text', 'mediumntext', 'largetext', 'varchar', 'char')) + || ( + isset($size) + && $size <= 191 + ) + ) + continue; + + $c .= '(191)'; + } + + $columns = implode(',', $index_info['columns']); + + // No name - make it up! + if (empty($index_info['name'])) + { + // No need for primary. + if (isset($index_info['type']) && $index_info['type'] == 'primary') + $index_info['name'] = ''; + else + $index_info['name'] = trim(implode('_', preg_replace('~(\(\d+\))~', '', $index_info['columns']))); + } + + // Log that we are going to want to remove this! + $db_package_log[] = array('remove_index', $short_table_name, $index_info['name']); + + // Let's get all our indexes. + $indexes = $smcFunc['db_list_indexes']($table_name, true); + // Do we already have it? + foreach ($indexes as $index) + { + if ($index['name'] == $index_info['name'] || ($index['type'] == 'primary' && isset($index_info['type']) && $index_info['type'] == 'primary')) + { + // If we want to overwrite simply remove the current one then continue. + if ($if_exists != 'update' || $index['type'] == 'primary') + return false; + else + $smcFunc['db_remove_index']($table_name, $index_info['name']); + } + } + + // If we're here we know we don't have the index - so just add it. + if (!empty($index_info['type']) && $index_info['type'] == 'primary') + { + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + ADD PRIMARY KEY (' . $columns . ')', + array( + 'security_override' => true, + ) + ); + } + else + { + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + ADD ' . (isset($index_info['type']) && $index_info['type'] == 'unique' ? 'UNIQUE' : 'INDEX') . ' ' . $index_info['name'] . ' (' . $columns . ')', + array( + 'security_override' => true, + ) + ); + } +} + +/** + * Remove an index. + * + * @param string $table_name The name of the table to remove the index from + * @param string $index_name The name of the index to remove + * @param array $parameters Not used? + * @param string $error + * @return boolean Whether or not the operation was successful + */ +function smf_db_remove_index($table_name, $index_name, $parameters = array(), $error = 'fatal') +{ + global $smcFunc, $db_prefix; + + $short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + + // Better exist! + $indexes = $smcFunc['db_list_indexes']($table_name, true); + + foreach ($indexes as $index) + { + // If the name is primary we want the primary key! + if ($index['type'] == 'primary' && $index_name == 'primary') + { + // Dropping primary key? + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + DROP PRIMARY KEY', + array( + 'security_override' => true, + ) + ); + + return true; + } + if ($index['name'] == $index_name) + { + // Drop the bugger... + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + DROP INDEX ' . $index_name, + array( + 'security_override' => true, + ) + ); + + return true; + } + } + + // Not to be found ;( + return false; +} + +/** + * Get the schema formatted name for a type. + * + * @param string $type_name The data type (int, varchar, smallint, etc.) + * @param int $type_size The size (8, 255, etc.) + * @param boolean $reverse + * @return array An array containing the appropriate type and size for this DB type + */ +function smf_db_calculate_type($type_name, $type_size = null, $reverse = false) +{ + // MySQL is actually the generic baseline. + + $type_name = strtolower($type_name); + // Generic => Specific. + if (!$reverse) + { + $types = array( + 'inet' => 'varbinary', + ); + } + else + { + $types = array( + 'varbinary' => 'inet', + ); + } + + // Got it? Change it! + if (isset($types[$type_name])) + { + if ($type_name == 'inet' && !$reverse) + { + $type_size = 16; + $type_name = 'varbinary'; + } + elseif ($type_name == 'varbinary' && $reverse && $type_size == 16) + { + $type_name = 'inet'; + $type_size = null; + } + elseif ($type_name == 'varbinary') + $type_name = 'varbinary'; + else + $type_name = $types[$type_name]; + } + elseif ($type_name == 'boolean') + $type_size = null; + + return array($type_name, $type_size); +} + +/** + * Get table structure. + * + * @param string $table_name The name of the table + * @return array An array of table structure - the name, the column info from {@link smf_db_list_columns()} and the index info from {@link smf_db_list_indexes()} + */ +function smf_db_table_structure($table_name) +{ + global $smcFunc, $db_prefix, $db_name; + + $parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + $real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name; + $database = !empty($match[2]) ? $match[2] : $db_name; + + // Find the table engine and add that to the info as well + $table_status = $smcFunc['db_query']('', ' + SHOW TABLE STATUS + IN {raw:db} + LIKE {string:table}', + array( + 'db' => $database, + 'table' => $real_table_name + ) + ); + + // Only one row, so no need for a loop... + $row = $smcFunc['db_fetch_assoc']($table_status); + + $smcFunc['db_free_result']($table_status); + + return array( + 'name' => $parsed_table_name, + 'columns' => $smcFunc['db_list_columns']($table_name, true), + 'indexes' => $smcFunc['db_list_indexes']($table_name, true), + 'engine' => $row['Engine'], + ); +} + +/** + * Return column information for a table. + * + * @param string $table_name The name of the table to get column info for + * @param bool $detail Whether or not to return detailed info. If true, returns the column info. If false, just returns the column names. + * @param array $parameters Not used? + * @return array An array of column names or detailed column info, depending on $detail + */ +function smf_db_list_columns($table_name, $detail = false, $parameters = array()) +{ + global $smcFunc, $db_prefix, $db_name; + + $parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + $real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name; + $database = !empty($match[2]) ? $match[2] : $db_name; + + $result = $smcFunc['db_query']('', ' + SELECT column_name "Field", COLUMN_TYPE "Type", is_nullable "Null", COLUMN_KEY "Key" , column_default "Default", extra "Extra" + FROM information_schema.columns + WHERE table_name = {string:table_name} + AND table_schema = {string:db_name} + ORDER BY ordinal_position', + array( + 'table_name' => $real_table_name, + 'db_name' => $db_name, + ) + ); + $columns = array(); + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + if (!$detail) + { + $columns[] = $row['Field']; + } + else + { + // Is there an auto_increment? + $auto = strpos($row['Extra'], 'auto_increment') !== false ? true : false; + + // Can we split out the size? + if (preg_match('~(.+?)\s*\((\d+)\)(?:(?:\s*)?(unsigned))?~i', $row['Type'], $matches) === 1) + { + $type = $matches[1]; + $size = $matches[2]; + if (!empty($matches[3]) && $matches[3] == 'unsigned') + $unsigned = true; + } + else + { + $type = $row['Type']; + $size = null; + } + + $columns[$row['Field']] = array( + 'name' => $row['Field'], + 'not_null' => $row['Null'] != 'YES', + 'null' => $row['Null'] == 'YES', + 'default' => isset($row['Default']) ? $row['Default'] : null, + 'type' => $type, + 'size' => $size, + 'auto' => $auto, + ); + + if (isset($unsigned)) + { + $columns[$row['Field']]['unsigned'] = $unsigned; + unset($unsigned); + } + } + } + $smcFunc['db_free_result']($result); + + return $columns; +} + +/** + * Get index information. + * + * @param string $table_name The name of the table to get indexes for + * @param bool $detail Whether or not to return detailed info. + * @param array $parameters Not used? + * @return array An array of index names or a detailed array of index info, depending on $detail + */ +function smf_db_list_indexes($table_name, $detail = false, $parameters = array()) +{ + global $smcFunc, $db_prefix, $db_name; + + $parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + $real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name; + $database = !empty($match[2]) ? $match[2] : $db_name; + + $result = $smcFunc['db_query']('', ' + SHOW KEYS + FROM {raw:table_name} + IN {raw:db}', + array( + 'db' => $database, + 'table_name' => $real_table_name, + ) + ); + $indexes = array(); + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + if (!$detail) + $indexes[] = $row['Key_name']; + else + { + // What is the type? + if ($row['Key_name'] == 'PRIMARY') + $type = 'primary'; + elseif (empty($row['Non_unique'])) + $type = 'unique'; + elseif (isset($row['Index_type']) && $row['Index_type'] == 'FULLTEXT') + $type = 'fulltext'; + else + $type = 'index'; + + // This is the first column we've seen? + if (empty($indexes[$row['Key_name']])) + { + $indexes[$row['Key_name']] = array( + 'name' => $row['Key_name'], + 'type' => $type, + 'columns' => array(), + ); + } + + // Is it a partial index? + if (!empty($row['Sub_part'])) + $indexes[$row['Key_name']]['columns'][] = $row['Column_name'] . '(' . $row['Sub_part'] . ')'; + else + $indexes[$row['Key_name']]['columns'][] = $row['Column_name']; + } + } + $smcFunc['db_free_result']($result); + + return $indexes; +} + +/** + * Creates a query for a column + * + * @param array $column An array of column info + * @return string The column definition + */ +function smf_db_create_query_column($column) +{ + global $smcFunc; + + $column = array_change_key_case($column); + + // Auto increment is easy here! + if (!empty($column['auto'])) + $default = 'auto_increment'; + // Make it null. + elseif (array_key_exists('default', $column) && is_null($column['default'])) + $default = 'DEFAULT NULL'; + // Numbers don't need quotes. + elseif (isset($column['default']) && is_numeric($column['default'])) + $default = 'DEFAULT ' . (strpos($column['default'], '.') ? floatval($column['default']) : intval($column['default'])); + // Non empty string. + elseif (isset($column['default'])) + $default = 'DEFAULT \'' . $smcFunc['db_escape_string']($column['default']) . '\''; + else + $default = ''; + + // Backwards compatible with the nullable column. + if (isset($column['null']) && !isset($column['not_null'])) + $column['not_null'] = !$column['null']; + + // Sort out the size... and stuff... + $column['size'] = isset($column['size']) && is_numeric($column['size']) ? $column['size'] : null; + list ($type, $size) = $smcFunc['db_calculate_type']($column['type'], $column['size']); + + // Allow unsigned integers (mysql only) + $unsigned = in_array($type, array('int', 'tinyint', 'smallint', 'mediumint', 'bigint')) && !empty($column['unsigned']) ? 'unsigned ' : ''; + + if ($size !== null) + $type = $type . '(' . $size . ')'; + + // Now just put it together! + return '`' . $column['name'] . '` ' . $type . ' ' . (!empty($unsigned) ? $unsigned : '') . (!empty($column['not_null']) ? 'NOT NULL' : '') . ' ' . $default; +} + +?> \ No newline at end of file diff --git a/Sources/DbPackages-postgresql.php b/Sources/DbPackages-postgresql.php new file mode 100644 index 0000000..71180a3 --- /dev/null +++ b/Sources/DbPackages-postgresql.php @@ -0,0 +1,959 @@ + 'smf_db_add_column', + 'db_add_index' => 'smf_db_add_index', + 'db_calculate_type' => 'smf_db_calculate_type', + 'db_change_column' => 'smf_db_change_column', + 'db_create_table' => 'smf_db_create_table', + 'db_drop_table' => 'smf_db_drop_table', + 'db_table_structure' => 'smf_db_table_structure', + 'db_list_columns' => 'smf_db_list_columns', + 'db_list_indexes' => 'smf_db_list_indexes', + 'db_remove_column' => 'smf_db_remove_column', + 'db_remove_index' => 'smf_db_remove_index', + ); + $db_package_log = array(); + } + + // We setup an array of SMF tables we can't do auto-remove on - in case a mod writer cocks it up! + $reservedTables = array( + 'admin_info_files', 'approval_queue', 'attachments', + 'background_tasks', 'ban_groups', 'ban_items', 'board_permissions', + 'board_permissions_view', 'boards', 'calendar', 'calendar_holidays', + 'categories', 'custom_fields', 'group_moderators', 'log_actions', + 'log_activity', 'log_banned', 'log_boards', 'log_comments', + 'log_digest', 'log_errors', 'log_floodcontrol', 'log_group_requests', + 'log_mark_read', 'log_member_notices', 'log_notify', 'log_online', + 'log_packages', 'log_polls', 'log_reported', 'log_reported_comments', + 'log_scheduled_tasks', 'log_search_messages', 'log_search_results', + 'log_search_subjects', 'log_search_topics', 'log_spider_hits', + 'log_spider_stats', 'log_subscribed', 'log_topics', 'mail_queue', + 'member_logins', 'membergroups', 'members', 'mentions', + 'message_icons', 'messages', 'moderator_groups', 'moderators', + 'package_servers', 'permission_profiles', 'permissions', + 'personal_messages', 'pm_labeled_messages', 'pm_labels', + 'pm_recipients', 'pm_rules', 'poll_choices', 'polls', 'qanda', + 'scheduled_tasks', 'sessions', 'settings', 'smiley_files', 'smileys', + 'spiders', 'subscriptions', 'themes', 'topics', 'user_alerts', + 'user_alerts_prefs', 'user_drafts', 'user_likes', + ); + foreach ($reservedTables as $k => $table_name) + $reservedTables[$k] = strtolower($db_prefix . $table_name); + + // We in turn may need the extra stuff. + db_extend('extra'); +} + +/** + * This function can be used to create a table without worrying about schema + * compatibilities across supported database systems. + * - If the table exists will, by default, do nothing. + * - Builds table with columns as passed to it - at least one column must be sent. + * The columns array should have one sub-array for each column - these sub arrays contain: + * 'name' = Column name + * 'type' = Type of column - values from (smallint, mediumint, int, text, varchar, char, tinytext, mediumtext, largetext) + * 'size' => Size of column (If applicable) - for example 255 for a large varchar, 10 for an int etc. + * If not set SMF will pick a size. + * - 'default' = Default value - do not set if no default required. + * - 'not_null' => Can it be null (true or false) - if not set default will be false. + * - 'auto' => Set to true to make it an auto incrementing column. Set to a numerical value to set from what + * it should begin counting. + * - Adds indexes as specified within indexes parameter. Each index should be a member of $indexes. Values are: + * - 'name' => Index name (If left empty SMF will generate). + * - 'type' => Type of index. Choose from 'primary', 'unique' or 'index'. If not set will default to 'index'. + * - 'columns' => Array containing columns that form part of key - in the order the index is to be created. + * - parameters: (None yet) + * - if_exists values: + * - 'ignore' will do nothing if the table exists. (And will return true) + * - 'overwrite' will drop any existing table of the same name. + * - 'error' will return false if the table already exists. + * - 'update' will update the table if the table already exists (no change of ai field and only colums with the same name keep the data) + * + * @param string $table_name The name of the table to create + * @param array $columns An array of column info in the specified format + * @param array $indexes An array of index info in the specified format + * @param array $parameters Currently not used + * @param string $if_exists What to do if the table exists. + * @param string $error + */ +function smf_db_create_table($table_name, $columns, $indexes = array(), $parameters = array(), $if_exists = 'ignore', $error = 'fatal') +{ + global $reservedTables, $smcFunc, $db_package_log, $db_prefix, $db_name; + + $db_trans = false; + $old_table_exists = false; + + // Strip out the table name, we might not need it in some cases + $real_prefix = preg_match('~^("?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix; + $database = !empty($match[2]) ? $match[2] : $db_name; + + // With or without the database name, the fullname looks like this. + $full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name); + $short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + + // First - no way do we touch SMF tables. + if (in_array(strtolower($short_table_name), $reservedTables)) + return false; + + // Log that we'll want to remove this on uninstall. + $db_package_log[] = array('remove_table', $short_table_name); + + // This... my friends... is a function in a half - let's start by checking if the table exists! + $tables = $smcFunc['db_list_tables']($database); + if (in_array($full_table_name, $tables)) + { + // This is a sad day... drop the table? If not, return false (error) by default. + if ($if_exists == 'overwrite') + $smcFunc['db_drop_table']($table_name); + elseif ($if_exists == 'update') + { + $smcFunc['db_drop_table']($table_name . '_old'); + $smcFunc['db_transaction']('begin'); + $db_trans = true; + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' RENAME TO ' . $short_table_name . '_old', + array( + 'security_override' => true, + ) + ); + $old_table_exists = true; + } + else + return $if_exists == 'ignore'; + } + + // If we've got this far - good news - no table exists. We can build our own! + if (!$db_trans) + $smcFunc['db_transaction']('begin'); + $table_query = 'CREATE TABLE ' . $short_table_name . "\n" . '('; + foreach ($columns as $column) + { + $column = array_change_key_case($column); + + // If we have an auto increment do it! + if (!empty($column['auto'])) + { + if (!$old_table_exists) + $smcFunc['db_query']('', ' + DROP SEQUENCE IF EXISTS ' . $short_table_name . '_seq', + array( + 'security_override' => true, + ) + ); + + if (!$old_table_exists) + $smcFunc['db_query']('', ' + CREATE SEQUENCE ' . $short_table_name . '_seq', + array( + 'security_override' => true, + ) + ); + $default = 'default nextval(\'' . $short_table_name . '_seq\')'; + } + elseif (isset($column['default']) && $column['default'] !== null) + $default = 'default \'' . $smcFunc['db_escape_string']($column['default']) . '\''; + else + $default = ''; + + // Sort out the size... + $column['size'] = isset($column['size']) && is_numeric($column['size']) ? $column['size'] : null; + list ($type, $size) = $smcFunc['db_calculate_type']($column['type'], $column['size']); + if ($size !== null) + $type = $type . '(' . $size . ')'; + + // backward compatibility + if (isset($column['null']) && !isset($column['not_null'])) + $column['not_null'] = !$column['null']; + + // Now just put it together! + $table_query .= "\n\t\"" . $column['name'] . '" ' . $type . ' ' . (!empty($column['not_null']) ? 'NOT NULL' : '') . ' ' . $default . ','; + } + + // Loop through the indexes a sec... + $index_queries = array(); + foreach ($indexes as $index) + { + // MySQL you can do a "column_name (length)", postgresql does not allow this. Strip it. + foreach ($index['columns'] as &$c) + $c = preg_replace('~\s+(\(\d+\))~', '', $c); + + $idx_columns = implode(',', $index['columns']); + + // Primary goes in the table... + if (isset($index['type']) && $index['type'] == 'primary') + $table_query .= "\n\t" . 'PRIMARY KEY (' . implode(',', $index['columns']) . '),'; + else + { + if (empty($index['name'])) + $index['name'] = trim(implode('_', preg_replace('~(\(\d+\))~', '', $index['columns']))); + + $index_queries[] = 'CREATE ' . (isset($index['type']) && $index['type'] == 'unique' ? 'UNIQUE' : '') . ' INDEX ' . $short_table_name . '_' . $index['name'] . ' ON ' . $short_table_name . ' (' . $idx_columns . ')'; + } + } + + // No trailing commas! + if (substr($table_query, -1) == ',') + $table_query = substr($table_query, 0, -1); + + $table_query .= ')'; + + // Create the table! + $smcFunc['db_query']('', $table_query, + array( + 'security_override' => true, + ) + ); + + // Fill the old data + if ($old_table_exists) + { + $same_col = array(); + + $request = $smcFunc['db_query']('', ' + SELECT count(*), column_name + FROM information_schema.columns + WHERE table_name in ({string:table1},{string:table2}) AND table_schema = {string:schema} + GROUP BY column_name + HAVING count(*) > 1', + array( + 'table1' => $short_table_name, + 'table2' => $short_table_name . '_old', + 'schema' => 'public', + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $same_col[] = $row['column_name']; + } + + $smcFunc['db_query']('', ' + INSERT INTO ' . $short_table_name . '(' + . implode(',', $same_col) . + ') + SELECT ' . implode(',', $same_col) . ' + FROM ' . $short_table_name . '_old', + array() + ); + } + + // And the indexes... + foreach ($index_queries as $query) + $smcFunc['db_query']('', $query, + array( + 'security_override' => true, + ) + ); + + // Go, go power rangers! + $smcFunc['db_transaction']('commit'); + + if ($old_table_exists) + $smcFunc['db_drop_table']($table_name . '_old'); + + return true; +} + +/** + * Drop a table and its associated sequences. + * + * @param string $table_name The name of the table to drop + * @param array $parameters Not used at the moment + * @param string $error + * @return boolean Whether or not the operation was successful + */ +function smf_db_drop_table($table_name, $parameters = array(), $error = 'fatal') +{ + global $reservedTables, $smcFunc, $db_prefix, $db_name; + + // After stripping away the database name, this is what's left. + $real_prefix = preg_match('~^("?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix; + $database = !empty($match[2]) ? $match[2] : $db_name; + + // Get some aliases. + $full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name); + $short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + + // God no - dropping one of these = bad. + if (in_array(strtolower($table_name), $reservedTables)) + return false; + + // Does it exist? + $tables = $smcFunc['db_list_tables']($database); + if (in_array($full_table_name, $tables)) + { + // We can then drop the table. + $smcFunc['db_transaction']('begin'); + + // the table + $table_query = 'DROP TABLE ' . $short_table_name; + + // and the assosciated sequence, if any + $sequence_query = 'DROP SEQUENCE IF EXISTS ' . $short_table_name . '_seq'; + + // drop them + $smcFunc['db_query']('', + $table_query, + array( + 'security_override' => true, + ) + ); + $smcFunc['db_query']('', + $sequence_query, + array( + 'security_override' => true, + ) + ); + + $smcFunc['db_transaction']('commit'); + + return true; + } + + // Otherwise do 'nout. + return false; +} + +/** + * This function adds a column. + * + * @param string $table_name The name of the table to add the column to + * @param array $column_info An array of column info (see {@link smf_db_create_table()}) + * @param array $parameters Not used? + * @param string $if_exists What to do if the column exists. If 'update', column is updated. + * @param string $error + * @return boolean Whether or not the operation was successful + */ +function smf_db_add_column($table_name, $column_info, $parameters = array(), $if_exists = 'update', $error = 'fatal') +{ + global $smcFunc, $db_package_log, $db_prefix; + + $short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + $column_info = array_change_key_case($column_info); + + // Log that we will want to uninstall this! + $db_package_log[] = array('remove_column', $short_table_name, $column_info['name']); + + // Does it exist - if so don't add it again! + $columns = $smcFunc['db_list_columns']($table_name, false); + foreach ($columns as $column) + if ($column == $column_info['name']) + { + // If we're going to overwrite then use change column. + if ($if_exists == 'update') + return $smcFunc['db_change_column']($table_name, $column_info['name'], $column_info); + else + return false; + } + + // Get the specifics... + $column_info['size'] = isset($column_info['size']) && is_numeric($column_info['size']) ? $column_info['size'] : null; + list ($type, $size) = $smcFunc['db_calculate_type']($column_info['type'], $column_info['size']); + if ($size !== null) + $type = $type . '(' . $size . ')'; + + // Now add the thing! + $query = ' + ALTER TABLE ' . $short_table_name . ' + ADD COLUMN ' . $column_info['name'] . ' ' . $type; + $smcFunc['db_query']('', $query, + array( + 'security_override' => true, + ) + ); + + // If there's more attributes they need to be done via a change on PostgreSQL. + unset($column_info['type'], $column_info['size']); + + if (count($column_info) != 1) + return $smcFunc['db_change_column']($table_name, $column_info['name'], $column_info); + else + return true; +} + +/** + * Removes a column. + * + * @param string $table_name The name of the table to drop the column from + * @param string $column_name The name of the column to drop + * @param array $parameters Not used? + * @param string $error + * @return boolean Whether or not the operation was successful + */ +function smf_db_remove_column($table_name, $column_name, $parameters = array(), $error = 'fatal') +{ + global $smcFunc, $db_prefix; + + $short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + + // Does it exist? + $columns = $smcFunc['db_list_columns']($table_name, true); + + foreach ($columns as $column) + if (strtolower($column['name']) == strtolower($column_name)) + { + // If there is an auto we need remove it! + if ($column['auto']) + $smcFunc['db_query']('', ' + DROP SEQUENCE IF EXISTS ' . $short_table_name . '_seq', + array( + 'security_override' => true, + ) + ); + + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + DROP COLUMN ' . $column_name, + array( + 'security_override' => true, + ) + ); + + return true; + } + + // If here we didn't have to work - joy! + return false; +} + +/** + * Change a column. You only need to specify the column attributes that are changing. + * + * @param string $table_name The name of the table this column is in + * @param string $old_column The name of the column we want to change + * @param array $column_info An array of info about the "new" column definition (see {@link smf_db_create_table()}) + * Note that $column_info also supports two additional parameters that only make sense when changing columns: + * - drop_default - to drop a default that was previously specified + * @return bool + */ +function smf_db_change_column($table_name, $old_column, $column_info) +{ + global $smcFunc, $db_prefix; + + $short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + $column_info = array_change_key_case($column_info); + + // Check it does exist! + $columns = $smcFunc['db_list_columns']($table_name, true); + $old_info = null; + foreach ($columns as $column) + if ($column['name'] == $old_column) + $old_info = $column; + + // Nothing? + if ($old_info == null) + return false; + + // backward compatibility + if (isset($column_info['null']) && !isset($column_info['not_null'])) + $column_info['not_null'] = !$column_info['null']; + + // Get the right bits. + if (isset($column_info['drop_default']) && !empty($column_info['drop_default'])) + $column_info['drop_default'] = true; + else + $column_info['drop_default'] = false; + if (!isset($column_info['name'])) + $column_info['name'] = $old_column; + if (!array_key_exists('default', $column_info) && array_key_exists('default', $old_info) && empty($column_info['drop_default'])) + $column_info['default'] = $old_info['default']; + if (!isset($column_info['not_null'])) + $column_info['not_null'] = $old_info['not_null']; + if (!isset($column_info['auto'])) + $column_info['auto'] = $old_info['auto']; + if (!isset($column_info['type'])) + $column_info['type'] = $old_info['type']; + if (!isset($column_info['size']) || !is_numeric($column_info['size'])) + $column_info['size'] = $old_info['size']; + if (!isset($column_info['unsigned']) || !in_array($column_info['type'], array('int', 'tinyint', 'smallint', 'mediumint', 'bigint'))) + $column_info['unsigned'] = ''; + + // If truly unspecified, make that clear, otherwise, might be confused with NULL... + // (Unspecified = no default whatsoever = column is not nullable with a value of null...) + if (($column_info['not_null'] === true) && !$column_info['drop_default'] && array_key_exists('default', $column_info) && is_null($column_info['default'])) + unset($column_info['default']); + + // If you need to drop the default, that needs it's own thing... + // Must be done first, in case the default type is inconsistent with the other changes. + if ($column_info['drop_default']) + { + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + ALTER COLUMN ' . $old_column . ' DROP DEFAULT', + array( + 'security_override' => true, + ) + ); + } + + // Now we check each bit individually and ALTER as required. + if (isset($column_info['name']) && $column_info['name'] != $old_column) + { + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + RENAME COLUMN ' . $old_column . ' TO ' . $column_info['name'], + array( + 'security_override' => true, + ) + ); + } + + // What about a change in type? + if (isset($column_info['type']) && ($column_info['type'] != $old_info['type'] || (isset($column_info['size']) && $column_info['size'] != $old_info['size']))) + { + $column_info['size'] = isset($column_info['size']) && is_numeric($column_info['size']) ? $column_info['size'] : null; + list ($type, $size) = $smcFunc['db_calculate_type']($column_info['type'], $column_info['size']); + if ($size !== null) + $type = $type . '(' . $size . ')'; + + // The alter is a pain. + $smcFunc['db_transaction']('begin'); + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + ADD COLUMN ' . $column_info['name'] . '_tempxx ' . $type, + array( + 'security_override' => true, + ) + ); + $smcFunc['db_query']('', ' + UPDATE ' . $short_table_name . ' + SET ' . $column_info['name'] . '_tempxx = CAST(' . $column_info['name'] . ' AS ' . $type . ')', + array( + 'security_override' => true, + ) + ); + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + DROP COLUMN ' . $column_info['name'], + array( + 'security_override' => true, + ) + ); + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + RENAME COLUMN ' . $column_info['name'] . '_tempxx TO ' . $column_info['name'], + array( + 'security_override' => true, + ) + ); + $smcFunc['db_transaction']('commit'); + } + + // Different default? + // Just go ahead & honor the setting. Type changes above introduce defaults that we might need to override here... + if (!$column_info['drop_default'] && array_key_exists('default', $column_info)) + { + // Fix the default. + $default = ''; + if (is_null($column_info['default'])) + $default = 'NULL'; + elseif (isset($column_info['default']) && is_numeric($column_info['default'])) + $default = strpos($column_info['default'], '.') ? floatval($column_info['default']) : intval($column_info['default']); + else + $default = '\'' . $smcFunc['db_escape_string']($column_info['default']) . '\''; + + $action = 'SET DEFAULT ' . $default; + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + ALTER COLUMN ' . $column_info['name'] . ' ' . $action, + array( + 'security_override' => true, + ) + ); + } + + // Is it null - or otherwise? + // Just go ahead & honor the setting. Type changes above introduce defaults that we might need to override here... + if ($column_info['not_null']) + $action = 'SET NOT NULL'; + else + $action = 'DROP NOT NULL'; + + $smcFunc['db_query']('', ' + ALTER TABLE ' . $short_table_name . ' + ALTER COLUMN ' . $column_info['name'] . ' ' . $action, + array( + 'security_override' => true, + ) + ); + + return true; +} + +/** + * Add an index. + * + * @param string $table_name The name of the table to add the index to + * @param array $index_info An array of index info (see {@link smf_db_create_table()}) + * @param array $parameters Not used? + * @param string $if_exists What to do if the index exists. If 'update', the definition will be updated. + * @param string $error + * @return boolean Whether or not the operation was successful + */ +function smf_db_add_index($table_name, $index_info, $parameters = array(), $if_exists = 'update', $error = 'fatal') +{ + global $smcFunc, $db_package_log, $db_prefix; + + $parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + $real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name; + + // No columns = no index. + if (empty($index_info['columns'])) + return false; + + // MySQL you can do a "column_name (length)", postgresql does not allow this. Strip it. + foreach ($index_info['columns'] as &$c) + $c = preg_replace('~\s+(\(\d+\))~', '', $c); + + $columns = implode(',', $index_info['columns']); + + // No name - make it up! + if (empty($index_info['name'])) + { + // No need for primary. + if (isset($index_info['type']) && $index_info['type'] == 'primary') + $index_info['name'] = ''; + else + $index_info['name'] = trim(implode('_', preg_replace('~(\(\d+\))~', '', $index_info['columns']))); + } + else + $index_info['name'] = $index_info['name']; + + // Log that we are going to want to remove this! + $db_package_log[] = array('remove_index', $parsed_table_name, $index_info['name']); + + // Let's get all our indexes. + $indexes = $smcFunc['db_list_indexes']($table_name, true); + + // Do we already have it? + foreach ($indexes as $index) + { + if ($index['name'] == $index_info['name'] || ($index['type'] == 'primary' && isset($index_info['type']) && $index_info['type'] == 'primary')) + { + // If we want to overwrite simply remove the current one then continue. + if ($if_exists != 'update' || $index['type'] == 'primary') + return false; + else + $smcFunc['db_remove_index']($table_name, $index_info['name']); + } + } + + // If we're here we know we don't have the index - so just add it. + if (!empty($index_info['type']) && $index_info['type'] == 'primary') + { + $smcFunc['db_query']('', ' + ALTER TABLE ' . $real_table_name . ' + ADD PRIMARY KEY (' . $columns . ')', + array( + 'security_override' => true, + ) + ); + } + else + { + $smcFunc['db_query']('', ' + CREATE ' . (isset($index_info['type']) && $index_info['type'] == 'unique' ? 'UNIQUE' : '') . ' INDEX ' . $real_table_name . '_' . $index_info['name'] . ' ON ' . $real_table_name . ' (' . $columns . ')', + array( + 'security_override' => true, + ) + ); + } +} + +/** + * Remove an index. + * + * @param string $table_name The name of the table to remove the index from + * @param string $index_name The name of the index to remove + * @param array $parameters Not used? + * @param string $error + * @return boolean Whether or not the operation was successful + */ +function smf_db_remove_index($table_name, $index_name, $parameters = array(), $error = 'fatal') +{ + global $smcFunc, $db_prefix; + + $parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + $real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name; + + // Better exist! + $indexes = $smcFunc['db_list_indexes']($table_name, true); + + // Do not add the table name to the index if it is arleady there. + if ($index_name != 'primary' && strpos($index_name, $real_table_name) !== false) + $index_name = str_replace($real_table_name . '_', '', $index_name); + + foreach ($indexes as $index) + { + // If the name is primary we want the primary key! + if ($index['type'] == 'primary' && $index_name == 'primary') + { + // Dropping primary key is odd... + $smcFunc['db_query']('', ' + ALTER TABLE ' . $real_table_name . ' + DROP CONSTRAINT ' . $index['name'], + array( + 'security_override' => true, + ) + ); + + return true; + } + + if ($index['name'] == $index_name) + { + // Drop the bugger... + $smcFunc['db_query']('', ' + DROP INDEX ' . $real_table_name . '_' . $index_name, + array( + 'security_override' => true, + ) + ); + + return true; + } + } + + // Not to be found ;( + return false; +} + +/** + * Get the schema formatted name for a type. + * + * @param string $type_name The data type (int, varchar, smallint, etc.) + * @param int $type_size The size (8, 255, etc.) + * @param boolean $reverse If true, returns specific types for a generic type + * @return array An array containing the appropriate type and size for this DB type + */ +function smf_db_calculate_type($type_name, $type_size = null, $reverse = false) +{ + // Let's be sure it's lowercase MySQL likes both, others no. + $type_name = strtolower($type_name); + // Generic => Specific. + if (!$reverse) + { + $types = array( + 'varchar' => 'character varying', + 'char' => 'character', + 'mediumint' => 'int', + 'tinyint' => 'smallint', + 'tinytext' => 'character varying', + 'mediumtext' => 'text', + 'largetext' => 'text', + 'inet' => 'inet', + 'time' => 'time without time zone', + 'datetime' => 'timestamp without time zone', + 'timestamp' => 'timestamp without time zone', + ); + } + else + { + $types = array( + 'character varying' => 'varchar', + 'character' => 'char', + 'integer' => 'int', + 'inet' => 'inet', + 'time without time zone' => 'time', + 'timestamp without time zone' => 'datetime', + 'numeric' => 'decimal', + ); + } + + // Got it? Change it! + if (isset($types[$type_name])) + { + if ($type_name == 'tinytext') + $type_size = 255; + $type_name = $types[$type_name]; + } + + // Only char fields got size + if (strpos($type_name, 'char') === false) + $type_size = null; + + return array($type_name, $type_size); +} + +/** + * Get table structure. + * + * @param string $table_name The name of the table + * @return array An array of table structure - the name, the column info from {@link smf_db_list_columns()} and the index info from {@link smf_db_list_indexes()} + */ +function smf_db_table_structure($table_name) +{ + global $smcFunc, $db_prefix; + + $parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + $real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name; + + return array( + 'name' => $real_table_name, + 'columns' => $smcFunc['db_list_columns']($table_name, true), + 'indexes' => $smcFunc['db_list_indexes']($table_name, true), + ); +} + +/** + * Return column information for a table. + * + * @param string $table_name The name of the table to get column info for + * @param bool $detail Whether or not to return detailed info. If true, returns the column info. If false, just returns the column names. + * @param array $parameters Not used? + * @return array An array of column names or detailed column info, depending on $detail + */ +function smf_db_list_columns($table_name, $detail = false, $parameters = array()) +{ + global $smcFunc, $db_prefix, $db_name; + + $parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + $real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name; + $database = !empty($match[2]) ? $match[2] : $db_name; + + $result = $smcFunc['db_query']('', ' + SELECT column_name, column_default, is_nullable, data_type, character_maximum_length + FROM information_schema.columns + WHERE table_schema = {string:schema_public} + AND table_name = {string:table_name} + ORDER BY ordinal_position', + array( + 'schema_public' => 'public', + 'table_name' => $real_table_name, + ) + ); + $columns = array(); + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + if (!$detail) + { + $columns[] = $row['column_name']; + } + else + { + $auto = false; + $default = null; + // What is the default? + if ($row['column_default'] !== null) + { + if (preg_match('~nextval\(\'(.+?)\'(.+?)*\)~i', $row['column_default'], $matches) != 0) + $auto = true; + elseif (substr($row['column_default'], 0, 4) != 'NULL' && trim($row['column_default']) != '') + { + $pos = strpos($row['column_default'], '::'); + $default = trim($pos === false ? $row['column_default'] : substr($row['column_default'], 0, $pos), '\''); + } + } + + // Make the type generic. + list ($type, $size) = $smcFunc['db_calculate_type']($row['data_type'], $row['character_maximum_length'], true); + + $columns[$row['column_name']] = array( + 'name' => $row['column_name'], + 'not_null' => $row['is_nullable'] != 'YES', + 'null' => $row['is_nullable'] == 'YES', + 'default' => $default, + 'type' => $type, + 'size' => $size, + 'auto' => $auto, + ); + } + } + $smcFunc['db_free_result']($result); + + return $columns; +} + +/** + * Get index information. + * + * @param string $table_name The name of the table to get indexes for + * @param bool $detail Whether or not to return detailed info. + * @param array $parameters Not used? + * @return array An array of index names or a detailed array of index info, depending on $detail + */ +function smf_db_list_indexes($table_name, $detail = false, $parameters = array()) +{ + global $smcFunc, $db_prefix, $db_name; + + $parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name); + $real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name; + $database = !empty($match[2]) ? $match[2] : $db_name; + + $result = $smcFunc['db_query']('', ' + SELECT CASE WHEN i.indisprimary THEN 1 ELSE 0 END AS is_primary, + CASE WHEN i.indisunique THEN 1 ELSE 0 END AS is_unique, + c2.relname AS name, + pg_get_indexdef(i.indexrelid) AS inddef + FROM pg_class AS c, pg_class AS c2, pg_index AS i + WHERE c.relname = {string:table_name} + AND c.oid = i.indrelid + AND i.indexrelid = c2.oid', + array( + 'table_name' => $real_table_name, + ) + ); + $indexes = array(); + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + // Try get the columns that make it up. + if (preg_match('~\(([^\)]+?)\)~i', $row['inddef'], $matches) == 0) + continue; + + $columns = explode(',', $matches[1]); + + if (empty($columns)) + continue; + + foreach ($columns as $k => $v) + $columns[$k] = trim($v); + + // Fix up the name to be consistent cross databases + if (substr($row['name'], -5) == '_pkey' && $row['is_primary'] == 1) + $row['name'] = 'PRIMARY'; + else + $row['name'] = str_replace($real_table_name . '_', '', $row['name']); + + if (!$detail) + $indexes[] = $row['name']; + else + { + $indexes[$row['name']] = array( + 'name' => $row['name'], + 'type' => $row['is_primary'] ? 'primary' : ($row['is_unique'] ? 'unique' : 'index'), + 'columns' => $columns, + ); + } + } + $smcFunc['db_free_result']($result); + + return $indexes; +} + +?> \ No newline at end of file diff --git a/Sources/DbSearch-mysql.php b/Sources/DbSearch-mysql.php new file mode 100644 index 0000000..3c132e6 --- /dev/null +++ b/Sources/DbSearch-mysql.php @@ -0,0 +1,81 @@ + 'smf_db_query', + 'db_search_support' => 'smf_db_search_support', + 'db_create_word_search' => 'smf_db_create_word_search', + 'db_support_ignore' => true, + ); + + db_extend(); + $version = $smcFunc['db_get_version'](); + $smcFunc['db_supports_pcre'] = version_compare($version, strpos($version, 'MariaDB') !== false ? '10.0.5' : '8.0.4', '>='); +} + +/** + * This function will tell you whether this database type supports this search type. + * + * @param string $search_type The search type. + * @return boolean Whether or not the specified search type is supported by this db system + */ +function smf_db_search_support($search_type) +{ + $supported_types = array('fulltext'); + + return in_array($search_type, $supported_types); +} + +/** + * Highly specific function, to create the custom word index table. + * + * @param string $size The size of the desired index. + */ +function smf_db_create_word_search($size) +{ + global $smcFunc; + + if ($size == 'small') + $size = 'smallint(5)'; + elseif ($size == 'medium') + $size = 'mediumint(8)'; + else + $size = 'int(10)'; + + $smcFunc['db_query']('', ' + CREATE TABLE {db_prefix}log_search_words ( + id_word {raw:size} unsigned NOT NULL default {string:string_zero}, + id_msg int(10) unsigned NOT NULL default {string:string_zero}, + PRIMARY KEY (id_word, id_msg) + ) ENGINE=InnoDB', + array( + 'string_zero' => '0', + 'size' => $size, + ) + ); +} + +?> \ No newline at end of file diff --git a/Sources/DbSearch-postgresql.php b/Sources/DbSearch-postgresql.php new file mode 100644 index 0000000..b322ed7 --- /dev/null +++ b/Sources/DbSearch-postgresql.php @@ -0,0 +1,189 @@ + 'smf_db_search_query', + 'db_search_support' => 'smf_db_search_support', + 'db_create_word_search' => 'smf_db_create_word_search', + 'db_support_ignore' => false, + 'db_search_language' => 'smf_db_search_language', + ); + + db_extend(); + + $smcFunc['db_support_ignore'] = true; + $smcFunc['db_supports_pcre'] = true; +} + +/** + * This function will tell you whether this database type supports this search type. + * + * @param string $search_type The search type + * @return boolean Whether or not the specified search type is supported by this DB system. + */ +function smf_db_search_support($search_type) +{ + $supported_types = array('custom', 'fulltext'); + + return in_array($search_type, $supported_types); +} + +/** + * Returns the correct query for this search type. + * + * @param string $identifier A query identifier + * @param string $db_string The query text + * @param array $db_values An array of values to pass to $smcFunc['db_query'] + * @param resource $connection The current DB connection resource + * @return resource The query result resource from $smcFunc['db_query'] + */ +function smf_db_search_query($identifier, $db_string, $db_values = array(), $connection = null) +{ + global $smcFunc; + + $replacements = array( + 'create_tmp_log_search_topics' => array( + '~ENGINE=MEMORY~i' => '', + ), + 'create_tmp_log_search_messages' => array( + '~ENGINE=MEMORY~i' => '', + ), + 'insert_into_log_messages_fulltext' => array( + '/NOT\sLIKE/' => 'NOT ILIKE', + '/\bLIKE\b/' => 'ILIKE', + '/NOT RLIKE/' => '!~*', + '/RLIKE/' => '~*', + ), + 'insert_log_search_results_subject' => array( + '/NOT\sLIKE/' => 'NOT ILIKE', + '/\bLIKE\b/' => 'ILIKE', + '/NOT RLIKE/' => '!~*', + '/RLIKE/' => '~*', + ), + 'insert_log_search_topics' => array( + '/NOT\sLIKE/' => 'NOT ILIKE', + '/\bLIKE\b/' => 'ILIKE', + '/NOT RLIKE/' => '!~*', + '/RLIKE/' => '~*', + ), + 'insert_log_search_results_no_index' => array( + '/NOT\sLIKE/' => 'NOT ILIKE', + '/\bLIKE\b/' => 'ILIKE', + '/NOT RLIKE/' => '!~*', + '/RLIKE/' => '~*', + ), + ); + + if (isset($replacements[$identifier])) + $db_string = preg_replace(array_keys($replacements[$identifier]), array_values($replacements[$identifier]), $db_string); + if (preg_match('~^\s*INSERT\sIGNORE~i', $db_string) != 0) + { + $db_string = preg_replace('~^\s*INSERT\sIGNORE~i', 'INSERT', $db_string); + if ($smcFunc['db_support_ignore']) + { + //pg style "INSERT INTO.... ON CONFLICT DO NOTHING" + $db_string = $db_string . ' ON CONFLICT DO NOTHING'; + } + else + { + // Don't error on multi-insert. + $db_values['db_error_skip'] = true; + } + } + + //fix double quotes + if ($identifier == 'insert_into_log_messages_fulltext') + $db_string = str_replace('"', "'", $db_string); + + $return = $smcFunc['db_query']('', $db_string, + $db_values, $connection + ); + + return $return; +} + +/** + * Highly specific function, to create the custom word index table. + * + * @param string $size The column size type (int, mediumint (8), etc.). Not used here. + */ +function smf_db_create_word_search($size) +{ + global $smcFunc; + + $size = 'int'; + + $smcFunc['db_query']('', ' + CREATE TABLE {db_prefix}log_search_words ( + id_word {raw:size} NOT NULL default {string:string_zero}, + id_msg int NOT NULL default {string:string_zero}, + PRIMARY KEY (id_word, id_msg) + )', + array( + 'size' => $size, + 'string_zero' => '0', + ) + ); +} + +/** + * Return the language for the textsearch index + */ +function smf_db_search_language() +{ + global $smcFunc, $modSettings; + + $language_ftx = 'english'; + + if (!empty($modSettings['search_language'])) + $language_ftx = $modSettings['search_language']; + else + { + $request = $smcFunc['db_query']('', ' + SELECT cfgname FROM pg_ts_config WHERE oid = current_setting({string:default_language})::regconfig', + array( + 'default_language' => 'default_text_search_config' + ) + ); + + if ($request !== false && $smcFunc['db_num_rows']($request) == 1) + { + $row = $smcFunc['db_fetch_assoc']($request); + $language_ftx = $row['cfgname']; + + $smcFunc['db_insert']('replace', + '{db_prefix}settings', + array('variable' => 'string', 'value' => 'string'), + array('search_language', $language_ftx), + array('variable') + ); + } + } + + return $language_ftx; +} + +?> \ No newline at end of file diff --git a/Sources/Display.php b/Sources/Display.php new file mode 100644 index 0000000..83ba283 --- /dev/null +++ b/Sources/Display.php @@ -0,0 +1,1799 @@ + true, 'order_pos' => 450), 'smf_attachments'); + + // Not only does a prefetch make things slower for the server, but it makes it impossible to know if they read it. + if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch') + { + ob_end_clean(); + send_http_status(403, 'Prefetch Forbidden'); + die; + } + + // How much are we sticking on each page? + $context['messages_per_page'] = empty($modSettings['disableCustomPerPage']) && !empty($options['messages_per_page']) ? $options['messages_per_page'] : $modSettings['defaultMaxMessages']; + + // Let's do some work on what to search index. + if (count($_GET) > 2) + foreach ($_GET as $k => $v) + { + if (!in_array($k, array('topic', 'board', 'start', session_name()))) + $context['robot_no_index'] = true; + } + + if (!empty($_REQUEST['start']) && (!is_numeric($_REQUEST['start']) || $_REQUEST['start'] % $context['messages_per_page'] != 0)) + $context['robot_no_index'] = true; + + // Find the previous or next topic. Make a fuss if there are no more. + if (isset($_REQUEST['prev_next']) && ($_REQUEST['prev_next'] == 'prev' || $_REQUEST['prev_next'] == 'next')) + { + // No use in calculating the next topic if there's only one. + if ($board_info['num_topics'] > 1) + { + // Just prepare some variables that are used in the query. + $gt_lt = $_REQUEST['prev_next'] == 'prev' ? '>' : '<'; + $order = $_REQUEST['prev_next'] == 'prev' ? '' : ' DESC'; + + $request = $smcFunc['db_query']('', ' + SELECT t2.id_topic + FROM {db_prefix}topics AS t + INNER JOIN {db_prefix}topics AS t2 ON ( + (t2.id_last_msg ' . $gt_lt . ' t.id_last_msg AND t2.is_sticky ' . $gt_lt . '= t.is_sticky) OR t2.is_sticky ' . $gt_lt . ' t.is_sticky) + WHERE t.id_topic = {int:current_topic} + AND t2.id_board = {int:current_board}' . (!$modSettings['postmod_active'] || allowedTo('approve_posts') ? '' : ' + AND (t2.approved = {int:is_approved} OR (t2.id_member_started != {int:id_member_started} AND t2.id_member_started = {int:current_member}))') . ' + ORDER BY t2.is_sticky' . $order . ', t2.id_last_msg' . $order . ' + LIMIT 1', + array( + 'current_board' => $board, + 'current_member' => $user_info['id'], + 'current_topic' => $topic, + 'is_approved' => 1, + 'id_member_started' => 0, + ) + ); + + // No more left. + if ($smcFunc['db_num_rows']($request) == 0) + { + $smcFunc['db_free_result']($request); + + // Roll over - if we're going prev, get the last - otherwise the first. + $request = $smcFunc['db_query']('', ' + SELECT id_topic + FROM {db_prefix}topics + WHERE id_board = {int:current_board}' . (!$modSettings['postmod_active'] || allowedTo('approve_posts') ? '' : ' + AND (approved = {int:is_approved} OR (id_member_started != {int:id_member_started} AND id_member_started = {int:current_member}))') . ' + ORDER BY is_sticky' . $order . ', id_last_msg' . $order . ' + LIMIT 1', + array( + 'current_board' => $board, + 'current_member' => $user_info['id'], + 'is_approved' => 1, + 'id_member_started' => 0, + ) + ); + } + + // Now you can be sure $topic is the id_topic to view. + list ($topic) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + $context['current_topic'] = $topic; + } + + // Go to the newest message on this topic. + $_REQUEST['start'] = 'new'; + } + + // Add 1 to the number of views of this topic (except for robots). + if (!$user_info['possibly_robot'] && (empty($_SESSION['last_read_topic']) || $_SESSION['last_read_topic'] != $topic)) + { + $smcFunc['db_query']('', ' + UPDATE {db_prefix}topics + SET num_views = num_views + 1 + WHERE id_topic = {int:current_topic}', + array( + 'current_topic' => $topic, + ) + ); + + $_SESSION['last_read_topic'] = $topic; + } + + $topic_parameters = array( + 'current_member' => $user_info['id'], + 'current_topic' => $topic, + 'current_board' => $board, + ); + $topic_selects = array(); + $topic_tables = array(); + $context['topicinfo'] = array(); + call_integration_hook('integrate_display_topic', array(&$topic_selects, &$topic_tables, &$topic_parameters)); + + // @todo Why isn't this cached? + // @todo if we get id_board in this query and cache it, we can save a query on posting + // Get all the important topic info. + $request = $smcFunc['db_query']('', ' + SELECT + t.num_replies, t.num_views, t.locked, ms.subject, t.is_sticky, t.id_poll, + t.id_member_started, t.id_first_msg, t.id_last_msg, t.approved, t.unapproved_posts, t.id_redirect_topic, + COALESCE(mem.real_name, ms.poster_name) AS topic_started_name, ms.poster_time AS topic_started_time, + ' . ($user_info['is_guest'] ? 't.id_last_msg + 1' : 'COALESCE(lt.id_msg, lmr.id_msg, -1) + 1') . ' AS new_from + ' . (!empty($board_info['recycle']) ? ', id_previous_board, id_previous_topic' : '') . ' + ' . (!empty($topic_selects) ? (', ' . implode(', ', $topic_selects)) : '') . ' + ' . (!$user_info['is_guest'] ? ', COALESCE(lt.unwatched, 0) as unwatched' : '') . ' + FROM {db_prefix}topics AS t + INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg) + LEFT JOIN {db_prefix}members AS mem on (mem.id_member = t.id_member_started)' . ($user_info['is_guest'] ? '' : ' + LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = {int:current_topic} AND lt.id_member = {int:current_member}) + LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = {int:current_board} AND lmr.id_member = {int:current_member})') . ' + ' . (!empty($topic_tables) ? implode("\n\t", $topic_tables) : '') . ' + WHERE t.id_topic = {int:current_topic} + LIMIT 1', + $topic_parameters + ); + + if ($smcFunc['db_num_rows']($request) == 0) + fatal_lang_error('not_a_topic', false, 404); + $context['topicinfo'] = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + // Is this a moved or merged topic that we are redirecting to? + if (!empty($context['topicinfo']['id_redirect_topic'])) + { + // Mark this as read... + if (!$user_info['is_guest'] && $context['topicinfo']['new_from'] != $context['topicinfo']['id_first_msg']) + { + // Mark this as read first + $smcFunc['db_insert']($context['topicinfo']['new_from'] == 0 ? 'ignore' : 'replace', + '{db_prefix}log_topics', + array( + 'id_member' => 'int', 'id_topic' => 'int', 'id_msg' => 'int', 'unwatched' => 'int', + ), + array( + $user_info['id'], $topic, $context['topicinfo']['id_first_msg'], $context['topicinfo']['unwatched'], + ), + array('id_member', 'id_topic') + ); + } + redirectexit('topic=' . $context['topicinfo']['id_redirect_topic'] . '.0', false, true); + } + + $can_approve_posts = allowedTo('approve_posts'); + + $context['real_num_replies'] = $context['num_replies'] = $context['topicinfo']['num_replies']; + $context['topic_started_time'] = timeformat($context['topicinfo']['topic_started_time']); + $context['topic_started_timestamp'] = $context['topicinfo']['topic_started_time']; + $context['topic_poster_name'] = $context['topicinfo']['topic_started_name']; + $context['topic_first_message'] = $context['topicinfo']['id_first_msg']; + $context['topic_last_message'] = $context['topicinfo']['id_last_msg']; + $context['topic_unwatched'] = isset($context['topicinfo']['unwatched']) ? $context['topicinfo']['unwatched'] : 0; + + // Add up unapproved replies to get real number of replies... + if ($modSettings['postmod_active'] && $can_approve_posts) + $context['real_num_replies'] += $context['topicinfo']['unapproved_posts'] - ($context['topicinfo']['approved'] ? 0 : 1); + + // If this topic has unapproved posts, we need to work out how many posts the user can see, for page indexing. + if ($modSettings['postmod_active'] && $context['topicinfo']['unapproved_posts'] && !$user_info['is_guest'] && !$can_approve_posts) + { + $request = $smcFunc['db_query']('', ' + SELECT COUNT(id_member) AS my_unapproved_posts + FROM {db_prefix}messages + WHERE id_topic = {int:current_topic} + AND id_member = {int:current_member} + AND approved = 0', + array( + 'current_topic' => $topic, + 'current_member' => $user_info['id'], + ) + ); + list ($myUnapprovedPosts) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + $context['total_visible_posts'] = $context['num_replies'] + $myUnapprovedPosts + ($context['topicinfo']['approved'] ? 1 : 0); + } + elseif ($user_info['is_guest']) + $context['total_visible_posts'] = $context['num_replies'] + ($context['topicinfo']['approved'] ? 1 : 0); + else + $context['total_visible_posts'] = $context['num_replies'] + $context['topicinfo']['unapproved_posts'] + ($context['topicinfo']['approved'] ? 1 : 0); + + // The start isn't a number; it's information about what to do, where to go. + if (!is_numeric($_REQUEST['start'])) + { + // Redirect to the page and post with new messages, originally by Omar Bazavilvazo. + if ($_REQUEST['start'] == 'new') + { + // Guests automatically go to the last post. + if ($user_info['is_guest']) + { + $context['start_from'] = $context['total_visible_posts'] - 1; + $_REQUEST['start'] = empty($options['view_newest_first']) ? $context['start_from'] : 0; + } + else + { + // Find the earliest unread message in the topic. (the use of topics here is just for both tables.) + $request = $smcFunc['db_query']('', ' + SELECT COALESCE(lt.id_msg, lmr.id_msg, -1) + 1 AS new_from + FROM {db_prefix}topics AS t + LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = {int:current_topic} AND lt.id_member = {int:current_member}) + LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = {int:current_board} AND lmr.id_member = {int:current_member}) + WHERE t.id_topic = {int:current_topic} + LIMIT 1', + array( + 'current_board' => $board, + 'current_member' => $user_info['id'], + 'current_topic' => $topic, + ) + ); + list ($new_from) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // Fall through to the next if statement. + $_REQUEST['start'] = 'msg' . $new_from; + } + } + + // Start from a certain time index, not a message. + if (substr($_REQUEST['start'], 0, 4) == 'from') + { + $timestamp = (int) substr($_REQUEST['start'], 4); + if ($timestamp === 0) + $_REQUEST['start'] = 0; + else + { + // Find the number of messages posted before said time... + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}messages + WHERE poster_time < {int:timestamp} + AND id_topic = {int:current_topic}' . ($modSettings['postmod_active'] && $context['topicinfo']['unapproved_posts'] && !allowedTo('approve_posts') ? ' + AND (approved = {int:is_approved}' . ($user_info['is_guest'] ? '' : ' OR id_member = {int:current_member}') . ')' : ''), + array( + 'current_topic' => $topic, + 'current_member' => $user_info['id'], + 'is_approved' => 1, + 'timestamp' => $timestamp, + ) + ); + list ($context['start_from']) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // Handle view_newest_first options, and get the correct start value. + $_REQUEST['start'] = empty($options['view_newest_first']) ? $context['start_from'] : $context['total_visible_posts'] - $context['start_from'] - 1; + } + } + + // Link to a message... + elseif (substr($_REQUEST['start'], 0, 3) == 'msg') + { + $virtual_msg = (int) substr($_REQUEST['start'], 3); + if (!$context['topicinfo']['unapproved_posts'] && $virtual_msg >= $context['topicinfo']['id_last_msg']) + $context['start_from'] = $context['total_visible_posts'] - 1; + elseif (!$context['topicinfo']['unapproved_posts'] && $virtual_msg <= $context['topicinfo']['id_first_msg']) + $context['start_from'] = 0; + else + { + // Find the start value for that message...... + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}messages + WHERE id_msg < {int:virtual_msg} + AND id_topic = {int:current_topic}' . ($modSettings['postmod_active'] && $context['topicinfo']['unapproved_posts'] && !allowedTo('approve_posts') ? ' + AND (approved = {int:is_approved}' . ($user_info['is_guest'] ? '' : ' OR id_member = {int:current_member}') . ')' : ''), + array( + 'current_member' => $user_info['id'], + 'current_topic' => $topic, + 'virtual_msg' => $virtual_msg, + 'is_approved' => 1, + 'no_member' => 0, + ) + ); + list ($context['start_from']) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + } + + // We need to reverse the start as well in this case. + $_REQUEST['start'] = empty($options['view_newest_first']) ? $context['start_from'] : $context['total_visible_posts'] - $context['start_from'] - 1; + } + } + + // Create a previous next string if the selected theme has it as a selected option. + $context['previous_next'] = $modSettings['enablePreviousNext'] ? '' . $txt['previous_next_back'] . ' - ' . $txt['previous_next_forward'] . '' : ''; + + // Do we need to show the visual verification image? + $context['require_verification'] = !$user_info['is_mod'] && !$user_info['is_admin'] && !empty($modSettings['posts_require_captcha']) && ($user_info['posts'] < $modSettings['posts_require_captcha'] || ($user_info['is_guest'] && $modSettings['posts_require_captcha'] == -1)); + if ($context['require_verification']) + { + require_once($sourcedir . '/Subs-Editor.php'); + $verificationOptions = array( + 'id' => 'post', + ); + $context['require_verification'] = create_control_verification($verificationOptions); + $context['visual_verification_id'] = $verificationOptions['id']; + } + + // Are we showing signatures - or disabled fields? + $context['signature_enabled'] = substr($modSettings['signature_settings'], 0, 1) == 1; + $context['disabled_fields'] = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : array(); + + // Prevent signature images from going outside the box. + if ($context['signature_enabled']) + { + list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']); + $sig_limits = explode(',', $sig_limits); + + if (!empty($sig_limits[5]) || !empty($sig_limits[6])) + addInlineCss(' + .signature img { ' . (!empty($sig_limits[5]) ? 'max-width: ' . (int) $sig_limits[5] . 'px; ' : '') . (!empty($sig_limits[6]) ? 'max-height: ' . (int) $sig_limits[6] . 'px; ' : '') . '}'); + } + + // Censor the title... + censorText($context['topicinfo']['subject']); + $context['page_title'] = $context['topicinfo']['subject']; + + // Default this topic to not marked for notifications... of course... + $context['is_marked_notify'] = false; + + // Did we report a post to a moderator just now? + $context['report_sent'] = isset($_GET['reportsent']); + + // Let's get nosey, who is viewing this topic? + if (!empty($settings['display_who_viewing'])) + { + // Start out with no one at all viewing it. + $context['view_members'] = array(); + $context['view_members_list'] = array(); + $context['view_num_hidden'] = 0; + + // Search for members who have this topic set in their GET data. + $request = $smcFunc['db_query']('', ' + SELECT + lo.id_member, lo.log_time, mem.real_name, mem.member_name, mem.show_online, + mg.online_color, mg.id_group, mg.group_name + FROM {db_prefix}log_online AS lo + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lo.id_member) + LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = CASE WHEN mem.id_group = {int:reg_id_group} THEN mem.id_post_group ELSE mem.id_group END) + WHERE INSTR(lo.url, {string:in_url_string}) > 0 OR lo.session = {string:session}', + array( + 'reg_id_group' => 0, + 'in_url_string' => '"topic":' . $topic, + 'session' => $user_info['is_guest'] ? 'ip' . $user_info['ip'] : session_id(), + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (empty($row['id_member'])) + continue; + + if (!empty($row['online_color'])) + $link = '' . $row['real_name'] . ''; + else + $link = '' . $row['real_name'] . ''; + + $is_buddy = in_array($row['id_member'], $user_info['buddies']); + if ($is_buddy) + $link = '' . $link . ''; + + // Add them both to the list and to the more detailed list. + if (!empty($row['show_online']) || allowedTo('moderate_forum')) + $context['view_members_list'][$row['log_time'] . $row['member_name']] = empty($row['show_online']) ? '' . $link . '' : $link; + $context['view_members'][$row['log_time'] . $row['member_name']] = array( + 'id' => $row['id_member'], + 'username' => $row['member_name'], + 'name' => $row['real_name'], + 'group' => $row['id_group'], + 'href' => $scripturl . '?action=profile;u=' . $row['id_member'], + 'link' => $link, + 'is_buddy' => $is_buddy, + 'hidden' => empty($row['show_online']), + ); + + if (empty($row['show_online'])) + $context['view_num_hidden']++; + } + + // The number of guests is equal to the rows minus the ones we actually used ;). + $context['view_num_guests'] = $smcFunc['db_num_rows']($request) - count($context['view_members']); + $smcFunc['db_free_result']($request); + + // Sort the list. + krsort($context['view_members']); + krsort($context['view_members_list']); + } + + // If all is set, but not allowed... just unset it. + $can_show_all = !empty($modSettings['enableAllMessages']) && $context['total_visible_posts'] > $context['messages_per_page'] && $context['total_visible_posts'] < $modSettings['enableAllMessages']; + if (isset($_REQUEST['all']) && !$can_show_all) + unset($_REQUEST['all']); + // Otherwise, it must be allowed... so pretend start was -1. + elseif (isset($_REQUEST['all'])) + $_REQUEST['start'] = -1; + + // Construct the page index, allowing for the .START method... + $context['page_index'] = constructPageIndex($scripturl . '?topic=' . $topic . '.%1$d', $_REQUEST['start'], $context['total_visible_posts'], $context['messages_per_page'], true); + $context['start'] = $_REQUEST['start']; + + // This is information about which page is current, and which page we're on - in case you don't like the constructed page index. (again, wireles..) + $context['page_info'] = array( + 'current_page' => $_REQUEST['start'] / $context['messages_per_page'] + 1, + 'num_pages' => floor(($context['total_visible_posts'] - 1) / $context['messages_per_page']) + 1, + ); + + // Figure out all the link to the next/prev/first/last/etc. + if (!($can_show_all && isset($_REQUEST['all']))) + { + $context['links'] = array( + 'first' => $_REQUEST['start'] >= $context['messages_per_page'] ? $scripturl . '?topic=' . $topic . '.0' : '', + 'prev' => $_REQUEST['start'] >= $context['messages_per_page'] ? $scripturl . '?topic=' . $topic . '.' . ($_REQUEST['start'] - $context['messages_per_page']) : '', + 'next' => $_REQUEST['start'] + $context['messages_per_page'] < $context['total_visible_posts'] ? $scripturl . '?topic=' . $topic . '.' . ($_REQUEST['start'] + $context['messages_per_page']) : '', + 'last' => $_REQUEST['start'] + $context['messages_per_page'] < $context['total_visible_posts'] ? $scripturl . '?topic=' . $topic . '.' . (floor($context['total_visible_posts'] / $context['messages_per_page']) * $context['messages_per_page']) : '', + 'up' => $scripturl . '?board=' . $board . '.0' + ); + } + + // If they are viewing all the posts, show all the posts, otherwise limit the number. + if ($can_show_all) + { + if (isset($_REQUEST['all'])) + { + // No limit! (actually, there is a limit, but...) + $context['messages_per_page'] = -1; + $context['page_index'] .= sprintf(strtr($settings['page_index']['current_page'], array('%1$d' => '%1$s')), $txt['all']); + + // Set start back to 0... + $_REQUEST['start'] = 0; + } + // They aren't using it, but the *option* is there, at least. + else + $context['page_index'] .= sprintf(strtr($settings['page_index']['page'], array('{URL}' => $scripturl . '?topic=' . $topic . '.0;all')), '', $txt['all']); + } + + // Build the link tree. + $context['linktree'][] = array( + 'url' => $scripturl . '?topic=' . $topic . '.0', + 'name' => $context['topicinfo']['subject'], + ); + + // Build a list of this board's moderators. + $context['moderators'] = &$board_info['moderators']; + $context['moderator_groups'] = &$board_info['moderator_groups']; + $context['link_moderators'] = array(); + if (!empty($board_info['moderators'])) + { + // Add a link for each moderator... + foreach ($board_info['moderators'] as $mod) + $context['link_moderators'][] = '' . $mod['name'] . ''; + } + if (!empty($board_info['moderator_groups'])) + { + // Add a link for each moderator group as well... + foreach ($board_info['moderator_groups'] as $mod_group) + $context['link_moderators'][] = '' . $mod_group['name'] . ''; + } + + if (!empty($context['link_moderators'])) + { + // And show it after the board's name. + $context['linktree'][count($context['linktree']) - 2]['extra_after'] = '(' . (count($context['link_moderators']) == 1 ? $txt['moderator'] : $txt['moderators']) . ': ' . implode(', ', $context['link_moderators']) . ')'; + } + + // Information about the current topic... + $context['is_locked'] = $context['topicinfo']['locked']; + $context['is_sticky'] = $context['topicinfo']['is_sticky']; + $context['is_approved'] = $context['topicinfo']['approved']; + $context['is_poll'] = $context['topicinfo']['id_poll'] > 0 && $modSettings['pollMode'] == '1' && allowedTo('poll_view'); + + // Did this user start the topic or not? + $context['user']['started'] = $user_info['id'] == $context['topicinfo']['id_member_started'] && !$user_info['is_guest']; + $context['topic_starter_id'] = $context['topicinfo']['id_member_started']; + + // Set the topic's information for the template. + $context['subject'] = $context['topicinfo']['subject']; + $context['num_views'] = comma_format($context['topicinfo']['num_views']); + $context['num_views_text'] = $context['num_views'] == 1 ? $txt['read_one_time'] : sprintf($txt['read_many_times'], $context['num_views']); + $context['mark_unread_time'] = !empty($virtual_msg) ? $virtual_msg : $context['topicinfo']['new_from']; + + // Set a canonical URL for this page. + $context['canonical_url'] = $scripturl . '?topic=' . $topic . '.' . ($can_show_all ? '0;all' : $context['start']); + + // For quick reply we need a response prefix in the default forum language. + if (!isset($context['response_prefix']) && !($context['response_prefix'] = cache_get_data('response_prefix', 600))) + { + if ($language === $user_info['language']) + $context['response_prefix'] = $txt['response_prefix']; + else + { + loadLanguage('index', $language, false); + $context['response_prefix'] = $txt['response_prefix']; + loadLanguage('index'); + } + cache_put_data('response_prefix', $context['response_prefix'], 600); + } + + // If we want to show event information in the topic, prepare the data. + if (allowedTo('calendar_view') && !empty($modSettings['cal_showInTopic']) && !empty($modSettings['cal_enabled'])) + { + require_once($sourcedir . '/Subs-Calendar.php'); + + // Any calendar information for this topic? + $request = $smcFunc['db_query']('', ' + SELECT cal.id_event, cal.start_date, cal.end_date, cal.title, cal.id_member, mem.real_name, cal.start_time, cal.end_time, cal.timezone, cal.location + FROM {db_prefix}calendar AS cal + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = cal.id_member) + WHERE cal.id_topic = {int:current_topic} + ORDER BY start_date', + array( + 'current_topic' => $topic, + ) + ); + $context['linked_calendar_events'] = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // Get the various time and date properties for this event + list($start, $end, $allday, $span, $tz, $tz_abbrev) = buildEventDatetimes($row); + + // Sanity check + if (!empty($start['error_count']) || !empty($start['warning_count']) || !empty($end['error_count']) || !empty($end['warning_count'])) + continue; + + $linked_calendar_event = array( + 'id' => $row['id_event'], + 'title' => $row['title'], + 'can_edit' => allowedTo('calendar_edit_any') || ($row['id_member'] == $user_info['id'] && allowedTo('calendar_edit_own')), + 'modify_href' => $scripturl . '?action=post;msg=' . $context['topicinfo']['id_first_msg'] . ';topic=' . $topic . '.0;calendar;eventid=' . $row['id_event'] . ';' . $context['session_var'] . '=' . $context['session_id'], + 'can_export' => allowedTo('calendar_edit_any') || ($row['id_member'] == $user_info['id'] && allowedTo('calendar_edit_own')), + 'export_href' => $scripturl . '?action=calendar;sa=ical;eventid=' . $row['id_event'] . ';' . $context['session_var'] . '=' . $context['session_id'], + 'year' => $start['year'], + 'month' => $start['month'], + 'day' => $start['day'], + 'hour' => !$allday ? $start['hour'] : null, + 'minute' => !$allday ? $start['minute'] : null, + 'second' => !$allday ? $start['second'] : null, + 'start_date' => $row['start_date'], + 'start_date_local' => $start['date_local'], + 'start_date_orig' => $start['date_orig'], + 'start_time' => !$allday ? $row['start_time'] : null, + 'start_time_local' => !$allday ? $start['time_local'] : null, + 'start_time_orig' => !$allday ? $start['time_orig'] : null, + 'start_timestamp' => $start['timestamp'], + 'start_iso_gmdate' => $start['iso_gmdate'], + 'end_year' => $end['year'], + 'end_month' => $end['month'], + 'end_day' => $end['day'], + 'end_hour' => !$allday ? $end['hour'] : null, + 'end_minute' => !$allday ? $end['minute'] : null, + 'end_second' => !$allday ? $end['second'] : null, + 'end_date' => $row['end_date'], + 'end_date_local' => $end['date_local'], + 'end_date_orig' => $end['date_orig'], + 'end_time' => !$allday ? $row['end_time'] : null, + 'end_time_local' => !$allday ? $end['time_local'] : null, + 'end_time_orig' => !$allday ? $end['time_orig'] : null, + 'end_timestamp' => $end['timestamp'], + 'end_iso_gmdate' => $end['iso_gmdate'], + 'allday' => $allday, + 'tz' => !$allday ? $tz : null, + 'tz_abbrev' => !$allday ? $tz_abbrev : null, + 'span' => $span, + 'location' => $row['location'], + 'is_last' => false + ); + + $context['linked_calendar_events'][] = $linked_calendar_event; + } + $smcFunc['db_free_result']($request); + + if (!empty($context['linked_calendar_events'])) + $context['linked_calendar_events'][count($context['linked_calendar_events']) - 1]['is_last'] = true; + } + + // Create the poll info if it exists. + if ($context['is_poll']) + { + // Get the question and if it's locked. + $request = $smcFunc['db_query']('', ' + SELECT + p.question, p.voting_locked, p.hide_results, p.expire_time, p.max_votes, p.change_vote, + p.guest_vote, p.id_member, COALESCE(mem.real_name, p.poster_name) AS poster_name, p.num_guest_voters, p.reset_poll + FROM {db_prefix}polls AS p + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = p.id_member) + WHERE p.id_poll = {int:id_poll} + LIMIT 1', + array( + 'id_poll' => $context['topicinfo']['id_poll'], + ) + ); + $pollinfo = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + } + + // Create the poll info if it exists and is valid. + if ($context['is_poll'] && empty($pollinfo)) + $context['is_poll'] = false; + elseif ($context['is_poll']) + { + $request = $smcFunc['db_query']('', ' + SELECT COUNT(DISTINCT id_member) AS total + FROM {db_prefix}log_polls + WHERE id_poll = {int:id_poll} + AND id_member != {int:not_guest}', + array( + 'id_poll' => $context['topicinfo']['id_poll'], + 'not_guest' => 0, + ) + ); + list ($pollinfo['total']) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // Total voters needs to include guest voters + $pollinfo['total'] += $pollinfo['num_guest_voters']; + + // Get all the options, and calculate the total votes. + $request = $smcFunc['db_query']('', ' + SELECT pc.id_choice, pc.label, pc.votes, COALESCE(lp.id_choice, -1) AS voted_this + FROM {db_prefix}poll_choices AS pc + LEFT JOIN {db_prefix}log_polls AS lp ON (lp.id_choice = pc.id_choice AND lp.id_poll = {int:id_poll} AND lp.id_member = {int:current_member} AND lp.id_member != {int:not_guest}) + WHERE pc.id_poll = {int:id_poll} + ORDER BY pc.id_choice', + array( + 'current_member' => $user_info['id'], + 'id_poll' => $context['topicinfo']['id_poll'], + 'not_guest' => 0, + ) + ); + $pollOptions = array(); + $realtotal = 0; + $pollinfo['has_voted'] = false; + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + censorText($row['label']); + $pollOptions[$row['id_choice']] = $row; + $realtotal += $row['votes']; + $pollinfo['has_voted'] |= $row['voted_this'] != -1; + } + $smcFunc['db_free_result']($request); + + // Got we multi choice? + if ($pollinfo['max_votes'] > 1) + $realtotal = $pollinfo['total']; + + // If this is a guest we need to do our best to work out if they have voted, and what they voted for. + if ($user_info['is_guest'] && $pollinfo['guest_vote'] && allowedTo('poll_vote')) + { + if (!empty($_COOKIE['guest_poll_vote']) && preg_match('~^[0-9,;]+$~', $_COOKIE['guest_poll_vote']) && strpos($_COOKIE['guest_poll_vote'], ';' . $context['topicinfo']['id_poll'] . ',') !== false) + { + // ;id,timestamp,[vote,vote...]; etc + $guestinfo = explode(';', $_COOKIE['guest_poll_vote']); + // Find the poll we're after. + foreach ($guestinfo as $i => $guestvoted) + { + $guestvoted = explode(',', $guestvoted); + if ($guestvoted[0] == $context['topicinfo']['id_poll']) + break; + } + // Has the poll been reset since guest voted? + if ($pollinfo['reset_poll'] > $guestvoted[1]) + { + // Remove the poll info from the cookie to allow guest to vote again + unset($guestinfo[$i]); + if (!empty($guestinfo)) + $_COOKIE['guest_poll_vote'] = ';' . implode(';', $guestinfo); + else + unset($_COOKIE['guest_poll_vote']); + } + else + { + // What did they vote for? + unset($guestvoted[0], $guestvoted[1]); + foreach ($pollOptions as $choice => $details) + { + $pollOptions[$choice]['voted_this'] = in_array($choice, $guestvoted) ? 1 : -1; + $pollinfo['has_voted'] |= $pollOptions[$choice]['voted_this'] != -1; + } + unset($choice, $details, $guestvoted); + } + unset($guestinfo, $guestvoted, $i); + } + } + + // Set up the basic poll information. + $context['poll'] = array( + 'id' => $context['topicinfo']['id_poll'], + 'image' => 'normal_' . (empty($pollinfo['voting_locked']) ? 'poll' : 'locked_poll'), + 'question' => parse_bbc($pollinfo['question']), + 'total_votes' => $pollinfo['total'], + 'change_vote' => !empty($pollinfo['change_vote']), + 'is_locked' => !empty($pollinfo['voting_locked']), + 'options' => array(), + 'lock' => allowedTo('poll_lock_any') || ($context['user']['started'] && allowedTo('poll_lock_own')), + 'edit' => allowedTo('poll_edit_any') || ($context['user']['started'] && allowedTo('poll_edit_own')), + 'remove' => allowedTo('poll_remove_any') || ($context['user']['started'] && allowedTo('poll_remove_own')), + 'allowed_warning' => $pollinfo['max_votes'] > 1 ? sprintf($txt['poll_options_limit'], min(count($pollOptions), $pollinfo['max_votes'])) : '', + 'is_expired' => !empty($pollinfo['expire_time']) && $pollinfo['expire_time'] < time(), + 'expire_time' => !empty($pollinfo['expire_time']) ? timeformat($pollinfo['expire_time']) : 0, + 'has_voted' => !empty($pollinfo['has_voted']), + 'starter' => array( + 'id' => $pollinfo['id_member'], + 'name' => $pollinfo['poster_name'], + 'href' => $pollinfo['id_member'] == 0 ? '' : $scripturl . '?action=profile;u=' . $pollinfo['id_member'], + 'link' => $pollinfo['id_member'] == 0 ? $pollinfo['poster_name'] : '' . $pollinfo['poster_name'] . '' + ) + ); + + // Make the lock, edit and remove permissions defined above more directly accessible. + $context['allow_lock_poll'] = $context['poll']['lock']; + $context['allow_edit_poll'] = $context['poll']['edit']; + $context['can_remove_poll'] = $context['poll']['remove']; + + // You're allowed to vote if: + // 1. the poll did not expire, and + // 2. you're either not a guest OR guest voting is enabled... and + // 3. you're not trying to view the results, and + // 4. the poll is not locked, and + // 5. you have the proper permissions, and + // 6. you haven't already voted before. + $context['allow_vote'] = !$context['poll']['is_expired'] && (!$user_info['is_guest'] || ($pollinfo['guest_vote'] && allowedTo('poll_vote'))) && empty($pollinfo['voting_locked']) && allowedTo('poll_vote') && !$context['poll']['has_voted']; + + // You're allowed to view the results if: + // 1. you're just a super-nice-guy, or + // 2. anyone can see them (hide_results == 0), or + // 3. you can see them after you voted (hide_results == 1), or + // 4. you've waited long enough for the poll to expire. (whether hide_results is 1 or 2.) + $context['allow_results_view'] = allowedTo('moderate_board') || $pollinfo['hide_results'] == 0 || ($pollinfo['hide_results'] == 1 && $context['poll']['has_voted']) || $context['poll']['is_expired']; + + // Show the results if: + // 1. You're allowed to see them (see above), and + // 2. $_REQUEST['viewresults'] or $_REQUEST['viewResults'] is set + $context['poll']['show_results'] = $context['allow_results_view'] && (isset($_REQUEST['viewresults']) || isset($_REQUEST['viewResults'])); + + // Show the button if: + // 1. You can vote in the poll (see above), and + // 2. Results are visible to everyone (hidden = 0), and + // 3. You aren't already viewing the results + $context['show_view_results_button'] = $context['allow_vote'] && $context['allow_results_view'] && !$context['poll']['show_results']; + + // You're allowed to change your vote if: + // 1. the poll did not expire, and + // 2. you're not a guest... and + // 3. the poll is not locked, and + // 4. you have the proper permissions, and + // 5. you have already voted, and + // 6. the poll creator has said you can! + $context['allow_change_vote'] = !$context['poll']['is_expired'] && !$user_info['is_guest'] && empty($pollinfo['voting_locked']) && allowedTo('poll_vote') && $context['poll']['has_voted'] && $context['poll']['change_vote']; + + // You're allowed to return to voting options if: + // 1. you are (still) allowed to vote. + // 2. you are currently seeing the results. + $context['allow_return_vote'] = $context['allow_vote'] && $context['poll']['show_results']; + + // Calculate the percentages and bar lengths... + $divisor = $realtotal == 0 ? 1 : $realtotal; + + // Determine if a decimal point is needed in order for the options to add to 100%. + $precision = $realtotal == 100 ? 0 : 1; + + // Now look through each option, and... + foreach ($pollOptions as $i => $option) + { + // First calculate the percentage, and then the width of the bar... + $bar = round(($option['votes'] * 100) / $divisor, $precision); + $barWide = $bar == 0 ? 1 : floor(($bar * 8) / 3); + + // Now add it to the poll's contextual theme data. + $context['poll']['options'][$i] = array( + 'id' => 'options-' . $i, + 'percent' => $bar, + 'votes' => $option['votes'], + 'voted_this' => $option['voted_this'] != -1, + 'bar_ndt' => $bar > 0 ? '
' : '', + 'bar_width' => $barWide, + 'option' => parse_bbc($option['label']), + 'vote_button' => '' + ); + } + + // Build the poll moderation button array. + $context['poll_buttons'] = array(); + + if ($context['allow_return_vote']) + $context['poll_buttons']['vote'] = array('text' => 'poll_return_vote', 'image' => 'poll_options.png', 'url' => $scripturl . '?topic=' . $context['current_topic'] . '.' . $context['start']); + + if ($context['show_view_results_button']) + $context['poll_buttons']['results'] = array('text' => 'poll_results', 'image' => 'poll_results.png', 'url' => $scripturl . '?topic=' . $context['current_topic'] . '.' . $context['start'] . ';viewresults'); + + if ($context['allow_change_vote']) + $context['poll_buttons']['change_vote'] = array('text' => 'poll_change_vote', 'image' => 'poll_change_vote.png', 'url' => $scripturl . '?action=vote;topic=' . $context['current_topic'] . '.' . $context['start'] . ';poll=' . $context['poll']['id'] . ';' . $context['session_var'] . '=' . $context['session_id']); + + if ($context['allow_lock_poll']) + $context['poll_buttons']['lock'] = array('text' => (!$context['poll']['is_locked'] ? 'poll_lock' : 'poll_unlock'), 'image' => 'poll_lock.png', 'url' => $scripturl . '?action=lockvoting;topic=' . $context['current_topic'] . '.' . $context['start'] . ';' . $context['session_var'] . '=' . $context['session_id']); + + if ($context['allow_edit_poll']) + $context['poll_buttons']['edit'] = array('text' => 'poll_edit', 'image' => 'poll_edit.png', 'url' => $scripturl . '?action=editpoll;topic=' . $context['current_topic'] . '.' . $context['start']); + + if ($context['can_remove_poll']) + $context['poll_buttons']['remove_poll'] = array('text' => 'poll_remove', 'image' => 'admin_remove_poll.png', 'custom' => 'data-confirm="' . $txt['poll_remove_warn'] . '"', 'class' => 'you_sure', 'url' => $scripturl . '?action=removepoll;topic=' . $context['current_topic'] . '.' . $context['start'] . ';' . $context['session_var'] . '=' . $context['session_id']); + + // Allow mods to add additional buttons here + call_integration_hook('integrate_poll_buttons'); + } + + $limit = $context['messages_per_page']; + $start = $_REQUEST['start']; + $ascending = empty($options['view_newest_first']); + $firstIndex = 0; + + // Jump to page + // Calculate the fastest way to get the messages! + if ($start >= $context['total_visible_posts'] / 2 && $context['messages_per_page'] != -1) + { + $DBascending = !$ascending; + $limit = $context['total_visible_posts'] <= $start + $limit ? $context['total_visible_posts'] - $start : $limit; + $start = $context['total_visible_posts'] <= $start + $limit ? 0 : $context['total_visible_posts'] - $start - $limit; + $firstIndex = empty($options['view_newest_first']) ? $start - 1 : $limit - 1; + } + else + $DBascending = $ascending; + + // Get each post and poster in this topic. + $request = $smcFunc['db_query']('', ' + SELECT id_msg, id_member, approved + FROM {db_prefix}messages + WHERE id_topic = {int:current_topic}' . (!$modSettings['postmod_active'] || $can_approve_posts ? '' : ' + AND (approved = {int:is_approved}' . ($user_info['is_guest'] ? '' : ' OR id_member = {int:current_member}') . ')') . ' + ORDER BY id_msg ' . ($DBascending ? '' : 'DESC') . ($context['messages_per_page'] == -1 ? '' : ' + LIMIT {int:start}, {int:max}'), + array( + 'current_member' => $user_info['id'], + 'current_topic' => $topic, + 'is_approved' => 1, + 'blank_id_member' => 0, + 'start' => $start, + 'max' => $limit, + ) + ); + + $messages = array(); + $all_posters = array(); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (!empty($row['id_member'])) + $all_posters[$row['id_msg']] = $row['id_member']; + $messages[] = $row['id_msg']; + } + + // Sort the messages into the correct display order + if (!$DBascending) + sort($messages); + + $smcFunc['db_free_result']($request); + $posters = array_unique($all_posters); + + call_integration_hook('integrate_display_message_list', array(&$messages, &$posters)); + + // Guests can't mark topics read or for notifications, just can't sorry. + if (!$user_info['is_guest'] && !empty($messages)) + { + $mark_at_msg = max($messages); + if ($mark_at_msg >= $context['topicinfo']['id_last_msg']) + $mark_at_msg = $modSettings['maxMsgID']; + if ($mark_at_msg >= $context['topicinfo']['new_from']) + { + $smcFunc['db_insert']($context['topicinfo']['new_from'] == 0 ? 'ignore' : 'replace', + '{db_prefix}log_topics', + array( + 'id_member' => 'int', 'id_topic' => 'int', 'id_msg' => 'int', 'unwatched' => 'int', + ), + array( + $user_info['id'], $topic, $mark_at_msg, $context['topicinfo']['unwatched'], + ), + array('id_member', 'id_topic') + ); + } + + // Check for notifications on this topic OR board. + $request = $smcFunc['db_query']('', ' + SELECT sent, id_topic + FROM {db_prefix}log_notify + WHERE (id_topic = {int:current_topic} OR id_board = {int:current_board}) + AND id_member = {int:current_member} + LIMIT 2', + array( + 'current_board' => $board, + 'current_member' => $user_info['id'], + 'current_topic' => $topic, + ) + ); + $do_once = true; + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // Find if this topic is marked for notification... + if (!empty($row['id_topic'])) + $context['is_marked_notify'] = true; + + // Only do this once, but mark the notifications as "not sent yet" for next time. + if (!empty($row['sent']) && $do_once) + { + $smcFunc['db_query']('', ' + UPDATE {db_prefix}log_notify + SET sent = {int:is_not_sent} + WHERE (id_topic = {int:current_topic} OR id_board = {int:current_board}) + AND id_member = {int:current_member}', + array( + 'current_board' => $board, + 'current_member' => $user_info['id'], + 'current_topic' => $topic, + 'is_not_sent' => 0, + ) + ); + $do_once = false; + } + } + + // Have we recently cached the number of new topics in this board, and it's still a lot? + if (isset($_REQUEST['topicseen']) && isset($_SESSION['topicseen_cache'][$board]) && $_SESSION['topicseen_cache'][$board] > 5) + $_SESSION['topicseen_cache'][$board]--; + // Mark board as seen if this is the only new topic. + elseif (isset($_REQUEST['topicseen'])) + { + // Use the mark read tables... and the last visit to figure out if this should be read or not. + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}topics AS t + LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = {int:current_board} AND lb.id_member = {int:current_member}) + LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member}) + WHERE t.id_board = {int:current_board} + AND t.id_last_msg > COALESCE(lb.id_msg, 0) + AND t.id_last_msg > COALESCE(lt.id_msg, 0)' . (empty($_SESSION['id_msg_last_visit']) ? '' : ' + AND t.id_last_msg > {int:id_msg_last_visit}'), + array( + 'current_board' => $board, + 'current_member' => $user_info['id'], + 'id_msg_last_visit' => (int) $_SESSION['id_msg_last_visit'], + ) + ); + list ($numNewTopics) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // If there're no real new topics in this board, mark the board as seen. + if (empty($numNewTopics)) + $_REQUEST['boardseen'] = true; + else + $_SESSION['topicseen_cache'][$board] = $numNewTopics; + } + // Probably one less topic - maybe not, but even if we decrease this too fast it will only make us look more often. + elseif (isset($_SESSION['topicseen_cache'][$board])) + $_SESSION['topicseen_cache'][$board]--; + + // Mark board as seen if we came using last post link from BoardIndex. (or other places...) + if (isset($_REQUEST['boardseen'])) + { + $smcFunc['db_insert']('replace', + '{db_prefix}log_boards', + array('id_msg' => 'int', 'id_member' => 'int', 'id_board' => 'int'), + array($modSettings['maxMsgID'], $user_info['id'], $board), + array('id_member', 'id_board') + ); + } + + // Mark any alerts about this topic or the posts on this page as read. + if (!empty($user_info['alerts'])) + { + $smcFunc['db_query']('', ' + UPDATE {db_prefix}user_alerts + SET is_read = {int:now} + WHERE is_read = 0 AND id_member = {int:current_member} + AND + ( + (content_id IN ({array_int:messages}) AND content_type = {string:msg}) + OR + (content_id = {int:current_topic} AND (content_type = {string:topic} OR (content_type = {string:board} AND content_action = {string:topic}))) + )', + array( + 'topic' => 'topic', + 'board' => 'board', + 'msg' => 'msg', + 'current_member' => $user_info['id'], + 'current_topic' => $topic, + 'messages' => $messages, + 'now' => time(), + ) + ); + // If changes made, update the member record as well + if ($smcFunc['db_affected_rows']() > 0) + { + require_once($sourcedir . '/Profile-Modify.php'); + $user_info['alerts'] = alert_count($user_info['id'], true); + updateMemberData($user_info['id'], array('alerts' => $user_info['alerts'])); + } + } + } + + // Get notification preferences + $context['topicinfo']['notify_prefs'] = array(); + if (!empty($user_info['id'])) + { + require_once($sourcedir . '/Subs-Notify.php'); + $prefs = getNotifyPrefs($user_info['id'], array('topic_notify', 'topic_notify_' . $context['current_topic']), true); + $pref = !empty($prefs[$user_info['id']]) && $context['is_marked_notify'] ? $prefs[$user_info['id']] : array(); + $context['topicinfo']['notify_prefs'] = array( + 'is_custom' => isset($pref['topic_notify_' . $topic]), + 'pref' => isset($pref['topic_notify_' . $context['current_topic']]) ? $pref['topic_notify_' . $context['current_topic']] : (!empty($pref['topic_notify']) ? $pref['topic_notify'] : 0), + ); + } + + $context['topic_notification'] = !empty($user_info['id']) ? $context['topicinfo']['notify_prefs'] : array(); + // 0 => unwatched, 1 => normal, 2 => receive alerts, 3 => receive emails + $context['topic_notification_mode'] = !$user_info['is_guest'] ? ($context['topic_unwatched'] ? 0 : ($context['topicinfo']['notify_prefs']['pref'] & 0x02 ? 3 : ($context['topicinfo']['notify_prefs']['pref'] & 0x01 ? 2 : 1))) : 0; + + $context['loaded_attachments'] = array(); + + // If there _are_ messages here... (probably an error otherwise :!) + if (!empty($messages)) + { + // Fetch attachments. + if (!empty($modSettings['attachmentEnable']) && allowedTo('view_attachments')) + { + require_once($sourcedir . '/Subs-Attachments.php'); + prepareAttachsByMsg($messages); + } + + $msg_parameters = array( + 'message_list' => $messages, + 'new_from' => $context['topicinfo']['new_from'], + ); + $msg_selects = array(); + $msg_tables = array(); + call_integration_hook('integrate_query_message', array(&$msg_selects, &$msg_tables, &$msg_parameters)); + + // What? It's not like it *couldn't* be only guests in this topic... + loadMemberData($posters); + $messages_request = $smcFunc['db_query']('', ' + SELECT + id_msg, icon, subject, poster_time, poster_ip, id_member, modified_time, modified_name, modified_reason, body, + smileys_enabled, poster_name, poster_email, approved, likes, + id_msg_modified < {int:new_from} AS is_read + ' . (!empty($msg_selects) ? (', ' . implode(', ', $msg_selects)) : '') . ' + FROM {db_prefix}messages + ' . (!empty($msg_tables) ? implode("\n\t", $msg_tables) : '') . ' + WHERE id_msg IN ({array_int:message_list}) + ORDER BY id_msg' . (empty($options['view_newest_first']) ? '' : ' DESC'), + $msg_parameters + ); + + // And the likes + if (!empty($modSettings['enable_likes'])) + $context['my_likes'] = $context['user']['is_guest'] ? array() : prepareLikesContext($topic); + + // Go to the last message if the given time is beyond the time of the last message. + if (isset($context['start_from']) && $context['start_from'] >= $context['topicinfo']['num_replies']) + $context['start_from'] = $context['topicinfo']['num_replies']; + + // Since the anchor information is needed on the top of the page we load these variables beforehand. + $context['first_message'] = isset($messages[$firstIndex]) ? $messages[$firstIndex] : $messages[0]; + if (empty($options['view_newest_first'])) + $context['first_new_message'] = isset($context['start_from']) && $_REQUEST['start'] == $context['start_from']; + else + $context['first_new_message'] = isset($context['start_from']) && $_REQUEST['start'] == $context['topicinfo']['num_replies'] - $context['start_from']; + } + else + { + $messages_request = false; + $context['first_message'] = 0; + $context['first_new_message'] = false; + + $context['likes'] = array(); + } + + $context['jump_to'] = array( + 'label' => addslashes(un_htmlspecialchars($txt['jump_to'])), + 'board_name' => strtr($smcFunc['htmlspecialchars'](strip_tags($board_info['name'])), array('&' => '&')), + 'child_level' => $board_info['child_level'], + ); + + // Set the callback. (do you REALIZE how much memory all the messages would take?!?) + // This will be called from the template. + $context['get_message'] = 'prepareDisplayContext'; + + // Now set all the wonderful, wonderful permissions... like moderation ones... + $common_permissions = array( + 'can_approve' => 'approve_posts', + 'can_ban' => 'manage_bans', + 'can_sticky' => 'make_sticky', + 'can_merge' => 'merge_any', + 'can_split' => 'split_any', + 'calendar_post' => 'calendar_post', + 'can_send_pm' => 'pm_send', + 'can_report_moderator' => 'report_any', + 'can_moderate_forum' => 'moderate_forum', + 'can_issue_warning' => 'issue_warning', + 'can_restore_topic' => 'move_any', + 'can_restore_msg' => 'move_any', + 'can_like' => 'likes_like', + ); + foreach ($common_permissions as $contextual => $perm) + $context[$contextual] = allowedTo($perm); + + // Permissions with _any/_own versions. $context[YYY] => ZZZ_any/_own. + $anyown_permissions = array( + 'can_move' => 'move', + 'can_lock' => 'lock', + 'can_delete' => 'remove', + 'can_add_poll' => 'poll_add', + 'can_remove_poll' => 'poll_remove', + 'can_reply' => 'post_reply', + 'can_reply_unapproved' => 'post_unapproved_replies', + ); + foreach ($anyown_permissions as $contextual => $perm) + $context[$contextual] = allowedTo($perm . '_any') || ($context['user']['started'] && allowedTo($perm . '_own')); + + if (!$user_info['is_admin'] && $context['can_move'] && !$modSettings['topic_move_any']) + { + // We'll use this in a minute + $boards_allowed = array_diff(boardsAllowedTo('post_new'), array($board)); + + /* You can't move this unless you have permission + to start new topics on at least one other board */ + $context['can_move'] = count($boards_allowed) > 1; + } + + // If a topic is locked, you can't remove it unless it's yours and you locked it or you can lock_any + if ($context['topicinfo']['locked']) + { + $context['can_delete'] &= (($context['topicinfo']['locked'] == 1 && $context['user']['started']) || allowedTo('lock_any')); + } + + // Cleanup all the permissions with extra stuff... + $context['can_mark_notify'] = !$context['user']['is_guest']; + $context['calendar_post'] &= !empty($modSettings['cal_enabled']); + $context['can_add_poll'] &= $modSettings['pollMode'] == '1' && $context['topicinfo']['id_poll'] <= 0; + $context['can_remove_poll'] &= $modSettings['pollMode'] == '1' && $context['topicinfo']['id_poll'] > 0; + $context['can_reply'] &= empty($context['topicinfo']['locked']) || allowedTo('moderate_board'); + $context['can_reply_unapproved'] &= $modSettings['postmod_active'] && (empty($context['topicinfo']['locked']) || allowedTo('moderate_board')); + $context['can_issue_warning'] &= $modSettings['warning_settings'][0] == 1; + // Handle approval flags... + $context['can_reply_approved'] = $context['can_reply']; + $context['can_reply'] |= $context['can_reply_unapproved']; + $context['can_quote'] = $context['can_reply'] && (empty($modSettings['disabledBBC']) || !in_array('quote', explode(',', $modSettings['disabledBBC']))); + $context['can_mark_unread'] = !$user_info['is_guest']; + $context['can_unwatch'] = !$user_info['is_guest']; + $context['can_set_notify'] = !$user_info['is_guest']; + + $context['can_print'] = empty($modSettings['disable_print_topic']); + + // Start this off for quick moderation - it will be or'd for each post. + $context['can_remove_post'] = allowedTo('delete_any') || (allowedTo('delete_replies') && $context['user']['started']); + + // Can restore topic? That's if the topic is in the recycle board and has a previous restore state. + $context['can_restore_topic'] &= !empty($board_info['recycle']) && !empty($context['topicinfo']['id_previous_board']); + $context['can_restore_msg'] &= !empty($board_info['recycle']) && !empty($context['topicinfo']['id_previous_topic']); + + // Check if the draft functions are enabled and that they have permission to use them (for quick reply.) + $context['drafts_save'] = !empty($modSettings['drafts_post_enabled']) && allowedTo('post_draft') && $context['can_reply']; + $context['drafts_autosave'] = !empty($context['drafts_save']) && !empty($modSettings['drafts_autosave_enabled']) && !empty($options['drafts_autosave_enabled']); + if (!empty($context['drafts_save'])) + loadLanguage('Drafts'); + + // When was the last time this topic was replied to? Should we warn them about it? + if (!empty($modSettings['oldTopicDays']) && ($context['can_reply'] || $context['can_reply_unapproved']) && empty($context['topicinfo']['is_sticky'])) + { + $request = $smcFunc['db_query']('', ' + SELECT poster_time + FROM {db_prefix}messages + WHERE id_msg = {int:id_last_msg} + LIMIT 1', + array( + 'id_last_msg' => $context['topicinfo']['id_last_msg'], + ) + ); + + list ($lastPostTime) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + $context['oldTopicError'] = $lastPostTime + $modSettings['oldTopicDays'] * 86400 < time(); + } + + // You can't link an existing topic to the calendar unless you can modify the first post... + $context['calendar_post'] &= allowedTo('modify_any') || (allowedTo('modify_own') && $context['user']['started']); + + // Load up the "double post" sequencing magic. + checkSubmitOnce('register'); + $context['name'] = isset($_SESSION['guest_name']) ? $_SESSION['guest_name'] : ''; + $context['email'] = isset($_SESSION['guest_email']) ? $_SESSION['guest_email'] : ''; + // Needed for the editor and message icons. + require_once($sourcedir . '/Subs-Editor.php'); + + // Now create the editor. + $editorOptions = array( + 'id' => 'quickReply', + 'value' => '', + 'labels' => array( + 'post_button' => $txt['post'], + ), + // add height and width for the editor + 'height' => '150px', + 'width' => '100%', + // We do HTML preview here. + 'preview_type' => 1, + // This is required + 'required' => true, + ); + create_control_richedit($editorOptions); + + // Store the ID. + $context['post_box_name'] = $editorOptions['id']; + + $context['attached'] = ''; + $context['make_poll'] = isset($_REQUEST['poll']); + + // Message icons - customized icons are off? + $context['icons'] = getMessageIcons($board); + + if (!empty($context['icons'])) + $context['icons'][count($context['icons']) - 1]['is_last'] = true; + + // Build the normal button array. + $context['normal_buttons'] = array(); + + if ($context['can_reply']) + $context['normal_buttons']['reply'] = array('text' => 'reply', 'url' => $scripturl . '?action=post;topic=' . $context['current_topic'] . '.' . $context['start'] . ';last_msg=' . $context['topic_last_message'], 'active' => true); + + if ($context['can_add_poll']) + $context['normal_buttons']['add_poll'] = array('text' => 'add_poll', 'url' => $scripturl . '?action=editpoll;add;topic=' . $context['current_topic'] . '.' . $context['start']); + + if ($context['can_mark_unread']) + $context['normal_buttons']['mark_unread'] = array('text' => 'mark_unread', 'url' => $scripturl . '?action=markasread;sa=topic;t=' . $context['mark_unread_time'] . ';topic=' . $context['current_topic'] . '.' . $context['start'] . ';' . $context['session_var'] . '=' . $context['session_id']); + + if ($context['can_print']) + $context['normal_buttons']['print'] = array('text' => 'print', 'custom' => 'rel="nofollow"', 'url' => $scripturl . '?action=printpage;topic=' . $context['current_topic'] . '.0'); + + if ($context['can_set_notify']) + $context['normal_buttons']['notify'] = array( + 'text' => 'notify_topic_' . $context['topic_notification_mode'], + 'sub_buttons' => array( + array( + 'test' => 'can_unwatch', + 'text' => 'notify_topic_0', + 'url' => $scripturl . '?action=notifytopic;topic=' . $context['current_topic'] . ';mode=0;' . $context['session_var'] . '=' . $context['session_id'], + ), + array( + 'text' => 'notify_topic_1', + 'url' => $scripturl . '?action=notifytopic;topic=' . $context['current_topic'] . ';mode=1;' . $context['session_var'] . '=' . $context['session_id'], + ), + array( + 'text' => 'notify_topic_2', + 'url' => $scripturl . '?action=notifytopic;topic=' . $context['current_topic'] . ';mode=2;' . $context['session_var'] . '=' . $context['session_id'], + ), + array( + 'text' => 'notify_topic_3', + 'url' => $scripturl . '?action=notifytopic;topic=' . $context['current_topic'] . ';mode=3;' . $context['session_var'] . '=' . $context['session_id'], + ), + ), + ); + + // Build the mod button array + $context['mod_buttons'] = array(); + + if ($context['can_move']) + $context['mod_buttons']['move'] = array('text' => 'move_topic', 'url' => $scripturl . '?action=movetopic;current_board=' . $context['current_board'] . ';topic=' . $context['current_topic'] . '.0'); + + if ($context['can_delete']) + $context['mod_buttons']['delete'] = array('text' => 'remove_topic', 'custom' => 'data-confirm="' . $txt['are_sure_remove_topic'] . '"', 'class' => 'you_sure', 'url' => $scripturl . '?action=removetopic2;topic=' . $context['current_topic'] . '.0;' . $context['session_var'] . '=' . $context['session_id']); + + if ($context['can_lock']) + $context['mod_buttons']['lock'] = array('text' => empty($context['is_locked']) ? 'set_lock' : 'set_unlock', 'url' => $scripturl . '?action=lock;topic=' . $context['current_topic'] . '.' . $context['start'] . ';sa=' . ($context['is_locked'] ? 'unlock' : 'lock') . ';' . $context['session_var'] . '=' . $context['session_id']); + + if ($context['can_sticky']) + $context['mod_buttons']['sticky'] = array('text' => empty($context['is_sticky']) ? 'set_sticky' : 'set_nonsticky', 'url' => $scripturl . '?action=sticky;topic=' . $context['current_topic'] . '.' . $context['start'] . ';sa=' . ($context['is_sticky'] ? 'nonsticky' : 'sticky') . ';' . $context['session_var'] . '=' . $context['session_id']); + + if ($context['can_merge']) + $context['mod_buttons']['merge'] = array('text' => 'merge', 'url' => $scripturl . '?action=mergetopics;board=' . $context['current_board'] . '.0;from=' . $context['current_topic']); + + if ($context['calendar_post']) + $context['mod_buttons']['calendar'] = array('text' => 'calendar_link', 'url' => $scripturl . '?action=post;calendar;msg=' . $context['topic_first_message'] . ';topic=' . $context['current_topic'] . '.0'); + + // Restore topic. eh? No monkey business. + if ($context['can_restore_topic']) + $context['mod_buttons']['restore_topic'] = array('text' => 'restore_topic', 'url' => $scripturl . '?action=restoretopic;topics=' . $context['current_topic'] . ';' . $context['session_var'] . '=' . $context['session_id']); + + // Show a message in case a recently posted message became unapproved. + $context['becomesUnapproved'] = !empty($_SESSION['becomesUnapproved']); + unset($_SESSION['becomesUnapproved']); + + // Allow adding new mod buttons easily. + // Note: $context['normal_buttons'] and $context['mod_buttons'] are added for backward compatibility with 2.0, but are deprecated and should not be used + call_integration_hook('integrate_display_buttons', array(&$context['normal_buttons'])); + // Note: integrate_mod_buttons is no more necessary and deprecated, but is kept for backward compatibility with 2.0 + call_integration_hook('integrate_mod_buttons', array(&$context['mod_buttons'])); + + // If any buttons have a 'test' check, run those tests now to keep things clean. + foreach (array('normal_buttons', 'mod_buttons') as $button_strip) + { + foreach ($context[$button_strip] as $key => $value) + { + if (isset($value['test']) && empty($context[$value['test']])) + { + unset($context[$button_strip][$key]); + } + elseif (isset($value['sub_buttons'])) + { + foreach ($value['sub_buttons'] as $subkey => $subvalue) + { + if (isset($subvalue['test']) && empty($context[$subvalue['test']])) + unset($context[$button_strip][$key]['sub_buttons'][$subkey]); + } + } + } + } + + // Load the drafts js file + if ($context['drafts_autosave']) + loadJavaScriptFile('drafts.js', array('defer' => false, 'minimize' => true), 'smf_drafts'); + + // Spellcheck + if ($context['show_spellchecking']) + loadJavaScriptFile('spellcheck.js', array('defer' => false, 'minimize' => true), 'smf_spellcheck'); + + // topic.js + loadJavaScriptFile('topic.js', array('defer' => false, 'minimize' => true), 'smf_topic'); + + // quotedText.js + loadJavaScriptFile('quotedText.js', array('defer' => true, 'minimize' => true), 'smf_quotedText'); + + // Mentions + if (!empty($modSettings['enable_mentions']) && allowedTo('mention')) + { + loadJavaScriptFile('jquery.atwho.min.js', array('defer' => true), 'smf_atwho'); + loadJavaScriptFile('jquery.caret.min.js', array('defer' => true), 'smf_caret'); + loadJavaScriptFile('mentions.js', array('defer' => true, 'minimize' => true), 'smf_mentions'); + } +} + +/** + * Callback for the message display. + * It actually gets and prepares the message context. + * This function will start over from the beginning if reset is set to true, which is + * useful for showing an index before or after the posts. + * + * @param bool $reset Whether or not to reset the db seek pointer + * @return array A large array of contextual data for the posts + */ +function prepareDisplayContext($reset = false) +{ + global $settings, $txt, $modSettings, $scripturl, $options, $user_info, $smcFunc; + global $memberContext, $context, $messages_request, $topic, $board_info, $sourcedir; + + static $counter = null; + + // If the query returned false, bail. + if ($messages_request == false) + return false; + + // Remember which message this is. (ie. reply #83) + if ($counter === null || $reset) + $counter = empty($options['view_newest_first']) ? $context['start'] : $context['total_visible_posts'] - $context['start']; + + // Start from the beginning... + if ($reset) + return @$smcFunc['db_data_seek']($messages_request, 0); + + // Attempt to get the next message. + $message = $smcFunc['db_fetch_assoc']($messages_request); + if (!$message) + { + $smcFunc['db_free_result']($messages_request); + return false; + } + + // $context['icon_sources'] says where each icon should come from - here we set up the ones which will always exist! + if (empty($context['icon_sources'])) + { + $context['icon_sources'] = array(); + foreach ($context['stable_icons'] as $icon) + $context['icon_sources'][$icon] = 'images_url'; + } + + // Message Icon Management... check the images exist. + if (!empty($modSettings['messageIconChecks_enable'])) + { + // If the current icon isn't known, then we need to do something... + if (!isset($context['icon_sources'][$message['icon']])) + $context['icon_sources'][$message['icon']] = file_exists($settings['theme_dir'] . '/images/post/' . $message['icon'] . '.png') ? 'images_url' : 'default_images_url'; + } + elseif (!isset($context['icon_sources'][$message['icon']])) + $context['icon_sources'][$message['icon']] = 'images_url'; + + // If you're a lazy bum, you probably didn't give a subject... + $message['subject'] = $message['subject'] != '' ? $message['subject'] : $txt['no_subject']; + + // Are you allowed to remove at least a single reply? + $context['can_remove_post'] |= allowedTo('delete_own') && (empty($modSettings['edit_disable_time']) || $message['poster_time'] + $modSettings['edit_disable_time'] * 60 >= time()) && $message['id_member'] == $user_info['id']; + + // If the topic is locked, you might not be able to delete the post... + if ($context['is_locked']) + { + $context['can_remove_post'] &= ($context['user']['started'] && $context['is_locked'] == 1) || allowedTo('lock_any'); + } + + // If it couldn't load, or the user was a guest.... someday may be done with a guest table. + if (!loadMemberContext($message['id_member'], true)) + { + // Notice this information isn't used anywhere else.... + $memberContext[$message['id_member']]['name'] = $message['poster_name']; + $memberContext[$message['id_member']]['id'] = 0; + $memberContext[$message['id_member']]['group'] = $txt['guest_title']; + $memberContext[$message['id_member']]['link'] = $message['poster_name']; + $memberContext[$message['id_member']]['email'] = $message['poster_email']; + $memberContext[$message['id_member']]['show_email'] = allowedTo('moderate_forum'); + $memberContext[$message['id_member']]['is_guest'] = true; + } + else + { + // Define this here to make things a bit more readable + $can_view_warning = $user_info['is_mod'] || allowedTo('moderate_forum') || allowedTo('view_warning_any') || ($message['id_member'] == $user_info['id'] && allowedTo('view_warning_own')); + + $memberContext[$message['id_member']]['can_view_profile'] = allowedTo('profile_view') || ($message['id_member'] == $user_info['id'] && !$user_info['is_guest']); + $memberContext[$message['id_member']]['is_topic_starter'] = $message['id_member'] == $context['topic_starter_id']; + $memberContext[$message['id_member']]['can_see_warning'] = !isset($context['disabled_fields']['warning_status']) && $memberContext[$message['id_member']]['warning_status'] && $can_view_warning; + // Show the email if it's your post... + $memberContext[$message['id_member']]['show_email'] |= ($message['id_member'] == $user_info['id']); + } + + $memberContext[$message['id_member']]['ip'] = inet_dtop($message['poster_ip']); + $memberContext[$message['id_member']]['show_profile_buttons'] = !empty($modSettings['show_profile_buttons']) && (!empty($memberContext[$message['id_member']]['can_view_profile']) || (!empty($memberContext[$message['id_member']]['website']['url']) && !isset($context['disabled_fields']['website'])) || $memberContext[$message['id_member']]['show_email'] || $context['can_send_pm']); + + // Do the censor thang. + censorText($message['body']); + censorText($message['subject']); + + // Run BBC interpreter on the message. + $message['body'] = parse_bbc($message['body'], $message['smileys_enabled'], $message['id_msg']); + + // If it's in the recycle bin we need to override whatever icon we did have. + if (!empty($board_info['recycle'])) + $message['icon'] = 'recycled'; + + require_once($sourcedir . '/Subs-Attachments.php'); + + // Compose the memory eat- I mean message array. + $output = array( + 'attachment' => loadAttachmentContext($message['id_msg'], $context['loaded_attachments']), + 'id' => $message['id_msg'], + 'href' => $scripturl . '?msg=' . $message['id_msg'], + 'link' => '' . $message['subject'] . '', + 'member' => &$memberContext[$message['id_member']], + 'icon' => $message['icon'], + 'icon_url' => $settings[$context['icon_sources'][$message['icon']]] . '/post/' . $message['icon'] . '.png', + 'subject' => $message['subject'], + 'time' => timeformat($message['poster_time']), + 'timestamp' => $message['poster_time'], + 'counter' => $counter, + 'modified' => array( + 'time' => timeformat($message['modified_time']), + 'timestamp' => $message['modified_time'], + 'name' => $message['modified_name'], + 'reason' => $message['modified_reason'] + ), + 'body' => $message['body'], + 'new' => empty($message['is_read']), + 'approved' => $message['approved'], + 'first_new' => isset($context['start_from']) && $context['start_from'] == $counter, + 'is_ignored' => !empty($modSettings['enable_buddylist']) && !empty($options['posts_apply_ignore_list']) && in_array($message['id_member'], $context['user']['ignoreusers']), + 'can_approve' => !$message['approved'] && $context['can_approve'], + 'can_unapprove' => !empty($modSettings['postmod_active']) && $context['can_approve'] && $message['approved'], + 'can_modify' => (!$context['is_locked'] || allowedTo('moderate_board')) && (allowedTo('modify_any') || (allowedTo('modify_replies') && $context['user']['started']) || (allowedTo('modify_own') && $message['id_member'] == $user_info['id'] && (empty($modSettings['edit_disable_time']) || !$message['approved'] || $message['poster_time'] + $modSettings['edit_disable_time'] * 60 > time()))), + 'can_remove' => allowedTo('delete_any') || (allowedTo('delete_replies') && $context['user']['started']) || (allowedTo('delete_own') && $message['id_member'] == $user_info['id'] && (empty($modSettings['edit_disable_time']) || $message['poster_time'] + $modSettings['edit_disable_time'] * 60 > time())), + 'can_see_ip' => allowedTo('moderate_forum') || ($message['id_member'] == $user_info['id'] && !empty($user_info['id'])), + 'css_class' => $message['approved'] ? 'windowbg' : 'approvebg', + ); + + // Does the file contains any attachments? if so, change the icon. + if (!empty($output['attachment'])) + { + $output['icon'] = 'clip'; + $output['icon_url'] = $settings[$context['icon_sources'][$output['icon']]] . '/post/' . $output['icon'] . '.png'; + } + + // Are likes enable? + if (!empty($modSettings['enable_likes'])) + $output['likes'] = array( + 'count' => $message['likes'], + 'you' => in_array($message['id_msg'], $context['my_likes']), + 'can_like' => !$context['user']['is_guest'] && $message['id_member'] != $context['user']['id'] && !empty($context['can_like']), + ); + + // Is this user the message author? + $output['is_message_author'] = $message['id_member'] == $user_info['id']; + if (!empty($output['modified']['name'])) + $output['modified']['last_edit_text'] = sprintf($txt['last_edit_by'], $output['modified']['time'], $output['modified']['name']); + + // Did they give a reason for editing? + if (!empty($output['modified']['name']) && !empty($output['modified']['reason'])) + $output['modified']['last_edit_text'] .= ' ' . sprintf($txt['last_edit_reason'], $output['modified']['reason']); + + // Any custom profile fields? + if (!empty($memberContext[$message['id_member']]['custom_fields'])) + foreach ($memberContext[$message['id_member']]['custom_fields'] as $custom) + $output['custom_fields'][$context['cust_profile_fields_placement'][$custom['placement']]][] = $custom; + + $output['quickbuttons'] = array( + 'quote' => array( + 'label' => $txt['quote_action'], + 'href' => $scripturl.'?action=post;quote='.$output['id'].';topic='.$context['current_topic'], '.'.$context['start'].';last_msg='.$context['topic_last_message'], + 'javascript' => 'onclick="return oQuickReply.quote('.$output['id'].');"', + 'icon' => 'quote', + 'show' => $context['can_quote'] + ), + 'quote_selected' => array( + 'label' => $txt['quote_selected_action'], + 'id' => 'quoteSelected_'. $output['id'], + 'href' => 'javascript:void(0)', + 'custom' => 'style="display:none"', + 'icon' => 'quote_selected', + 'show' => $context['can_quote'] + ), + 'quick_edit' => array( + 'label' => $txt['quick_edit'], + 'class' => 'quick_edit', + 'id' => 'modify_button_'. $output['id'], + 'custom' => 'onclick="oQuickModify.modifyMsg(\''.$output['id'].'\', \''.!empty($modSettings['toggle_subject']).'\')"', + 'icon' => 'quick_edit_button', + 'show' => $output['can_modify'] + ), + 'more' => array( + 'modify' => array( + 'label' => $txt['modify'], + 'href' => $scripturl.'?action=post;msg='.$output['id'].';topic='.$context['current_topic'].'.'.$context['start'], + 'icon' => 'modify_button', + 'show' => $output['can_modify'] + ), + 'remove_topic' => array( + 'label' => $txt['remove_topic'], + 'href' => $scripturl.'?action=removetopic2;topic='.$context['current_topic'].'.'.$context['start'].';'.$context['session_var'].'='.$context['session_id'], + 'javascript' => 'data-confirm="'.$txt['are_sure_remove_topic'].'"', + 'class' => 'you_sure', + 'icon' => 'remove_button', + 'show' => $context['can_delete'] && ($context['topic_first_message'] == $output['id']) + ), + 'remove' => array( + 'label' => $txt['remove'], + 'href' => $scripturl.'?action=deletemsg;topic='.$context['current_topic'].'.'.$context['start'].';msg='.$output['id'].';'.$context['session_var'].'='.$context['session_id'], + 'javascript' => 'data-confirm="'.$txt['remove_message_question'].'"', + 'class' => 'you_sure', + 'icon' => 'remove_button', + 'show' => $output['can_remove'] && ($context['topic_first_message'] != $output['id']) + ), + 'split' => array( + 'label' => $txt['split'], + 'href' => $scripturl.'?action=splittopics;topic='.$context['current_topic'].'.0;at='.$output['id'], + 'icon' => 'split_button', + 'show' => $context['can_split'] && !empty($context['real_num_replies']) + ), + 'report' => array( + 'label' => $txt['report_to_mod'], + 'href' => $scripturl.'?action=reporttm;topic='.$context['current_topic'].'.'.$output['counter'].';msg='.$output['id'], + 'icon' => 'error', + 'show' => $context['can_report_moderator'] + ), + 'warn' => array( + 'label' => $txt['issue_warning'], + 'href' => $scripturl.'?action=profile;area=issuewarning;u='.$output['member']['id'].';msg='.$output['id'], + 'icon' => 'warn_button', + 'show' => $context['can_issue_warning'] && !$output['is_message_author'] && !$output['member']['is_guest'] + ), + 'restore' => array( + 'label' => $txt['restore_message'], + 'href' => $scripturl.'?action=restoretopic;msgs='.$output['id'].';'.$context['session_var'].'='.$context['session_id'], + 'icon' => 'restore_button', + 'show' => $context['can_restore_msg'] + ), + 'approve' => array( + 'label' => $txt['approve'], + 'href' => $scripturl.'?action=moderate;area=postmod;sa=approve;topic='.$context['current_topic'].'.'.$context['start'].';msg='.$output['id'].';'.$context['session_var'].'='.$context['session_id'], + 'icon' => 'approve_button', + 'show' => $output['can_approve'] + ), + 'unapprove' => array( + 'label' => $txt['unapprove'], + 'href' => $scripturl.'?action=moderate;area=postmod;sa=approve;topic='.$context['current_topic'].'.'.$context['start'].';msg='.$output['id'].';'.$context['session_var'].'='.$context['session_id'], + 'icon' => 'unapprove_button', + 'show' => $output['can_unapprove'] + ), + ), + 'quickmod' => array( + 'class' => 'inline_mod_check', + 'id' => 'in_topic_mod_check_'. $output['id'], + 'custom' => 'style="display: none;"', + 'content' => '', + 'show' => !empty($options['display_quick_mod']) && $options['display_quick_mod'] == 1 && $output['can_remove'] + ) + ); + + if (empty($options['view_newest_first'])) + $counter++; + + else + $counter--; + + call_integration_hook('integrate_prepare_display_context', array(&$output, &$message, $counter)); + + return $output; +} + +/** + * Once upon a time, this function handled downloading attachments. + * Now it's just an alias retained for the sake of backwards compatibility. + */ +function Download() +{ + global $sourcedir; + require_once($sourcedir . '/ShowAttachments.php'); + showAttachment(); +} + +/** + * In-topic quick moderation. + */ +function QuickInTopicModeration() +{ + global $sourcedir, $topic, $board, $user_info, $smcFunc, $modSettings, $context; + + // Check the session = get or post. + checkSession('request'); + + require_once($sourcedir . '/RemoveTopic.php'); + + if (empty($_REQUEST['msgs'])) + redirectexit('topic=' . $topic . '.' . $_REQUEST['start']); + + $messages = array(); + foreach ($_REQUEST['msgs'] as $dummy) + $messages[] = (int) $dummy; + + // We are restoring messages. We handle this in another place. + if (isset($_REQUEST['restore_selected'])) + redirectexit('action=restoretopic;msgs=' . implode(',', $messages) . ';' . $context['session_var'] . '=' . $context['session_id']); + if (isset($_REQUEST['split_selection'])) + { + $request = $smcFunc['db_query']('', ' + SELECT subject + FROM {db_prefix}messages + WHERE id_msg = {int:message} + LIMIT 1', + array( + 'message' => min($messages), + ) + ); + list($subname) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + $_SESSION['split_selection'][$topic] = $messages; + redirectexit('action=splittopics;sa=selectTopics;topic=' . $topic . '.0;subname_enc=' . urlencode($subname) . ';' . $context['session_var'] . '=' . $context['session_id']); + } + + // Allowed to delete any message? + if (allowedTo('delete_any')) + $allowed_all = true; + // Allowed to delete replies to their messages? + elseif (allowedTo('delete_replies')) + { + $request = $smcFunc['db_query']('', ' + SELECT id_member_started + FROM {db_prefix}topics + WHERE id_topic = {int:current_topic} + LIMIT 1', + array( + 'current_topic' => $topic, + ) + ); + list ($starter) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + $allowed_all = $starter == $user_info['id']; + } + else + $allowed_all = false; + + // Make sure they're allowed to delete their own messages, if not any. + if (!$allowed_all) + isAllowedTo('delete_own'); + + // Allowed to remove which messages? + $request = $smcFunc['db_query']('', ' + SELECT id_msg, subject, id_member, poster_time + FROM {db_prefix}messages + WHERE id_msg IN ({array_int:message_list}) + AND id_topic = {int:current_topic}' . (!$allowed_all ? ' + AND id_member = {int:current_member}' : '') . ' + LIMIT {int:limit}', + array( + 'current_member' => $user_info['id'], + 'current_topic' => $topic, + 'message_list' => $messages, + 'limit' => count($messages), + ) + ); + $messages = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (!$allowed_all && !empty($modSettings['edit_disable_time']) && $row['poster_time'] + $modSettings['edit_disable_time'] * 60 < time()) + continue; + + $messages[$row['id_msg']] = array($row['subject'], $row['id_member']); + } + $smcFunc['db_free_result']($request); + + // Get the first message in the topic - because you can't delete that! + $request = $smcFunc['db_query']('', ' + SELECT id_first_msg, id_last_msg + FROM {db_prefix}topics + WHERE id_topic = {int:current_topic} + LIMIT 1', + array( + 'current_topic' => $topic, + ) + ); + list ($first_message, $last_message) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // Delete all the messages we know they can delete. ($messages) + foreach ($messages as $message => $info) + { + // Just skip the first message - if it's not the last. + if ($message == $first_message && $message != $last_message) + continue; + // If the first message is going then don't bother going back to the topic as we're effectively deleting it. + elseif ($message == $first_message) + $topicGone = true; + + removeMessage($message); + + // Log this moderation action ;). + if (allowedTo('delete_any') && (!allowedTo('delete_own') || $info[1] != $user_info['id'])) + logAction('delete', array('topic' => $topic, 'subject' => $info[0], 'member' => $info[1], 'board' => $board)); + } + + redirectexit(!empty($topicGone) ? 'board=' . $board : 'topic=' . $topic . '.' . $_REQUEST['start']); +} + +?> \ No newline at end of file diff --git a/Sources/Drafts.php b/Sources/Drafts.php new file mode 100644 index 0000000..736d2a8 --- /dev/null +++ b/Sources/Drafts.php @@ -0,0 +1,868 @@ +]~', '', $_POST['icon']); + $draft['smileys_enabled'] = isset($_POST['ns']) ? (int) $_POST['ns'] : 1; + $draft['locked'] = isset($_POST['lock']) ? (int) $_POST['lock'] : 0; + $draft['sticky'] = isset($_POST['sticky']) ? (int) $_POST['sticky'] : 0; + $draft['subject'] = strtr($smcFunc['htmlspecialchars']($_POST['subject']), array("\r" => '', "\n" => '', "\t" => '')); + $draft['body'] = $smcFunc['htmlspecialchars']($_POST['message'], ENT_QUOTES); + + // message and subject still need a bit more work + preparsecode($draft['body']); + if ($smcFunc['strlen']($draft['subject']) > 100) + $draft['subject'] = $smcFunc['substr']($draft['subject'], 0, 100); + + // Modifying an existing draft, like hitting the save draft button or autosave enabled? + if (!empty($id_draft) && !empty($draft_info)) + { + $smcFunc['db_query']('', ' + UPDATE {db_prefix}user_drafts + SET + id_topic = {int:id_topic}, + id_board = {int:id_board}, + poster_time = {int:poster_time}, + subject = {string:subject}, + smileys_enabled = {int:smileys_enabled}, + body = {string:body}, + icon = {string:icon}, + locked = {int:locked}, + is_sticky = {int:is_sticky} + WHERE id_draft = {int:id_draft}', + array( + 'id_topic' => $topic_id, + 'id_board' => $board, + 'poster_time' => time(), + 'subject' => $draft['subject'], + 'smileys_enabled' => (int) $draft['smileys_enabled'], + 'body' => $draft['body'], + 'icon' => $draft['icon'], + 'locked' => $draft['locked'], + 'is_sticky' => $draft['sticky'], + 'id_draft' => $id_draft, + ) + ); + + // some items to return to the form + $context['draft_saved'] = true; + $context['id_draft'] = $id_draft; + + // cleanup + unset($_POST['save_draft']); + } + // otherwise creating a new draft + else + { + $id_draft = $smcFunc['db_insert']('', + '{db_prefix}user_drafts', + array( + 'id_topic' => 'int', + 'id_board' => 'int', + 'type' => 'int', + 'poster_time' => 'int', + 'id_member' => 'int', + 'subject' => 'string-255', + 'smileys_enabled' => 'int', + 'body' => (!empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] > 65534 ? 'string-' . $modSettings['max_messageLength'] : 'string-65534'), + 'icon' => 'string-16', + 'locked' => 'int', + 'is_sticky' => 'int' + ), + array( + $topic_id, + $board, + 0, + time(), + $user_info['id'], + $draft['subject'], + $draft['smileys_enabled'], + $draft['body'], + $draft['icon'], + $draft['locked'], + $draft['sticky'] + ), + array( + 'id_draft' + ), + 1 + ); + + // everything go as expected? + if (!empty($id_draft)) + { + $context['draft_saved'] = true; + $context['id_draft'] = $id_draft; + } + else + $post_errors[] = 'draft_not_saved'; + + // cleanup + unset($_POST['save_draft']); + } + + // if we were called from the autosave function, send something back + if (!empty($id_draft) && isset($_REQUEST['xml']) && (!in_array('session_timeout', $post_errors))) + { + $context['draft_saved_on'] = time(); + XmlDraft($id_draft); + } + + return true; +} + +/** + * Saves a PM draft in the user_drafts table + * The core draft feature must be enabled, as well as the pm draft option + * Determines if this is a new or and update to an existing pm draft + * + * @param string $post_errors A string of info about errors encountered trying to save this draft + * @param array $recipientList An array of data about who this PM is being sent to + * @return boolean false if you can't save the draft, true if we're doing this via XML more than 5 seconds after the last save, nothing otherwise + */ +function SavePMDraft(&$post_errors, $recipientList) +{ + global $context, $user_info, $smcFunc, $modSettings; + + // PM survey says ... can you stay or must you go + if (empty($modSettings['drafts_pm_enabled']) || !allowedTo('pm_draft') || !isset($_POST['save_draft'])) + return false; + + // read in what you sent us + $id_pm_draft = (int) $_POST['id_pm_draft']; + $draft_info = ReadDraft($id_pm_draft, 1); + + // 5 seconds is the same limit we have for posting + if (isset($_REQUEST['xml']) && !empty($draft_info['poster_time']) && time() < $draft_info['poster_time'] + 5) + { + $context['draft_saved_on'] = $draft_info['poster_time']; + + // Send something back to the javascript caller + if (!empty($id_draft)) + XmlDraft($id_draft); + + return true; + } + + // determine who this is being sent to + if (isset($_REQUEST['xml'])) + { + $recipientList['to'] = isset($_POST['recipient_to']) ? explode(',', $_POST['recipient_to']) : array(); + $recipientList['bcc'] = isset($_POST['recipient_bcc']) ? explode(',', $_POST['recipient_bcc']) : array(); + } + elseif (!empty($draft_info['to_list']) && empty($recipientList)) + $recipientList = $smcFunc['json_decode']($draft_info['to_list'], true); + + // prepare the data we got from the form + $reply_id = empty($_POST['replied_to']) ? 0 : (int) $_POST['replied_to']; + $draft['body'] = $smcFunc['htmlspecialchars']($_POST['message'], ENT_QUOTES); + $draft['subject'] = strtr($smcFunc['htmlspecialchars']($_POST['subject']), array("\r" => '', "\n" => '', "\t" => '')); + + // message and subject always need a bit more work + preparsecode($draft['body']); + if ($smcFunc['strlen']($draft['subject']) > 100) + $draft['subject'] = $smcFunc['substr']($draft['subject'], 0, 100); + + // Modifying an existing PM draft? + if (!empty($id_pm_draft) && !empty($draft_info)) + { + $smcFunc['db_query']('', ' + UPDATE {db_prefix}user_drafts + SET id_reply = {int:id_reply}, + type = {int:type}, + poster_time = {int:poster_time}, + subject = {string:subject}, + body = {string:body}, + to_list = {string:to_list} + WHERE id_draft = {int:id_pm_draft}', + array( + 'id_reply' => $reply_id, + 'type' => 1, + 'poster_time' => time(), + 'subject' => $draft['subject'], + 'body' => $draft['body'], + 'id_pm_draft' => $id_pm_draft, + 'to_list' => $smcFunc['json_encode']($recipientList), + ) + ); + + // some items to return to the form + $context['draft_saved'] = true; + $context['id_pm_draft'] = $id_pm_draft; + } + // otherwise creating a new PM draft. + else + { + $id_pm_draft = $smcFunc['db_insert']('', + '{db_prefix}user_drafts', + array( + 'id_reply' => 'int', + 'type' => 'int', + 'poster_time' => 'int', + 'id_member' => 'int', + 'subject' => 'string-255', + 'body' => 'string-65534', + 'to_list' => 'string-255', + ), + array( + $reply_id, + 1, + time(), + $user_info['id'], + $draft['subject'], + $draft['body'], + $smcFunc['json_encode']($recipientList), + ), + array( + 'id_draft' + ), + 1 + ); + + // everything go as expected, if not toss back an error + if (!empty($id_pm_draft)) + { + $context['draft_saved'] = true; + $context['id_pm_draft'] = $id_pm_draft; + } + else + $post_errors[] = 'draft_not_saved'; + } + + // if we were called from the autosave function, send something back + if (!empty($id_pm_draft) && isset($_REQUEST['xml']) && !in_array('session_timeout', $post_errors)) + { + $context['draft_saved_on'] = time(); + XmlDraft($id_pm_draft); + } + + return; +} + +/** + * Reads a draft in from the user_drafts table + * Validates that the draft is the user''s draft + * Optionally loads the draft in to context or superglobal for loading in to the form + * + * @param int $id_draft ID of the draft to load + * @param int $type Type of draft - 0 for post or 1 for PM + * @param boolean $check Validate that this draft belongs to the current user + * @param boolean $load Whether or not to load the data into variables for use on a form + * @return boolean|array False if the data couldn't be loaded, true if it's a PM draft or an array of info about the draft if it's a post draft + */ +function ReadDraft($id_draft, $type = 0, $check = true, $load = false) +{ + global $context, $user_info, $smcFunc, $modSettings; + + // like purell always clean to be sure + $id_draft = (int) $id_draft; + $type = (int) $type; + + // nothing to read, nothing to do + if (empty($id_draft)) + return false; + + // load in this draft from the DB + $request = $smcFunc['db_query']('', ' + SELECT is_sticky, locked, smileys_enabled, icon, body , subject, + id_board, id_draft, id_reply, to_list + FROM {db_prefix}user_drafts + WHERE id_draft = {int:id_draft}' . ($check ? ' + AND id_member = {int:id_member}' : '') . ' + AND type = {int:type}' . (!empty($modSettings['drafts_keep_days']) ? ' + AND poster_time > {int:time}' : '') . ' + LIMIT 1', + array( + 'id_member' => $user_info['id'], + 'id_draft' => $id_draft, + 'type' => $type, + 'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0), + ) + ); + + // no results? + if (!$smcFunc['db_num_rows']($request)) + return false; + + // load up the data + $draft_info = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + // Load it up for the templates as well + if (!empty($load)) + { + if ($type === 0) + { + // a standard post draft? + $context['sticky'] = !empty($draft_info['is_sticky']) ? $draft_info['is_sticky'] : ''; + $context['locked'] = !empty($draft_info['locked']) ? $draft_info['locked'] : ''; + $context['use_smileys'] = !empty($draft_info['smileys_enabled']) ? true : false; + $context['icon'] = !empty($draft_info['icon']) ? $draft_info['icon'] : 'xx'; + $context['message'] = !empty($draft_info['body']) ? str_replace('
', "\n", un_htmlspecialchars(stripslashes($draft_info['body']))) : ''; + $context['subject'] = !empty($draft_info['subject']) ? stripslashes($draft_info['subject']) : ''; + $context['board'] = !empty($draft_info['id_board']) ? $draft_info['id_board'] : ''; + $context['id_draft'] = !empty($draft_info['id_draft']) ? $draft_info['id_draft'] : 0; + } + elseif ($type === 1) + { + // one of those pm drafts? then set it up like we have an error + $_REQUEST['subject'] = !empty($draft_info['subject']) ? stripslashes($draft_info['subject']) : ''; + $_REQUEST['message'] = !empty($draft_info['body']) ? str_replace('
', "\n", un_htmlspecialchars(stripslashes($draft_info['body']))) : ''; + $_REQUEST['replied_to'] = !empty($draft_info['id_reply']) ? $draft_info['id_reply'] : 0; + $context['id_pm_draft'] = !empty($draft_info['id_draft']) ? $draft_info['id_draft'] : 0; + $recipients = $smcFunc['json_decode']($draft_info['to_list'], true); + + // make sure we only have integers in this array + $recipients['to'] = array_map('intval', $recipients['to']); + $recipients['bcc'] = array_map('intval', $recipients['bcc']); + + // pretend we messed up to populate the pm message form + messagePostError(array(), array(), $recipients); + return true; + } + } + + return $draft_info; +} + +/** + * Deletes one or many drafts from the DB + * Validates the drafts are from the user + * is supplied an array of drafts will attempt to remove all of them + * + * @param int $id_draft The ID of the draft to delete + * @param boolean $check Whether or not to check that the draft belongs to the current user + * @return boolean False if it couldn't be deleted (doesn't return anything otherwise) + */ +function DeleteDraft($id_draft, $check = true) +{ + global $user_info, $smcFunc; + + // Only a single draft. + if (is_numeric($id_draft)) + $id_draft = array($id_draft); + + // can't delete nothing + if (empty($id_draft) || ($check && empty($user_info['id']))) + return false; + + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}user_drafts + WHERE id_draft IN ({array_int:id_draft})' . ($check ? ' + AND id_member = {int:id_member}' : ''), + array( + 'id_draft' => $id_draft, + 'id_member' => empty($user_info['id']) ? -1 : $user_info['id'], + ) + ); +} + +/** + * Loads in a group of drafts for the user of a given type (0/posts, 1/pm's) + * loads a specific draft for forum use if selected. + * Used in the posting screens to allow draft selection + * Will load a draft if selected is supplied via post + * + * @param int $member_id ID of the member to show drafts for + * @param boolean|integer $topic If $type is 1, this can be set to only load drafts for posts in the specific topic + * @param int $draft_type The type of drafts to show - 0 for post drafts, 1 for PM drafts + * @return boolean False if the drafts couldn't be loaded, nothing otherwise + */ +function ShowDrafts($member_id, $topic = false, $draft_type = 0) +{ + global $smcFunc, $scripturl, $context, $txt, $modSettings; + + // Permissions + if (($draft_type === 0 && empty($context['drafts_save'])) || ($draft_type === 1 && empty($context['drafts_pm_save'])) || empty($member_id)) + return false; + + $context['drafts'] = array(); + + // has a specific draft has been selected? Load it up if there is not a message already in the editor + if (isset($_REQUEST['id_draft']) && empty($_POST['subject']) && empty($_POST['message'])) + ReadDraft((int) $_REQUEST['id_draft'], $draft_type, true, true); + + // load the drafts this user has available + $request = $smcFunc['db_query']('', ' + SELECT subject, poster_time, id_board, id_topic, id_draft + FROM {db_prefix}user_drafts + WHERE id_member = {int:id_member}' . ((!empty($topic) && empty($draft_type)) ? ' + AND id_topic = {int:id_topic}' : (!empty($topic) ? ' + AND id_reply = {int:id_topic}' : '')) . ' + AND type = {int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? ' + AND poster_time > {int:time}' : '') . ' + ORDER BY poster_time DESC', + array( + 'id_member' => $member_id, + 'id_topic' => (int) $topic, + 'draft_type' => $draft_type, + 'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0), + ) + ); + + // add them to the draft array for display + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (empty($row['subject'])) + $row['subject'] = $txt['no_subject']; + + // Post drafts + if ($draft_type === 0) + { + $tmp_subject = shorten_subject(stripslashes($row['subject']), 24); + $context['drafts'][] = array( + 'subject' => censorText($tmp_subject), + 'poster_time' => timeformat($row['poster_time']), + 'link' => '' . $row['subject'] . '', + ); + } + // PM drafts + elseif ($draft_type === 1) + { + $tmp_subject = shorten_subject(stripslashes($row['subject']), 24); + $context['drafts'][] = array( + 'subject' => censorText($tmp_subject), + 'poster_time' => timeformat($row['poster_time']), + 'link' => '' . (!empty($row['subject']) ? $row['subject'] : $txt['drafts_none']) . '', + ); + } + } + $smcFunc['db_free_result']($request); +} + +/** + * Returns an xml response to an autosave ajax request + * provides the id of the draft saved and the time it was saved + * + * @param int $id_draft + */ +function XmlDraft($id_draft) +{ + global $txt, $context; + + header('content-type: text/xml; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set'])); + + echo ' + + + '; + + obExit(false); +} + +/** + * Show all drafts of a given type by the current user + * Uses the showdraft template + * Allows for the deleting and loading/editing of drafts + * + * @param int $memID + * @param int $draft_type + */ +function showProfileDrafts($memID, $draft_type = 0) +{ + global $txt, $scripturl, $modSettings, $context, $smcFunc, $options; + + // Some initial context. + $context['start'] = isset($_REQUEST['start']) ? (int) $_REQUEST['start'] : 0; + $context['current_member'] = $memID; + + // If just deleting a draft, do it and then redirect back. + if (!empty($_REQUEST['delete'])) + { + checkSession('get'); + $id_delete = (int) $_REQUEST['delete']; + + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}user_drafts + WHERE id_draft = {int:id_draft} + AND id_member = {int:id_member} + AND type = {int:draft_type}', + array( + 'id_draft' => $id_delete, + 'id_member' => $memID, + 'draft_type' => $draft_type, + ) + ); + + redirectexit('action=profile;u=' . $memID . ';area=showdrafts;start=' . $context['start']); + } + + // Default to 10. + if (empty($_REQUEST['viewscount']) || !is_numeric($_REQUEST['viewscount'])) + $_REQUEST['viewscount'] = 10; + + // Get the count of applicable drafts on the boards they can (still) see ... + // @todo .. should we just let them see their drafts even if they have lost board access ? + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}user_drafts AS ud + INNER JOIN {db_prefix}boards AS b ON (b.id_board = ud.id_board AND {query_see_board}) + WHERE id_member = {int:id_member} + AND type={int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? ' + AND poster_time > {int:time}' : ''), + array( + 'id_member' => $memID, + 'draft_type' => $draft_type, + 'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0), + ) + ); + list ($msgCount) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + $maxPerPage = empty($modSettings['disableCustomPerPage']) && !empty($options['messages_per_page']) ? $options['messages_per_page'] : $modSettings['defaultMaxMessages']; + $maxIndex = $maxPerPage; + + // Make sure the starting place makes sense and construct our friend the page index. + $context['page_index'] = constructPageIndex($scripturl . '?action=profile;u=' . $memID . ';area=showdrafts', $context['start'], $msgCount, $maxIndex); + $context['current_page'] = $context['start'] / $maxIndex; + + // Reverse the query if we're past 50% of the pages for better performance. + $start = $context['start']; + $reverse = $_REQUEST['start'] > $msgCount / 2; + if ($reverse) + { + $maxIndex = $msgCount < $context['start'] + $maxPerPage + 1 && $msgCount > $context['start'] ? $msgCount - $context['start'] : $maxPerPage; + $start = $msgCount < $context['start'] + $maxPerPage + 1 || $msgCount < $context['start'] + $maxPerPage ? 0 : $msgCount - $context['start'] - $maxPerPage; + } + + // Find this user's drafts for the boards they can access + // @todo ... do we want to do this? If they were able to create a draft, do we remove thier access to said draft if they loose + // access to the board or if the topic moves to a board they can not see? + $request = $smcFunc['db_query']('', ' + SELECT + b.id_board, b.name AS bname, + ud.id_member, ud.id_draft, ud.body, ud.smileys_enabled, ud.subject, ud.poster_time, ud.icon, ud.id_topic, ud.locked, ud.is_sticky + FROM {db_prefix}user_drafts AS ud + INNER JOIN {db_prefix}boards AS b ON (b.id_board = ud.id_board AND {query_see_board}) + WHERE ud.id_member = {int:current_member} + AND type = {int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? ' + AND poster_time > {int:time}' : '') . ' + ORDER BY ud.id_draft ' . ($reverse ? 'ASC' : 'DESC') . ' + LIMIT {int:start}, {int:max}', + array( + 'current_member' => $memID, + 'draft_type' => $draft_type, + 'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0), + 'start' => $start, + 'max' => $maxIndex, + ) + ); + + // Start counting at the number of the first message displayed. + $counter = $reverse ? $context['start'] + $maxIndex + 1 : $context['start']; + $context['posts'] = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // Censor.... + if (empty($row['body'])) + $row['body'] = ''; + + $row['subject'] = $smcFunc['htmltrim']($row['subject']); + if (empty($row['subject'])) + $row['subject'] = $txt['no_subject']; + + censorText($row['body']); + censorText($row['subject']); + + // BBC-ilize the message. + $row['body'] = parse_bbc($row['body'], $row['smileys_enabled'], 'draft' . $row['id_draft']); + + // And the array... + $context['drafts'][$counter += $reverse ? -1 : 1] = array( + 'body' => $row['body'], + 'counter' => $counter, + 'board' => array( + 'name' => $row['bname'], + 'id' => $row['id_board'] + ), + 'topic' => array( + 'id' => $row['id_topic'], + 'link' => empty($row['id']) ? $row['subject'] : '' . $row['subject'] . '', + ), + 'subject' => $row['subject'], + 'time' => timeformat($row['poster_time']), + 'timestamp' => $row['poster_time'], + 'icon' => $row['icon'], + 'id_draft' => $row['id_draft'], + 'locked' => $row['locked'], + 'sticky' => $row['is_sticky'], + 'quickbuttons' => array( + 'edit' => array( + 'label' => $txt['draft_edit'], + 'href' => $scripturl.'?action=post;'.(empty($row['id_topic']) ? 'board='.$row['id_board'] : 'topic='.$row['id_topic']).'.0;id_draft='.$row['id_draft'], + 'icon' => 'modify_button' + ), + 'delete' => array( + 'label' => $txt['draft_delete'], + 'href' => $scripturl.'?action=profile;u='.$context['member']['id'].';area=showdrafts;delete='.$row['id_draft'].';'.$context['session_var'].'='.$context['session_id'], + 'javascript' => 'data-confirm="'.$txt['draft_remove'].'"', + 'class' => 'you_sure', + 'icon' => 'remove_button' + ), + ), + ); + } + $smcFunc['db_free_result']($request); + + // If the drafts were retrieved in reverse order, get them right again. + if ($reverse) + $context['drafts'] = array_reverse($context['drafts'], true); + + // Menu tab + $context[$context['profile_menu_name']]['tab_data'] = array( + 'title' => $txt['drafts_show'], + 'description' => $txt['drafts_show_desc'], + 'icon_class' => 'main_icons drafts' + ); + $context['sub_template'] = 'showDrafts'; +} + +/** + * Show all PM drafts of the current user + * Uses the showpmdraft template + * Allows for the deleting and loading/editing of drafts + * + * @param int $memID + */ +function showPMDrafts($memID = -1) +{ + global $txt, $user_info, $scripturl, $modSettings, $context, $smcFunc, $options; + + // init + $draft_type = 1; + $context['start'] = isset($_REQUEST['start']) ? (int) $_REQUEST['start'] : 0; + + // If just deleting a draft, do it and then redirect back. + if (!empty($_REQUEST['delete'])) + { + checkSession('get'); + $id_delete = (int) $_REQUEST['delete']; + $start = isset($_REQUEST['start']) ? (int) $_REQUEST['start'] : 0; + + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}user_drafts + WHERE id_draft = {int:id_draft} + AND id_member = {int:id_member} + AND type = {int:draft_type}', + array( + 'id_draft' => $id_delete, + 'id_member' => $memID, + 'draft_type' => $draft_type, + ) + ); + + // now redirect back to the list + redirectexit('action=pm;sa=showpmdrafts;start=' . $start); + } + + // perhaps a draft was selected for editing? if so pass this off + if (!empty($_REQUEST['id_draft']) && !empty($context['drafts_pm_save']) && $memID == $user_info['id']) + { + checkSession('get'); + $id_draft = (int) $_REQUEST['id_draft']; + redirectexit('action=pm;sa=send;id_draft=' . $id_draft); + } + + // Default to 10. + if (empty($_REQUEST['viewscount']) || !is_numeric($_REQUEST['viewscount'])) + $_REQUEST['viewscount'] = 10; + + // Get the count of applicable drafts + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}user_drafts + WHERE id_member = {int:id_member} + AND type={int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? ' + AND poster_time > {int:time}' : ''), + array( + 'id_member' => $memID, + 'draft_type' => $draft_type, + 'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0), + ) + ); + list ($msgCount) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + $maxPerPage = empty($modSettings['disableCustomPerPage']) && !empty($options['messages_per_page']) ? $options['messages_per_page'] : $modSettings['defaultMaxMessages']; + $maxIndex = $maxPerPage; + + // Make sure the starting place makes sense and construct our friend the page index. + $context['page_index'] = constructPageIndex($scripturl . '?action=pm;sa=showpmdrafts', $context['start'], $msgCount, $maxIndex); + $context['current_page'] = $context['start'] / $maxIndex; + + // Reverse the query if we're past 50% of the total for better performance. + $start = $context['start']; + $reverse = $_REQUEST['start'] > $msgCount / 2; + if ($reverse) + { + $maxIndex = $msgCount < $context['start'] + $maxPerPage + 1 && $msgCount > $context['start'] ? $msgCount - $context['start'] : $maxPerPage; + $start = $msgCount < $context['start'] + $maxPerPage + 1 || $msgCount < $context['start'] + $maxPerPage ? 0 : $msgCount - $context['start'] - $maxPerPage; + } + + // Load in this user's PM drafts + $request = $smcFunc['db_query']('', ' + SELECT + ud.id_member, ud.id_draft, ud.body, ud.subject, ud.poster_time, ud.id_reply, ud.to_list + FROM {db_prefix}user_drafts AS ud + WHERE ud.id_member = {int:current_member} + AND type = {int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? ' + AND poster_time > {int:time}' : '') . ' + ORDER BY ud.id_draft ' . ($reverse ? 'ASC' : 'DESC') . ' + LIMIT {int:start}, {int:max}', + array( + 'current_member' => $memID, + 'draft_type' => $draft_type, + 'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0), + 'start' => $start, + 'max' => $maxIndex, + ) + ); + + // Start counting at the number of the first message displayed. + $counter = $reverse ? $context['start'] + $maxIndex + 1 : $context['start']; + $context['posts'] = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // Censor.... + if (empty($row['body'])) + $row['body'] = ''; + + $row['subject'] = $smcFunc['htmltrim']($row['subject']); + if (empty($row['subject'])) + $row['subject'] = $txt['no_subject']; + + censorText($row['body']); + censorText($row['subject']); + + // BBC-ilize the message. + $row['body'] = parse_bbc($row['body'], true, 'draft' . $row['id_draft']); + + // Have they provide who this will go to? + $recipients = array( + 'to' => array(), + 'bcc' => array(), + ); + $recipient_ids = (!empty($row['to_list'])) ? $smcFunc['json_decode']($row['to_list'], true) : array(); + + // @todo ... this is a bit ugly since it runs an extra query for every message, do we want this? + // at least its only for draft PM's and only the user can see them ... so not heavily used .. still + if (!empty($recipient_ids['to']) || !empty($recipient_ids['bcc'])) + { + $recipient_ids['to'] = array_map('intval', $recipient_ids['to']); + $recipient_ids['bcc'] = array_map('intval', $recipient_ids['bcc']); + $allRecipients = array_merge($recipient_ids['to'], $recipient_ids['bcc']); + + $request_2 = $smcFunc['db_query']('', ' + SELECT id_member, real_name + FROM {db_prefix}members + WHERE id_member IN ({array_int:member_list})', + array( + 'member_list' => $allRecipients, + ) + ); + while ($result = $smcFunc['db_fetch_assoc']($request_2)) + { + $recipientType = in_array($result['id_member'], $recipient_ids['bcc']) ? 'bcc' : 'to'; + $recipients[$recipientType][] = $result['real_name']; + } + $smcFunc['db_free_result']($request_2); + } + + // Add the items to the array for template use + $context['drafts'][$counter += $reverse ? -1 : 1] = array( + 'body' => $row['body'], + 'counter' => $counter, + 'subject' => $row['subject'], + 'time' => timeformat($row['poster_time']), + 'timestamp' => $row['poster_time'], + 'id_draft' => $row['id_draft'], + 'recipients' => $recipients, + 'age' => floor((time() - $row['poster_time']) / 86400), + 'remaining' => (!empty($modSettings['drafts_keep_days']) ? floor($modSettings['drafts_keep_days'] - ((time() - $row['poster_time']) / 86400)) : 0), + 'quickbuttons' => array( + 'edit' => array( + 'label' => $txt['draft_edit'], + 'href' => $scripturl.'?action=pm;sa=showpmdrafts;id_draft='.$row['id_draft'].';'.$context['session_var'].'='.$context['session_id'], + 'icon' => 'modify_button' + ), + 'delete' => array( + 'label' => $txt['draft_delete'], + 'href' => $scripturl.'?action=pm;sa=showpmdrafts;delete='.$row['id_draft'].';'.$context['session_var'].'='.$context['session_id'], + 'javascript' => 'data-confirm="'.$txt['draft_remove'].'?"', + 'class' => 'you_sure', + 'icon' => 'remove_button' + ), + ), + ); + } + $smcFunc['db_free_result']($request); + + // if the drafts were retrieved in reverse order, then put them in the right order again. + if ($reverse) + $context['drafts'] = array_reverse($context['drafts'], true); + + // off to the template we go + $context['page_title'] = $txt['drafts']; + $context['sub_template'] = 'showPMDrafts'; + $context['linktree'][] = array( + 'url' => $scripturl . '?action=pm;sa=showpmdrafts', + 'name' => $txt['drafts'], + ); +} + +?> \ No newline at end of file diff --git a/Sources/Errors.php b/Sources/Errors.php new file mode 100644 index 0000000..bb2b443 --- /dev/null +++ b/Sources/Errors.php @@ -0,0 +1,586 @@ + 2) + { + var_dump($backtrace); + die('Error loop.'); + } + + // Check if error logging is actually on. + if (empty($modSettings['enableErrorLogging'])) + return $error_message; + + // Basically, htmlspecialchars it minus &. (for entities!) + $error_message = strtr($error_message, array('<' => '<', '>' => '>', '"' => '"')); + $error_message = strtr($error_message, array('<br />' => '
', '<br>' => '
', '<b>' => '', '</b>' => '', "\n" => '
')); + + // Add a file and line to the error message? + // Don't use the actual txt entries for file and line but instead use %1$s for file and %2$s for line + if ($file == null) + $file = ''; + else + // Windows style slashes don't play well, lets convert them to the unix style. + $file = str_replace('\\', '/', $file); + + if ($line == null) + $line = 0; + else + $line = (int) $line; + + // Just in case there's no id_member or IP set yet. + if (empty($user_info['id'])) + $user_info['id'] = 0; + if (empty($user_info['ip'])) + $user_info['ip'] = ''; + + // Find the best query string we can... + $query_string = empty($_SERVER['QUERY_STRING']) ? (empty($_SERVER['REQUEST_URL']) ? '' : str_replace($scripturl, '', $_SERVER['REQUEST_URL'])) : $_SERVER['QUERY_STRING']; + + // Don't log the session hash in the url twice, it's a waste. + if (!empty($smcFunc['htmlspecialchars'])) + $query_string = $smcFunc['htmlspecialchars']((SMF == 'SSI' || SMF == 'BACKGROUND' ? '' : '?') . preg_replace(array('~;sesc=[^&;]+~', '~' . session_name() . '=' . session_id() . '[&;]~'), array(';sesc', ''), $query_string)); + + // Just so we know what board error messages are from. + if (isset($_POST['board']) && !isset($_GET['board'])) + $query_string .= ($query_string == '' ? 'board=' : ';board=') . $_POST['board']; + + // What types of categories do we have? + $known_error_types = array( + 'general', + 'critical', + 'database', + 'undefined_vars', + 'user', + 'ban', + 'template', + 'debug', + 'cron', + 'paidsubs', + 'backup', + 'login', + ); + + // This prevents us from infinite looping if the hook or call produces an error. + $other_error_types = array(); + if (empty($tried_hook)) + { + $tried_hook = true; + // Allow the hook to change the error_type and know about the error. + call_integration_hook('integrate_error_types', array(&$other_error_types, &$error_type, $error_message, $file, $line)); + $known_error_types += $other_error_types; + } + // Make sure the category that was specified is a valid one + $error_type = in_array($error_type, $known_error_types) && $error_type !== true ? $error_type : 'general'; + + // leave out the call to log_error + array_splice($backtrace, 0, 1); + $backtrace = !empty($smcFunc['json_encode']) ? $smcFunc['json_encode']($backtrace) : json_encode($backtrace); + + // Don't log the same error countless times, as we can get in a cycle of depression... + $error_info = array($user_info['id'], time(), $user_info['ip'], $query_string, $error_message, (string) $sc, $error_type, $file, $line, $backtrace); + if (empty($last_error) || $last_error != $error_info) + { + // Insert the error into the database. + $smcFunc['db_error_insert']($error_info); + $last_error = $error_info; + + // Get an error count, if necessary + if (!isset($context['num_errors'])) + { + $query = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}log_errors', + array() + ); + + list($context['num_errors']) = $smcFunc['db_fetch_row']($query); + $smcFunc['db_free_result']($query); + } + else + $context['num_errors']++; + } + + // reset error call + $error_call = 0; + + // Return the message to make things simpler. + return $error_message; +} + +/** + * An irrecoverable error. This function stops execution and displays an error message. + * It logs the error message if $log is specified. + * + * @param string $error The error message + * @param string|bool $log = 'general' What type of error to log this as (false to not log it)) + * @param int $status The HTTP status code associated with this error + */ +function fatal_error($error, $log = 'general', $status = 500) +{ + global $txt; + + // Send the appropriate HTTP status header - set this to 0 or false if you don't want to send one at all + if (!empty($status)) + send_http_status($status); + + // We don't have $txt yet, but that's okay... + if (empty($txt)) + die($error); + + log_error_online($error); + setup_fatal_error_context($log ? log_error($error, $log) : $error); +} + +/** + * Shows a fatal error with a message stored in the language file. + * + * This function stops execution and displays an error message by key. + * - uses the string with the error_message_key key. + * - logs the error in the forum's default language while displaying the error + * message in the user's language. + * - uses Errors language file and applies the $sprintf information if specified. + * - the information is logged if log is specified. + * + * @param string $error The error message + * @param string|false $log The type of error, or false to not log it + * @param array $sprintf An array of data to be sprintf()'d into the specified message + * @param int $status = false The HTTP status code associated with this error + */ +function fatal_lang_error($error, $log = 'general', $sprintf = array(), $status = 403) +{ + global $txt, $language, $user_info, $context; + static $fatal_error_called = false; + + // Ensure this is an array. + $sprintf = (array) $sprintf; + + // Send the status header - set this to 0 or false if you don't want to send one at all + if (!empty($status)) + send_http_status($status); + + // Try to load a theme if we don't have one. + if (empty($context['theme_loaded']) && empty($fatal_error_called)) + { + $fatal_error_called = true; + loadTheme(); + } + + // If we have no theme stuff we can't have the language file... + if (empty($context['theme_loaded'])) + die($error); + + $reload_lang_file = true; + // Log the error in the forum's language, but don't waste the time if we aren't logging + if ($log) + { + loadLanguage('Errors', $language); + $reload_lang_file = $language != $user_info['language']; + if (empty($txt[$error])) + $error_message = $error; + else + $error_message = empty($sprintf) ? $txt[$error] : vsprintf($txt[$error], $sprintf); + log_error($error_message, $log); + } + + // Load the language file, only if it needs to be reloaded + if ($reload_lang_file) + { + loadLanguage('Errors'); + $error_message = empty($sprintf) ? $txt[$error] : vsprintf($txt[$error], $sprintf); + } + + log_error_online($error, $sprintf); + setup_fatal_error_context($error_message, $error); +} + +/** + * Handler for standard error messages, standard PHP error handler replacement. + * It dies with fatal_error() if the error_level matches with error_reporting. + * + * @param int $error_level A pre-defined error-handling constant (see {@link https://php.net/errorfunc.constants}) + * @param string $error_string The error message + * @param string $file The file where the error occurred + * @param int $line The line where the error occurred + */ +function smf_error_handler($error_level, $error_string, $file, $line) +{ + global $settings, $modSettings, $db_show_debug; + + // Error was suppressed with the @-operator. + if (error_reporting() == 0 || error_reporting() == (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR)) + return true; + + // Ignore errors that should should not be logged. + $error_match = error_reporting() & $error_level; + if (empty($error_match) || empty($modSettings['enableErrorLogging'])) + return false; + + if (strpos($file, 'eval()') !== false && !empty($settings['current_include_filename'])) + { + $array = debug_backtrace(); + $count = count($array); + for ($i = 0; $i < $count; $i++) + { + if ($array[$i]['function'] != 'loadSubTemplate') + continue; + + // This is a bug in PHP, with eval, it seems! + if (empty($array[$i]['args'])) + $i++; + break; + } + + if (isset($array[$i]) && !empty($array[$i]['args'])) + $file = realpath($settings['current_include_filename']) . ' (' . $array[$i]['args'][0] . ' sub template - eval?)'; + else + $file = realpath($settings['current_include_filename']) . ' (eval?)'; + } + + if (isset($db_show_debug) && $db_show_debug === true) + { + // Commonly, undefined indexes will occur inside attributes; try to show them anyway! + if ($error_level % 255 != E_ERROR) + { + $temporary = ob_get_contents(); + if (substr($temporary, -2) == '="') + echo '"'; + } + + // Debugging! This should look like a PHP error message. + echo '
+', $error_level % 255 == E_ERROR ? 'Error' : ($error_level % 255 == E_WARNING ? 'Warning' : 'Notice'), ': ', $error_string, ' in ', $file, ' on line ', $line, '
'; + } + + $error_type = stripos($error_string, 'undefined') !== false ? 'undefined_vars' : 'general'; + + $message = log_error($error_level . ': ' . $error_string, $error_type, $file, $line); + + // Let's give integrations a chance to ouput a bit differently + call_integration_hook('integrate_output_error', array($message, $error_type, $error_level, $file, $line)); + + // Dying on these errors only causes MORE problems (blank pages!) + if ($file == 'Unknown') + return; + + // If this is an E_ERROR or E_USER_ERROR.... die. Violently so. + if ($error_level % 255 == E_ERROR) + obExit(false); + else + return; + + // If this is an E_ERROR, E_USER_ERROR, E_WARNING, or E_USER_WARNING.... die. Violently so. + if ($error_level % 255 == E_ERROR || $error_level % 255 == E_WARNING) + fatal_error(allowedTo('admin_forum') ? $message : $error_string, false); + + // We should NEVER get to this point. Any fatal error MUST quit, or very bad things can happen. + if ($error_level % 255 == E_ERROR) + die('No direct access...'); +} + +/** + * It is called by {@link fatal_error()} and {@link fatal_lang_error()}. + * + * @uses template_fatal_error() + * + * @param string $error_message The error message + * @param string $error_code An error code + * @return void|false Normally doesn't return anything, but returns false if a recursive loop is detected + */ +function setup_fatal_error_context($error_message, $error_code = null) +{ + global $context, $txt, $ssi_on_error_method; + static $level = 0; + + // Attempt to prevent a recursive loop. + ++$level; + if ($level > 1) + return false; + + // Maybe they came from dlattach or similar? + if (SMF != 'SSI' && SMF != 'BACKGROUND' && empty($context['theme_loaded'])) + loadTheme(); + + // Don't bother indexing errors mate... + $context['robot_no_index'] = true; + + if (!isset($context['error_title'])) + $context['error_title'] = $txt['error_occured']; + $context['error_message'] = isset($context['error_message']) ? $context['error_message'] : $error_message; + + $context['error_code'] = isset($error_code) ? 'id="' . $error_code . '" ' : ''; + + $context['error_link'] = isset($context['error_link']) ? $context['error_link'] : 'javascript:document.location=document.referrer'; + + if (empty($context['page_title'])) + $context['page_title'] = $context['error_title']; + + loadTemplate('Errors'); + $context['sub_template'] = 'fatal_error'; + + // If this is SSI, what do they want us to do? + if (SMF == 'SSI') + { + if (!empty($ssi_on_error_method) && $ssi_on_error_method !== true && is_callable($ssi_on_error_method)) + $ssi_on_error_method(); + elseif (empty($ssi_on_error_method) || $ssi_on_error_method !== true) + loadSubTemplate('fatal_error'); + + // No layers? + if (empty($ssi_on_error_method) || $ssi_on_error_method !== true) + exit; + } + // Alternatively from the cron call? + elseif (SMF == 'BACKGROUND') + { + // We can't rely on even having language files available. + if (defined('FROM_CLI') && FROM_CLI) + echo 'cron error: ', $context['error_message']; + else + echo 'An error occurred. More information may be available in your logs.'; + exit; + } + + // We want whatever for the header, and a footer. (footer includes sub template!) + obExit(null, true, false, true); + + /* DO NOT IGNORE: + If you are creating a bridge to SMF or modifying this function, you MUST + make ABSOLUTELY SURE that this function quits and DOES NOT RETURN TO NORMAL + PROGRAM FLOW. Otherwise, security error messages will not be shown, and + your forum will be in a very easily hackable state. + */ + trigger_error('No direct access...', E_USER_ERROR); +} + +/** + * Show a message for the (full block) maintenance mode. + * It shows a complete page independent of language files or themes. + * It is used only if $maintenance = 2 in Settings.php. + * It stops further execution of the script. + */ +function display_maintenance_message() +{ + global $maintenance, $mtitle, $mmessage; + + set_fatal_error_headers(); + + if (!empty($maintenance)) + echo ' + + + + ', $mtitle, ' + + +

', $mtitle, '

+ ', $mmessage, ' + +'; + + die(); +} + +/** + * Show an error message for the connection problems. + * It shows a complete page independent of language files or themes. + * It is used only if there's no way to connect to the database. + * It stops further execution of the script. + */ +function display_db_error() +{ + global $mbname, $modSettings, $maintenance; + global $db_connection, $webmaster_email, $db_last_error, $db_error_send, $smcFunc, $sourcedir, $cache_enable; + + require_once($sourcedir . '/Logging.php'); + set_fatal_error_headers(); + + // For our purposes, we're gonna want this on if at all possible. + $cache_enable = '1'; + + if (($temp = cache_get_data('db_last_error', 600)) !== null) + $db_last_error = max($db_last_error, $temp); + + if ($db_last_error < time() - 3600 * 24 * 3 && empty($maintenance) && !empty($db_error_send)) + { + // Avoid writing to the Settings.php file if at all possible; use shared memory instead. + cache_put_data('db_last_error', time(), 600); + if (($temp = cache_get_data('db_last_error', 600)) === null) + logLastDatabaseError(); + + // Language files aren't loaded yet :(. + $db_error = @$smcFunc['db_error']($db_connection); + @mail($webmaster_email, $mbname . ': SMF Database Error!', 'There has been a problem with the database!' . ($db_error == '' ? '' : "\n" . $smcFunc['db_title'] . ' reported:' . "\n" . $db_error) . "\n\n" . 'This is a notice email to let you know that SMF could not connect to the database, contact your host if this continues.'); + } + + // What to do? Language files haven't and can't be loaded yet... + echo ' + + + + Connection Problems + + +

Connection Problems

+ Sorry, SMF was unable to connect to the database. This may be caused by the server being busy. Please try again later. + +'; + + die(); +} + +/** + * Show an error message for load average blocking problems. + * It shows a complete page independent of language files or themes. + * It is used only if the load averages are too high to continue execution. + * It stops further execution of the script. + */ +function display_loadavg_error() +{ + // If this is a load average problem, display an appropriate message (but we still don't have language files!) + + set_fatal_error_headers(); + + echo ' + + + + Temporarily Unavailable + + +

Temporarily Unavailable

+ Due to high stress on the server the forum is temporarily unavailable. Please try again later. + +'; + + die(); +} + +/** + * Small utility function for fatal error pages. + * Used by {@link display_db_error()}, {@link display_loadavg_error()}, + * {@link display_maintenance_message()} + */ +function set_fatal_error_headers() +{ + if (headers_sent()) + return; + + // Don't cache this page! + header('expires: Mon, 26 Jul 1997 05:00:00 GMT'); + header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + header('cache-control: no-cache'); + + // Send the right error codes. + send_http_status(503, 'Service Temporarily Unavailable'); + header('status: 503 Service Temporarily Unavailable'); + header('retry-after: 3600'); +} + +/** + * Small utility function for fatal error pages. + * Used by fatal_error(), fatal_lang_error() + * + * @param string $error The error + * @param array $sprintf An array of data to be sprintf()'d into the specified message + */ +function log_error_online($error, $sprintf = array()) +{ + global $smcFunc, $user_info, $modSettings; + + // Don't bother if Who's Online is disabled. + if (empty($modSettings['who_enabled'])) + return; + + // Maybe they came from SSI or similar where sessions are not recorded? + if (SMF == 'SSI' || SMF == 'BACKGROUND') + return; + + $session_id = !empty($user_info['is_guest']) ? 'ip' . $user_info['ip'] : session_id(); + + // First, we have to get the online log, because we need to break apart the serialized string. + $request = $smcFunc['db_query']('', ' + SELECT url + FROM {db_prefix}log_online + WHERE session = {string:session}', + array( + 'session' => $session_id, + ) + ); + if ($smcFunc['db_num_rows']($request) != 0) + { + // If this happened very early on in SMF startup, $smcFunc may not fully be defined. + if (!isset($smcFunc['json_decode'])) + { + $smcFunc['json_decode'] = 'smf_json_decode'; + $smcFunc['json_encode'] = 'json_encode'; + } + + list ($url) = $smcFunc['db_fetch_row']($request); + $url = $smcFunc['json_decode']($url, true); + $url['error'] = $error; + // Url field got a max length of 1024 in db + if (strlen($url['error']) > 500) + $url['error'] = substr($url['error'], 0, 500); + + if (!empty($sprintf)) + $url['error_params'] = $sprintf; + + $smcFunc['db_query']('', ' + UPDATE {db_prefix}log_online + SET url = {string:url} + WHERE session = {string:session}', + array( + 'url' => $smcFunc['json_encode']($url), + 'session' => $session_id, + ) + ); + } + $smcFunc['db_free_result']($request); +} + +?> \ No newline at end of file diff --git a/Sources/Groups.php b/Sources/Groups.php new file mode 100644 index 0000000..cabbad2 --- /dev/null +++ b/Sources/Groups.php @@ -0,0 +1,785 @@ + array('GroupList', 'view_groups'), + 'members' => array('MembergroupMembers', 'view_groups'), + 'requests' => array('GroupRequests', 'group_requests'), + ); + + call_integration_hook('integrate_manage_groups', array(&$subActions)); + + // Default to sub action 'index'. + $_REQUEST['sa'] = isset($_REQUEST['sa']) && isset($subActions[$_REQUEST['sa']]) ? $_REQUEST['sa'] : 'index'; + + // Get the template stuff up and running. + loadLanguage('ManageMembers'); + loadLanguage('ModerationCenter'); + loadTemplate('ManageMembergroups'); + + // If we can see the moderation center, and this has a mod bar entry, add the mod center bar. + if (allowedTo('access_mod_center') || $user_info['mod_cache']['bq'] != '0=1' || $user_info['mod_cache']['gq'] != '0=1' || allowedTo('manage_membergroups')) + { + require_once($sourcedir . '/ModerationCenter.php'); + $_GET['area'] = $_REQUEST['sa'] == 'requests' ? 'groups' : 'viewgroups'; + ModerationMain(true); + } + // Otherwise add something to the link tree, for normal people. + else + { + isAllowedTo('view_mlist'); + + $context['linktree'][] = array( + 'url' => $scripturl . '?action=groups', + 'name' => $txt['groups'], + ); + } + + // Call the actual function. + call_helper($subActions[$_REQUEST['sa']][0]); +} + +/** + * This very simply lists the groups, nothing snazy. + */ +function GroupList() +{ + global $txt, $context, $sourcedir, $scripturl; + + $context['page_title'] = $txt['viewing_groups']; + + // Making a list is not hard with this beauty. + require_once($sourcedir . '/Subs-List.php'); + + // Use the standard templates for showing this. + $listOptions = array( + 'id' => 'group_lists', + 'title' => $context['page_title'], + 'base_href' => $scripturl . '?action=moderate;area=viewgroups;sa=view', + 'default_sort_col' => 'group', + 'get_items' => array( + 'file' => $sourcedir . '/Subs-Membergroups.php', + 'function' => 'list_getMembergroups', + 'params' => array( + 'regular', + ), + ), + 'columns' => array( + 'group' => array( + 'header' => array( + 'value' => $txt['name'], + ), + 'data' => array( + 'function' => function($rowData) use ($scripturl) + { + // Since the moderator group has no explicit members, no link is needed. + if ($rowData['id_group'] == 3) + $group_name = $rowData['group_name']; + else + { + $color_style = empty($rowData['online_color']) ? '' : sprintf(' style="color: %1$s;"', $rowData['online_color']); + + if (allowedTo('manage_membergroups')) + { + $group_name = sprintf('%4$s', $scripturl, $rowData['id_group'], $color_style, $rowData['group_name']); + } + else + { + $group_name = sprintf('%4$s', $scripturl, $rowData['id_group'], $color_style, $rowData['group_name']); + } + } + + // Add a help option for moderator and administrator. + if ($rowData['id_group'] == 1) + $group_name .= sprintf(' (?)', $scripturl); + elseif ($rowData['id_group'] == 3) + $group_name .= sprintf(' (?)', $scripturl); + + return $group_name; + }, + ), + 'sort' => array( + 'default' => 'CASE WHEN mg.id_group < 4 THEN mg.id_group ELSE 4 END, mg.group_name', + 'reverse' => 'CASE WHEN mg.id_group < 4 THEN mg.id_group ELSE 4 END, mg.group_name DESC', + ), + ), + 'icons' => array( + 'header' => array( + 'value' => $txt['membergroups_icons'], + ), + 'data' => array( + 'db' => 'icons', + ), + 'sort' => array( + 'default' => 'mg.icons', + 'reverse' => 'mg.icons DESC', + ) + ), + 'moderators' => array( + 'header' => array( + 'value' => $txt['moderators'], + ), + 'data' => array( + 'function' => function($group) use ($txt) + { + return empty($group['moderators']) ? '' . $txt['membergroups_new_copy_none'] . '' : implode(', ', $group['moderators']); + }, + ), + ), + 'members' => array( + 'header' => array( + 'value' => $txt['membergroups_members_top'], + ), + 'data' => array( + 'function' => function($rowData) use ($txt) + { + // No explicit members for the moderator group. + return $rowData['id_group'] == 3 ? $txt['membergroups_guests_na'] : comma_format($rowData['num_members']); + }, + 'class' => 'centercol', + ), + 'sort' => array( + 'default' => 'CASE WHEN mg.id_group < 4 THEN mg.id_group ELSE 4 END, 1', + 'reverse' => 'CASE WHEN mg.id_group < 4 THEN mg.id_group ELSE 4 END, 1 DESC', + ), + ), + ), + ); + + // Create the request list. + createList($listOptions); + + $context['sub_template'] = 'show_list'; + $context['default_list'] = 'group_lists'; +} + +/** + * Display members of a group, and allow adding of members to a group. Silly function name though ;) + * It can be called from ManageMembergroups if it needs templating within the admin environment. + * It shows a list of members that are part of a given membergroup. + * It is called by ?action=moderate;area=viewgroups;sa=members;group=x + * It requires the manage_membergroups permission. + * It allows to add and remove members from the selected membergroup. + * It allows sorting on several columns. + * It redirects to itself. + * + * @uses template_group_members() + * @todo: use createList + */ +function MembergroupMembers() +{ + global $txt, $scripturl, $context, $modSettings, $sourcedir, $user_info, $settings, $smcFunc; + + $_REQUEST['group'] = isset($_REQUEST['group']) ? (int) $_REQUEST['group'] : 0; + + // No browsing of guests, membergroup 0 or moderators. + if (in_array($_REQUEST['group'], array(-1, 0, 3))) + fatal_lang_error('membergroup_does_not_exist', false); + + // Load up the group details. + $request = $smcFunc['db_query']('', ' + SELECT id_group AS id, group_name AS name, CASE WHEN min_posts = {int:min_posts} THEN 1 ELSE 0 END AS assignable, hidden, online_color, + icons, description, CASE WHEN min_posts != {int:min_posts} THEN 1 ELSE 0 END AS is_post_group, group_type + FROM {db_prefix}membergroups + WHERE id_group = {int:id_group} + LIMIT 1', + array( + 'min_posts' => -1, + 'id_group' => $_REQUEST['group'], + ) + ); + // Doesn't exist? + if ($smcFunc['db_num_rows']($request) == 0) + fatal_lang_error('membergroup_does_not_exist', false); + $context['group'] = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + // Fix the membergroup icons. + $context['group']['icons'] = explode('#', $context['group']['icons']); + $context['group']['icons'] = !empty($context['group']['icons'][0]) && !empty($context['group']['icons'][1]) ? str_repeat('*', $context['group']['icons'][0]) : ''; + $context['group']['can_moderate'] = allowedTo('manage_membergroups') && (allowedTo('admin_forum') || $context['group']['group_type'] != 1); + + $context['linktree'][] = array( + 'url' => $scripturl . '?action=groups;sa=members;group=' . $context['group']['id'], + 'name' => $context['group']['name'], + ); + $context['can_send_email'] = allowedTo('moderate_forum'); + + // Load all the group moderators, for fun. + $request = $smcFunc['db_query']('', ' + SELECT mem.id_member, mem.real_name + FROM {db_prefix}group_moderators AS mods + INNER JOIN {db_prefix}members AS mem ON (mem.id_member = mods.id_member) + WHERE mods.id_group = {int:id_group}', + array( + 'id_group' => $_REQUEST['group'], + ) + ); + $context['group']['moderators'] = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $context['group']['moderators'][] = array( + 'id' => $row['id_member'], + 'name' => $row['real_name'] + ); + + if ($user_info['id'] == $row['id_member'] && $context['group']['group_type'] != 1) + $context['group']['can_moderate'] = true; + } + $smcFunc['db_free_result']($request); + + // If this group is hidden then it can only "exists" if the user can moderate it! + if ($context['group']['hidden'] && !$context['group']['can_moderate']) + fatal_lang_error('membergroup_does_not_exist', false); + + // You can only assign membership if you are the moderator and/or can manage groups! + if (!$context['group']['can_moderate']) + $context['group']['assignable'] = 0; + // Non-admins cannot assign admins. + elseif ($context['group']['id'] == 1 && !allowedTo('admin_forum')) + $context['group']['assignable'] = 0; + + // Removing member from group? + if (isset($_POST['remove']) && !empty($_REQUEST['rem']) && is_array($_REQUEST['rem']) && $context['group']['assignable']) + { + checkSession(); + validateToken('mod-mgm'); + + // Only proven admins can remove admins. + if ($context['group']['id'] == 1) + validateSession(); + + // Make sure we're dealing with integers only. + foreach ($_REQUEST['rem'] as $key => $group) + $_REQUEST['rem'][$key] = (int) $group; + + require_once($sourcedir . '/Subs-Membergroups.php'); + removeMembersFromGroups($_REQUEST['rem'], $_REQUEST['group'], true); + } + // Must be adding new members to the group... + elseif (isset($_REQUEST['add']) && (!empty($_REQUEST['toAdd']) || !empty($_REQUEST['member_add'])) && $context['group']['assignable']) + { + // Demand an admin password before adding new admins -- every time, no matter what. + if ($context['group']['id'] == 1) + validateSession('admin', true); + + checkSession(); + validateToken('mod-mgm'); + + $member_query = array(); + $member_parameters = array(); + + // Get all the members to be added... taking into account names can be quoted ;) + $_REQUEST['toAdd'] = strtr($smcFunc['htmlspecialchars']($_REQUEST['toAdd'], ENT_QUOTES), array('"' => '"')); + preg_match_all('~"([^"]+)"~', $_REQUEST['toAdd'], $matches); + $member_names = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $_REQUEST['toAdd'])))); + + foreach ($member_names as $index => $member_name) + { + $member_names[$index] = trim($smcFunc['strtolower']($member_names[$index])); + + if (strlen($member_names[$index]) == 0) + unset($member_names[$index]); + } + + // Any passed by ID? + $member_ids = array(); + if (!empty($_REQUEST['member_add'])) + foreach ($_REQUEST['member_add'] as $id) + if ($id > 0) + $member_ids[] = (int) $id; + + // Construct the query pelements. + if (!empty($member_ids)) + { + $member_query[] = 'id_member IN ({array_int:member_ids})'; + $member_parameters['member_ids'] = $member_ids; + } + if (!empty($member_names)) + { + $member_query[] = 'LOWER(member_name) IN ({array_string:member_names})'; + $member_query[] = 'LOWER(real_name) IN ({array_string:member_names})'; + $member_parameters['member_names'] = $member_names; + } + + $members = array(); + if (!empty($member_query)) + { + $request = $smcFunc['db_query']('', ' + SELECT id_member + FROM {db_prefix}members + WHERE (' . implode(' OR ', $member_query) . ') + AND id_group != {int:id_group} + AND FIND_IN_SET({int:id_group}, additional_groups) = 0', + array_merge($member_parameters, array( + 'id_group' => $_REQUEST['group'], + )) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $members[] = $row['id_member']; + $smcFunc['db_free_result']($request); + } + + // @todo Add $_POST['additional'] to templates! + + // Do the updates... + if (!empty($members)) + { + require_once($sourcedir . '/Subs-Membergroups.php'); + addMembersToGroup($members, $_REQUEST['group'], isset($_POST['additional']) || $context['group']['hidden'] ? 'only_additional' : 'auto', true); + } + } + + // Sort out the sorting! + $sort_methods = array( + 'name' => 'real_name', + 'email' => 'email_address', + 'active' => 'last_login', + 'registered' => 'date_registered', + 'posts' => 'posts', + ); + + // They didn't pick one, default to by name.. + if (!isset($_REQUEST['sort']) || !isset($sort_methods[$_REQUEST['sort']])) + { + $context['sort_by'] = 'name'; + $querySort = 'real_name'; + } + // Otherwise default to ascending. + else + { + $context['sort_by'] = $_REQUEST['sort']; + $querySort = $sort_methods[$_REQUEST['sort']]; + } + + $context['sort_direction'] = isset($_REQUEST['desc']) ? 'down' : 'up'; + + // The where on the query is interesting. Non-moderators should only see people who are in this group as primary. + if ($context['group']['can_moderate']) + $where = $context['group']['is_post_group'] ? 'id_post_group = {int:group}' : 'id_group = {int:group} OR FIND_IN_SET({int:group}, additional_groups) != 0'; + else + $where = $context['group']['is_post_group'] ? 'id_post_group = {int:group}' : 'id_group = {int:group}'; + + // Count members of the group. + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}members + WHERE ' . $where, + array( + 'group' => $_REQUEST['group'], + ) + ); + list ($context['total_members']) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // Create the page index. + $context['page_index'] = constructPageIndex($scripturl . '?action=' . ($context['group']['can_moderate'] ? 'moderate;area=viewgroups' : 'groups') . ';sa=members;group=' . $_REQUEST['group'] . ';sort=' . $context['sort_by'] . (isset($_REQUEST['desc']) ? ';desc' : ''), $_REQUEST['start'], $context['total_members'], $modSettings['defaultMaxMembers']); + $context['total_members'] = comma_format($context['total_members']); + $context['start'] = $_REQUEST['start']; + $context['can_moderate_forum'] = allowedTo('moderate_forum'); + + // Load up all members of this group. + $request = $smcFunc['db_query']('', ' + SELECT id_member, member_name, real_name, email_address, member_ip, date_registered, last_login, + posts, is_activated, real_name + FROM {db_prefix}members + WHERE ' . $where . ' + ORDER BY ' . $querySort . ' ' . ($context['sort_direction'] == 'down' ? 'DESC' : 'ASC') . ' + LIMIT {int:start}, {int:max}', + array( + 'group' => $_REQUEST['group'], + 'start' => $context['start'], + 'max' => $modSettings['defaultMaxMembers'], + ) + ); + $context['members'] = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $row['member_ip'] = inet_dtop($row['member_ip']); + $last_online = empty($row['last_login']) ? $txt['never'] : timeformat($row['last_login']); + + // Italicize the online note if they aren't activated. + if ($row['is_activated'] % 10 != 1) + $last_online = '' . $last_online . ''; + + $context['members'][] = array( + 'id' => $row['id_member'], + 'name' => '' . $row['real_name'] . '', + 'email' => $row['email_address'], + 'ip' => '' . $row['member_ip'] . '', + 'registered' => timeformat($row['date_registered']), + 'last_online' => $last_online, + 'posts' => comma_format($row['posts']), + 'is_activated' => $row['is_activated'] % 10 == 1, + ); + } + $smcFunc['db_free_result']($request); + + // Select the template. + $context['sub_template'] = 'group_members'; + $context['page_title'] = $txt['membergroups_members_title'] . ': ' . $context['group']['name']; + createToken('mod-mgm'); + + if ($context['group']['assignable']) + loadJavaScriptFile('suggest.js', array('defer' => false, 'minimize' => true), 'smf_suggest'); +} + +/** + * Show and manage all group requests. + */ +function GroupRequests() +{ + global $txt, $context, $scripturl, $user_info, $sourcedir, $smcFunc, $modSettings; + + // Set up the template stuff... + $context['page_title'] = $txt['mc_group_requests']; + $context['sub_template'] = 'show_list'; + + // Verify we can be here. + if ($user_info['mod_cache']['gq'] == '0=1') + isAllowedTo('manage_membergroups'); + + // Normally, we act normally... + $where = ($user_info['mod_cache']['gq'] == '1=1' || $user_info['mod_cache']['gq'] == '0=1' ? $user_info['mod_cache']['gq'] : 'lgr.' . $user_info['mod_cache']['gq']); + + if (isset($_GET['closed'])) + $where .= ' AND lgr.status != {int:status_open}'; + else + $where .= ' AND lgr.status = {int:status_open}'; + + $where_parameters = array( + 'status_open' => 0, + ); + + // We've submitted? + if (isset($_POST[$context['session_var']]) && !empty($_POST['groupr']) && !empty($_POST['req_action'])) + { + checkSession(); + validateToken('mod-gr'); + + // Clean the values. + foreach ($_POST['groupr'] as $k => $request) + $_POST['groupr'][$k] = (int) $request; + + $log_changes = array(); + + // If we are giving a reason (And why shouldn't we?), then we don't actually do much. + if ($_POST['req_action'] == 'reason') + { + // Different sub template... + $context['sub_template'] = 'group_request_reason'; + // And a limitation. We don't care that the page number bit makes no sense, as we don't need it! + $where .= ' AND lgr.id_request IN ({array_int:request_ids})'; + $where_parameters['request_ids'] = $_POST['groupr']; + + $context['group_requests'] = list_getGroupRequests(0, $modSettings['defaultMaxListItems'], 'lgr.id_request', $where, $where_parameters); + + // Need to make another token for this. + createToken('mod-gr'); + + // Let obExit etc sort things out. + obExit(); + } + // Otherwise we do something! + else + { + $request = $smcFunc['db_query']('', ' + SELECT lgr.id_request + FROM {db_prefix}log_group_requests AS lgr + WHERE ' . $where . ' + AND lgr.id_request IN ({array_int:request_list})', + array( + 'request_list' => $_POST['groupr'], + 'status_open' => 0, + ) + ); + $request_list = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (!isset($log_changes[$row['id_request']])) + $log_changes[$row['id_request']] = array( + 'id_request' => $row['id_request'], + 'status' => $_POST['req_action'] == 'approve' ? 1 : 2, // 1 = approved, 2 = rejected + 'id_member_acted' => $user_info['id'], + 'member_name_acted' => $user_info['name'], + 'time_acted' => time(), + 'act_reason' => $_POST['req_action'] != 'approve' && !empty($_POST['groupreason']) && !empty($_POST['groupreason'][$row['id_request']]) ? $smcFunc['htmlspecialchars']($_POST['groupreason'][$row['id_request']], ENT_QUOTES) : '', + ); + $request_list[] = $row['id_request']; + } + $smcFunc['db_free_result']($request); + + // Add a background task to handle notifying people of this request + $data = $smcFunc['json_encode'](array('member_id' => $user_info['id'], 'member_ip' => $user_info['ip'], 'request_list' => $request_list, 'status' => $_POST['req_action'], 'reason' => isset($_POST['groupreason']) ? $_POST['groupreason'] : '', 'time' => time())); + $smcFunc['db_insert']('insert', '{db_prefix}background_tasks', + array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'), + array('$sourcedir/tasks/GroupAct-Notify.php', 'GroupAct_Notify_Background', $data, 0), array() + ); + + // Some changes to log? + if (!empty($log_changes)) + { + foreach ($log_changes as $id_request => $details) + { + $smcFunc['db_query']('', ' + UPDATE {db_prefix}log_group_requests + SET status = {int:status}, + id_member_acted = {int:id_member_acted}, + member_name_acted = {string:member_name_acted}, + time_acted = {int:time_acted}, + act_reason = {string:act_reason} + WHERE id_request = {int:id_request}', + $details + ); + } + } + } + } + + // We're going to want this for making our list. + require_once($sourcedir . '/Subs-List.php'); + + // This is all the information required for a group listing. + $listOptions = array( + 'id' => 'group_request_list', + 'width' => '100%', + 'items_per_page' => $modSettings['defaultMaxListItems'], + 'no_items_label' => $txt['mc_groupr_none_found'], + 'base_href' => $scripturl . '?action=groups;sa=requests', + 'default_sort_col' => 'member', + 'get_items' => array( + 'function' => 'list_getGroupRequests', + 'params' => array( + $where, + $where_parameters, + ), + ), + 'get_count' => array( + 'function' => 'list_getGroupRequestCount', + 'params' => array( + $where, + $where_parameters, + ), + ), + 'columns' => array( + 'member' => array( + 'header' => array( + 'value' => $txt['mc_groupr_member'], + ), + 'data' => array( + 'db' => 'member_link', + ), + 'sort' => array( + 'default' => 'mem.member_name', + 'reverse' => 'mem.member_name DESC', + ), + ), + 'group' => array( + 'header' => array( + 'value' => $txt['mc_groupr_group'], + ), + 'data' => array( + 'db' => 'group_link', + ), + 'sort' => array( + 'default' => 'mg.group_name', + 'reverse' => 'mg.group_name DESC', + ), + ), + 'reason' => array( + 'header' => array( + 'value' => $txt['mc_groupr_reason'], + ), + 'data' => array( + 'db' => 'reason', + ), + ), + 'date' => array( + 'header' => array( + 'value' => $txt['date'], + 'style' => 'width: 18%; white-space:nowrap;', + ), + 'data' => array( + 'db' => 'time_submitted', + ), + ), + 'action' => array( + 'header' => array( + 'value' => '', + 'style' => 'width: 4%;', + 'class' => 'centercol', + ), + 'data' => array( + 'sprintf' => array( + 'format' => '', + 'params' => array( + 'id' => false, + ), + ), + 'class' => 'centercol', + ), + ), + ), + 'form' => array( + 'href' => $scripturl . '?action=groups;sa=requests', + 'include_sort' => true, + 'include_start' => true, + 'hidden_fields' => array( + $context['session_var'] => $context['session_id'], + ), + 'token' => 'mod-gr', + ), + 'additional_rows' => array( + array( + 'position' => 'bottom_of_list', + 'value' => ' + + ', + 'class' => 'floatright', + ), + ), + ); + + if (isset($_GET['closed'])) + { + // Closed requests don't require interaction. + unset($listOptions['columns']['action'], $listOptions['form'], $listOptions['additional_rows'][0]); + $listOptions['base_href'] .= 'closed'; + } + + // Create the request list. + createToken('mod-gr'); + createList($listOptions); + + $context['default_list'] = 'group_request_list'; + $context[$context['moderation_menu_name']]['tab_data'] = array( + 'title' => $txt['mc_group_requests'], + ); +} + +/** + * Callback function for createList(). + * + * @param string $where The WHERE clause for the query + * @param array $where_parameters The parameters for the WHERE clause + * @return int The number of group requests + */ +function list_getGroupRequestCount($where, $where_parameters) +{ + global $smcFunc; + + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}log_group_requests AS lgr + WHERE ' . $where, + array_merge($where_parameters, array( + )) + ); + list ($totalRequests) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + return $totalRequests; +} + +/** + * Callback function for createList() + * + * @param int $start The result to start with + * @param int $items_per_page The number of items per page + * @param string $sort An SQL sort expression (column/direction) + * @param string $where Data for the WHERE clause + * @param string $where_parameters Parameter values to be inserted into the WHERE clause + * @return array An array of group requests + * Each group request has: + * 'id' + * 'member_link' + * 'group_link' + * 'reason' + * 'time_submitted' + */ +function list_getGroupRequests($start, $items_per_page, $sort, $where, $where_parameters) +{ + global $smcFunc, $scripturl, $txt; + + $request = $smcFunc['db_query']('', ' + SELECT + lgr.id_request, lgr.id_member, lgr.id_group, lgr.time_applied, lgr.reason, + lgr.status, lgr.id_member_acted, lgr.member_name_acted, lgr.time_acted, lgr.act_reason, + mem.member_name, mg.group_name, mg.online_color, mem.real_name + FROM {db_prefix}log_group_requests AS lgr + INNER JOIN {db_prefix}members AS mem ON (mem.id_member = lgr.id_member) + INNER JOIN {db_prefix}membergroups AS mg ON (mg.id_group = lgr.id_group) + WHERE ' . $where . ' + ORDER BY {raw:sort} + LIMIT {int:start}, {int:max}', + array_merge($where_parameters, array( + 'sort' => $sort, + 'start' => $start, + 'max' => $items_per_page, + )) + ); + $group_requests = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (empty($row['reason'])) + $reason = '(' . $txt['mc_groupr_no_reason'] . ')'; + else + $reason = censorText($row['reason']); + + if (isset($_GET['closed'])) + { + if ($row['status'] == 1) + $reason .= '

' . $txt['mc_groupr_approved'] . ''; + elseif ($row['status'] == 2) + $reason .= '

' . $txt['mc_groupr_rejected'] . ''; + + $reason .= ' (' . timeformat($row['time_acted']) . ')'; + if (!empty($row['act_reason'])) + $reason .= '

' . censorText($row['act_reason']); + } + + $group_requests[] = array( + 'id' => $row['id_request'], + 'member_link' => '' . $row['real_name'] . '', + 'group_link' => '' . $row['group_name'] . '', + 'reason' => $reason, + 'time_submitted' => timeformat($row['time_applied']), + ); + } + $smcFunc['db_free_result']($request); + + return $group_requests; +} + +?> \ No newline at end of file diff --git a/Sources/Help.php b/Sources/Help.php new file mode 100644 index 0000000..7444481 --- /dev/null +++ b/Sources/Help.php @@ -0,0 +1,147 @@ + 'HelpIndex', + ); + + // CRUD $subActions as needed. + call_integration_hook('integrate_manage_help', array(&$subActions)); + + $sa = isset($_GET['sa'], $subActions[$_GET['sa']]) ? $_GET['sa'] : 'index'; + call_helper($subActions[$sa]); +} + +/** + * The main page for the Help section + */ +function HelpIndex() +{ + global $scripturl, $context, $txt; + + // We need to know where our wiki is. + $context['wiki_url'] = 'https://wiki.simplemachines.org/smf'; + $context['wiki_prefix'] = 'SMF2.1:'; + + $context['canonical_url'] = $scripturl . '?action=help'; + + // Sections were are going to link... + $context['manual_sections'] = array( + 'registering' => 'Registering', + 'logging_in' => 'Logging_In', + 'profile' => 'Profile', + 'search' => 'Search', + 'posting' => 'Posting', + 'bbc' => 'Bulletin_board_code', + 'personal_messages' => 'Personal_messages', + 'memberlist' => 'Memberlist', + 'calendar' => 'Calendar', + 'features' => 'Features', + ); + + // Build the link tree. + $context['linktree'][] = array( + 'url' => $scripturl . '?action=help', + 'name' => $txt['help'], + ); + + // Lastly, some minor template stuff. + $context['page_title'] = $txt['manual_smf_user_help']; + $context['sub_template'] = 'manual'; +} + +/** + * Show some of the more detailed help to give the admin an idea... + * It shows a popup for administrative or user help. + * It uses the help parameter to decide what string to display and where to get + * the string from. ($helptxt or $txt?) + * It is accessed via ?action=helpadmin;help=?. + * + * Uses ManagePermissions language file, if the help starts with permissionhelp. + * @uses template_popup() with no layers. + */ +function ShowAdminHelp() +{ + global $txt, $helptxt, $context, $scripturl, $boarddir, $boardurl; + + if (!isset($_GET['help']) || !is_string($_GET['help'])) + fatal_lang_error('no_access', false); + + if (!isset($helptxt)) + $helptxt = array(); + + // Load the admin help language file and template. + loadLanguage('Help'); + + // Permission specific help? + if (isset($_GET['help']) && substr($_GET['help'], 0, 14) == 'permissionhelp') + loadLanguage('ManagePermissions'); + + loadTemplate('Help'); + + // Allow mods to load their own language file here + call_integration_hook('integrate_helpadmin'); + + // What help string should be used? + if (isset($helptxt[$_GET['help']])) + $context['help_text'] = $helptxt[$_GET['help']]; + elseif (isset($txt[$_GET['help']])) + $context['help_text'] = $txt[$_GET['help']]; + else + fatal_lang_error('not_found', false, array(), 404); + + switch ($_GET['help']) { + case 'cal_short_months': + $context['help_text'] = sprintf($context['help_text'], $txt['months_short'][1], $txt['months_titles'][1]); + break; + case 'cal_short_days': + $context['help_text'] = sprintf($context['help_text'], $txt['days_short'][1], $txt['days'][1]); + break; + case 'cron_is_real_cron': + $context['help_text'] = sprintf($context['help_text'], allowedTo('admin_forum') ? $boarddir : '[' . $txt['hidden'] . ']', $boardurl); + break; + case 'queryless_urls': + $context['help_text'] = sprintf($context['help_text'], (isset($_SERVER['SERVER_SOFTWARE']) && (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false || strpos($_SERVER['SERVER_SOFTWARE'], 'lighttpd') !== false) ? $helptxt['queryless_urls_supported'] : $helptxt['queryless_urls_unsupported'])); + break; + } + + // Does this text contain a link that we should fill in? + if (preg_match('~%([0-9]+\$)?s\?~', $context['help_text'], $match)) + $context['help_text'] = sprintf($context['help_text'], $scripturl, $context['session_id'], $context['session_var']); + + // Set the page title to something relevant. + $context['page_title'] = $context['forum_name'] . ' - ' . $txt['help']; + + // Don't show any template layers, just the popup sub template. + $context['template_layers'] = array(); + $context['sub_template'] = 'popup'; +} + +?> \ No newline at end of file diff --git a/Sources/Likes.php b/Sources/Likes.php new file mode 100644 index 0000000..81e6d3d --- /dev/null +++ b/Sources/Likes.php @@ -0,0 +1,715 @@ +_error] + */ + protected $_error = false; + + /** + * @var string The unique type to like, needs to be unique and it needs to be no longer than 6 characters, only numbers and letters are allowed. + */ + protected $_type = ''; + + /** + * @var string A generic string used if you need to pass any extra info. It gets set via $_GET['extra']. + */ + protected $_extra = false; + + /** + * @var integer a valid ID to identify your like content. + */ + protected $_content = 0; + + /** + * @var integer The number of times your content has been liked. + */ + protected $_numLikes = 0; + + /** + * @var boolean If the current user has already liked this content. + */ + protected $_alreadyLiked = false; + + /** + * @var array $_validLikes mostly used for external integration, needs to be filled as an array with the following keys: + * => 'can_like' boolean|string whether or not the current user can actually like your content. + * for can_like: Return a boolean true if the user can, otherwise return a string, the string will be used as key in a regular $txt language error var. The code assumes you already loaded your language file. If no value is returned or the $txt var isn't set, the code will use a generic error message. + * => 'redirect' string To add support for non JS users, It is highly encouraged to set a valid URL to redirect the user to, if you don't provide any, the code will redirect the user to the main page. The code only performs a light check to see if the redirect is valid so be extra careful while building it. + * => 'type' string 6 letters or numbers. The unique identifier for your content, the code doesn't check for duplicate entries, if there are 2 or more exact hook calls, the code will take the first registered one so make sure you provide a unique identifier. Must match with what you sent in $_GET['ltype']. + * => 'flush_cache' boolean this is optional, it tells the code to reset your like content's cache entry after a new entry has been inserted. + * => 'callback' callable optional, useful if you don't want to issue a separate hook for updating your data, it is called immediately after the data was inserted or deleted and before the actual hook. Uses call_helper(); so the same format for your function/method can be applied here. + * => 'json' boolean optional defaults to false, if true the Like class will return a json object as response instead of HTML. + */ + protected $_validLikes = array( + 'can_like' => false, + 'redirect' => '', + 'type' => '', + 'flush_cache' => '', + 'callback' => false, + 'json' => false, + ); + + /** + * @var array The current user info ($user_info). + */ + protected $_user; + + /** + * @var integer The topic ID, used for liking messages. + */ + protected $_idTopic = 0; + + /** + * @var boolean to know if response(); will be executed as normal. If this is set to false it indicates the method already solved its own way to send back a response. + */ + protected $_setResponse = true; + + /** + * Likes::__construct() + * + * Sets the basic data needed for the rest of the process. + */ + public function __construct() + { + global $db_show_debug; + + $this->_type = isset($_GET['ltype']) ? $_GET['ltype'] : ''; + $this->_content = isset($_GET['like']) ? (int) $_GET['like'] : 0; + $this->_js = isset($_GET['js']) ? true : false; + $this->_sa = isset($_GET['sa']) ? $_GET['sa'] : 'like'; + $this->_extra = isset($_GET['extra']) ? $_GET['extra'] : false; + + // We do not want to output debug information here. + if ($this->_js) + $db_show_debug = false; + } + + /** + * Likes::call() + * + * The main handler. Verifies permissions (whether the user can see the content in question), dispatch different method for different sub-actions. + * Accessed from index.php?action=likes + */ + public function call() + { + global $context; + + $this->_user = $context['user']; + + // Make sure the user can see and like your content. + $this->check(); + + $subActions = array( + 'like', + 'view', + 'delete', + 'insert', + '_count', + ); + + // So at this point, whatever type of like the user supplied and the item of content in question, + // we know it exists, now we need to figure out what we're doing with that. + if (in_array($this->_sa, $subActions) && !is_string($this->_error)) + { + // To avoid ambiguity, turn the property to a normal var. + $call = $this->_sa; + + // Guest can only view likes. + if ($call != 'view') + is_not_guest(); + + checkSession('get'); + + // Call the appropriate method. + $this->$call(); + } + + // else An error message. + $this->response(); + } + + /** + * Likes::get() + * + * A simple getter for all protected properties. + * Accessed from index.php?action=likes + * + * @param string $property The name of the property to get. + * @return mixed Either return the property or false if there isn't a property with that name. + */ + public function get($property = '') + { + // All properties inside Likes are protected, thus, an underscore is used. + $property = '_' . $property; + return property_exists($this, $property) ? $this->$property : false; + } + + /** + * Likes::check() + * + * Performs basic checks on the data provided, checks for a valid msg like. + * Calls integrate_valid_likes hook for retrieving all the data needed and apply checks based on the data provided. + */ + protected function check() + { + global $smcFunc, $modSettings; + + // This feature is currently disable. + if (empty($modSettings['enable_likes'])) + return $this->_error = 'like_disable'; + + // Zerothly, they did indicate some kind of content to like, right? + preg_match('~^([a-z0-9\-\_]{1,6})~i', $this->_type, $matches); + $this->_type = isset($matches[1]) ? $matches[1] : ''; + + if ($this->_type == '' || $this->_content <= 0) + return $this->_error = 'cannot_'; + + // First we need to verify if the user can see the type of content or not. This is set up to be extensible, + // so we'll check for the one type we do know about, and if it's not that, we'll defer to any hooks. + if ($this->_type == 'msg') + { + // So we're doing something off a like. We need to verify that it exists, and that the current user can see it. + // Fortunately for messages, this is quite easy to do - and we'll get the topic id while we're at it, because + // we need this later for other things. + $request = $smcFunc['db_query']('', ' + SELECT m.id_topic, m.id_member + FROM {db_prefix}messages AS m + WHERE {query_see_message_board} + AND m.id_msg = {int:msg}', + array( + 'msg' => $this->_content, + ) + ); + if ($smcFunc['db_num_rows']($request) == 1) + list ($this->_idTopic, $topicOwner) = $smcFunc['db_fetch_row']($request); + + $smcFunc['db_free_result']($request); + if (empty($this->_idTopic)) + return $this->_error = 'cannot_'; + + // So we know what topic it's in and more importantly we know the user can see it. + // If we're not viewing, we need some info set up. + $this->_validLikes['type'] = 'msg'; + $this->_validLikes['flush_cache'] = 'likes_topic_' . $this->_idTopic . '_' . $this->_user['id']; + $this->_validLikes['redirect'] = 'topic=' . $this->_idTopic . '.msg' . $this->_content . '#msg' . $this->_content; + + $this->_validLikes['can_like'] = ($this->_user['id'] == $topicOwner ? 'cannot_like_content' : (allowedTo('likes_like') ? true : 'cannot_like_content')); + } + + else + { + // Modders: This will give you whatever the user offers up in terms of liking, e.g. $this->_type=msg, $this->_content=1 + // When you hook this, check $this->_type first. If it is not something your mod worries about, return false. + // Otherwise, fill an array according to the docs for $this->_validLikes. Determine (however you need to) that the user can see and can_like the relevant liked content (and it exists) Remember that users can't like their own content. + // If the user can like it, you MUST return your type in the 'type' key back. + // See also issueLike() for further notes. + $can_like = call_integration_hook('integrate_valid_likes', array($this->_type, $this->_content, $this->_sa, $this->_js, $this->_extra)); + + $found = false; + if (!empty($can_like)) + { + $can_like = (array) $can_like; + foreach ($can_like as $result) + { + if ($result !== false) + { + // Match the type with what we already have. + if (!isset($result['type']) || $result['type'] != $this->_type) + return $this->_error = 'not_valid_like_type'; + + // Fill out the rest. + $this->_type = $result['type']; + $this->_validLikes = array_merge($this->_validLikes, $result); + $found = true; + break; + } + } + } + + if (!$found) + return $this->_error = 'cannot_'; + } + + // Does the user can like this? Viewing a list of likes doesn't require this permission. + if ($this->_sa != 'view' && isset($this->_validLikes['can_like']) && is_string($this->_validLikes['can_like'])) + return $this->_error = $this->_validLikes['can_like']; + } + + /** + * Likes::delete() + * + * Deletes an entry from user_likes table, needs 3 properties: $_content, $_type and $_user['id']. + */ + protected function delete() + { + global $smcFunc; + + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}user_likes + WHERE content_id = {int:like_content} + AND content_type = {string:like_type} + AND id_member = {int:id_member}', + array( + 'like_content' => $this->_content, + 'like_type' => $this->_type, + 'id_member' => $this->_user['id'], + ) + ); + + // Are we calling this directly? if so, set a proper data for the response. Do note that __METHOD__ returns both the class name and the function name. + if ($this->_sa == __FUNCTION__) + $this->_data = __FUNCTION__; + + // Check to see if there is an unread alert to delete as well... + $result = $smcFunc['db_query']('', ' + SELECT id_alert, id_member FROM {db_prefix}user_alerts + WHERE content_id = {int:like_content} + AND content_type = {string:like_type} + AND id_member_started = {int:id_member_started} + AND content_action = {string:content_action} + AND is_read = {int:unread}', + array( + 'like_content' => $this->_content, + 'like_type' => $this->_type, + 'id_member_started' => $this->_user['id'], + 'content_action' => 'like', + 'unread' => 0, + ) + ); + // Found one? + if ($smcFunc['db_num_rows']($result) == 1) + { + list($alert, $member) = $smcFunc['db_fetch_row']($result); + + // Delete it + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}user_alerts + WHERE id_alert = {int:alert}', + array( + 'alert' => $alert, + ) + ); + // Decrement counter for member who received the like + updateMemberData($member, array('alerts' => '-')); + } + } + + /** + * Likes::insert() + * + * Inserts a new entry on user_likes table. Creates a background task for the inserted entry. + */ + protected function insert() + { + global $smcFunc; + + // Any last minute changes? Temporarily turn the passed properties to normal vars to prevent unexpected behaviour with other methods using these properties. + $type = $this->_type; + $content = $this->_content; + $user = $this->_user; + $time = time(); + + call_integration_hook('integrate_issue_like_before', array(&$type, &$content, &$user, &$time)); + + // Insert the like. + $smcFunc['db_insert']('insert', + '{db_prefix}user_likes', + array('content_id' => 'int', 'content_type' => 'string-6', 'id_member' => 'int', 'like_time' => 'int'), + array($content, $type, $user['id'], $time), + array('content_id', 'content_type', 'id_member') + ); + + // Add a background task to process sending alerts. + // Mod author, you can add your own background task for your own custom like event using the "integrate_issue_like" hook or your callback, both are immediately called after this. + if ($this->_type == 'msg') + $smcFunc['db_insert']('insert', + '{db_prefix}background_tasks', + array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'), + array('$sourcedir/tasks/Likes-Notify.php', 'Likes_Notify_Background', $smcFunc['json_encode'](array( + 'content_id' => $content, + 'content_type' => $type, + 'sender_id' => $user['id'], + 'sender_name' => $user['name'], + 'time' => $time, + )), 0), + array('id_task') + ); + + // Are we calling this directly? if so, set a proper data for the response. Do note that __METHOD__ returns both the class name and the function name. + if ($this->_sa == __FUNCTION__) + $this->_data = __FUNCTION__; + } + + /** + * Likes::_count() + * + * Sets $_numLikes with the actual number of likes your content has, needs two properties: $_content and $_view. When called directly it will return the number of likes as response. + */ + protected function _count() + { + global $smcFunc; + + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}user_likes + WHERE content_id = {int:like_content} + AND content_type = {string:like_type}', + array( + 'like_content' => $this->_content, + 'like_type' => $this->_type, + ) + ); + list ($this->_numLikes) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // If you want to call this directly, fill out _data property too. + if ($this->_sa == __FUNCTION__) + $this->_data = $this->_numLikes; + } + + /** + * Likes::like() + * + * Performs a like action, either like or unlike. Counts the total of likes and calls a hook after the event. + */ + protected function like() + { + global $smcFunc; + + // Safety first! + if (empty($this->_type) || empty($this->_content)) + return $this->_error = 'cannot_'; + + // Do we already like this? + $request = $smcFunc['db_query']('', ' + SELECT content_id, content_type, id_member + FROM {db_prefix}user_likes + WHERE content_id = {int:like_content} + AND content_type = {string:like_type} + AND id_member = {int:id_member}', + array( + 'like_content' => $this->_content, + 'like_type' => $this->_type, + 'id_member' => $this->_user['id'], + ) + ); + $this->_alreadyLiked = (bool) $smcFunc['db_num_rows']($request) != 0; + $smcFunc['db_free_result']($request); + + if ($this->_alreadyLiked) + $this->delete(); + + else + $this->insert(); + + // Now, how many people like this content now? We *could* just +1 / -1 the relevant container but that has proven to become unstable. + $this->_count(); + + // Update the likes count for messages. + if ($this->_type == 'msg') + $this->msgIssueLike(); + + // Any callbacks? + elseif (!empty($this->_validLikes['callback'])) + { + $call = call_helper($this->_validLikes['callback'], true); + + if (!empty($call)) + call_user_func_array($call, array($this)); + } + + // Sometimes there might be other things that need updating after we do this like. + call_integration_hook('integrate_issue_like', array($this)); + + // Now some clean up. This is provided here for any like handlers that want to do any cache flushing. + // This way a like handler doesn't need to explicitly declare anything in integrate_issue_like, but do so + // in integrate_valid_likes where it absolutely has to exist. + if (!empty($this->_validLikes['flush_cache'])) + cache_put_data($this->_validLikes['flush_cache'], null); + + // All done, start building the data to pass as response. + $this->_data = array( + 'id_topic' => !empty($this->_idTopic) ? $this->_idTopic : 0, + 'id_content' => $this->_content, + 'count' => $this->_numLikes, + 'can_like' => $this->_validLikes['can_like'], + 'already_liked' => empty($this->_alreadyLiked), + 'type' => $this->_type, + ); + } + + /** + * Likes::msgIssueLike() + * + * Partly it indicates how it's supposed to work and partly it deals with updating the count of likes + * attached to this message now. + */ + function msgIssueLike() + { + global $smcFunc; + + if ($this->_type !== 'msg') + return; + + $smcFunc['db_query']('', ' + UPDATE {db_prefix}messages + SET likes = {int:num_likes} + WHERE id_msg = {int:id_msg}', + array( + 'id_msg' => $this->_content, + 'num_likes' => $this->_numLikes, + ) + ); + + // Note that we could just as easily have cleared the cache here, or set up the redirection address + // but if your liked content doesn't need to do anything other than have the record in smf_user_likes, + // there's no point in creating another function unnecessarily. + } + + /** + * Likes::view() + * + * This is for viewing the people who liked a thing. + * Accessed from index.php?action=likes;view and should generally load in a popup. + * We use a template for this in case themers want to style it. + */ + function view() + { + global $smcFunc, $txt, $context, $memberContext; + + // Firstly, load what we need. We already know we can see this, so that's something. + $context['likers'] = array(); + $request = $smcFunc['db_query']('', ' + SELECT id_member, like_time + FROM {db_prefix}user_likes + WHERE content_id = {int:like_content} + AND content_type = {string:like_type} + ORDER BY like_time DESC', + array( + 'like_content' => $this->_content, + 'like_type' => $this->_type, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $context['likers'][$row['id_member']] = array('timestamp' => $row['like_time']); + + // Now to get member data, including avatars and so on. + $members = array_keys($context['likers']); + $loaded = loadMemberData($members); + if (count($loaded) != count($members)) + { + $members = array_diff($members, $loaded); + foreach ($members as $not_loaded) + unset ($context['likers'][$not_loaded]); + } + + foreach ($context['likers'] as $liker => $dummy) + { + $loaded = loadMemberContext($liker); + if (!$loaded) + { + unset ($context['likers'][$liker]); + continue; + } + + $context['likers'][$liker]['profile'] = &$memberContext[$liker]; + $context['likers'][$liker]['time'] = !empty($dummy['timestamp']) ? timeformat($dummy['timestamp']) : ''; + } + + $count = count($context['likers']); + $title_base = isset($txt['likes_' . $count]) ? 'likes_' . $count : 'likes_n'; + $context['page_title'] = strip_tags(sprintf($txt[$title_base], '', comma_format($count))); + + // Lastly, setting up for display. + loadTemplate('Likes'); + loadLanguage('Help'); // For the close window button. + $context['template_layers'] = array(); + $context['sub_template'] = 'popup'; + + // We already took care of our response so there is no need to bother with respond(); + $this->_setResponse = false; + } + + /** + * Likes::response() + * + * Checks if the user can use JavaScript and acts accordingly. + * Calls the appropriate sub-template for each method + * Handles error messages. + */ + protected function response() + { + global $context, $txt; + + // Don't do anything if someone else has already take care of the response. + if (!$this->_setResponse) + return; + + // Want a json response huh? + if ($this->_validLikes['json']) + return $this->jsonResponse(); + + // Set everything up for display. + loadTemplate('Likes'); + $context['template_layers'] = array(); + + // If there are any errors, process them first. + if ($this->_error) + { + // If this is a generic error, set it up good. + if ($this->_error == 'cannot_') + $this->_error = $this->_sa == 'view' ? 'cannot_view_likes' : 'cannot_like_content'; + + // Is this request coming from an ajax call? + if ($this->_js) + { + $context['sub_template'] = 'generic'; + $context['data'] = isset($txt[$this->_error]) ? $txt[$this->_error] : $txt['like_error']; + } + + // Nope? then just do a redirect to whatever URL was provided. + else + redirectexit(!empty($this->_validLikes['redirect']) ? $this->_validLikes['redirect'] . ';error=' . $this->_error : ''); + + return; + } + + // A like operation. + else + { + // Not an ajax request so send the user back to the previous location or the main page. + if (!$this->_js) + redirectexit(!empty($this->_validLikes['redirect']) ? $this->_validLikes['redirect'] : ''); + + // These fine gentlemen all share the same template. + $generic = array('delete', 'insert', '_count'); + if (in_array($this->_sa, $generic)) + { + $context['sub_template'] = 'generic'; + $context['data'] = isset($txt['like_' . $this->_data]) ? $txt['like_' . $this->_data] : $this->_data; + } + + // Directly pass the current called sub-action and the data generated by its associated Method. + else + { + $context['sub_template'] = $this->_sa; + $context['data'] = $this->_data; + } + } + } + + /** + * Outputs a JSON-encoded response + */ + protected function jsonResponse() + { + global $smcFunc; + + $print = array( + 'data' => $this->_data, + ); + + // If there is an error, send it. + if ($this->_error) + { + if ($this->_error == 'cannot_') + $this->_error = $this->_sa == 'view' ? 'cannot_view_likes' : 'cannot_like_content'; + + $print['error'] = $this->_error; + } + + // Do you want to add something at the very last minute? + call_integration_hook('integrate_likes_json_response', array(&$print)); + + // Print the data. + smf_serverResponse($smcFunc['json_encode']($print)); + die; + } +} + +/** + * What's this? I dunno, what are you talking about? Never seen this before, nope. No sir. + */ +function BookOfUnknown() +{ + global $context, $scripturl; + + echo ' + + + The Book of Unknown, ', @$_GET['verse'] == '2:18' ? '2:18' : '4:16', ' + + + +
'; + + if (!isset($_GET['verse']) || ($_GET['verse'] != '2:18' && $_GET['verse'] != '22:1-2')) + $_GET['verse'] = '4:16'; + + if ($_GET['verse'] == '2:18') + echo ' + Woe, it was that his name wasn\'t known, that he came in mystery, and was recognized by none. And it became to be in those days something.  Something not yet unknown to mankind.  And thus what was to be known the secret project began into its existence.  Henceforth the opposition was only weary and fearful, for now their match was at arms against them.'; + elseif ($_GET['verse'] == '4:16') + echo ' + And it came to pass that the unbelievers dwindled in number and saw rise of many proselytizers, and the opposition found fear in the face of the x and the j while those who stood with the something grew stronger and came together.  Still, this was only the beginning, and what lay in the future was unknown to all, even those on the right side.'; + elseif ($_GET['verse'] == '22:1-2') + echo ' +

Now behold, that which was once the secret project was unknown no longer.  Alas, it needed more than only one, but yet even thought otherwise.  It became that the opposition rumored and lied, but still to no avail.  Their match, though not perfect, had them outdone.

+

Let it continue.  The end.

'; + + echo ' +
+
'; + + if ($_GET['verse'] == '2:18') + echo ' + from The Book of Unknown, 2:18'; + elseif ($_GET['verse'] == '4:16') + echo ' + from The Book of Unknown, 4:16'; + elseif ($_GET['verse'] == '22:1-2') + echo ' + from The Book of Unknown, 22:1-2'; + + echo ' +
+ +'; + + obExit(false); +} + +?> \ No newline at end of file diff --git a/Sources/Load.php b/Sources/Load.php new file mode 100644 index 0000000..6b741c0 --- /dev/null +++ b/Sources/Load.php @@ -0,0 +1,4055 @@ + $db_character_set, + ) + ); + + // We need some caching support, maybe. + loadCacheAccelerator(); + + // Try to load it from the cache first; it'll never get cached if the setting is off. + if (($modSettings = cache_get_data('modSettings', 90)) == null) + { + $request = $smcFunc['db_query']('', ' + SELECT variable, value + FROM {db_prefix}settings', + array( + ) + ); + $modSettings = array(); + if (!$request) + display_db_error(); + foreach ($smcFunc['db_fetch_all']($request) as $row) + $modSettings[$row['variable']] = $row['value']; + $smcFunc['db_free_result']($request); + + // Do a few things to protect against missing settings or settings with invalid values... + if (empty($modSettings['defaultMaxTopics']) || $modSettings['defaultMaxTopics'] <= 0 || $modSettings['defaultMaxTopics'] > 999) + $modSettings['defaultMaxTopics'] = 20; + if (empty($modSettings['defaultMaxMessages']) || $modSettings['defaultMaxMessages'] <= 0 || $modSettings['defaultMaxMessages'] > 999) + $modSettings['defaultMaxMessages'] = 15; + if (empty($modSettings['defaultMaxMembers']) || $modSettings['defaultMaxMembers'] <= 0 || $modSettings['defaultMaxMembers'] > 999) + $modSettings['defaultMaxMembers'] = 30; + if (empty($modSettings['defaultMaxListItems']) || $modSettings['defaultMaxListItems'] <= 0 || $modSettings['defaultMaxListItems'] > 999) + $modSettings['defaultMaxListItems'] = 15; + + // We explicitly do not use $smcFunc['json_decode'] here yet, as $smcFunc is not fully loaded. + if (!is_array($modSettings['attachmentUploadDir'])) + { + $attachmentUploadDir = smf_json_decode($modSettings['attachmentUploadDir'], true, false); + $modSettings['attachmentUploadDir'] = !empty($attachmentUploadDir) ? $attachmentUploadDir : $modSettings['attachmentUploadDir']; + } + + if (!empty($cache_enable)) + cache_put_data('modSettings', $modSettings, 90); + } + + // Going anything further when the files don't match the database can make nasty messes (unless we're actively installing or upgrading) + if (!defined('SMF_INSTALLING') && (!isset($_REQUEST['action']) || $_REQUEST['action'] !== 'admin' || !isset($_REQUEST['area']) || $_REQUEST['area'] !== 'packages') && !empty($modSettings['smfVersion']) && version_compare(strtolower(strtr($modSettings['smfVersion'], array(' ' => '.'))), strtolower(strtr(SMF_VERSION, array(' ' => '.'))), '!=')) + { + // Wipe the cached $modSettings values so they don't interfere with anything later + cache_put_data('modSettings', null); + + // Redirect to the upgrader if we can + if (file_exists($boarddir . '/upgrade.php')) + header('location: ' . $boardurl . '/upgrade.php'); + + die('SMF file version (' . SMF_VERSION . ') does not match SMF database version (' . $modSettings['smfVersion'] . ').
Run the SMF upgrader to fix this.
More information.'); + } + + $modSettings['cache_enable'] = $cache_enable; + + // Used to force browsers to download fresh CSS and JavaScript when necessary + $modSettings['browser_cache'] = !empty($modSettings['browser_cache']) ? (int) $modSettings['browser_cache'] : 0; + $context['browser_cache'] = '?' . preg_replace('~\W~', '', strtolower(SMF_FULL_VERSION)) . '_' . $modSettings['browser_cache']; + + // Disable image proxy if we don't have SSL enabled + if (empty($modSettings['force_ssl'])) + $image_proxy_enabled = false; + + // UTF-8 ? + $utf8 = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8'; + $context['utf8'] = $utf8; + + // Set a list of common functions. + $ent_list = '&(?:#' . (empty($modSettings['disableEntityCheck']) ? '\d{1,7}' : '021') . '|quot|amp|lt|gt|nbsp);'; + $ent_check = empty($modSettings['disableEntityCheck']) ? function($string) + { + $string = preg_replace_callback('~(&#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'entity_fix__callback', (string) $string); + return $string; + } : function($string) + { + return (string) $string; + }; + $fix_utf8mb4 = function($string) use ($utf8, $smcFunc) + { + if (!$utf8 || $smcFunc['db_mb4']) + return $string; + + $i = 0; + $len = strlen($string); + $new_string = ''; + while ($i < $len) + { + $ord = ord($string[$i]); + if ($ord < 128) + { + $new_string .= $string[$i]; + $i++; + } + elseif ($ord < 224) + { + $new_string .= $string[$i] . $string[$i + 1]; + $i += 2; + } + elseif ($ord < 240) + { + $new_string .= $string[$i] . $string[$i + 1] . $string[$i + 2]; + $i += 3; + } + elseif ($ord < 248) + { + // Magic happens. + $val = (ord($string[$i]) & 0x07) << 18; + $val += (ord($string[$i + 1]) & 0x3F) << 12; + $val += (ord($string[$i + 2]) & 0x3F) << 6; + $val += (ord($string[$i + 3]) & 0x3F); + $new_string .= '&#' . $val . ';'; + $i += 4; + } + } + return $new_string; + }; + + // global array of anonymous helper functions, used mostly to properly handle multi byte strings + $smcFunc += array( + 'entity_fix' => function($string) + { + $num = $string[0] === 'x' ? hexdec(substr($string, 1)) : (int) $string; + return $num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202E || $num === 0x202D ? '' : '&#' . $num . ';'; + }, + 'htmlspecialchars' => function($string, $quote_style = ENT_COMPAT, $charset = 'ISO-8859-1') use ($ent_check, $utf8, $fix_utf8mb4, &$smcFunc) + { + $string = $smcFunc['normalize']($string); + + return $fix_utf8mb4($ent_check(htmlspecialchars($string, $quote_style, $utf8 ? 'UTF-8' : $charset))); + }, + 'htmltrim' => function($string) use ($utf8, $ent_check) + { + // Preg_replace space characters depend on the character set in use + $space_chars = $utf8 ? '\p{Z}\p{C}' : '\x00-\x20\x80-\xA0'; + + return preg_replace('~^(?:[' . $space_chars . ']| )+|(?:[' . $space_chars . ']| )+$~' . ($utf8 ? 'u' : ''), '', $ent_check($string)); + }, + 'strlen' => function($string) use ($ent_list, $utf8, $ent_check) + { + return strlen(preg_replace('~' . $ent_list . ($utf8 ? '|.~u' : '~'), '_', $ent_check($string))); + }, + 'strpos' => function($haystack, $needle, $offset = 0) use ($utf8, $ent_check, $ent_list, $modSettings) + { + $haystack_arr = preg_split('~(' . $ent_list . '|.)~' . ($utf8 ? 'u' : ''), $ent_check($haystack), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + + if (strlen($needle) === 1) + { + $result = array_search($needle, array_slice($haystack_arr, $offset)); + return is_int($result) ? $result + $offset : false; + } + else + { + $needle_arr = preg_split('~(' . $ent_list . '|.)~' . ($utf8 ? 'u' : '') . '', $ent_check($needle), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $needle_size = count($needle_arr); + + $result = array_search($needle_arr[0], array_slice($haystack_arr, $offset)); + while ((int) $result === $result) + { + $offset += $result; + if (array_slice($haystack_arr, $offset, $needle_size) === $needle_arr) + return $offset; + $result = array_search($needle_arr[0], array_slice($haystack_arr, ++$offset)); + } + return false; + } + }, + 'substr' => function($string, $start, $length = null) use ($utf8, $ent_check, $ent_list, $modSettings) + { + $ent_arr = preg_split('~(' . $ent_list . '|.)~' . ($utf8 ? 'u' : '') . '', $ent_check($string), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + return $length === null ? implode('', array_slice($ent_arr, $start)) : implode('', array_slice($ent_arr, $start, $length)); + }, + 'strtolower' => function($string) use (&$smcFunc) + { + return $smcFunc['convert_case']($string, 'lower'); + }, + 'strtoupper' => function($string) use (&$smcFunc) + { + return $smcFunc['convert_case']($string, 'upper'); + }, + 'truncate' => function($string, $length) use ($utf8, $ent_check, $ent_list, &$smcFunc) + { + $string = $ent_check($string); + preg_match('~^(' . $ent_list . '|.){' . $smcFunc['strlen'](substr($string, 0, $length)) . '}~' . ($utf8 ? 'u' : ''), $string, $matches); + $string = $matches[0]; + while (strlen($string) > $length) + $string = preg_replace('~(?:' . $ent_list . '|.)$~' . ($utf8 ? 'u' : ''), '', $string); + return $string; + }, + 'ucfirst' => function($string) use (&$smcFunc) + { + return $smcFunc['convert_case']($string, 'ucfirst'); + }, + 'ucwords' => function($string) use (&$smcFunc) + { + return $smcFunc['convert_case']($string, 'ucwords'); + }, + 'convert_case' => function($string, $case, $simple = false, $form = 'c') use (&$smcFunc, $utf8, $ent_check, $fix_utf8mb4, $sourcedir) + { + if (!$utf8) + { + switch ($case) + { + case 'upper': + $string = strtoupper($string); + break; + + case 'lower': + case 'fold'; + $string = strtolower($string); + break; + + case 'title': + $string = ucwords(strtolower($string)); + break; + + case 'ucwords': + $string = ucwords($string); + break; + + case 'ucfirst': + $string = ucfirst($string); + break; + + default: + break; + } + } + else + { + // Convert numeric entities to characters, except special ones. + if (function_exists('mb_decode_numericentity') && strpos($string, '&#') !== false) + { + $string = strtr($ent_check($string), array( + '"' => '"', + '&' => '&', + ''' => ''', + '<' => '<', + '>' => '>', + ' ' => ' ', + )); + + $string = mb_decode_numericentity($string, array(0, 0x10FFFF, 0, 0xFFFFFF), 'UTF-8'); + } + + // Use optmized function for compatibility casefolding. + if ($form === 'kc_casefold' || ($case === 'fold' && $form === 'kc')) + { + $string = $smcFunc['normalize']($string, 'kc_casefold'); + } + // Everything else. + else + { + require_once($sourcedir . '/Subs-Charset.php'); + $string = $smcFunc['normalize'](utf8_convert_case($string, $case, $simple), $form); + } + } + + return $fix_utf8mb4($string); + }, + 'json_decode' => 'smf_json_decode', + 'json_encode' => 'json_encode', + 'random_int' => function($min = 0, $max = PHP_INT_MAX) + { + global $sourcedir; + + // Oh, wouldn't it be great if I *was* crazy? Then the world would be okay. + if (!is_callable('random_int')) + require_once($sourcedir . '/random_compat/random.php'); + + return random_int($min, $max); + }, + 'random_bytes' => function($length = 64) + { + global $sourcedir; + + if (!is_callable('random_bytes')) + require_once($sourcedir . '/random_compat/random.php'); + + // Make sure length is valid + $length = max(1, (int) $length); + + return random_bytes($length); + }, + 'normalize' => function($string, $form = 'c') use ($utf8) + { + global $sourcedir; + + $string = (string) $string; + + if (!$utf8) + return $string; + + require_once($sourcedir . '/Subs-Charset.php'); + + $normalize_func = 'utf8_normalize_' . strtolower((string) $form); + + if (!function_exists($normalize_func)) + return false; + + return $normalize_func($string); + }, + ); + + // Setting the timezone is a requirement for some functions. + if (isset($modSettings['default_timezone']) && in_array($modSettings['default_timezone'], timezone_identifiers_list())) + date_default_timezone_set($modSettings['default_timezone']); + else + { + // Get PHP's default timezone, if set + $ini_tz = ini_get('date.timezone'); + if (!empty($ini_tz)) + $modSettings['default_timezone'] = $ini_tz; + else + $modSettings['default_timezone'] = ''; + + // If date.timezone is unset, invalid, or just plain weird, make a best guess + if (!in_array($modSettings['default_timezone'], timezone_identifiers_list())) + { + $server_offset = @mktime(0, 0, 0, 1, 1, 1970) * -1; + $modSettings['default_timezone'] = timezone_name_from_abbr('', $server_offset, 0); + + if (empty($modSettings['default_timezone'])) + $modSettings['default_timezone'] = 'UTC'; + } + + date_default_timezone_set($modSettings['default_timezone']); + } + + // Check the load averages? + if (!empty($modSettings['loadavg_enable'])) + { + if (($modSettings['load_average'] = cache_get_data('loadavg', 90)) == null) + { + $modSettings['load_average'] = @file_get_contents('/proc/loadavg'); + if (!empty($modSettings['load_average']) && preg_match('~^([^ ]+?) ([^ ]+?) ([^ ]+)~', $modSettings['load_average'], $matches) != 0) + $modSettings['load_average'] = (float) $matches[1]; + elseif (($modSettings['load_average'] = @`uptime`) != null && preg_match('~load average[s]?: (\d+\.\d+), (\d+\.\d+), (\d+\.\d+)~i', $modSettings['load_average'], $matches) != 0) + $modSettings['load_average'] = (float) $matches[1]; + else + unset($modSettings['load_average']); + + if (!empty($modSettings['load_average']) || $modSettings['load_average'] === 0.0) + cache_put_data('loadavg', $modSettings['load_average'], 90); + } + + if (!empty($modSettings['load_average']) || $modSettings['load_average'] === 0.0) + call_integration_hook('integrate_load_average', array($modSettings['load_average'])); + + if (!empty($modSettings['loadavg_forum']) && !empty($modSettings['load_average']) && $modSettings['load_average'] >= $modSettings['loadavg_forum']) + display_loadavg_error(); + } + + // Ensure we know who can manage boards. + if (!isset($modSettings['board_manager_groups'])) + { + require_once($sourcedir . '/Subs-Members.php'); + $board_managers = groupsAllowedTo('manage_boards', null); + $board_managers = implode(',', $board_managers['allowed']); + updateSettings(array('board_manager_groups' => $board_managers)); + } + + // Is post moderation alive and well? Everywhere else assumes this has been defined, so let's make sure it is. + $modSettings['postmod_active'] = !empty($modSettings['postmod_active']); + + // Here to justify the name of this function. :P + // It should be added to the install and upgrade scripts. + // But since the converters need to be updated also. This is easier. + if (empty($modSettings['currentAttachmentUploadDir'])) + { + updateSettings(array( + 'attachmentUploadDir' => $smcFunc['json_encode'](array(1 => $modSettings['attachmentUploadDir'])), + 'currentAttachmentUploadDir' => 1, + )); + } + + // Respect PHP's limits. + $post_max_kb = floor(memoryReturnBytes(ini_get('post_max_size')) / 1024); + $file_max_kb = floor(memoryReturnBytes(ini_get('upload_max_filesize')) / 1024); + $modSettings['attachmentPostLimit'] = empty($modSettings['attachmentPostLimit']) ? $post_max_kb : min($modSettings['attachmentPostLimit'], $post_max_kb); + $modSettings['attachmentSizeLimit'] = empty($modSettings['attachmentSizeLimit']) ? $file_max_kb : min($modSettings['attachmentSizeLimit'], $file_max_kb); + $modSettings['attachmentNumPerPostLimit'] = !isset($modSettings['attachmentNumPerPostLimit']) ? 4 : $modSettings['attachmentNumPerPostLimit']; + + // Integration is cool. + if (defined('SMF_INTEGRATION_SETTINGS')) + { + $integration_settings = $smcFunc['json_decode'](SMF_INTEGRATION_SETTINGS, true); + foreach ($integration_settings as $hook => $function) + add_integration_function($hook, $function, false); + } + + // Any files to pre include? + if (!empty($modSettings['integrate_pre_include'])) + { + $pre_includes = explode(',', $modSettings['integrate_pre_include']); + foreach ($pre_includes as $include) + { + $include = strtr(trim($include), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir)); + if (file_exists($include)) + require_once($include); + } + } + + // This determines the server... not used in many places, except for login fixing. + $context['server'] = array( + 'is_iis' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false, + 'is_apache' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false, + 'is_litespeed' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'LiteSpeed') !== false, + 'is_lighttpd' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'lighttpd') !== false, + 'is_nginx' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'nginx') !== false, + 'is_cgi' => isset($_SERVER['SERVER_SOFTWARE']) && strpos(php_sapi_name(), 'cgi') !== false, + 'is_windows' => DIRECTORY_SEPARATOR === '\\', + 'iso_case_folding' => ord(strtolower(chr(138))) === 154, + ); + // A bug in some versions of IIS under CGI (older ones) makes cookie setting not work with Location: headers. + $context['server']['needs_login_fix'] = $context['server']['is_cgi'] && $context['server']['is_iis']; + + // Define a list of icons used across multiple places. + $context['stable_icons'] = array('xx', 'thumbup', 'thumbdown', 'exclamation', 'question', 'lamp', 'smiley', 'angry', 'cheesy', 'grin', 'sad', 'wink', 'poll', 'moved', 'recycled', 'clip'); + + // Define an array for custom profile fields placements. + $context['cust_profile_fields_placement'] = array( + 'standard', + 'icons', + 'above_signature', + 'below_signature', + 'below_avatar', + 'above_member', + 'bottom_poster', + 'before_member', + 'after_member', + ); + + // Define an array for content-related elements (e.g. description, keywords, Open Graph) for the HTML head. + $context['meta_tags'] = array(); + + // Define an array of allowed HTML tags. + $context['allowed_html_tags'] = array( + '', + '
', + ); + + // These are the only valid image types for SMF attachments, by default anyway. + // Note: The values are for image mime types, not file extensions. + $context['valid_image_types'] = array( + IMAGETYPE_GIF => 'gif', + IMAGETYPE_JPEG => 'jpeg', + IMAGETYPE_PNG => 'png', + IMAGETYPE_PSD => 'psd', + IMAGETYPE_BMP => 'bmp', + IMAGETYPE_TIFF_II => 'tiff', + IMAGETYPE_TIFF_MM => 'tiff', + IMAGETYPE_IFF => 'iff' + ); + + // Define a list of allowed tags for descriptions. + $context['description_allowed_tags'] = array( + 'abbr', 'anchor', 'b', 'br', 'center', 'color', 'font', 'hr', 'i', 'img', + 'iurl', 'left', 'li', 'list', 'ltr', 'pre', 'right', 's', 'sub', + 'sup', 'table', 'td', 'tr', 'u', 'url', + ); + + // Define a list of deprecated BBC tags + // Even when enabled, they'll only work in old posts and not new ones + $context['legacy_bbc'] = array( + 'acronym', 'bdo', 'black', 'blue', 'flash', 'ftp', 'glow', + 'green', 'move', 'red', 'shadow', 'tt', 'white', + ); + + // Define a list of BBC tags that require permissions to use + $context['restricted_bbc'] = array( + 'html', + ); + + // Login Cookie times. Format: time => txt + $context['login_cookie_times'] = array( + 3153600 => 'always_logged_in', + 60 => 'one_hour', + 1440 => 'one_day', + 10080 => 'one_week', + 43200 => 'one_month', + ); + + $context['show_spellchecking'] = false; + + // Call pre load integration functions. + call_integration_hook('integrate_pre_load'); +} + +/** + * Load all the important user information. + * What it does: + * - sets up the $user_info array + * - assigns $user_info['query_wanna_see_board'] for what boards the user can see. + * - first checks for cookie or integration validation. + * - uses the current session if no integration function or cookie is found. + * - checks password length, if member is activated and the login span isn't over. + * - if validation fails for the user, $id_member is set to 0. + * - updates the last visit time when needed. + */ +function loadUserSettings() +{ + global $modSettings, $user_settings, $sourcedir, $smcFunc; + global $cookiename, $user_info, $language, $context, $cache_enable; + + require_once($sourcedir . '/Subs-Auth.php'); + + // Check first the integration, then the cookie, and last the session. + if (count($integration_ids = call_integration_hook('integrate_verify_user')) > 0) + { + $id_member = 0; + foreach ($integration_ids as $integration_id) + { + $integration_id = (int) $integration_id; + if ($integration_id > 0) + { + $id_member = $integration_id; + $already_verified = true; + break; + } + } + } + else + $id_member = 0; + + if (empty($id_member) && isset($_COOKIE[$cookiename])) + { + // First try 2.1 json-format cookie + $cookie_data = $smcFunc['json_decode']($_COOKIE[$cookiename], true, false); + + // Legacy format (for recent 2.0 --> 2.1 upgrades) + if (empty($cookie_data)) + $cookie_data = safe_unserialize($_COOKIE[$cookiename]); + + list($id_member, $password, $login_span, $cookie_domain, $cookie_path) = array_pad((array) $cookie_data, 5, ''); + + $id_member = !empty($id_member) && strlen($password) > 0 ? (int) $id_member : 0; + + // Make sure the cookie is set to the correct domain and path + if (array($cookie_domain, $cookie_path) !== url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']))) + setLoginCookie((int) $login_span - time(), $id_member); + } + elseif (empty($id_member) && isset($_SESSION['login_' . $cookiename]) && ($_SESSION['USER_AGENT'] == $_SERVER['HTTP_USER_AGENT'] || !empty($modSettings['disableCheckUA']))) + { + // @todo Perhaps we can do some more checking on this, such as on the first octet of the IP? + $cookie_data = $smcFunc['json_decode']($_SESSION['login_' . $cookiename], true); + + if (empty($cookie_data)) + $cookie_data = safe_unserialize($_SESSION['login_' . $cookiename]); + + list($id_member, $password, $login_span) = array_pad((array) $cookie_data, 3, ''); + $id_member = !empty($id_member) && strlen($password) == 40 && (int) $login_span > time() ? (int) $id_member : 0; + } + + // Only load this stuff if the user isn't a guest. + if ($id_member != 0) + { + // Is the member data cached? + if (empty($cache_enable) || $cache_enable < 2 || ($user_settings = cache_get_data('user_settings-' . $id_member, 60)) == null) + { + $request = $smcFunc['db_query']('', ' + SELECT mem.*, COALESCE(a.id_attach, 0) AS id_attach, a.filename, a.attachment_type, a.width AS "attachment_width", a.height AS "attachment_height" + FROM {db_prefix}members AS mem + LEFT JOIN {db_prefix}attachments AS a ON (a.id_member = {int:id_member}) + WHERE mem.id_member = {int:id_member} + LIMIT 1', + array( + 'id_member' => $id_member, + ) + ); + $user_settings = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + if (!empty($user_settings['avatar'])) + $user_settings['avatar'] = get_proxied_url($user_settings['avatar']); + + if (!empty($cache_enable) && $cache_enable >= 2) + cache_put_data('user_settings-' . $id_member, $user_settings, 60); + } + + // Did we find 'im? If not, junk it. + if (!empty($user_settings)) + { + // As much as the password should be right, we can assume the integration set things up. + if (!empty($already_verified) && $already_verified === true) + $check = true; + // SHA-512 hash should be 128 characters long. + elseif (strlen($password) == 128) + $check = hash_equals(hash_salt($user_settings['passwd'], $user_settings['password_salt']), $password); + else + $check = false; + + // Wrong password or not activated - either way, you're going nowhere. + $id_member = $check && ($user_settings['is_activated'] == 1 || $user_settings['is_activated'] == 11) ? (int) $user_settings['id_member'] : 0; + } + else + $id_member = 0; + + // Check if we are forcing TFA + $force_tfasetup = !empty($modSettings['tfa_mode']) && $modSettings['tfa_mode'] >= 2 && $id_member && empty($user_settings['tfa_secret']) && SMF != 'SSI' && !isset($_REQUEST['xml']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != '.xml'); + + // Don't force TFA on popups + if ($force_tfasetup) + { + if (isset($_REQUEST['action']) && $_REQUEST['action'] == 'profile' && isset($_REQUEST['area']) && in_array($_REQUEST['area'], array('popup', 'alerts_popup'))) + $force_tfasetup = false; + elseif (isset($_REQUEST['action']) && $_REQUEST['action'] == 'pm' && (isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'popup')) + $force_tfasetup = false; + + call_integration_hook('integrate_force_tfasetup', array(&$force_tfasetup)); + } + + // If we no longer have the member maybe they're being all hackey, stop brute force! + if (!$id_member) + { + require_once($sourcedir . '/LogInOut.php'); + validatePasswordFlood( + !empty($user_settings['id_member']) ? $user_settings['id_member'] : $id_member, + !empty($user_settings['member_name']) ? $user_settings['member_name'] : '', + !empty($user_settings['passwd_flood']) ? $user_settings['passwd_flood'] : false, + $id_member != 0 + ); + } + // Validate for Two Factor Authentication + elseif (!empty($modSettings['tfa_mode']) && $id_member && !empty($user_settings['tfa_secret']) && (empty($_REQUEST['action']) || !in_array($_REQUEST['action'], array('login2', 'logintfa')))) + { + $tfacookie = $cookiename . '_tfa'; + $tfasecret = null; + + $verified = call_integration_hook('integrate_verify_tfa', array($id_member, $user_settings)); + + if (empty($verified) || !in_array(true, $verified)) + { + if (!empty($_COOKIE[$tfacookie])) + { + $tfa_data = $smcFunc['json_decode']($_COOKIE[$tfacookie], true); + + list ($tfamember, $tfasecret) = array_pad((array) $tfa_data, 2, ''); + + if (!isset($tfamember, $tfasecret) || (int) $tfamember != $id_member) + $tfasecret = null; + } + + // They didn't finish logging in before coming here? Then they're no one to us. + if (empty($tfasecret) || !hash_equals(hash_salt($user_settings['tfa_backup'], $user_settings['password_salt']), $tfasecret)) + { + setLoginCookie(-3600, $id_member); + $id_member = 0; + $user_settings = array(); + } + } + } + // When authenticating their two factor code, make sure to reset their ID for security + elseif (!empty($modSettings['tfa_mode']) && $id_member && !empty($user_settings['tfa_secret']) && $_REQUEST['action'] == 'logintfa') + { + $id_member = 0; + $context['tfa_member'] = $user_settings; + $user_settings = array(); + } + // Are we forcing 2FA? Need to check if the user groups actually require 2FA + elseif ($force_tfasetup) + { + if ($modSettings['tfa_mode'] == 2) //only do this if we are just forcing SOME membergroups + { + //Build an array of ALL user membergroups. + $full_groups = array($user_settings['id_group']); + if (!empty($user_settings['additional_groups'])) + { + $full_groups = array_merge($full_groups, explode(',', $user_settings['additional_groups'])); + $full_groups = array_unique($full_groups); //duplicates, maybe? + } + + //Find out if any group requires 2FA + $request = $smcFunc['db_query']('', ' + SELECT COUNT(id_group) AS total + FROM {db_prefix}membergroups + WHERE tfa_required = {int:tfa_required} + AND id_group IN ({array_int:full_groups})', + array( + 'tfa_required' => 1, + 'full_groups' => $full_groups, + ) + ); + $row = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + } + else + $row['total'] = 1; //simplifies logics in the next "if" + + $area = !empty($_REQUEST['area']) ? $_REQUEST['area'] : ''; + $action = !empty($_REQUEST['action']) ? $_REQUEST['action'] : ''; + + if ($row['total'] > 0 && !in_array($action, array('profile', 'logout')) || ($action == 'profile' && $area != 'tfasetup')) + redirectexit('action=profile;area=tfasetup;forced'); + } + } + + // Found 'im, let's set up the variables. + if ($id_member != 0) + { + // Let's not update the last visit time in these cases... + // 1. SSI doesn't count as visiting the forum. + // 2. RSS feeds and XMLHTTP requests don't count either. + // 3. If it was set within this session, no need to set it again. + // 4. New session, yet updated < five hours ago? Maybe cache can help. + // 5. We're still logging in or authenticating + if (SMF != 'SSI' && !isset($_REQUEST['xml']) && (!isset($_REQUEST['action']) || !in_array($_REQUEST['action'], array('.xml', 'login2', 'logintfa'))) && empty($_SESSION['id_msg_last_visit']) && (empty($cache_enable) || ($_SESSION['id_msg_last_visit'] = cache_get_data('user_last_visit-' . $id_member, 5 * 3600)) === null)) + { + // @todo can this be cached? + // Do a quick query to make sure this isn't a mistake. + $result = $smcFunc['db_query']('', ' + SELECT poster_time + FROM {db_prefix}messages + WHERE id_msg = {int:id_msg} + LIMIT 1', + array( + 'id_msg' => $user_settings['id_msg_last_visit'], + ) + ); + list ($visitTime) = $smcFunc['db_fetch_row']($result); + $smcFunc['db_free_result']($result); + + $_SESSION['id_msg_last_visit'] = $user_settings['id_msg_last_visit']; + + // If it was *at least* five hours ago... + if ($visitTime < time() - 5 * 3600) + { + updateMemberData($id_member, array('id_msg_last_visit' => (int) $modSettings['maxMsgID'], 'last_login' => time(), 'member_ip' => $_SERVER['REMOTE_ADDR'], 'member_ip2' => $_SERVER['BAN_CHECK_IP'])); + $user_settings['last_login'] = time(); + + if (!empty($cache_enable) && $cache_enable >= 2) + cache_put_data('user_settings-' . $id_member, $user_settings, 60); + + if (!empty($cache_enable)) + cache_put_data('user_last_visit-' . $id_member, $_SESSION['id_msg_last_visit'], 5 * 3600); + } + } + elseif (empty($_SESSION['id_msg_last_visit'])) + $_SESSION['id_msg_last_visit'] = $user_settings['id_msg_last_visit']; + + $username = $user_settings['member_name']; + + if (empty($user_settings['additional_groups'])) + $user_info = array( + 'groups' => array($user_settings['id_group'], $user_settings['id_post_group']) + ); + + else + $user_info = array( + 'groups' => array_merge( + array($user_settings['id_group'], $user_settings['id_post_group']), + explode(',', $user_settings['additional_groups']) + ) + ); + + // Because history has proven that it is possible for groups to go bad - clean up in case. + $user_info['groups'] = array_map('intval', $user_info['groups']); + + // This is a logged in user, so definitely not a spider. + $user_info['possibly_robot'] = false; + + // Figure out the new time offset. + if (!empty($user_settings['timezone'])) + { + // Get the offsets from UTC for the server, then for the user. + $tz_system = new DateTimeZone($modSettings['default_timezone']); + $tz_user = new DateTimeZone($user_settings['timezone']); + $time_system = new DateTime('now', $tz_system); + $time_user = new DateTime('now', $tz_user); + $user_settings['time_offset'] = ($tz_user->getOffset($time_user) - $tz_system->getOffset($time_system)) / 3600; + } + // We need a time zone. + else + { + if (!empty($user_settings['time_offset'])) + { + $tz_system = new DateTimeZone($modSettings['default_timezone']); + $time_system = new DateTime('now', $tz_system); + + $user_settings['timezone'] = @timezone_name_from_abbr('', $tz_system->getOffset($time_system) + $user_settings['time_offset'] * 3600, (int) $time_system->format('I')); + } + + if (empty($user_settings['timezone'])) + { + $user_settings['timezone'] = $modSettings['default_timezone']; + $user_settings['time_offset'] = 0; + } + } + } + // If the user is a guest, initialize all the critical user settings. + else + { + // This is what a guest's variables should be. + $username = ''; + $user_info = array('groups' => array(-1)); + $user_settings = array(); + + if (isset($_COOKIE[$cookiename]) && empty($context['tfa_member'])) + $_COOKIE[$cookiename] = ''; + + // Expire the 2FA cookie + if (isset($_COOKIE[$cookiename . '_tfa']) && empty($context['tfa_member'])) + { + $tfa_data = $smcFunc['json_decode']($_COOKIE[$cookiename . '_tfa'], true); + + list (,, $exp) = array_pad((array) $tfa_data, 3, 0); + + if (time() > $exp) + { + $_COOKIE[$cookiename . '_tfa'] = ''; + setTFACookie(-3600, 0, ''); + } + } + + // Create a login token if it doesn't exist yet. + if (!isset($_SESSION['token']['post-login'])) + createToken('login'); + else + list ($context['login_token_var'],,, $context['login_token']) = $_SESSION['token']['post-login']; + + // Do we perhaps think this is a search robot? Check every five minutes just in case... + if ((!empty($modSettings['spider_mode']) || !empty($modSettings['spider_group'])) && (!isset($_SESSION['robot_check']) || $_SESSION['robot_check'] < time() - 300)) + { + require_once($sourcedir . '/ManageSearchEngines.php'); + $user_info['possibly_robot'] = SpiderCheck(); + } + elseif (!empty($modSettings['spider_mode'])) + $user_info['possibly_robot'] = isset($_SESSION['id_robot']) ? $_SESSION['id_robot'] : 0; + // If we haven't turned on proper spider hunts then have a guess! + else + { + $ci_user_agent = strtolower($_SERVER['HTTP_USER_AGENT']); + $user_info['possibly_robot'] = (strpos($_SERVER['HTTP_USER_AGENT'], 'Mozilla') === false && strpos($_SERVER['HTTP_USER_AGENT'], 'Opera') === false) || strpos($ci_user_agent, 'googlebot') !== false || strpos($ci_user_agent, 'slurp') !== false || strpos($ci_user_agent, 'crawl') !== false || strpos($ci_user_agent, 'bingbot') !== false || strpos($ci_user_agent, 'bingpreview') !== false || strpos($ci_user_agent, 'adidxbot') !== false || strpos($ci_user_agent, 'msnbot') !== false; + } + + $user_settings['timezone'] = $modSettings['default_timezone']; + $user_settings['time_offset'] = 0; + } + + // Set up the $user_info array. + $user_info += array( + 'id' => $id_member, + 'username' => $username, + 'name' => isset($user_settings['real_name']) ? $user_settings['real_name'] : '', + 'email' => isset($user_settings['email_address']) ? $user_settings['email_address'] : '', + 'passwd' => isset($user_settings['passwd']) ? $user_settings['passwd'] : '', + 'language' => empty($user_settings['lngfile']) || empty($modSettings['userLanguage']) ? $language : $user_settings['lngfile'], + 'is_guest' => $id_member == 0, + 'is_admin' => in_array(1, $user_info['groups']), + 'theme' => empty($user_settings['id_theme']) ? 0 : $user_settings['id_theme'], + 'last_login' => empty($user_settings['last_login']) ? 0 : $user_settings['last_login'], + 'ip' => $_SERVER['REMOTE_ADDR'], + 'ip2' => $_SERVER['BAN_CHECK_IP'], + 'posts' => empty($user_settings['posts']) ? 0 : $user_settings['posts'], + 'time_format' => empty($user_settings['time_format']) ? $modSettings['time_format'] : $user_settings['time_format'], + 'timezone' => $user_settings['timezone'], + 'time_offset' => $user_settings['time_offset'], + 'avatar' => array( + 'url' => isset($user_settings['avatar']) ? $user_settings['avatar'] : '', + 'filename' => empty($user_settings['filename']) ? '' : $user_settings['filename'], + 'custom_dir' => !empty($user_settings['attachment_type']) && $user_settings['attachment_type'] == 1, + 'id_attach' => isset($user_settings['id_attach']) ? $user_settings['id_attach'] : 0, + 'width' => isset($user_settings['attachment_width']) > 0 ? $user_settings['attachment_width']: 0, + 'height' => isset($user_settings['attachment_height']) > 0 ? $user_settings['attachment_height'] : 0, + ), + 'smiley_set' => isset($user_settings['smiley_set']) ? $user_settings['smiley_set'] : '', + 'messages' => empty($user_settings['instant_messages']) ? 0 : $user_settings['instant_messages'], + 'unread_messages' => empty($user_settings['unread_messages']) ? 0 : $user_settings['unread_messages'], + 'alerts' => empty($user_settings['alerts']) ? 0 : $user_settings['alerts'], + 'total_time_logged_in' => empty($user_settings['total_time_logged_in']) ? 0 : $user_settings['total_time_logged_in'], + 'buddies' => !empty($modSettings['enable_buddylist']) && !empty($user_settings['buddy_list']) ? explode(',', $user_settings['buddy_list']) : array(), + 'ignoreboards' => !empty($user_settings['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $user_settings['ignore_boards']) : array(), + 'ignoreusers' => !empty($user_settings['pm_ignore_list']) ? explode(',', $user_settings['pm_ignore_list']) : array(), + 'warning' => isset($user_settings['warning']) ? $user_settings['warning'] : 0, + 'permissions' => array(), + ); + $user_info['groups'] = array_unique($user_info['groups']); + $user_info['can_manage_boards'] = !empty($user_info['is_admin']) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($user_info['groups'], explode(',', $modSettings['board_manager_groups']))) > 0); + + // Make sure that the last item in the ignore boards array is valid. If the list was too long it could have an ending comma that could cause problems. + if (!empty($user_info['ignoreboards']) && empty($user_info['ignoreboards'][$tmp = count($user_info['ignoreboards']) - 1])) + unset($user_info['ignoreboards'][$tmp]); + + // Allow the user to change their language. + if (!empty($modSettings['userLanguage'])) + { + $languages = getLanguages(); + + // Is it valid? + if (!empty($_GET['language']) && isset($languages[strtr($_GET['language'], './\\:', '____')])) + { + $user_info['language'] = strtr($_GET['language'], './\\:', '____'); + + // Make it permanent for members. + if (!empty($user_info['id'])) + updateMemberData($user_info['id'], array('lngfile' => $user_info['language'])); + else + $_SESSION['language'] = $user_info['language']; + // Reload same url with new language, if it exist + if (isset($_SESSION['old_url'])) + redirectexit($_SESSION['old_url']); + } + elseif (!empty($_SESSION['language']) && isset($languages[strtr($_SESSION['language'], './\\:', '____')])) + $user_info['language'] = strtr($_SESSION['language'], './\\:', '____'); + } + + $temp = build_query_board($user_info['id']); + $user_info['query_see_board'] = $temp['query_see_board']; + $user_info['query_see_message_board'] = $temp['query_see_message_board']; + $user_info['query_see_topic_board'] = $temp['query_see_topic_board']; + $user_info['query_wanna_see_board'] = $temp['query_wanna_see_board']; + $user_info['query_wanna_see_message_board'] = $temp['query_wanna_see_message_board']; + $user_info['query_wanna_see_topic_board'] = $temp['query_wanna_see_topic_board']; + + call_integration_hook('integrate_user_info'); +} + +/** + * Load minimal user info from members table. + * Intended for use by background tasks that need to populate $user_info. + * + * @param int|array $user_ids The users IDs to get the data for. + * @return array + * @throws Exception + */ +function loadMinUserInfo($user_ids = array()) +{ + global $smcFunc, $modSettings, $language, $modSettings; + static $user_info_min = array(); + + $user_ids = (array) $user_ids; + + // Already loaded? + if (!empty($user_ids)) + $user_ids = array_diff($user_ids, array_keys($user_info_min)); + + if (empty($user_ids)) + return $user_info_min; + + $columns_to_load = array( + 'id_member', + 'member_name', + 'real_name', + 'time_offset', + 'additional_groups', + 'id_group', + 'id_post_group', + 'lngfile', + 'smiley_set', + 'timezone', + ); + + call_integration_hook('integrate_load_min_user_settings_columns', array(&$columns_to_load)); + + $request = $smcFunc['db_query']('', ' + SELECT {raw:columns} + FROM {db_prefix}members + WHERE id_member IN ({array_int:user_ids})', + array( + 'user_ids' => array_map('intval', array_unique($user_ids)), + 'columns' => implode(', ', $columns_to_load) + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $user_info_min[$row['id_member']] = array( + 'id' => $row['id_member'], + 'username' => $row['member_name'], + 'name' => isset($row['real_name']) ? $row['real_name'] : '', + 'language' => (empty($row['lngfile']) || empty($modSettings['userLanguage'])) ? $language : $row['lngfile'], + 'is_guest' => false, + 'time_format' => empty($row['time_format']) ? $modSettings['time_format'] : $row['time_format'], + 'smiley_set' => empty($row['smiley_set']) ? $modSettings['smiley_sets_default'] : $row['smiley_set'], + ); + + if (empty($row['additional_groups'])) + $user_info_min[$row['id_member']]['groups'] = array($row['id_group'], $row['id_post_group']); + + else + $user_info_min[$row['id_member']]['groups'] = array_merge( + array($row['id_group'], $row['id_post_group']), + explode(',', $row['additional_groups']) + ); + + $user_info_min[$row['id_member']]['is_admin'] = in_array(1, $user_info_min[$row['id_member']]['groups']); + + if (!empty($row['timezone'])) + { + $tz_system = new \DateTimeZone($modSettings['default_timezone']); + $tz_user = new \DateTimeZone($row['timezone']); + $time_system = new \DateTime('now', $tz_system); + $time_user = new \DateTime('now', $tz_user); + $row['time_offset'] = ($tz_user->getOffset($time_user) - + $tz_system->getOffset($time_system)) / 3600; + } + else + { + if (!empty($row['time_offset'])) + { + $tz_system = new \DateTimeZone($modSettings['default_timezone']); + $time_system = new \DateTime('now', $tz_system); + + $row['timezone'] = @timezone_name_from_abbr('', $tz_system->getOffset($time_system) + $row['time_offset'] * 3600, (int) $time_system->format('I')); + } + + if (empty($row['timezone'])) + { + $row['timezone'] = $modSettings['default_timezone']; + $row['time_offset'] = 0; + } + } + + $user_info_min[$row['id_member']]['timezone'] = $row['timezone']; + $user_info_min[$row['id_member']]['time_offset'] = $row['time_offset']; + } + + $smcFunc['db_free_result']($request); + + call_integration_hook('integrate_load_min_user_settings', array(&$user_info_min)); + + return $user_info_min; +} + +/** + * Check for moderators and see if they have access to the board. + * What it does: + * - sets up the $board_info array for current board information. + * - if cache is enabled, the $board_info array is stored in cache. + * - redirects to appropriate post if only message id is requested. + * - is only used when inside a topic or board. + * - determines the local moderators for the board. + * - adds group id 3 if the user is a local moderator for the board they are in. + * - prevents access if user is not in proper group nor a local moderator of the board. + */ +function loadBoard() +{ + global $txt, $scripturl, $context, $modSettings; + global $board_info, $board, $topic, $user_info, $smcFunc, $cache_enable; + + // Assume they are not a moderator. + $user_info['is_mod'] = false; + $context['user']['is_mod'] = &$user_info['is_mod']; + + // Start the linktree off empty.. + $context['linktree'] = array(); + + // Have they by chance specified a message id but nothing else? + if (empty($_REQUEST['action']) && empty($topic) && empty($board) && !empty($_REQUEST['msg'])) + { + // Make sure the message id is really an int. + $_REQUEST['msg'] = (int) $_REQUEST['msg']; + + // Looking through the message table can be slow, so try using the cache first. + if (($topic = cache_get_data('msg_topic-' . $_REQUEST['msg'], 120)) === null) + { + $request = $smcFunc['db_query']('', ' + SELECT id_topic + FROM {db_prefix}messages + WHERE id_msg = {int:id_msg} + LIMIT 1', + array( + 'id_msg' => $_REQUEST['msg'], + ) + ); + + // So did it find anything? + if ($smcFunc['db_num_rows']($request)) + { + list ($topic) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + // Save save save. + cache_put_data('msg_topic-' . $_REQUEST['msg'], $topic, 120); + } + } + + // Remember redirection is the key to avoiding fallout from your bosses. + if (!empty($topic)) + redirectexit('topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg']); + else + { + loadPermissions(); + loadTheme(); + fatal_lang_error('topic_gone', false); + } + } + + // Load this board only if it is specified. + if (empty($board) && empty($topic)) + { + $board_info = array('moderators' => array(), 'moderator_groups' => array()); + return; + } + + if (!empty($cache_enable) && (empty($topic) || $cache_enable >= 3)) + { + // @todo SLOW? + if (!empty($topic)) + $temp = cache_get_data('topic_board-' . $topic, 120); + else + $temp = cache_get_data('board-' . $board, 120); + + if (!empty($temp)) + { + $board_info = $temp; + $board = $board_info['id']; + } + } + + if (empty($temp)) + { + $custom_column_selects = array(); + $custom_column_parameters = [ + 'current_topic' => $topic, + 'board_link' => empty($topic) ? $smcFunc['db_quote']('{int:current_board}', array('current_board' => $board)) : 't.id_board', + ]; + + call_integration_hook('integrate_load_board', array(&$custom_column_selects, &$custom_column_parameters)); + + $request = $smcFunc['db_query']('load_board_info', ' + SELECT + c.id_cat, b.name AS bname, b.description, b.num_topics, b.member_groups, b.deny_member_groups, + b.id_parent, c.name AS cname, COALESCE(mg.id_group, 0) AS id_moderator_group, mg.group_name, + COALESCE(mem.id_member, 0) AS id_moderator, + mem.real_name' . (!empty($topic) ? ', b.id_board' : '') . ', b.child_level, + b.id_theme, b.override_theme, b.count_posts, b.id_profile, b.redirect, + b.unapproved_topics, b.unapproved_posts' . (!empty($topic) ? ', t.approved, t.id_member_started' : '') . ' + ' . (!empty($custom_column_selects) ? (', ' . implode(', ', $custom_column_selects)) : '') . ' + FROM {db_prefix}boards AS b' . (!empty($topic) ? ' + INNER JOIN {db_prefix}topics AS t ON (t.id_topic = {int:current_topic})' : '') . ' + LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat) + LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = {raw:board_link}) + LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = modgs.id_group) + LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = {raw:board_link}) + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = mods.id_member) + WHERE b.id_board = {raw:board_link}', + $custom_column_parameters + ); + + // If there aren't any, skip. + if ($smcFunc['db_num_rows']($request) > 0) + { + $row = $smcFunc['db_fetch_assoc']($request); + + // Set the current board. + if (!empty($row['id_board'])) + $board = $row['id_board']; + + // Basic operating information. (globals... :/) + $board_info = array( + 'id' => $board, + 'moderators' => array(), + 'moderator_groups' => array(), + 'cat' => array( + 'id' => $row['id_cat'], + 'name' => $row['cname'] + ), + 'name' => $row['bname'], + 'description' => $row['description'], + 'num_topics' => $row['num_topics'], + 'unapproved_topics' => $row['unapproved_topics'], + 'unapproved_posts' => $row['unapproved_posts'], + 'unapproved_user_topics' => 0, + 'parent_boards' => getBoardParents($row['id_parent']), + 'parent' => $row['id_parent'], + 'child_level' => $row['child_level'], + 'theme' => $row['id_theme'], + 'override_theme' => !empty($row['override_theme']), + 'profile' => $row['id_profile'], + 'redirect' => $row['redirect'], + 'recycle' => !empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board']) && $modSettings['recycle_board'] == $board, + 'posts_count' => empty($row['count_posts']), + 'cur_topic_approved' => empty($topic) || $row['approved'], + 'cur_topic_starter' => empty($topic) ? 0 : $row['id_member_started'], + ); + + // Load the membergroups allowed, and check permissions. + $board_info['groups'] = $row['member_groups'] == '' ? array() : explode(',', $row['member_groups']); + $board_info['deny_groups'] = $row['deny_member_groups'] == '' ? array() : explode(',', $row['deny_member_groups']); + + call_integration_hook('integrate_board_info', array(&$board_info, $row)); + + if (!empty($modSettings['board_manager_groups'])) + { + $board_info['groups'] = array_unique(array_merge($board_info['groups'], explode(',', $modSettings['board_manager_groups']))); + $board_info['deny_groups'] = array_diff($board_info['deny_groups'], explode(',', $modSettings['board_manager_groups'])); + } + + do + { + if (!empty($row['id_moderator'])) + $board_info['moderators'][$row['id_moderator']] = array( + 'id' => $row['id_moderator'], + 'name' => $row['real_name'], + 'href' => $scripturl . '?action=profile;u=' . $row['id_moderator'], + 'link' => '' . $row['real_name'] . '' + ); + + if (!empty($row['id_moderator_group'])) + $board_info['moderator_groups'][$row['id_moderator_group']] = array( + 'id' => $row['id_moderator_group'], + 'name' => $row['group_name'], + 'href' => $scripturl . '?action=groups;sa=members;group=' . $row['id_moderator_group'], + 'link' => '' . $row['group_name'] . '' + ); + } + while ($row = $smcFunc['db_fetch_assoc']($request)); + + // If the board only contains unapproved posts and the user isn't an approver then they can't see any topics. + // If that is the case do an additional check to see if they have any topics waiting to be approved. + if ($board_info['num_topics'] == 0 && $modSettings['postmod_active'] && !allowedTo('approve_posts')) + { + // Free the previous result + $smcFunc['db_free_result']($request); + + // @todo why is this using id_topic? + // @todo Can this get cached? + $request = $smcFunc['db_query']('', ' + SELECT COUNT(id_topic) + FROM {db_prefix}topics + WHERE id_member_started={int:id_member} + AND approved = {int:unapproved} + AND id_board = {int:board}', + array( + 'id_member' => $user_info['id'], + 'unapproved' => 0, + 'board' => $board, + ) + ); + + list ($board_info['unapproved_user_topics']) = $smcFunc['db_fetch_row']($request); + } + + if (!empty($cache_enable) && (empty($topic) || $cache_enable >= 3)) + { + // @todo SLOW? + if (!empty($topic)) + cache_put_data('topic_board-' . $topic, $board_info, 120); + cache_put_data('board-' . $board, $board_info, 120); + } + } + else + { + // Otherwise the topic is invalid, there are no moderators, etc. + $board_info = array( + 'moderators' => array(), + 'moderator_groups' => array(), + 'error' => 'exist' + ); + $topic = null; + $board = 0; + } + $smcFunc['db_free_result']($request); + } + + if (!empty($topic)) + $_GET['board'] = (int) $board; + + if (!empty($board)) + { + // Get this into an array of keys for array_intersect + $moderator_groups = array_keys($board_info['moderator_groups']); + + // Now check if the user is a moderator. + $user_info['is_mod'] = isset($board_info['moderators'][$user_info['id']]) || count(array_intersect($user_info['groups'], $moderator_groups)) != 0; + + if (count(array_intersect($user_info['groups'], $board_info['groups'])) == 0 && !$user_info['is_admin']) + $board_info['error'] = 'access'; + if (!empty($modSettings['deny_boards_access']) && count(array_intersect($user_info['groups'], $board_info['deny_groups'])) != 0 && !$user_info['is_admin']) + $board_info['error'] = 'access'; + + // Build up the linktree. + $context['linktree'] = array_merge( + $context['linktree'], + array(array( + 'url' => $scripturl . '#c' . $board_info['cat']['id'], + 'name' => $board_info['cat']['name'] + )), + array_reverse($board_info['parent_boards']), + array(array( + 'url' => $scripturl . '?board=' . $board . '.0', + 'name' => $board_info['name'] + )) + ); + } + + // Set the template contextual information. + $context['user']['is_mod'] = &$user_info['is_mod']; + $context['current_topic'] = $topic; + $context['current_board'] = $board; + + // No posting in redirection boards! + if (!empty($_REQUEST['action']) && $_REQUEST['action'] == 'post' && !empty($board_info['redirect'])) + $board_info['error'] = 'post_in_redirect'; + + // Hacker... you can't see this topic, I'll tell you that. (but moderators can!) + if (!empty($board_info['error']) && (!empty($modSettings['deny_boards_access']) || $board_info['error'] != 'access' || !$user_info['is_mod'])) + { + // The permissions and theme need loading, just to make sure everything goes smoothly. + loadPermissions(); + loadTheme(); + + $_GET['board'] = ''; + $_GET['topic'] = ''; + + // The linktree should not give the game away mate! + $context['linktree'] = array( + array( + 'url' => $scripturl, + 'name' => $context['forum_name_html_safe'] + ) + ); + + // If it's a prefetching agent or we're requesting an attachment. + if ((isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch') || (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'dlattach')) + { + ob_end_clean(); + send_http_status(403); + die; + } + elseif ($board_info['error'] == 'post_in_redirect') + { + // Slightly different error message here... + fatal_lang_error('cannot_post_redirect', false); + } + elseif ($user_info['is_guest']) + { + loadLanguage('Errors'); + is_not_guest($txt['topic_gone']); + } + else + fatal_lang_error('topic_gone', false); + } + + if ($user_info['is_mod']) + $user_info['groups'][] = 3; +} + +/** + * Load this user's permissions. + */ +function loadPermissions() +{ + global $user_info, $board, $board_info, $modSettings, $smcFunc, $sourcedir, $cache_enable; + + if ($user_info['is_admin']) + { + banPermissions(); + return; + } + + if (!empty($cache_enable)) + { + $cache_groups = $user_info['groups']; + asort($cache_groups); + $cache_groups = implode(',', $cache_groups); + // If it's a spider then cache it different. + if ($user_info['possibly_robot']) + $cache_groups .= '-spider'; + + if ($cache_enable >= 2 && !empty($board) && ($temp = cache_get_data('permissions:' . $cache_groups . ':' . $board, 240)) != null && time() - 240 > $modSettings['settings_updated']) + { + list ($user_info['permissions']) = $temp; + banPermissions(); + + return; + } + elseif (($temp = cache_get_data('permissions:' . $cache_groups, 240)) != null && time() - 240 > $modSettings['settings_updated']) + list ($user_info['permissions'], $removals) = $temp; + } + + // If it is detected as a robot, and we are restricting permissions as a special group - then implement this. + $spider_restrict = $user_info['possibly_robot'] && !empty($modSettings['spider_group']) ? ' OR (id_group = {int:spider_group} AND add_deny = 0)' : ''; + + if (empty($user_info['permissions'])) + { + // Get the general permissions. + $request = $smcFunc['db_query']('', ' + SELECT permission, add_deny + FROM {db_prefix}permissions + WHERE id_group IN ({array_int:member_groups}) + ' . $spider_restrict, + array( + 'member_groups' => $user_info['groups'], + 'spider_group' => !empty($modSettings['spider_group']) ? $modSettings['spider_group'] : 0, + ) + ); + $removals = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (empty($row['add_deny'])) + $removals[] = $row['permission']; + else + $user_info['permissions'][] = $row['permission']; + } + $smcFunc['db_free_result']($request); + + if (isset($cache_groups)) + cache_put_data('permissions:' . $cache_groups, array($user_info['permissions'], $removals), 240); + } + + // Get the board permissions. + if (!empty($board)) + { + // Make sure the board (if any) has been loaded by loadBoard(). + if (!isset($board_info['profile'])) + fatal_lang_error('no_board'); + + $request = $smcFunc['db_query']('', ' + SELECT permission, add_deny + FROM {db_prefix}board_permissions + WHERE (id_group IN ({array_int:member_groups}) + ' . $spider_restrict . ') + AND id_profile = {int:id_profile}', + array( + 'member_groups' => $user_info['groups'], + 'id_profile' => $board_info['profile'], + 'spider_group' => !empty($modSettings['spider_group']) ? $modSettings['spider_group'] : 0, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (empty($row['add_deny'])) + $removals[] = $row['permission']; + else + $user_info['permissions'][] = $row['permission']; + } + $smcFunc['db_free_result']($request); + } + + // Remove all the permissions they shouldn't have ;). + if (!empty($modSettings['permission_enable_deny'])) + $user_info['permissions'] = array_diff($user_info['permissions'], $removals); + + if (isset($cache_groups) && !empty($board) && $cache_enable >= 2) + cache_put_data('permissions:' . $cache_groups . ':' . $board, array($user_info['permissions'], null), 240); + + // Banned? Watch, don't touch.. + banPermissions(); + + // Load the mod cache so we can know what additional boards they should see, but no sense in doing it for guests + if (!$user_info['is_guest']) + { + if (!isset($_SESSION['mc']) || $_SESSION['mc']['time'] <= $modSettings['settings_updated']) + { + require_once($sourcedir . '/Subs-Auth.php'); + rebuildModCache(); + } + else + $user_info['mod_cache'] = $_SESSION['mc']; + + // This is a useful phantom permission added to the current user, and only the current user while they are logged in. + // For example this drastically simplifies certain changes to the profile area. + $user_info['permissions'][] = 'is_not_guest'; + // And now some backwards compatibility stuff for mods and whatnot that aren't expecting the new permissions. + $user_info['permissions'][] = 'profile_view_own'; + if (in_array('profile_view', $user_info['permissions'])) + $user_info['permissions'][] = 'profile_view_any'; + } +} + +/** + * Loads an array of users' data by ID or member_name. + * + * @param array|string $users An array of users by id or name or a single username/id + * @param bool $is_name Whether $users contains names + * @param string $set What kind of data to load (normal, profile, minimal) + * @return array The ids of the members loaded + */ +function loadMemberData($users, $is_name = false, $set = 'normal') +{ + global $user_profile, $modSettings, $board_info, $smcFunc, $context; + global $user_info, $cache_enable, $txt; + + // Can't just look for no users :P. + if (empty($users)) + return array(); + + // Pass the set value + $context['loadMemberContext_set'] = $set; + + // Make sure it's an array. + $users = !is_array($users) ? array($users) : array_unique($users); + $loaded_ids = array(); + + if (!$is_name && !empty($cache_enable) && $cache_enable >= 3) + { + $users = array_values($users); + for ($i = 0, $n = count($users); $i < $n; $i++) + { + $data = cache_get_data('member_data-' . $set . '-' . $users[$i], 240); + if ($data == null) + continue; + + $loaded_ids[] = $data['id_member']; + $user_profile[$data['id_member']] = $data; + unset($users[$i]); + } + } + + // Used by default + $select_columns = ' + COALESCE(lo.log_time, 0) AS is_online, COALESCE(a.id_attach, 0) AS id_attach, a.filename, a.attachment_type, a.width "attachment_width", a.height "attachment_height", + mem.signature, mem.personal_text, mem.avatar, mem.id_member, mem.member_name, + mem.real_name, mem.email_address, mem.date_registered, mem.website_title, mem.website_url, + mem.birthdate, mem.member_ip, mem.member_ip2, mem.posts, mem.last_login, mem.id_post_group, mem.lngfile, mem.id_group, mem.time_offset, mem.timezone, mem.show_online, + mg.online_color AS member_group_color, COALESCE(mg.group_name, {string:blank_string}) AS member_group, + pg.online_color AS post_group_color, COALESCE(pg.group_name, {string:blank_string}) AS post_group, + mem.is_activated, mem.warning, ' . (!empty($modSettings['titlesEnable']) ? 'mem.usertitle, ' : '') . ' + CASE WHEN mem.id_group = 0 OR mg.icons = {string:blank_string} THEN pg.icons ELSE mg.icons END AS icons'; + $select_tables = ' + LEFT JOIN {db_prefix}log_online AS lo ON (lo.id_member = mem.id_member) + LEFT JOIN {db_prefix}attachments AS a ON (a.id_member = mem.id_member) + LEFT JOIN {db_prefix}membergroups AS pg ON (pg.id_group = mem.id_post_group) + LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = mem.id_group)'; + + // We add or replace according the the set + switch ($set) + { + case 'normal': + $select_columns .= ', mem.buddy_list, mem.additional_groups'; + break; + case 'profile': + $select_columns .= ', mem.additional_groups, mem.id_theme, mem.pm_ignore_list, mem.pm_receive_from, + mem.time_format, mem.timezone, mem.secret_question, mem.smiley_set, mem.tfa_secret, + mem.total_time_logged_in, lo.url, mem.ignore_boards, mem.password_salt, mem.pm_prefs, mem.buddy_list, mem.alerts'; + break; + case 'minimal': + $select_columns = ' + mem.id_member, mem.member_name, mem.real_name, mem.email_address, mem.date_registered, + mem.posts, mem.last_login, mem.member_ip, mem.member_ip2, mem.lngfile, mem.id_group'; + $select_tables = ''; + break; + default: + { + loadLanguage('Errors'); + trigger_error(sprintf($txt['invalid_member_data_set'], $set), E_USER_WARNING); + } + } + + // Allow mods to easily add to the selected member data + call_integration_hook('integrate_load_member_data', array(&$select_columns, &$select_tables, &$set)); + + if (!empty($users)) + { + // Load the member's data. + $request = $smcFunc['db_query']('', ' + SELECT' . $select_columns . ' + FROM {db_prefix}members AS mem' . $select_tables . ' + WHERE mem.' . ($is_name ? 'member_name' : 'id_member') . ' IN ({' . ($is_name ? 'array_string' : 'array_int') . ':users})', + array( + 'blank_string' => '', + 'users' => $users, + ) + ); + $new_loaded_ids = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // If the image proxy is enabled, we still want the original URL when they're editing the profile... + $row['avatar_original'] = !empty($row['avatar']) ? $row['avatar'] : ''; + + // Take care of proxying avatar if required, do this here for maximum reach + if (!empty($row['avatar'])) + $row['avatar'] = get_proxied_url($row['avatar']); + + // Keep track of the member's normal member group + $row['primary_group'] = !empty($row['member_group']) ? $row['member_group'] : ''; + + if (isset($row['member_ip'])) + $row['member_ip'] = inet_dtop($row['member_ip']); + if (isset($row['member_ip2'])) + $row['member_ip2'] = inet_dtop($row['member_ip2']); + $row['id_member'] = (int) $row['id_member']; + $new_loaded_ids[] = $row['id_member']; + $loaded_ids[] = $row['id_member']; + $row['options'] = array(); + $user_profile[$row['id_member']] = $row; + } + $smcFunc['db_free_result']($request); + } + + if (!empty($new_loaded_ids) && $set !== 'minimal') + { + $request = $smcFunc['db_query']('', ' + SELECT id_member, variable, value + FROM {db_prefix}themes + WHERE id_member IN ({array_int:loaded_ids})', + array( + 'loaded_ids' => $new_loaded_ids, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $user_profile[$row['id_member']]['options'][$row['variable']] = $row['value']; + $smcFunc['db_free_result']($request); + } + + $additional_mods = array(); + + // Are any of these users in groups assigned to moderate this board? + if (!empty($loaded_ids) && !empty($board_info['moderator_groups']) && $set === 'normal') + { + foreach ($loaded_ids as $a_member) + { + if (!empty($user_profile[$a_member]['additional_groups'])) + $groups = array_merge(array($user_profile[$a_member]['id_group']), explode(',', $user_profile[$a_member]['additional_groups'])); + else + $groups = array($user_profile[$a_member]['id_group']); + + $temp = array_intersect($groups, array_keys($board_info['moderator_groups'])); + + if (!empty($temp)) + { + $additional_mods[] = $a_member; + } + } + } + + if (!empty($new_loaded_ids) && !empty($cache_enable) && $cache_enable >= 3) + { + for ($i = 0, $n = count($new_loaded_ids); $i < $n; $i++) + cache_put_data('member_data-' . $set . '-' . $new_loaded_ids[$i], $user_profile[$new_loaded_ids[$i]], 240); + } + + // Are we loading any moderators? If so, fix their group data... + if (!empty($loaded_ids) && (!empty($board_info['moderators']) || !empty($board_info['moderator_groups'])) && $set === 'normal' && count($temp_mods = array_merge(array_intersect($loaded_ids, array_keys($board_info['moderators'])), $additional_mods)) !== 0) + { + if (($row = cache_get_data('moderator_group_info', 480)) == null) + { + $request = $smcFunc['db_query']('', ' + SELECT group_name AS member_group, online_color AS member_group_color, icons + FROM {db_prefix}membergroups + WHERE id_group = {int:moderator_group} + LIMIT 1', + array( + 'moderator_group' => 3, + ) + ); + $row = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + cache_put_data('moderator_group_info', $row, 480); + } + + foreach ($temp_mods as $id) + { + // By popular demand, don't show admins or global moderators as moderators. + if ($user_profile[$id]['id_group'] != 1 && $user_profile[$id]['id_group'] != 2) + $user_profile[$id]['member_group'] = $row['member_group']; + + // If the Moderator group has no color or icons, but their group does... don't overwrite. + if (!empty($row['icons'])) + $user_profile[$id]['icons'] = $row['icons']; + if (!empty($row['member_group_color'])) + $user_profile[$id]['member_group_color'] = $row['member_group_color']; + } + } + + return $loaded_ids; +} + +/** + * Loads the user's basic values... meant for template/theme usage. + * + * @param int $user The ID of a user previously loaded by {@link loadMemberData()} + * @param bool $display_custom_fields Whether or not to display custom profile fields + * @return boolean|array False if the data wasn't loaded or the loaded data. + * @throws Exception + */ +function loadMemberContext($user, $display_custom_fields = false) +{ + global $memberContext, $user_profile, $txt, $scripturl, $user_info; + global $context, $modSettings, $settings, $smcFunc; + static $already_loaded_custom_fields = array(); + static $loadedLanguages = array(); + + // If this person's data is already loaded, skip it. + if (!empty($memberContext[$user]) && !empty($already_loaded_custom_fields[$user]) >= $display_custom_fields) + return $memberContext[$user]; + + // We can't load guests or members not loaded by loadMemberData()! + if ($user == 0) + return false; + if (!isset($user_profile[$user])) + { + loadLanguage('Errors'); + trigger_error(sprintf($txt['user_not_loaded'], $user), E_USER_WARNING); + return false; + } + + // Well, it's loaded now anyhow. + $profile = $user_profile[$user]; + + // These minimal values are always loaded + $memberContext[$user] = array( + 'username' => $profile['member_name'], + 'name' => $profile['real_name'], + 'id' => $profile['id_member'], + 'href' => $scripturl . '?action=profile;u=' . $profile['id_member'], + 'link' => '' . $profile['real_name'] . '', + 'email' => $profile['email_address'], + 'show_email' => !$user_info['is_guest'] && ($user_info['id'] == $profile['id_member'] || allowedTo('moderate_forum')), + 'registered' => empty($profile['date_registered']) ? $txt['not_applicable'] : timeformat($profile['date_registered']), + 'registered_timestamp' => empty($profile['date_registered']) ? 0 : $profile['date_registered'], + ); + + // If the set isn't minimal then load the monstrous array. + if ($context['loadMemberContext_set'] != 'minimal') + { + // Censor everything. + censorText($profile['signature']); + censorText($profile['personal_text']); + + // Set things up to be used before hand. + $profile['signature'] = str_replace(array("\n", "\r"), array('
', ''), $profile['signature']); + $profile['signature'] = parse_bbc($profile['signature'], true, 'sig' . $profile['id_member'], get_signature_allowed_bbc_tags()); + + $profile['is_online'] = (!empty($profile['show_online']) || allowedTo('moderate_forum')) && $profile['is_online'] > 0; + $profile['icons'] = empty($profile['icons']) ? array('', '') : explode('#', $profile['icons']); + // Setup the buddy status here (One whole in_array call saved :P) + $profile['buddy'] = in_array($profile['id_member'], $user_info['buddies']); + $buddy_list = !empty($profile['buddy_list']) ? explode(',', $profile['buddy_list']) : array(); + + //We need a little fallback for the membergroup icons. If it doesn't exist in the current theme, fallback to default theme + if (isset($profile['icons'][1]) && file_exists($settings['actual_theme_dir'] . '/images/membericons/' . $profile['icons'][1])) //icon is set and exists + $group_icon_url = $settings['images_url'] . '/membericons/' . $profile['icons'][1]; + elseif (isset($profile['icons'][1])) //icon is set and doesn't exist, fallback to default + $group_icon_url = $settings['default_images_url'] . '/membericons/' . $profile['icons'][1]; + else //not set, bye bye + $group_icon_url = ''; + + // Go the extra mile and load the user's native language name. + if (empty($loadedLanguages)) + $loadedLanguages = getLanguages(); + + // Figure out the new time offset. + if (!empty($profile['timezone'])) + { + // Get the offsets from UTC for the server, then for the user. + $tz_system = new DateTimeZone($modSettings['default_timezone']); + $tz_user = new DateTimeZone($profile['timezone']); + $time_system = new DateTime('now', $tz_system); + $time_user = new DateTime('now', $tz_user); + $profile['time_offset'] = ($tz_user->getOffset($time_user) - $tz_system->getOffset($time_system)) / 3600; + } + // We need a time zone. + else + { + if (!empty($profile['time_offset'])) + { + $tz_system = new DateTimeZone($modSettings['default_timezone']); + $time_system = new DateTime('now', $tz_system); + + $profile['timezone'] = @timezone_name_from_abbr('', $tz_system->getOffset($time_system) + $profile['time_offset'] * 3600, (int) $time_system->format('I')); + } + + if (empty($profile['timezone'])) + { + $profile['timezone'] = $modSettings['default_timezone']; + $profile['time_offset'] = 0; + } + } + + $memberContext[$user] += array( + 'username_color' => '' . $profile['member_name'] . '', + 'name_color' => '' . $profile['real_name'] . '', + 'link_color' => '' . $profile['real_name'] . '', + 'is_buddy' => $profile['buddy'], + 'is_reverse_buddy' => in_array($user_info['id'], $buddy_list), + 'buddies' => $buddy_list, + 'title' => !empty($modSettings['titlesEnable']) ? $profile['usertitle'] : '', + 'blurb' => $profile['personal_text'], + 'website' => array( + 'title' => $profile['website_title'], + 'url' => $profile['website_url'], + ), + 'birth_date' => empty($profile['birthdate']) ? '1004-01-01' : (substr($profile['birthdate'], 0, 4) === '0004' ? '1004' . substr($profile['birthdate'], 4) : $profile['birthdate']), + 'signature' => $profile['signature'], + 'real_posts' => $profile['posts'], + 'posts' => $profile['posts'] > 500000 ? $txt['geek'] : comma_format($profile['posts']), + 'last_login' => empty($profile['last_login']) ? $txt['never'] : timeformat($profile['last_login']), + 'last_login_timestamp' => empty($profile['last_login']) ? 0 : $profile['last_login'], + 'ip' => $smcFunc['htmlspecialchars']($profile['member_ip']), + 'ip2' => $smcFunc['htmlspecialchars']($profile['member_ip2']), + 'online' => array( + 'is_online' => $profile['is_online'], + 'text' => $smcFunc['htmlspecialchars']($txt[$profile['is_online'] ? 'online' : 'offline']), + 'member_online_text' => sprintf($txt[$profile['is_online'] ? 'member_is_online' : 'member_is_offline'], $smcFunc['htmlspecialchars']($profile['real_name'])), + 'href' => $scripturl . '?action=pm;sa=send;u=' . $profile['id_member'], + 'link' => '' . $txt[$profile['is_online'] ? 'online' : 'offline'] . '', + 'label' => $txt[$profile['is_online'] ? 'online' : 'offline'] + ), + 'language' => !empty($loadedLanguages[$profile['lngfile']]) && !empty($loadedLanguages[$profile['lngfile']]['name']) ? $loadedLanguages[$profile['lngfile']]['name'] : $smcFunc['ucwords'](strtr($profile['lngfile'], array('_' => ' ', '-utf8' => ''))), + 'is_activated' => isset($profile['is_activated']) ? $profile['is_activated'] : 1, + 'is_banned' => isset($profile['is_activated']) ? $profile['is_activated'] >= 10 : 0, + 'options' => $profile['options'], + 'is_guest' => false, + 'primary_group' => $profile['primary_group'], + 'group' => $profile['member_group'], + 'group_color' => $profile['member_group_color'], + 'group_id' => $profile['id_group'], + 'post_group' => $profile['post_group'], + 'post_group_color' => $profile['post_group_color'], + 'group_icons' => str_repeat('*', empty($profile['icons'][0]) || empty($profile['icons'][1]) ? 0 : $profile['icons'][0]), + 'warning' => $profile['warning'], + 'warning_status' => !empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= $profile['warning'] ? 'mute' : (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= $profile['warning'] ? 'moderate' : (!empty($modSettings['warning_watch']) && $modSettings['warning_watch'] <= $profile['warning'] ? 'watch' : (''))), + 'local_time' => timeformat(time(), false, $profile['timezone']), + 'custom_fields' => array(), + ); + } + + // If the set isn't minimal then load their avatar as well. + if ($context['loadMemberContext_set'] != 'minimal') + { + $avatarData = set_avatar_data(array( + 'filename' => $profile['filename'], + 'avatar' => $profile['avatar'], + 'email' => $profile['email_address'], + )); + + if (!empty($avatarData['image'])) + $memberContext[$user]['avatar'] = $avatarData; + } + + // Are we also loading the members custom fields into context? + if ($display_custom_fields && !empty($modSettings['displayFields'])) + { + $memberContext[$user]['custom_fields'] = array(); + + if (!isset($context['display_fields'])) + $context['display_fields'] = $smcFunc['json_decode']($modSettings['displayFields'], true); + + foreach ($context['display_fields'] as $custom) + { + if (!isset($custom['col_name']) || trim($custom['col_name']) == '' || empty($profile['options'][$custom['col_name']])) + continue; + + $value = $profile['options'][$custom['col_name']]; + + $fieldOptions = array(); + $currentKey = 0; + + // Create a key => value array for multiple options fields + if (!empty($custom['options'])) + foreach ($custom['options'] as $k => $v) + { + $fieldOptions[] = $v; + if (empty($currentKey)) + $currentKey = $v == $value ? $k : 0; + } + + // BBC? + if ($custom['bbc']) + $value = parse_bbc($value); + + // ... or checkbox? + elseif (isset($custom['type']) && $custom['type'] == 'check') + $value = $value ? $txt['yes'] : $txt['no']; + + // Enclosing the user input within some other text? + $simple_value = $value; + if (!empty($custom['enclose'])) + $value = strtr($custom['enclose'], array( + '{SCRIPTURL}' => $scripturl, + '{IMAGES_URL}' => $settings['images_url'], + '{DEFAULT_IMAGES_URL}' => $settings['default_images_url'], + '{INPUT}' => tokenTxtReplace($value), + '{KEY}' => $currentKey, + )); + + $memberContext[$user]['custom_fields'][] = array( + 'title' => tokenTxtReplace(!empty($custom['title']) ? $custom['title'] : $custom['col_name']), + 'col_name' => tokenTxtReplace($custom['col_name']), + 'value' => un_htmlspecialchars(tokenTxtReplace($value)), + 'simple' => tokenTxtReplace($simple_value), + 'raw' => $profile['options'][$custom['col_name']], + 'placement' => !empty($custom['placement']) ? $custom['placement'] : 0, + ); + } + } + + call_integration_hook('integrate_member_context', array(&$memberContext[$user], $user, $display_custom_fields)); + + $already_loaded_custom_fields[$user] = !empty($already_loaded_custom_fields[$user]) | $display_custom_fields; + + return $memberContext[$user]; +} + +/** + * Loads the user's custom profile fields + * + * @param integer|array $users A single user ID or an array of user IDs + * @param string|array $params Either a string or an array of strings with profile field names + * @return array|boolean An array of data about the fields and their values or false if nothing was loaded + */ +function loadMemberCustomFields($users, $params) +{ + global $smcFunc, $txt, $scripturl, $settings; + + // Do not waste my time... + if (empty($users) || empty($params)) + return false; + + // Make sure it's an array. + $users = (array) array_unique($users); + $params = (array) array_unique($params); + $return = array(); + + $request = $smcFunc['db_query']('', ' + SELECT c.id_field, c.col_name, c.field_name, c.field_desc, c.field_type, c.field_order, c.field_length, c.field_options, c.mask, show_reg, + c.show_display, c.show_profile, c.private, c.active, c.bbc, c.can_search, c.default_value, c.enclose, c.placement, t.variable, t.value, t.id_member + FROM {db_prefix}themes AS t + LEFT JOIN {db_prefix}custom_fields AS c ON (c.col_name = t.variable) + WHERE id_member IN ({array_int:loaded_ids}) + AND variable IN ({array_string:params}) + ORDER BY field_order', + array( + 'loaded_ids' => $users, + 'params' => $params, + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $fieldOptions = array(); + $currentKey = 0; + $row['field_name'] = tokenTxtReplace($row['field_name']); + $row['field_desc'] = tokenTxtReplace($row['field_desc']); + + // Create a key => value array for multiple options fields + if (!empty($row['field_options'])) + foreach (explode(',', $row['field_options']) as $k => $v) + { + $fieldOptions[] = $v; + if (empty($currentKey)) + $currentKey = $v == $row['value'] ? $k : 0; + } + + // BBC? + if (!empty($row['bbc'])) + $row['value'] = parse_bbc($row['value']); + + // ... or checkbox? + elseif (isset($row['type']) && $row['type'] == 'check') + $row['value'] = !empty($row['value']) ? $txt['yes'] : $txt['no']; + + // Enclosing the user input within some other text? + if (!empty($row['enclose'])) + $row['value'] = strtr($row['enclose'], array( + '{SCRIPTURL}' => $scripturl, + '{IMAGES_URL}' => $settings['images_url'], + '{DEFAULT_IMAGES_URL}' => $settings['default_images_url'], + '{INPUT}' => un_htmlspecialchars($row['value']), + '{KEY}' => $currentKey, + )); + + // Send a simple array if there is just 1 param + if (count($params) == 1) + $return[$row['id_member']] = $row; + + // More than 1? knock yourself out... + else + { + if (!isset($return[$row['id_member']])) + $return[$row['id_member']] = array(); + + $return[$row['id_member']][$row['variable']] = $row; + } + } + + $smcFunc['db_free_result']($request); + + return !empty($return) ? $return : false; +} + +/** + * Loads information about what browser the user is viewing with and places it in $context + * - uses the class from {@link Class-BrowserDetect.php} + */ +function detectBrowser() +{ + // Load the current user's browser of choice + $detector = new browser_detector; + $detector->detectBrowser(); +} + +/** + * Are we using this browser? + * + * Wrapper function for detectBrowser + * + * @param string $browser The browser we are checking for. + * @return bool Whether or not the current browser is what we're looking for + */ +function isBrowser($browser) +{ + global $context; + + // Don't know any browser! + if (empty($context['browser'])) + detectBrowser(); + + return !empty($context['browser'][$browser]) || !empty($context['browser']['is_' . $browser]) ? true : false; +} + +/** + * Load a theme, by ID. + * + * @param int $id_theme The ID of the theme to load + * @param bool $initialize Whether or not to initialize a bunch of theme-related variables/settings + */ +function loadTheme($id_theme = 0, $initialize = true) +{ + global $user_info, $user_settings, $board_info, $boarddir, $maintenance; + global $txt, $boardurl, $scripturl, $mbname, $modSettings; + global $context, $settings, $options, $sourcedir, $smcFunc, $language, $board, $cache_enable; + + if (empty($id_theme)) + { + // The theme was specified by the board. + if (!empty($board_info['theme'])) + $id_theme = $board_info['theme']; + // The theme is the forum's default. + else + $id_theme = $modSettings['theme_guests']; + + // Sometimes the user can choose their own theme. + if (!empty($modSettings['theme_allow']) || allowedTo('admin_forum')) + { + // The theme was specified by REQUEST. + if (!empty($_REQUEST['theme']) && (allowedTo('admin_forum') || in_array($_REQUEST['theme'], explode(',', $modSettings['knownThemes'])))) + { + $id_theme = (int) $_REQUEST['theme']; + $_SESSION['id_theme'] = $id_theme; + } + // The theme was specified by REQUEST... previously. + elseif (!empty($_SESSION['id_theme'])) + $id_theme = (int) $_SESSION['id_theme']; + // The theme is just the user's choice. (might use ?board=1;theme=0 to force board theme.) + elseif (!empty($user_info['theme'])) + $id_theme = $user_info['theme']; + } + + // Verify the id_theme... no foul play. + // Always allow the board specific theme, if they are overriding. + if (!empty($board_info['theme']) && $board_info['override_theme']) + $id_theme = $board_info['theme']; + elseif (!empty($modSettings['enableThemes'])) + { + $themes = explode(',', $modSettings['enableThemes']); + if (!in_array($id_theme, $themes)) + $id_theme = $modSettings['theme_guests']; + else + $id_theme = (int) $id_theme; + } + } + + // Allow mod authors the option to override the theme id for custom page themes + call_integration_hook('integrate_pre_load_theme', array(&$id_theme)); + + // We already load the basic stuff? + if (empty($settings['theme_id']) || $settings['theme_id'] != $id_theme) + { + $member = empty($user_info['id']) ? -1 : $user_info['id']; + + if (!empty($cache_enable) && $cache_enable >= 2 && ($temp = cache_get_data('theme_settings-' . $id_theme . ':' . $member, 60)) != null && time() - 60 > $modSettings['settings_updated']) + { + $themeData = $temp; + $flag = true; + } + elseif (($temp = cache_get_data('theme_settings-' . $id_theme, 90)) != null && time() - 60 > $modSettings['settings_updated']) + $themeData = $temp + array($member => array()); + else + $themeData = array(-1 => array(), 0 => array(), $member => array()); + + if (empty($flag)) + { + // Load variables from the current or default theme, global or this user's. + $result = $smcFunc['db_query']('', ' + SELECT variable, value, id_member, id_theme + FROM {db_prefix}themes + WHERE id_member' . (empty($themeData[0]) ? ' IN (-1, 0, {int:id_member})' : ' = {int:id_member}') . ' + AND id_theme' . ($id_theme == 1 ? ' = {int:id_theme}' : ' IN ({int:id_theme}, 1)') . ' + ORDER BY id_theme asc', + array( + 'id_theme' => $id_theme, + 'id_member' => $member, + ) + ); + // Pick between $settings and $options depending on whose data it is. + foreach ($smcFunc['db_fetch_all']($result) as $row) + { + // There are just things we shouldn't be able to change as members. + if ($row['id_member'] != 0 && in_array($row['variable'], array('actual_theme_url', 'actual_images_url', 'base_theme_dir', 'base_theme_url', 'default_images_url', 'default_theme_dir', 'default_theme_url', 'default_template', 'images_url', 'number_recent_posts', 'smiley_sets_default', 'theme_dir', 'theme_id', 'theme_layers', 'theme_templates', 'theme_url'))) + continue; + + // If this is the theme_dir of the default theme, store it. + if (in_array($row['variable'], array('theme_dir', 'theme_url', 'images_url')) && $row['id_theme'] == '1' && empty($row['id_member'])) + $themeData[0]['default_' . $row['variable']] = $row['value']; + + // If this isn't set yet, is a theme option, or is not the default theme.. + if (!isset($themeData[$row['id_member']][$row['variable']]) || $row['id_theme'] != '1') + $themeData[$row['id_member']][$row['variable']] = substr($row['variable'], 0, 5) == 'show_' ? $row['value'] == '1' : $row['value']; + } + $smcFunc['db_free_result']($result); + + if (!empty($themeData[-1])) + foreach ($themeData[-1] as $k => $v) + { + if (!isset($themeData[$member][$k])) + $themeData[$member][$k] = $v; + } + + if (!empty($cache_enable) && $cache_enable >= 2) + cache_put_data('theme_settings-' . $id_theme . ':' . $member, $themeData, 60); + // Only if we didn't already load that part of the cache... + elseif (!isset($temp)) + cache_put_data('theme_settings-' . $id_theme, array(-1 => $themeData[-1], 0 => $themeData[0]), 90); + } + + $settings = $themeData[0]; + $options = $themeData[$member]; + + $settings['theme_id'] = $id_theme; + + $settings['actual_theme_url'] = $settings['theme_url']; + $settings['actual_images_url'] = $settings['images_url']; + $settings['actual_theme_dir'] = $settings['theme_dir']; + + $settings['template_dirs'] = array(); + // This theme first. + $settings['template_dirs'][] = $settings['theme_dir']; + + // Based on theme (if there is one). + if (!empty($settings['base_theme_dir'])) + $settings['template_dirs'][] = $settings['base_theme_dir']; + + // Lastly the default theme. + if ($settings['theme_dir'] != $settings['default_theme_dir']) + $settings['template_dirs'][] = $settings['default_theme_dir']; + } + + if (!$initialize) + return; + + // Perhaps we've changed the agreement or privacy policy? Only redirect if: + // 1. They're not a guest or admin + // 2. This isn't called from SSI + // 3. This isn't an XML request + // 4. They're not trying to do any of the following actions: + // 4a. View or accept the agreement and/or policy + // 4b. Login or logout + // 4c. Get a feed (RSS, ATOM, etc.) + $agreement_actions = array( + 'agreement' => true, + 'acceptagreement' => true, + 'login2' => true, + 'logintfa' => true, + 'logout' => true, + 'pm' => array('sa' => array('popup')), + 'profile' => array('area' => array('popup', 'alerts_popup')), + 'xmlhttp' => true, + '.xml' => true, + ); + if (empty($user_info['is_guest']) && empty($user_info['is_admin']) && SMF != 'SSI' && !isset($_REQUEST['xml']) && !is_filtered_request($agreement_actions, 'action')) + { + require_once($sourcedir . '/Agreement.php'); + $can_accept_agreement = !empty($modSettings['requireAgreement']) && canRequireAgreement(); + $can_accept_privacy_policy = !empty($modSettings['requirePolicyAgreement']) && canRequirePrivacyPolicy(); + + if ($can_accept_agreement || $can_accept_privacy_policy) + redirectexit('action=agreement'); + } + + // Check to see if we're forcing SSL + if (!empty($modSettings['force_ssl']) && empty($maintenance) && + !httpsOn() && SMF != 'SSI') + { + if (isset($_GET['sslRedirect'])) + { + loadLanguage('Errors'); + fatal_lang_error('login_ssl_required', false); + } + + redirectexit(strtr($_SERVER['REQUEST_URL'], array('http://' => 'https://')) . (strpos($_SERVER['REQUEST_URL'], '?') > 0 ? ';' : '?') . 'sslRedirect'); + } + + // Check to see if they're accessing it from the wrong place. + if (isset($_SERVER['HTTP_HOST']) || isset($_SERVER['SERVER_NAME'])) + { + $detected_url = httpsOn() ? 'https://' : 'http://'; + $detected_url .= empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] . (empty($_SERVER['SERVER_PORT']) || $_SERVER['SERVER_PORT'] == '80' ? '' : ':' . $_SERVER['SERVER_PORT']) : $_SERVER['HTTP_HOST']; + $temp = preg_replace('~/' . basename($scripturl) . '(/.+)?$~', '', strtr(dirname($_SERVER['PHP_SELF']), '\\', '/')); + if ($temp != '/') + $detected_url .= $temp; + } + if (isset($detected_url) && $detected_url != $boardurl) + { + // Try #1 - check if it's in a list of alias addresses. + if (!empty($modSettings['forum_alias_urls'])) + { + $aliases = explode(',', $modSettings['forum_alias_urls']); + + foreach ($aliases as $alias) + { + // Rip off all the boring parts, spaces, etc. + if ($detected_url == trim($alias) || strtr($detected_url, array('http://' => '', 'https://' => '')) == trim($alias)) + $do_fix = true; + } + } + + // Hmm... check #2 - is it just different by a www? Send them to the correct place!! + if (empty($do_fix) && strtr($detected_url, array('://' => '://www.')) == $boardurl && (empty($_GET) || count($_GET) == 1) && SMF != 'SSI') + { + // Okay, this seems weird, but we don't want an endless loop - this will make $_GET not empty ;). + if (empty($_GET)) + redirectexit('wwwRedirect'); + else + { + $k = key($_GET); + $v = current($_GET); + + if ($k != 'wwwRedirect') + redirectexit('wwwRedirect;' . $k . '=' . $v); + } + } + + // #3 is just a check for SSL... + if (strtr($detected_url, array('https://' => 'http://')) == $boardurl) + $do_fix = true; + + // Okay, #4 - perhaps it's an IP address? We're gonna want to use that one, then. (assuming it's the IP or something...) + if (!empty($do_fix) || preg_match('~^http[s]?://(?:[\d\.:]+|\[[\d:]+\](?::\d+)?)(?:$|/)~', $detected_url) == 1) + { + // Caching is good ;). + $oldurl = $boardurl; + + // Fix $boardurl and $scripturl. + $boardurl = $detected_url; + $scripturl = strtr($scripturl, array($oldurl => $boardurl)); + $_SERVER['REQUEST_URL'] = strtr($_SERVER['REQUEST_URL'], array($oldurl => $boardurl)); + + // Fix the theme urls... + $settings['theme_url'] = strtr($settings['theme_url'], array($oldurl => $boardurl)); + $settings['default_theme_url'] = strtr($settings['default_theme_url'], array($oldurl => $boardurl)); + $settings['actual_theme_url'] = strtr($settings['actual_theme_url'], array($oldurl => $boardurl)); + $settings['images_url'] = strtr($settings['images_url'], array($oldurl => $boardurl)); + $settings['default_images_url'] = strtr($settings['default_images_url'], array($oldurl => $boardurl)); + $settings['actual_images_url'] = strtr($settings['actual_images_url'], array($oldurl => $boardurl)); + + // And just a few mod settings :). + $modSettings['smileys_url'] = strtr($modSettings['smileys_url'], array($oldurl => $boardurl)); + $modSettings['avatar_url'] = strtr($modSettings['avatar_url'], array($oldurl => $boardurl)); + $modSettings['custom_avatar_url'] = strtr($modSettings['custom_avatar_url'], array($oldurl => $boardurl)); + + // Clean up after loadBoard(). + if (isset($board_info['moderators'])) + { + foreach ($board_info['moderators'] as $k => $dummy) + { + $board_info['moderators'][$k]['href'] = strtr($dummy['href'], array($oldurl => $boardurl)); + $board_info['moderators'][$k]['link'] = strtr($dummy['link'], array('"' . $oldurl => '"' . $boardurl)); + } + } + foreach ($context['linktree'] as $k => $dummy) + $context['linktree'][$k]['url'] = strtr($dummy['url'], array($oldurl => $boardurl)); + } + } + // Set up the contextual user array. + if (!empty($user_info)) + { + $context['user'] = array( + 'id' => $user_info['id'], + 'is_logged' => !$user_info['is_guest'], + 'is_guest' => &$user_info['is_guest'], + 'is_admin' => &$user_info['is_admin'], + 'is_mod' => &$user_info['is_mod'], + // A user can mod if they have permission to see the mod center, or they are a board/group/approval moderator. + 'can_mod' => allowedTo('access_mod_center') || (!$user_info['is_guest'] && ($user_info['mod_cache']['gq'] != '0=1' || $user_info['mod_cache']['bq'] != '0=1' || ($modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap'])))), + 'name' => $user_info['username'], + 'language' => $user_info['language'], + 'email' => $user_info['email'], + 'ignoreusers' => $user_info['ignoreusers'], + ); + if (!$context['user']['is_guest']) + $context['user']['name'] = $user_info['name']; + elseif ($context['user']['is_guest'] && !empty($txt['guest_title'])) + $context['user']['name'] = $txt['guest_title']; + + // Determine the current smiley set. + $smiley_sets_known = explode(',', $modSettings['smiley_sets_known']); + $user_info['smiley_set'] = (!in_array($user_info['smiley_set'], $smiley_sets_known) && $user_info['smiley_set'] != 'none') || empty($modSettings['smiley_sets_enable']) ? (!empty($settings['smiley_sets_default']) ? $settings['smiley_sets_default'] : $modSettings['smiley_sets_default']) : $user_info['smiley_set']; + $context['user']['smiley_set'] = $user_info['smiley_set']; + } + else + { + // What to do when there is no $user_info (e.g., an error very early in the login process) + $context['user'] = array( + 'id' => -1, + 'is_logged' => false, + 'is_guest' => true, + 'is_mod' => false, + 'can_mod' => false, + 'name' => $txt['guest_title'], + 'language' => $language, + 'email' => '', + 'ignoreusers' => array(), + ); + // Note we should stuff $user_info with some guest values also... + $user_info = array( + 'id' => 0, + 'is_guest' => true, + 'is_admin' => false, + 'is_mod' => false, + 'username' => $txt['guest_title'], + 'language' => $language, + 'email' => '', + 'smiley_set' => '', + 'permissions' => array(), + 'groups' => array(), + 'ignoreusers' => array(), + 'possibly_robot' => true, + 'time_offset' => 0, + 'timezone' => $modSettings['default_timezone'], + 'time_format' => $modSettings['time_format'], + ); + } + + // Some basic information... + if (!isset($context['html_headers'])) + $context['html_headers'] = ''; + if (!isset($context['javascript_files'])) + $context['javascript_files'] = array(); + if (!isset($context['css_files'])) + $context['css_files'] = array(); + if (!isset($context['css_header'])) + $context['css_header'] = array(); + if (!isset($context['javascript_inline'])) + $context['javascript_inline'] = array('standard' => array(), 'defer' => array()); + if (!isset($context['javascript_vars'])) + $context['javascript_vars'] = array(); + + $context['login_url'] = $scripturl . '?action=login2'; + $context['menu_separator'] = !empty($settings['use_image_buttons']) ? ' ' : ' | '; + $context['session_var'] = $_SESSION['session_var']; + $context['session_id'] = $_SESSION['session_value']; + $context['forum_name'] = $mbname; + $context['forum_name_html_safe'] = $smcFunc['htmlspecialchars']($context['forum_name']); + $context['header_logo_url_html_safe'] = empty($settings['header_logo_url']) ? '' : $smcFunc['htmlspecialchars']($settings['header_logo_url']); + $context['current_action'] = isset($_REQUEST['action']) ? $smcFunc['htmlspecialchars']($_REQUEST['action']) : null; + $context['current_subaction'] = isset($_REQUEST['sa']) ? $_REQUEST['sa'] : null; + $context['can_register'] = empty($modSettings['registration_method']) || $modSettings['registration_method'] != 3; + if (isset($modSettings['load_average'])) + $context['load_average'] = $modSettings['load_average']; + + // Detect the browser. This is separated out because it's also used in attachment downloads + detectBrowser(); + + // Set the top level linktree up. + // Note that if we're dealing with certain very early errors (e.g., login) the linktree might not be set yet... + if (empty($context['linktree'])) + $context['linktree'] = array(); + array_unshift($context['linktree'], array( + 'url' => $scripturl, + 'name' => $context['forum_name_html_safe'] + )); + + // This allows sticking some HTML on the page output - useful for controls. + $context['insert_after_template'] = ''; + + if (!isset($txt)) + $txt = array(); + + $simpleActions = array( + 'findmember', + 'helpadmin', + 'printpage', + ); + + // Parent action => array of areas + $simpleAreas = array( + 'profile' => array('popup', 'alerts_popup',), + ); + + // Parent action => array of subactions + $simpleSubActions = array( + 'pm' => array('popup',), + 'signup' => array('usernamecheck'), + ); + + // Extra params like ;preview ;js, etc. + $extraParams = array( + 'preview', + 'splitjs', + ); + + // Actions that specifically uses XML output. + $xmlActions = array( + 'quotefast', + 'jsmodify', + 'xmlhttp', + 'post2', + 'suggest', + 'stats', + 'notifytopic', + 'notifyboard', + ); + + call_integration_hook('integrate_simple_actions', array(&$simpleActions, &$simpleAreas, &$simpleSubActions, &$extraParams, &$xmlActions)); + + $context['simple_action'] = in_array($context['current_action'], $simpleActions) || + (isset($simpleAreas[$context['current_action']]) && isset($_REQUEST['area']) && in_array($_REQUEST['area'], $simpleAreas[$context['current_action']])) || + (isset($simpleSubActions[$context['current_action']]) && in_array($context['current_subaction'], $simpleSubActions[$context['current_action']])); + + // See if theres any extra param to check. + $requiresXML = false; + foreach ($extraParams as $key => $extra) + if (isset($_REQUEST[$extra])) + $requiresXML = true; + + // Output is fully XML, so no need for the index template. + if (isset($_REQUEST['xml']) && (in_array($context['current_action'], $xmlActions) || $requiresXML)) + { + loadLanguage('index+Modifications'); + loadTemplate('Xml'); + $context['template_layers'] = array(); + } + + // These actions don't require the index template at all. + elseif (!empty($context['simple_action'])) + { + loadLanguage('index+Modifications'); + $context['template_layers'] = array(); + } + + else + { + // Custom templates to load, or just default? + if (isset($settings['theme_templates'])) + $templates = explode(',', $settings['theme_templates']); + else + $templates = array('index'); + + // Load each template... + foreach ($templates as $template) + loadTemplate($template); + + // ...and attempt to load their associated language files. + $required_files = implode('+', array_merge($templates, array('Modifications'))); + loadLanguage($required_files, '', false); + + // Custom template layers? + if (isset($settings['theme_layers'])) + $context['template_layers'] = explode(',', $settings['theme_layers']); + else + $context['template_layers'] = array('html', 'body'); + } + + // Initialize the theme. + loadSubTemplate('init', 'ignore'); + + // Allow overriding the board wide time/number formats. + if (empty($user_settings['time_format']) && !empty($txt['time_format'])) + $user_info['time_format'] = $txt['time_format']; + + // Set the character set from the template. + $context['character_set'] = empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']; + $context['right_to_left'] = !empty($txt['lang_rtl']); + + // Guests may still need a name. + if ($context['user']['is_guest'] && empty($context['user']['name'])) + $context['user']['name'] = $txt['guest_title']; + + // Any theme-related strings that need to be loaded? + if (!empty($settings['require_theme_strings'])) + loadLanguage('ThemeStrings', '', false); + + // Make a special URL for the language. + $settings['lang_images_url'] = $settings['images_url'] . '/' . (!empty($txt['image_lang']) ? $txt['image_lang'] : $user_info['language']); + + // And of course, let's load the default CSS file. + loadCSSFile('index.css', array('minimize' => true, 'order_pos' => 1), 'smf_index'); + + // Here is my luvly Responsive CSS + loadCSSFile('responsive.css', array('force_current' => false, 'validate' => true, 'minimize' => true, 'order_pos' => 9000), 'smf_responsive'); + + if ($context['right_to_left']) + loadCSSFile('rtl.css', array('order_pos' => 4000), 'smf_rtl'); + + // We allow theme variants, because we're cool. + $context['theme_variant'] = ''; + $context['theme_variant_url'] = ''; + if (!empty($settings['theme_variants'])) + { + // Overriding - for previews and that ilk. + if (!empty($_REQUEST['variant'])) + $_SESSION['id_variant'] = $_REQUEST['variant']; + // User selection? + if (empty($settings['disable_user_variant']) || allowedTo('admin_forum')) + $context['theme_variant'] = !empty($_SESSION['id_variant']) && in_array($_SESSION['id_variant'], $settings['theme_variants']) ? $_SESSION['id_variant'] : (!empty($options['theme_variant']) && in_array($options['theme_variant'], $settings['theme_variants']) ? $options['theme_variant'] : ''); + // If not a user variant, select the default. + if ($context['theme_variant'] == '' || !in_array($context['theme_variant'], $settings['theme_variants'])) + $context['theme_variant'] = !empty($settings['default_variant']) && in_array($settings['default_variant'], $settings['theme_variants']) ? $settings['default_variant'] : $settings['theme_variants'][0]; + + // Do this to keep things easier in the templates. + $context['theme_variant'] = '_' . $context['theme_variant']; + $context['theme_variant_url'] = $context['theme_variant'] . '/'; + + if (!empty($context['theme_variant'])) + { + loadCSSFile('index' . $context['theme_variant'] . '.css', array('order_pos' => 300), 'smf_index' . $context['theme_variant']); + if ($context['right_to_left']) + loadCSSFile('rtl' . $context['theme_variant'] . '.css', array('order_pos' => 4200), 'smf_rtl' . $context['theme_variant']); + } + } + + // Let's be compatible with old themes! + if (!function_exists('template_html_above') && in_array('html', $context['template_layers'])) + $context['template_layers'] = array('main'); + + $context['tabindex'] = 1; + + // Compatibility. + if (!isset($settings['theme_version'])) + $modSettings['memberCount'] = $modSettings['totalMembers']; + + // Default JS variables for use in every theme + $context['javascript_vars'] = array( + 'smf_theme_url' => '"' . $settings['theme_url'] . '"', + 'smf_default_theme_url' => '"' . $settings['default_theme_url'] . '"', + 'smf_images_url' => '"' . $settings['images_url'] . '"', + 'smf_smileys_url' => '"' . $modSettings['smileys_url'] . '"', + 'smf_smiley_sets' => '"' . $modSettings['smiley_sets_known'] . '"', + 'smf_smiley_sets_default' => '"' . $modSettings['smiley_sets_default'] . '"', + 'smf_avatars_url' => '"' . $modSettings['avatar_url'] . '"', + 'smf_scripturl' => '"' . $scripturl . '"', + 'smf_iso_case_folding' => $context['server']['iso_case_folding'] ? 'true' : 'false', + 'smf_charset' => '"' . $context['character_set'] . '"', + 'smf_session_id' => '"' . $context['session_id'] . '"', + 'smf_session_var' => '"' . $context['session_var'] . '"', + 'smf_member_id' => $context['user']['id'], + 'ajax_notification_text' => JavaScriptEscape($txt['ajax_in_progress']), + 'help_popup_heading_text' => JavaScriptEscape($txt['help_popup']), + 'banned_text' => JavaScriptEscape(sprintf($txt['your_ban'], $context['user']['name'])), + 'smf_txt_expand' => JavaScriptEscape($txt['code_expand']), + 'smf_txt_shrink' => JavaScriptEscape($txt['code_shrink']), + 'smf_collapseAlt' => JavaScriptEscape($txt['hide']), + 'smf_expandAlt' => JavaScriptEscape($txt['show']), + 'smf_quote_expand' => !empty($modSettings['quote_expand']) ? $modSettings['quote_expand'] : 'false', + 'allow_xhjr_credentials' => !empty($modSettings['allow_cors_credentials']) ? 'true' : 'false', + ); + + // Add the JQuery library to the list of files to load. + $jQueryUrls = array ('cdn' => 'https://ajax.googleapis.com/ajax/libs/jquery/'. JQUERY_VERSION . '/jquery.min.js', 'jquery_cdn' => 'https://code.jquery.com/jquery-'. JQUERY_VERSION . '.min.js', 'microsoft_cdn' => 'https://ajax.aspnetcdn.com/ajax/jQuery/jquery-'. JQUERY_VERSION . '.min.js'); + + if (isset($modSettings['jquery_source']) && array_key_exists($modSettings['jquery_source'], $jQueryUrls)) + loadJavaScriptFile($jQueryUrls[$modSettings['jquery_source']], array('external' => true, 'seed' => false), 'smf_jquery'); + + elseif (isset($modSettings['jquery_source']) && $modSettings['jquery_source'] == 'local') + loadJavaScriptFile('jquery-' . JQUERY_VERSION . '.min.js', array('seed' => false), 'smf_jquery'); + + elseif (isset($modSettings['jquery_source'], $modSettings['jquery_custom']) && $modSettings['jquery_source'] == 'custom') + loadJavaScriptFile($modSettings['jquery_custom'], array('external' => true, 'seed' => false), 'smf_jquery'); + + // Fall back to the forum default + else + loadJavaScriptFile('https://ajax.googleapis.com/ajax/libs/jquery/' . JQUERY_VERSION . '/jquery.min.js', array('external' => true, 'seed' => false), 'smf_jquery'); + + // Queue our JQuery plugins! + loadJavaScriptFile('smf_jquery_plugins.js', array('minimize' => true), 'smf_jquery_plugins'); + if (!$user_info['is_guest']) + { + loadJavaScriptFile('jquery.custom-scrollbar.js', array('minimize' => true), 'smf_jquery_scrollbar'); + loadCSSFile('jquery.custom-scrollbar.css', array('force_current' => false, 'validate' => true), 'smf_scrollbar'); + } + + // script.js and theme.js, always required, so always add them! Makes index.template.php cleaner and all. + loadJavaScriptFile('script.js', array('defer' => false, 'minimize' => true), 'smf_script'); + loadJavaScriptFile('theme.js', array('minimize' => true), 'smf_theme'); + + // And we should probably trigger the cron too. + if (empty($modSettings['cron_is_real_cron'])) + { + $ts = time(); + $ts -= $ts % 15; + addInlineJavaScript(' + function triggerCron() + { + $.get(' . JavaScriptEscape($boardurl) . ' + "/cron.php?ts=' . $ts . '"); + } + window.setTimeout(triggerCron, 1);', true); + + // Robots won't normally trigger cron.php, so for them run the scheduled tasks directly. + if (isBrowser('possibly_robot') && (empty($modSettings['next_task_time']) || $modSettings['next_task_time'] < time() || (!empty($modSettings['mail_next_send']) && $modSettings['mail_next_send'] < time() && empty($modSettings['mail_queue_use_cron'])))) + { + require_once($sourcedir . '/ScheduledTasks.php'); + + // What to do, what to do?! + if (empty($modSettings['next_task_time']) || $modSettings['next_task_time'] < time()) + AutoTask(); + else + ReduceMailQueue(); + } + } + + // Filter out the restricted boards from the linktree + if (!$user_info['is_admin'] && !empty($board)) + { + foreach ($context['linktree'] as $k => $element) + { + if (!empty($element['groups']) && + (count(array_intersect($user_info['groups'], $element['groups'])) == 0 || + (!empty($modSettings['deny_boards_access']) && count(array_intersect($user_info['groups'], $element['deny_groups'])) != 0))) + { + $context['linktree'][$k]['name'] = $txt['restricted_board']; + $context['linktree'][$k]['extra_before'] = ''; + $context['linktree'][$k]['extra_after'] = ''; + unset($context['linktree'][$k]['url']); + } + } + } + + // Any files to include at this point? + if (!empty($modSettings['integrate_theme_include'])) + { + $theme_includes = explode(',', $modSettings['integrate_theme_include']); + foreach ($theme_includes as $include) + { + $include = strtr(trim($include), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir'])); + if (file_exists($include)) + require_once($include); + } + } + + // Call load theme integration functions. + call_integration_hook('integrate_load_theme'); + + // We are ready to go. + $context['theme_loaded'] = true; +} + +/** + * Load a template - if the theme doesn't include it, use the default. + * What this function does: + * - loads a template file with the name template_name from the current, default, or base theme. + * - detects a wrong default theme directory and tries to work around it. + * + * @uses template_include() to include the file. + * @param string $template_name The name of the template to load + * @param array|string $style_sheets The name of a single stylesheet or an array of names of stylesheets to load + * @param bool $fatal If true, dies with an error message if the template cannot be found + * @return boolean Whether or not the template was loaded + */ +function loadTemplate($template_name, $style_sheets = array(), $fatal = true) +{ + global $context, $settings, $txt, $scripturl, $boarddir, $db_show_debug; + + // Do any style sheets first, cause we're easy with those. + if (!empty($style_sheets)) + { + if (!is_array($style_sheets)) + $style_sheets = array($style_sheets); + + foreach ($style_sheets as $sheet) + loadCSSFile($sheet . '.css', array(), $sheet); + } + + // No template to load? + if ($template_name === false) + return true; + + $loaded = false; + foreach ($settings['template_dirs'] as $template_dir) + { + if (file_exists($template_dir . '/' . $template_name . '.template.php')) + { + $loaded = true; + template_include($template_dir . '/' . $template_name . '.template.php', true); + break; + } + } + + if ($loaded) + { + if ($db_show_debug === true) + $context['debug']['templates'][] = $template_name . ' (' . basename($template_dir) . ')'; + + // If they have specified an initialization function for this template, go ahead and call it now. + if (function_exists('template_' . $template_name . '_init')) + call_user_func('template_' . $template_name . '_init'); + } + // Hmmm... doesn't exist?! I don't suppose the directory is wrong, is it? + elseif (!file_exists($settings['default_theme_dir']) && file_exists($boarddir . '/Themes/default')) + { + $settings['default_theme_dir'] = $boarddir . '/Themes/default'; + $settings['template_dirs'][] = $settings['default_theme_dir']; + + if (!empty($context['user']['is_admin']) && !isset($_GET['th'])) + { + loadLanguage('Errors'); + echo ' +'; + } + + loadTemplate($template_name); + } + // Cause an error otherwise. + elseif ($template_name != 'Errors' && $template_name != 'index' && $fatal) + fatal_lang_error('theme_template_error', 'template', array((string) $template_name)); + elseif ($fatal) + die(log_error(sprintf(isset($txt['theme_template_error']) ? $txt['theme_template_error'] : 'Unable to load Themes/default/%s.template.php!', (string) $template_name), 'template')); + else + return false; +} + +/** + * Load a sub-template. + * What it does: + * - loads the sub template specified by sub_template_name, which must be in an already-loaded template. + * - if ?debug is in the query string, shows administrators a marker after every sub template + * for debugging purposes. + * + * @todo get rid of reading $_REQUEST directly + * + * @param string $sub_template_name The name of the sub-template to load + * @param bool $fatal Whether to die with an error if the sub-template can't be loaded + */ +function loadSubTemplate($sub_template_name, $fatal = false) +{ + global $context, $txt, $db_show_debug; + + if ($db_show_debug === true) + $context['debug']['sub_templates'][] = $sub_template_name; + + // Figure out what the template function is named. + $theme_function = 'template_' . $sub_template_name; + if (function_exists($theme_function)) + $theme_function(); + elseif ($fatal === false) + fatal_lang_error('theme_template_error', 'template', array((string) $sub_template_name)); + elseif ($fatal !== 'ignore') + die(log_error(sprintf(isset($txt['theme_template_error']) ? $txt['theme_template_error'] : 'Unable to load the %s sub template!', (string) $sub_template_name), 'template')); + + // Are we showing debugging for templates? Just make sure not to do it before the doctype... + if (allowedTo('admin_forum') && isset($_REQUEST['debug']) && !in_array($sub_template_name, array('init', 'main_below')) && ob_get_length() > 0 && !isset($_REQUEST['xml'])) + { + echo ' +
---- ', $sub_template_name, ' ends ----
'; + } +} + +/** + * Add a CSS file for output later + * + * @param string $fileName The name of the file to load + * @param array $params An array of parameters + * Keys are the following: + * - ['external'] (true/false): define if the file is a externally located file. Needs to be set to true if you are loading an external file + * - ['default_theme'] (true/false): force use of default theme url + * - ['force_current'] (true/false): if this is false, we will attempt to load the file from the default theme if not found in the current theme + * - ['validate'] (true/false): if true script will validate the local file exists + * - ['rtl'] (string): additional file to load in RTL mode + * - ['seed'] (true/false/string): if true or null, use cache stale, false do not, or used a supplied string + * - ['minimize'] boolean to add your file to the main minimized file. Useful when you have a file thats loaded everywhere and for everyone. + * - ['order_pos'] int define the load order, when not define it's loaded in the middle, before index.css = -500, after index.css = 500, middle = 3000, end (i.e. after responsive.css) = 10000 + * - ['attributes'] array extra attributes to add to the element + * @param string $id An ID to stick on the end of the filename for caching purposes + */ +function loadCSSFile($fileName, $params = array(), $id = '') +{ + global $settings, $context, $modSettings; + + if (empty($context['css_files_order'])) + $context['css_files_order'] = array(); + + $params['seed'] = (!array_key_exists('seed', $params) || (array_key_exists('seed', $params) && $params['seed'] === true)) ? + (array_key_exists('browser_cache', $context) ? $context['browser_cache'] : '') : + (is_string($params['seed']) ? '?' . ltrim($params['seed'], '?') : ''); + $params['force_current'] = isset($params['force_current']) ? $params['force_current'] : false; + $themeRef = !empty($params['default_theme']) ? 'default_theme' : 'theme'; + $params['minimize'] = isset($params['minimize']) ? $params['minimize'] : true; + $params['external'] = isset($params['external']) ? $params['external'] : false; + $params['validate'] = isset($params['validate']) ? $params['validate'] : true; + $params['order_pos'] = isset($params['order_pos']) ? (int) $params['order_pos'] : 3000; + $params['attributes'] = isset($params['attributes']) ? $params['attributes'] : array(); + + // Account for shorthand like admin.css?alp21 filenames + $id = (empty($id) ? strtr(str_replace('.css', '', basename($fileName)), '?', '_') : $id) . '_css'; + + $fileName = str_replace(pathinfo($fileName, PATHINFO_EXTENSION), strtok(pathinfo($fileName, PATHINFO_EXTENSION), '?'), $fileName); + + // Is this a local file? + if (empty($params['external'])) + { + // Are we validating the the file exists? + if (!empty($params['validate']) && ($mtime = @filemtime($settings[$themeRef . '_dir'] . '/css/' . $fileName)) === false) + { + // Maybe the default theme has it? + if ($themeRef === 'theme' && !$params['force_current'] && ($mtime = @filemtime($settings['default_theme_dir'] . '/css/' . $fileName) !== false)) + { + $fileUrl = $settings['default_theme_url'] . '/css/' . $fileName; + $filePath = $settings['default_theme_dir'] . '/css/' . $fileName; + } + else + { + $fileUrl = false; + $filePath = false; + } + } + else + { + $fileUrl = $settings[$themeRef . '_url'] . '/css/' . $fileName; + $filePath = $settings[$themeRef . '_dir'] . '/css/' . $fileName; + $mtime = @filemtime($filePath); + } + } + // An external file doesn't have a filepath. Mock one for simplicity. + else + { + $fileUrl = $fileName; + $filePath = $fileName; + + // Always turn these off for external files. + $params['minimize'] = false; + $params['seed'] = false; + } + + $mtime = empty($mtime) ? 0 : $mtime; + + // Add it to the array for use in the template + if (!empty($fileName) && !empty($fileUrl)) + { + // find a free number/position + while (isset($context['css_files_order'][$params['order_pos']])) + $params['order_pos']++; + $context['css_files_order'][$params['order_pos']] = $id; + + $context['css_files'][$id] = array('fileUrl' => $fileUrl, 'filePath' => $filePath, 'fileName' => $fileName, 'options' => $params, 'mtime' => $mtime); + } + + if (!empty($context['right_to_left']) && !empty($params['rtl'])) + loadCSSFile($params['rtl'], array_diff_key($params, array('rtl' => 0))); + + if ($mtime > $modSettings['browser_cache']) + updateSettings(array('browser_cache' => $mtime)); +} + +/** + * Add a block of inline css code to be executed later + * + * - only use this if you have to, generally external css files are better, but for very small changes + * or for scripts that require help from PHP/whatever, this can be useful. + * - all code added with this function is added to the same '; + } + + if (!empty($context['export_javascript_vars'])) + { + $stylesheet['css_js'] .= ' + '; + } + + if (!empty($context['export_javascript_files'])) + { + foreach ($context['export_javascript_files'] as $js_file) + { + $stylesheet['css_js'] .= ' + '; + } + } + + if (!empty($context['export_javascript_inline']['standard'])) + { + $stylesheet['css_js'] .= ' + '; + } + + if (!empty($context['export_javascript_inline']['defer'])) + { + $stylesheet['css_js'] .= ' + '; + } + + $stylesheet['css_js'] .= ' + '; + + // End of the XSLT stylesheet + $stylesheet['footer'] = ($format == 'XML_XSLT' ? "\t" : '') . ''; + } + + // Let mods adjust the XSLT stylesheet. + call_integration_hook('integrate_export_xslt_stylesheet', array(&$stylesheet, $format)); + + // Remember for later. + $xslt_key = isset($xslt_key) ? $xslt_key : $smcFunc['json_encode'](array($format, $uid, $xslt_variables)); + $xslts[$xslt_key] = array('stylesheet' => implode("\n", (array) $stylesheet), 'doctype' => $doctype); + + return $xslts[$xslt_key]; +} + +/** + * Loads and prepares CSS and JavaScript for insertion into an XSLT stylesheet. + */ +function export_load_css_js() +{ + global $context, $modSettings, $sourcedir, $smcFunc, $user_info; + + // If we're not running a background task, we need to preserve any existing CSS and JavaScript. + if (SMF != 'BACKGROUND') + { + foreach (array('css_files', 'css_header', 'javascript_vars', 'javascript_files', 'javascript_inline') as $var) + { + if (isset($context[$var])) + $context['real_' . $var] = $context[$var]; + + if ($var == 'javascript_inline') + { + foreach ($context[$var] as $key => $value) + $context[$var][$key] = array(); + } + else + $context[$var] = array(); + } + } + // Autoloading is unavailable for background tasks, so we have to do things the hard way... + else + { + if (!empty($modSettings['minimize_files']) && (!class_exists('MatthiasMullie\\Minify\\CSS') || !class_exists('MatthiasMullie\\Minify\\JS'))) + { + // Include, not require, because minimization is nice to have but not vital here. + include_once(implode(DIRECTORY_SEPARATOR, array($sourcedir, 'minify', 'src', 'Exception.php'))); + include_once(implode(DIRECTORY_SEPARATOR, array($sourcedir, 'minify', 'src', 'Exceptions', 'BasicException.php'))); + include_once(implode(DIRECTORY_SEPARATOR, array($sourcedir, 'minify', 'src', 'Exceptions', 'FileImportException.php'))); + include_once(implode(DIRECTORY_SEPARATOR, array($sourcedir, 'minify', 'src', 'Exceptions', 'IOException.php'))); + + include_once(implode(DIRECTORY_SEPARATOR, array($sourcedir, 'minify', 'src', 'Minify.php'))); + include_once(implode(DIRECTORY_SEPARATOR, array($sourcedir, 'minify', 'path-converter', 'src', 'Converter.php'))); + + include_once(implode(DIRECTORY_SEPARATOR, array($sourcedir, 'minify', 'src', 'CSS.php'))); + include_once(implode(DIRECTORY_SEPARATOR, array($sourcedir, 'minify', 'src', 'JS.php'))); + + if (!class_exists('MatthiasMullie\\Minify\\CSS') || !class_exists('MatthiasMullie\\Minify\\JS')) + $modSettings['minimize_files'] = false; + } + } + + // Load our standard CSS files. + loadCSSFile('index.css', array('minimize' => true, 'order_pos' => 1), 'smf_index'); + loadCSSFile('responsive.css', array('force_current' => false, 'validate' => true, 'minimize' => true, 'order_pos' => 9000), 'smf_responsive'); + + if ($context['right_to_left']) + loadCSSFile('rtl.css', array('order_pos' => 4000), 'smf_rtl'); + + // In case any mods added relevant CSS. + call_integration_hook('integrate_pre_css_output'); + + // This next chunk mimics some of template_css() + $css_to_minify = array(); + $normal_css_files = array(); + + usort( + $context['css_files'], + function ($a, $b) + { + return $a['options']['order_pos'] < $b['options']['order_pos'] ? -1 : ($a['options']['order_pos'] > $b['options']['order_pos'] ? 1 : 0); + } + ); + + foreach ($context['css_files'] as $css_file) + { + if (!isset($css_file['options']['minimize'])) + $css_file['options']['minimize'] = true; + + if (!empty($css_file['options']['minimize']) && !empty($modSettings['minimize_files'])) + $css_to_minify[] = $css_file; + else + $normal_css_files[] = $css_file; + } + + $minified_css_files = !empty($css_to_minify) ? custMinify($css_to_minify, 'css') : array(); + + $context['css_files'] = array(); + foreach (array_merge($minified_css_files, $normal_css_files) as $css_file) + { + // Embed the CSS in a ~i', '', $text); + $text = preg_replace('~\\<\\!--.*?-->~i', '', $text); + $text = preg_replace('~\\<\\!\\[CDATA\\[.*?\\]\\]\\>~i', '', $text); + + // Do the smileys ultra first! + preg_match_all('~]+alt="([^"]+)"[^>]+class="smiley"[^>]*>(?:\s)?~i', $text, $matches); + if (!empty($matches[0])) + { + // Get all our smiley codes + $request = $smcFunc['db_query']('', ' + SELECT code + FROM {db_prefix}smileys + ORDER BY LENGTH(code) DESC', + array() + ); + $smiley_codes = $smcFunc['db_fetch_all']($request); + $smcFunc['db_free_result']($request); + + foreach ($matches[1] as $k => $possible_code) + { + $possible_code = un_htmlspecialchars($possible_code); + + if (in_array($possible_code, $smiley_codes)) + $matches[1][$k] = '-[]-smf_smily_start#|#' . $possible_code . '-[]-smf_smily_end#|#'; + else + $matches[1][$k] = $matches[0][$k]; + } + + // Replace the tags! + $text = str_replace($matches[0], $matches[1], $text); + + // Now sort out spaces + $text = str_replace(array('-[]-smf_smily_end#|#-[]-smf_smily_start#|#', '-[]-smf_smily_end#|#', '-[]-smf_smily_start#|#'), ' ', $text); + } + + // Only try to buy more time if the client didn't quit. + if (connection_aborted() && $context['server']['is_apache']) + @apache_reset_timeout(); + + $parts = preg_split('~(<[A-Za-z]+\s*[^<>]*?style="?[^<>"]+"?[^<>]*?(?:/?)>|)~', $text, -1, PREG_SPLIT_DELIM_CAPTURE); + $replacement = ''; + $stack = array(); + + foreach ($parts as $part) + { + if (preg_match('~(<([A-Za-z]+)\s*[^<>]*?)style="?([^<>"]+)"?([^<>]*?(/?)>)~', $part, $matches) === 1) + { + // If it's being closed instantly, we can't deal with it...yet. + if ($matches[5] === '/') + continue; + else + { + // Get an array of styles that apply to this element. (The strtr is there to combat HTML generated by Word.) + $styles = explode(';', strtr($matches[3], array('"' => ''))); + $curElement = $matches[2]; + $precedingStyle = $matches[1]; + $afterStyle = $matches[4]; + $curCloseTags = ''; + $extra_attr = ''; + + foreach ($styles as $type_value_pair) + { + // Remove spaces and convert uppercase letters. + $clean_type_value_pair = strtolower(strtr(trim($type_value_pair), '=', ':')); + + // Something like 'font-weight: bold' is expected here. + if (strpos($clean_type_value_pair, ':') === false) + continue; + + // Capture the elements of a single style item (e.g. 'font-weight' and 'bold'). + list ($style_type, $style_value) = explode(':', $type_value_pair); + + $style_value = trim($style_value); + + switch (trim($style_type)) + { + case 'font-weight': + if ($style_value === 'bold') + { + $curCloseTags .= '[/b]'; + $replacement .= '[b]'; + } + break; + + case 'text-decoration': + if ($style_value == 'underline') + { + $curCloseTags .= '[/u]'; + $replacement .= '[u]'; + } + elseif ($style_value == 'line-through') + { + $curCloseTags .= '[/s]'; + $replacement .= '[s]'; + } + break; + + case 'text-align': + if ($style_value == 'left') + { + $curCloseTags .= '[/left]'; + $replacement .= '[left]'; + } + elseif ($style_value == 'center') + { + $curCloseTags .= '[/center]'; + $replacement .= '[center]'; + } + elseif ($style_value == 'right') + { + $curCloseTags .= '[/right]'; + $replacement .= '[right]'; + } + break; + + case 'font-style': + if ($style_value == 'italic') + { + $curCloseTags .= '[/i]'; + $replacement .= '[i]'; + } + break; + + case 'color': + $curCloseTags .= '[/color]'; + $replacement .= '[color=' . $style_value . ']'; + break; + + case 'font-size': + // Sometimes people put decimals where decimals should not be. + if (preg_match('~(\d)+\.\d+(p[xt])~i', $style_value, $dec_matches) === 1) + $style_value = $dec_matches[1] . $dec_matches[2]; + + $curCloseTags .= '[/size]'; + $replacement .= '[size=' . $style_value . ']'; + break; + + case 'font-family': + // Only get the first freaking font if there's a list! + if (strpos($style_value, ',') !== false) + $style_value = substr($style_value, 0, strpos($style_value, ',')); + + $curCloseTags .= '[/font]'; + $replacement .= '[font=' . strtr($style_value, array("'" => '')) . ']'; + break; + + // This is a hack for images with dimensions embedded. + case 'width': + case 'height': + if (preg_match('~[1-9]\d*~i', $style_value, $dimension) === 1) + $extra_attr .= ' ' . $style_type . '="' . $dimension[0] . '"'; + break; + + case 'list-style-type': + if (preg_match('~none|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha~i', $style_value, $listType) === 1) + $extra_attr .= ' listtype="' . $listType[0] . '"'; + break; + } + } + + // Preserve some tags stripping the styling. + if (in_array($matches[2], array('a', 'font', 'td'))) + { + $replacement .= $precedingStyle . $afterStyle; + $curCloseTags = '' . $curCloseTags; + } + + // If there's something that still needs closing, push it to the stack. + if (!empty($curCloseTags)) + array_push($stack, array( + 'element' => strtolower($curElement), + 'closeTags' => $curCloseTags + ) + ); + elseif (!empty($extra_attr)) + $replacement .= $precedingStyle . $extra_attr . $afterStyle; + } + } + + elseif (preg_match('~~', $part, $matches) === 1) + { + // Is this the element that we've been waiting for to be closed? + if (!empty($stack) && strtolower($matches[1]) === $stack[count($stack) - 1]['element']) + { + $byebyeTag = array_pop($stack); + $replacement .= $byebyeTag['closeTags']; + } + + // Must've been something else. + else + $replacement .= $part; + } + // In all other cases, just add the part to the replacement. + else + $replacement .= $part; + } + + // Now put back the replacement in the text. + $text = $replacement; + + // We are not finished yet, request more time. + if (connection_aborted() && $context['server']['is_apache']) + @apache_reset_timeout(); + + // Let's pull out any legacy alignments. + while (preg_match('~<([A-Za-z]+)\s+[^<>]*?(align="*(left|center|right)"*)[^<>]*?(/?)>~i', $text, $matches) === 1) + { + // Find the position in the text of this tag over again. + $start_pos = strpos($text, $matches[0]); + if ($start_pos === false) + break; + + // End tag? + if ($matches[4] != '/' && strpos($text, '', $start_pos) !== false) + { + $end_pos = strpos($text, '', $start_pos); + + // Remove the align from that tag so it's never checked again. + $tag = substr($text, $start_pos, strlen($matches[0])); + $content = substr($text, $start_pos + strlen($matches[0]), $end_pos - $start_pos - strlen($matches[0])); + $tag = str_replace($matches[2], '', $tag); + + // Put the tags back into the body. + $text = substr($text, 0, $start_pos) . $tag . '[' . $matches[3] . ']' . $content . '[/' . $matches[3] . ']' . substr($text, $end_pos); + } + else + { + // Just get rid of this evil tag. + $text = substr($text, 0, $start_pos) . substr($text, $start_pos + strlen($matches[0])); + } + } + + // Let's do some special stuff for fonts - cause we all love fonts. + while (preg_match('~]*)>~i', $text, $matches) === 1) + { + // Find the position of this again. + $start_pos = strpos($text, $matches[0]); + $end_pos = false; + if ($start_pos === false) + break; + + // This must have an end tag - and we must find the right one. + $lower_text = strtolower($text); + + $start_pos_test = $start_pos + 4; + // How many starting tags must we find closing ones for first? + $start_font_tag_stack = 0; + while ($start_pos_test < strlen($text)) + { + // Where is the next starting font? + $next_start_pos = strpos($lower_text, '', $start_pos_test); + + // Did we past another starting tag before an end one? + if ($next_start_pos !== false && $next_start_pos < $next_end_pos) + { + $start_font_tag_stack++; + $start_pos_test = $next_start_pos + 4; + } + // Otherwise we have an end tag but not the right one? + elseif ($start_font_tag_stack) + { + $start_font_tag_stack--; + $start_pos_test = $next_end_pos + 4; + } + // Otherwise we're there! + else + { + $end_pos = $next_end_pos; + break; + } + } + if ($end_pos === false) + break; + + // Now work out what the attributes are. + $attribs = fetchTagAttributes($matches[1]); + $tags = array(); + $sizes_equivalence = array(1 => '8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt'); + foreach ($attribs as $s => $v) + { + if ($s == 'size') + { + // Cast before empty chech because casting a string results in a 0 and we don't have zeros in the array! ;) + $v = (int) trim($v); + $v = empty($v) ? 1 : $v; + $tags[] = array('[size=' . $sizes_equivalence[$v] . ']', '[/size]'); + } + elseif ($s == 'face') + $tags[] = array('[font=' . trim(strtolower($v)) . ']', '[/font]'); + elseif ($s == 'color') + $tags[] = array('[color=' . trim(strtolower($v)) . ']', '[/color]'); + } + + // As before add in our tags. + $before = $after = ''; + foreach ($tags as $tag) + { + $before .= $tag[0]; + if (isset($tag[1])) + $after = $tag[1] . $after; + } + + // Remove the tag so it's never checked again. + $content = substr($text, $start_pos + strlen($matches[0]), $end_pos - $start_pos - strlen($matches[0])); + + // Put the tags back into the body. + $text = substr($text, 0, $start_pos) . $before . $content . $after . substr($text, $end_pos + 7); + } + + // Almost there, just a little more time. + if (connection_aborted() && $context['server']['is_apache']) + @apache_reset_timeout(); + + if (count($parts = preg_split('~<(/?)(li|ol|ul)([^>]*)>~i', $text, -1, PREG_SPLIT_DELIM_CAPTURE)) > 1) + { + // A toggle that dermines whether we're directly under a
    or
      . + $inList = false; + + // Keep track of the number of nested list levels. + $listDepth = 0; + + // Map what we can expect from the HTML to what is supported by SMF. + $listTypeMapping = array( + '1' => 'decimal', + 'A' => 'upper-alpha', + 'a' => 'lower-alpha', + 'I' => 'upper-roman', + 'i' => 'lower-roman', + 'disc' => 'disc', + 'square' => 'square', + 'circle' => 'circle', + ); + + // $i: text, $i + 1: '/', $i + 2: tag, $i + 3: tail. + for ($i = 0, $numParts = count($parts) - 1; $i < $numParts; $i += 4) + { + $tag = strtolower($parts[$i + 2]); + $isOpeningTag = $parts[$i + 1] === ''; + + if ($isOpeningTag) + { + switch ($tag) + { + case 'ol': + case 'ul': + + // We have a problem, we're already in a list. + if ($inList) + { + // Inject a list opener, we'll deal with the ol/ul next loop. + array_splice($parts, $i, 0, array( + '', + '', + str_repeat("\t", $listDepth) . '[li]', + '', + )); + $numParts = count($parts) - 1; + + // The inlist status changes a bit. + $inList = false; + } + + // Just starting a new list. + else + { + $inList = true; + + if ($tag === 'ol') + $listType = 'decimal'; + elseif (preg_match('~type="?(' . implode('|', array_keys($listTypeMapping)) . ')"?~', $parts[$i + 3], $match) === 1) + $listType = $listTypeMapping[$match[1]]; + else + $listType = null; + + $listDepth++; + + $parts[$i + 2] = '[list' . ($listType === null ? '' : ' type=' . $listType) . ']' . "\n"; + $parts[$i + 3] = ''; + } + break; + + case 'li': + + // This is how it should be: a list item inside the list. + if ($inList) + { + $parts[$i + 2] = str_repeat("\t", $listDepth) . '[li]'; + $parts[$i + 3] = ''; + + // Within a list item, it's almost as if you're outside. + $inList = false; + } + + // The li is no direct child of a list. + else + { + // We are apparently in a list item. + if ($listDepth > 0) + { + $parts[$i + 2] = '[/li]' . "\n" . str_repeat("\t", $listDepth) . '[li]'; + $parts[$i + 3] = ''; + } + + // We're not even near a list. + else + { + // Quickly create a list with an item. + $listDepth++; + + $parts[$i + 2] = '[list]' . "\n\t" . '[li]'; + $parts[$i + 3] = ''; + } + } + + break; + } + } + + // Handle all the closing tags. + else + { + switch ($tag) + { + case 'ol': + case 'ul': + + // As we expected it, closing the list while we're in it. + if ($inList) + { + $inList = false; + + $listDepth--; + + $parts[$i + 1] = ''; + $parts[$i + 2] = str_repeat("\t", $listDepth) . '[/list]'; + $parts[$i + 3] = ''; + } + + else + { + // We're in a list item. + if ($listDepth > 0) + { + // Inject closure for this list item first. + // The content of $parts[$i] is left as is! + array_splice($parts, $i + 1, 0, array( + '', // $i + 1 + '[/li]' . "\n", // $i + 2 + '', // $i + 3 + '', // $i + 4 + )); + $numParts = count($parts) - 1; + + // Now that we've closed the li, we're in list space. + $inList = true; + } + + // We're not even in a list, ignore + else + { + $parts[$i + 1] = ''; + $parts[$i + 2] = ''; + $parts[$i + 3] = ''; + } + } + break; + + case 'li': + + if ($inList) + { + // There's no use for a after
        or
          , ignore. + $parts[$i + 1] = ''; + $parts[$i + 2] = ''; + $parts[$i + 3] = ''; + } + + else + { + // Remove the trailing breaks from the list item. + $parts[$i] = preg_replace('~\s*\s*$~', '', $parts[$i]); + $parts[$i + 1] = ''; + $parts[$i + 2] = '[/li]' . "\n"; + $parts[$i + 3] = ''; + + // And we're back in the [list] space. + $inList = true; + } + + break; + } + } + + // If we're in the [list] space, no content is allowed. + if ($inList && trim(preg_replace('~\s*\s*~', '', $parts[$i + 4])) !== '') + { + // Fix it by injecting an extra list item. + array_splice($parts, $i + 4, 0, array( + '', // No content. + '', // Opening tag. + 'li', // It's a
        • . + '', // No tail. + )); + $numParts = count($parts) - 1; + } + } + + $text = implode('', $parts); + + if ($inList) + { + $listDepth--; + $text .= str_repeat("\t", $listDepth) . '[/list]'; + } + + for ($i = $listDepth; $i > 0; $i--) + $text .= '[/li]' . "\n" . str_repeat("\t", $i - 1) . '[/list]'; + } + + // I love my own image... + while (preg_match('~]*)/*>~i', $text, $matches) === 1) + { + // Find the position of the image. + $start_pos = strpos($text, $matches[0]); + if ($start_pos === false) + break; + $end_pos = $start_pos + strlen($matches[0]); + + $params = ''; + $src = ''; + + $attrs = fetchTagAttributes($matches[1]); + foreach ($attrs as $attrib => $value) + { + if (in_array($attrib, array('width', 'height'))) + $params .= ' ' . $attrib . '=' . (int) $value; + elseif ($attrib == 'alt' && trim($value) != '') + $params .= ' alt=' . trim($value); + elseif ($attrib == 'src') + $src = trim($value); + } + + $tag = ''; + if (!empty($src)) + { + // Attempt to fix the path in case it's not present. + if (preg_match('~^https?://~i', $src) === 0 && is_array($parsedURL = parse_iri($scripturl)) && isset($parsedURL['host'])) + { + $baseURL = (isset($parsedURL['scheme']) ? $parsedURL['scheme'] : 'http') . '://' . $parsedURL['host'] . (empty($parsedURL['port']) ? '' : ':' . $parsedURL['port']); + + if (substr($src, 0, 1) === '/') + $src = $baseURL . $src; + else + $src = $baseURL . (empty($parsedURL['path']) ? '/' : preg_replace('~/(?:index\\.php)?$~', '', $parsedURL['path'])) . '/' . $src; + } + + $tag = '[img' . $params . ']' . $src . '[/img]'; + } + + // Replace the tag + $text = substr($text, 0, $start_pos) . $tag . substr($text, $end_pos); + } + + // The final bits are the easy ones - tags which map to tags which map to tags - etc etc. + $tags = array( + '~~i' => function() + { + return '[b]'; + }, + '~~i' => function() + { + return '[/b]'; + }, + '~~i' => function() + { + return '[i]'; + }, + '~~i' => function() + { + return '[/i]'; + }, + '~~i' => function() + { + return '[u]'; + }, + '~~i' => function() + { + return '[/u]'; + }, + '~~i' => function() + { + return '[b]'; + }, + '~~i' => function() + { + return '[/b]'; + }, + '~~i' => function() + { + return '[i]'; + }, + '~~i' => function() + { + return '[i]'; + }, + '~~i' => function() + { + return "[s]"; + }, + '~~i' => function() + { + return "[/s]"; + }, + '~~i' => function() + { + return '[s]'; + }, + '~~i' => function() + { + return '[/s]'; + }, + '~~i' => function() + { + return '[s]'; + }, + '~~i' => function() + { + return '[/s]'; + }, + '~~i' => function() + { + return '[center]'; + }, + '~~i' => function() + { + return '[/center]'; + }, + '~~i' => function() + { + return '[pre]'; + }, + '~~i' => function() + { + return '[/pre]'; + }, + '~~i' => function() + { + return '[sub]'; + }, + '~~i' => function() + { + return '[/sub]'; + }, + '~~i' => function() + { + return '[sup]'; + }, + '~~i' => function() + { + return '[/sup]'; + }, + '~~i' => function() + { + return '[tt]'; + }, + '~~i' => function() + { + return '[/tt]'; + }, + '~~i' => function() + { + return '[table]'; + }, + '~~i' => function() + { + return '[/table]'; + }, + '~~i' => function() + { + return '[tr]'; + }, + '~~i' => function() + { + return '[/tr]'; + }, + '~<(td|th)\s[^<>]*?colspan="?(\d{1,2})"?.*?' . '>~i' => function($matches) + { + return str_repeat('[td][/td]', $matches[2] - 1) . '[td]'; + }, + '~<(td|th)(\s(.)*?)*?' . '>~i' => function() + { + return '[td]'; + }, + '~~i' => function() + { + return '[/td]'; + }, + '~]*?)?' . '>~i' => function() + { + return "\n"; + }, + '~]*>(\n)?~i' => function($matches) + { + return "[hr]\n" . $matches[0]; + }, + '~(\n)?\\[hr\\]~i' => function() + { + return "\n[hr]"; + }, + '~^\n\\[hr\\]~i' => function() + { + return "[hr]"; + }, + '~~i' => function() + { + return "<blockquote>"; + }, + '~~i' => function() + { + return "</blockquote>"; + }, + '~~i' => function() + { + return "<ins>"; + }, + '~~i' => function() + { + return "</ins>"; + }, + ); + + foreach ($tags as $tag => $replace) + $text = preg_replace_callback($tag, $replace, $text); + + // Please give us just a little more time. + if (connection_aborted() && $context['server']['is_apache']) + @apache_reset_timeout(); + + // What about URL's - the pain in the ass of the tag world. + while (preg_match('~]*)>([^<>]*)~i', $text, $matches) === 1) + { + // Find the position of the URL. + $start_pos = strpos($text, $matches[0]); + if ($start_pos === false) + break; + $end_pos = $start_pos + strlen($matches[0]); + + $tag_type = 'url'; + $href = ''; + + $attrs = fetchTagAttributes($matches[1]); + foreach ($attrs as $attrib => $value) + { + if ($attrib == 'href') + { + $href = trim($value); + + // Are we dealing with an FTP link? + if (preg_match('~^ftps?://~', $href) === 1) + $tag_type = 'ftp'; + + // Or is this a link to an email address? + elseif (substr($href, 0, 7) == 'mailto:') + { + $tag_type = 'email'; + $href = substr($href, 7); + } + + // No http(s), so attempt to fix this potential relative URL. + elseif (preg_match('~^https?://~i', $href) === 0 && is_array($parsedURL = parse_iri($scripturl)) && isset($parsedURL['host'])) + { + $baseURL = (isset($parsedURL['scheme']) ? $parsedURL['scheme'] : 'http') . '://' . $parsedURL['host'] . (empty($parsedURL['port']) ? '' : ':' . $parsedURL['port']); + + if (substr($href, 0, 1) === '/') + $href = $baseURL . $href; + else + $href = $baseURL . (empty($parsedURL['path']) ? '/' : preg_replace('~/(?:index\\.php)?$~', '', $parsedURL['path'])) . '/' . $href; + } + } + + // External URL? + if ($attrib == 'target' && $tag_type == 'url') + { + if (trim($value) == '_blank') + $tag_type == 'iurl'; + } + } + + $tag = ''; + if ($href != '') + { + if ($matches[2] == $href) + $tag = '[' . $tag_type . ']' . $href . '[/' . $tag_type . ']'; + else + $tag = '[' . $tag_type . '=' . $href . ']' . $matches[2] . '[/' . $tag_type . ']'; + } + + // Replace the tag + $text = substr($text, 0, $start_pos) . $tag . substr($text, $end_pos); + } + + $text = strip_tags($text); + + // Some tags often end up as just dummy tags - remove those. + $text = preg_replace('~\[[bisu]\]\s*\[/[bisu]\]~', '', $text); + + // Fix up entities. + $text = preg_replace('~&~i', '&#38;', $text); + + $text = legalise_bbc($text); + + return $text; +} + +/** + * Returns an array of attributes associated with a tag. + * + * @param string $text A tag + * @return array An array of attributes + */ +function fetchTagAttributes($text) +{ + $attribs = array(); + $key = $value = ''; + $tag_state = 0; // 0 = key, 1 = attribute with no string, 2 = attribute with string + for ($i = 0; $i < strlen($text); $i++) + { + // We're either moving from the key to the attribute or we're in a string and this is fine. + if ($text[$i] == '=') + { + if ($tag_state == 0) + $tag_state = 1; + elseif ($tag_state == 2) + $value .= '='; + } + // A space is either moving from an attribute back to a potential key or in a string is fine. + elseif ($text[$i] == ' ') + { + if ($tag_state == 2) + $value .= ' '; + elseif ($tag_state == 1) + { + $attribs[$key] = $value; + $key = $value = ''; + $tag_state = 0; + } + } + // A quote? + elseif ($text[$i] == '"') + { + // Must be either going into or out of a string. + if ($tag_state == 1) + $tag_state = 2; + else + $tag_state = 1; + } + // Otherwise it's fine. + else + { + if ($tag_state == 0) + $key .= $text[$i]; + else + $value .= $text[$i]; + } + } + + // Anything left? + if ($key != '' && $value != '') + $attribs[$key] = $value; + + return $attribs; +} + +/** + * Attempt to clean up illegal BBC caused by browsers like Opera which don't obey the rules + * + * @param string $text Text + * @return string Cleaned up text + */ +function legalise_bbc($text) +{ + global $modSettings; + + // Don't care about the texts that are too short. + if (strlen($text) < 3) + return $text; + + // A list of tags that's disabled by the admin. + $disabled = empty($modSettings['disabledBBC']) ? array() : array_flip(explode(',', strtolower($modSettings['disabledBBC']))); + + // Get a list of all the tags that are not disabled. + $all_tags = parse_bbc(false); + $valid_tags = array(); + $self_closing_tags = array(); + foreach ($all_tags as $tag) + { + if (!isset($disabled[$tag['tag']])) + $valid_tags[$tag['tag']] = !empty($tag['block_level']); + if (isset($tag['type']) && $tag['type'] == 'closed') + $self_closing_tags[] = $tag['tag']; + } + + // Right - we're going to start by going through the whole lot to make sure we don't have align stuff crossed as this happens load and is stupid! + $align_tags = array('left', 'center', 'right', 'pre'); + + // Remove those align tags that are not valid. + $align_tags = array_intersect($align_tags, array_keys($valid_tags)); + + // These keep track of where we are! + if (!empty($align_tags) && count($matches = preg_split('~(\\[/?(?:' . implode('|', $align_tags) . ')\\])~', $text, -1, PREG_SPLIT_DELIM_CAPTURE)) > 1) + { + // The first one is never a tag. + $isTag = false; + + // By default we're not inside a tag too. + $insideTag = null; + + foreach ($matches as $i => $match) + { + // We're only interested in tags, not text. + if ($isTag) + { + $isClosingTag = substr($match, 1, 1) === '/'; + $tagName = substr($match, $isClosingTag ? 2 : 1, -1); + + // We're closing the exact same tag that we opened. + if ($isClosingTag && $insideTag === $tagName) + $insideTag = null; + + // We're opening a tag and we're not yet inside one either + elseif (!$isClosingTag && $insideTag === null) + $insideTag = $tagName; + + // In all other cases, this tag must be invalid + else + unset($matches[$i]); + } + + // The next one is gonna be the other one. + $isTag = !$isTag; + } + + // We're still inside a tag and had no chance for closure? + if ($insideTag !== null) + $matches[] = '[/' . $insideTag . ']'; + + // And a complete text string again. + $text = implode('', $matches); + } + + // Quickly remove any tags which are back to back. + $backToBackPattern = '~\\[(' . implode('|', array_diff(array_keys($valid_tags), array('td', 'anchor'))) . ')[^<>\\[\\]]*\\]\s*\\[/\\1\\]~'; + $lastlen = 0; + while (strlen($text) !== $lastlen) + $lastlen = strlen($text = preg_replace($backToBackPattern, '', $text)); + + // Need to sort the tags by name length. + uksort( + $valid_tags, + function($a, $b) + { + return strlen($a) < strlen($b) ? 1 : -1; + } + ); + + // These inline tags can compete with each other regarding style. + $competing_tags = array( + 'color', + 'size', + ); + + // These keep track of where we are! + if (count($parts = preg_split(sprintf('~(\\[)(/?)(%1$s)((?:[\\s=][^\\]\\[]*)?\\])~', implode('|', array_keys($valid_tags))), $text, -1, PREG_SPLIT_DELIM_CAPTURE)) > 1) + { + // Start outside [nobbc] or [code] blocks. + $inCode = false; + $inNoBbc = false; + + // A buffer containing all opened inline elements. + $inlineElements = array(); + + // A buffer containing all opened block elements. + $blockElements = array(); + + // A buffer containing the opened inline elements that might compete. + $competingElements = array(); + + // $i: text, $i + 1: '[', $i + 2: '/', $i + 3: tag, $i + 4: tag tail. + for ($i = 0, $n = count($parts) - 1; $i < $n; $i += 5) + { + $tag = $parts[$i + 3]; + $isOpeningTag = $parts[$i + 2] === ''; + $isClosingTag = $parts[$i + 2] === '/'; + $isBlockLevelTag = isset($valid_tags[$tag]) && $valid_tags[$tag] && !in_array($tag, $self_closing_tags); + $isCompetingTag = in_array($tag, $competing_tags); + + // Check if this might be one of those cleaned out tags. + if ($tag === '') + continue; + + // Special case: inside [code] blocks any code is left untouched. + elseif ($tag === 'code') + { + // We're inside a code block and closing it. + if ($inCode && $isClosingTag) + { + $inCode = false; + + // Reopen tags that were closed before the code block. + if (!empty($inlineElements)) + $parts[$i + 4] .= '[' . implode('][', array_keys($inlineElements)) . ']'; + } + + // We're outside a coding and nobbc block and opening it. + elseif (!$inCode && !$inNoBbc && $isOpeningTag) + { + // If there are still inline elements left open, close them now. + if (!empty($inlineElements)) + { + $parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']'; + //$inlineElements = array(); + } + + $inCode = true; + } + + // Nothing further to do. + continue; + } + + // Special case: inside [nobbc] blocks any BBC is left untouched. + elseif ($tag === 'nobbc') + { + // We're inside a nobbc block and closing it. + if ($inNoBbc && $isClosingTag) + { + $inNoBbc = false; + + // Some inline elements might've been closed that need reopening. + if (!empty($inlineElements)) + $parts[$i + 4] .= '[' . implode('][', array_keys($inlineElements)) . ']'; + } + + // We're outside a nobbc and coding block and opening it. + elseif (!$inNoBbc && !$inCode && $isOpeningTag) + { + // Can't have inline elements still opened. + if (!empty($inlineElements)) + { + $parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']'; + //$inlineElements = array(); + } + + $inNoBbc = true; + } + + continue; + } + + // So, we're inside one of the special blocks: ignore any tag. + elseif ($inCode || $inNoBbc) + continue; + + // We're dealing with an opening tag. + if ($isOpeningTag) + { + // Everyting inside the square brackets of the opening tag. + $elementContent = $parts[$i + 3] . substr($parts[$i + 4], 0, -1); + + // A block level opening tag. + if ($isBlockLevelTag) + { + // Are there inline elements still open? + if (!empty($inlineElements)) + { + // Close all the inline tags, a block tag is coming... + $parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']'; + + // Now open them again, we're inside the block tag now. + $parts[$i + 5] = '[' . implode('][', array_keys($inlineElements)) . ']' . $parts[$i + 5]; + } + + $blockElements[] = $tag; + } + + // Inline opening tag. + elseif (!in_array($tag, $self_closing_tags)) + { + // Can't have two opening elements with the same contents! + if (isset($inlineElements[$elementContent])) + { + // Get rid of this tag. + $parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = ''; + + // Now try to find the corresponding closing tag. + $curLevel = 1; + for ($j = $i + 5, $m = count($parts) - 1; $j < $m; $j += 5) + { + // Find the tags with the same tagname + if ($parts[$j + 3] === $tag) + { + // If it's an opening tag, increase the level. + if ($parts[$j + 2] === '') + $curLevel++; + + // A closing tag, decrease the level. + else + { + $curLevel--; + + // Gotcha! Clean out this closing tag gone rogue. + if ($curLevel === 0) + { + $parts[$j + 1] = $parts[$j + 2] = $parts[$j + 3] = $parts[$j + 4] = ''; + break; + } + } + } + } + } + + // Otherwise, add this one to the list. + else + { + if ($isCompetingTag) + { + if (!isset($competingElements[$tag])) + $competingElements[$tag] = array(); + + $competingElements[$tag][] = $parts[$i + 4]; + + if (count($competingElements[$tag]) > 1) + $parts[$i] .= '[/' . $tag . ']'; + } + + $inlineElements[$elementContent] = $tag; + } + } + } + + // Closing tag. + else + { + // Closing the block tag. + if ($isBlockLevelTag) + { + // Close the elements that should've been closed by closing this tag. + if (!empty($blockElements)) + { + $addClosingTags = array(); + while ($element = array_pop($blockElements)) + { + if ($element === $tag) + break; + + // Still a block tag was open not equal to this tag. + $addClosingTags[] = $element['type']; + } + + if (!empty($addClosingTags)) + $parts[$i + 1] = '[/' . implode('][/', array_reverse($addClosingTags)) . ']' . $parts[$i + 1]; + + // Apparently the closing tag was not found on the stack. + if (!is_string($element) || $element !== $tag) + { + // Get rid of this particular closing tag, it was never opened. + $parts[$i + 1] = substr($parts[$i + 1], 0, -1); + $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = ''; + continue; + } + } + else + { + // Get rid of this closing tag! + $parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = ''; + continue; + } + + // Inline elements are still left opened? + if (!empty($inlineElements)) + { + // Close them first.. + $parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']'; + + // Then reopen them. + $parts[$i + 5] = '[' . implode('][', array_keys($inlineElements)) . ']' . $parts[$i + 5]; + } + } + // Inline tag. + else + { + // Are we expecting this tag to end? + if (in_array($tag, $inlineElements)) + { + foreach (array_reverse($inlineElements, true) as $tagContentToBeClosed => $tagToBeClosed) + { + // Closing it one way or the other. + unset($inlineElements[$tagContentToBeClosed]); + + // Was this the tag we were looking for? + if ($tagToBeClosed === $tag) + break; + + // Nope, close it and look further! + else + $parts[$i] .= '[/' . $tagToBeClosed . ']'; + } + + if ($isCompetingTag && !empty($competingElements[$tag])) + { + array_pop($competingElements[$tag]); + + if (count($competingElements[$tag]) > 0) + $parts[$i + 5] = '[' . $tag . $competingElements[$tag][count($competingElements[$tag]) - 1] . $parts[$i + 5]; + } + } + + // Unexpected closing tag, ex-ter-mi-nate. + else + $parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = ''; + } + } + } + + // Close the code tags. + if ($inCode) + $parts[$i] .= '[/code]'; + + // The same for nobbc tags. + elseif ($inNoBbc) + $parts[$i] .= '[/nobbc]'; + + // Still inline tags left unclosed? Close them now, better late than never. + elseif (!empty($inlineElements)) + $parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']'; + + // Now close the block elements. + if (!empty($blockElements)) + $parts[$i] .= '[/' . implode('][/', array_reverse($blockElements)) . ']'; + + $text = implode('', $parts); + } + + // Final clean up of back to back tags. + $lastlen = 0; + while (strlen($text) !== $lastlen) + $lastlen = strlen($text = preg_replace($backToBackPattern, '', $text)); + + return $text; +} + +/** + * Retrieves a list of message icons. + * - Based on the settings, the array will either contain a list of default + * message icons or a list of custom message icons retrieved from the database. + * - The board_id is needed for the custom message icons (which can be set for + * each board individually). + * + * @param int $board_id The ID of the board + * @return array An array of info about available icons + */ +function getMessageIcons($board_id) +{ + global $modSettings, $txt, $settings, $smcFunc; + + if (empty($modSettings['messageIcons_enable'])) + { + loadLanguage('Post'); + + $icons = array( + array('value' => 'xx', 'name' => $txt['standard']), + array('value' => 'thumbup', 'name' => $txt['thumbs_up']), + array('value' => 'thumbdown', 'name' => $txt['thumbs_down']), + array('value' => 'exclamation', 'name' => $txt['exclamation_point']), + array('value' => 'question', 'name' => $txt['question_mark']), + array('value' => 'lamp', 'name' => $txt['lamp']), + array('value' => 'smiley', 'name' => $txt['icon_smiley']), + array('value' => 'angry', 'name' => $txt['icon_angry']), + array('value' => 'cheesy', 'name' => $txt['icon_cheesy']), + array('value' => 'grin', 'name' => $txt['icon_grin']), + array('value' => 'sad', 'name' => $txt['icon_sad']), + array('value' => 'wink', 'name' => $txt['icon_wink']), + array('value' => 'poll', 'name' => $txt['icon_poll']), + ); + + foreach ($icons as $k => $dummy) + { + $icons[$k]['url'] = $settings['images_url'] . '/post/' . $dummy['value'] . '.png'; + $icons[$k]['is_last'] = false; + } + } + // Otherwise load the icons, and check we give the right image too... + else + { + if (($temp = cache_get_data('posting_icons-' . $board_id, 480)) == null) + { + $request = $smcFunc['db_query']('', ' + SELECT title, filename + FROM {db_prefix}message_icons + WHERE id_board IN (0, {int:board_id}) + ORDER BY icon_order', + array( + 'board_id' => $board_id, + ) + ); + $icon_data = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $icon_data[] = $row; + $smcFunc['db_free_result']($request); + + $icons = array(); + foreach ($icon_data as $icon) + { + $icons[$icon['filename']] = array( + 'value' => $icon['filename'], + 'name' => $icon['title'], + 'url' => $settings[file_exists($settings['theme_dir'] . '/images/post/' . $icon['filename'] . '.png') ? 'images_url' : 'default_images_url'] . '/post/' . $icon['filename'] . '.png', + 'is_last' => false, + ); + } + + cache_put_data('posting_icons-' . $board_id, $icons, 480); + } + else + $icons = $temp; + } + call_integration_hook('integrate_load_message_icons', array(&$icons)); + + return array_values($icons); +} + +/** + * Creates a box that can be used for richedit stuff like BBC, Smileys etc. + * + * @param array $editorOptions Various options for the editor + */ +function create_control_richedit($editorOptions) +{ + global $txt, $modSettings, $options, $smcFunc, $editortxt; + global $context, $settings, $user_info, $scripturl; + + // Load the Post language file... for the moment at least. + loadLanguage('Post'); + loadLanguage('Editor'); + loadLanguage('Drafts'); + + $context['richedit_buttons'] = array( + 'save_draft' => array( + 'type' => 'submit', + 'value' => $txt['draft_save'], + 'onclick' => !empty($context['drafts_pm_save']) ? 'submitThisOnce(this);' : (!empty($context['drafts_save']) ? 'return confirm(' . JavaScriptEscape($txt['draft_save_note']) . ') && submitThisOnce(this);' : ''), + 'accessKey' => 'd', + 'show' => !empty($context['drafts_pm_save']) || !empty($context['drafts_save']) + ), + 'id_pm_draft' => array( + 'type' => 'hidden', + 'value' => empty($context['id_pm_draft']) ? 0 : $context['id_pm_draft'], + 'show' => !empty($context['drafts_pm_save']) + ), + 'id_draft' => array( + 'type' => 'hidden', + 'value' => empty($context['id_draft']) ? 0 : $context['id_draft'], + 'show' => !empty($context['drafts_save']) + ), + 'spell_check' => array( + 'type' => 'submit', + 'value' => $txt['spell_check'], + 'show' => !empty($context['show_spellchecking']) + ), + 'preview' => array( + 'type' => 'submit', + 'value' => $txt['preview'], + 'accessKey' => 'p' + ) + ); + + // Every control must have a ID! + assert(isset($editorOptions['id'])); + assert(isset($editorOptions['value'])); + + // Is this the first richedit - if so we need to ensure some template stuff is initialised. + if (empty($context['controls']['richedit'])) + { + // Some general stuff. + $settings['smileys_url'] = $modSettings['smileys_url'] . '/' . $user_info['smiley_set']; + if (!empty($context['drafts_autosave'])) + $context['drafts_autosave_frequency'] = empty($modSettings['drafts_autosave_frequency']) ? 60000 : $modSettings['drafts_autosave_frequency'] * 1000; + + // This really has some WYSIWYG stuff. + loadCSSFile('jquery.sceditor.css', array('default_theme' => true, 'validate' => true), 'smf_jquery_sceditor'); + loadTemplate('GenericControls'); + + /* + * THEME AUTHORS: + If you want to change or tweak the CSS for the editor, + include a file named 'jquery.sceditor.theme.css' in your theme. + */ + loadCSSFile('jquery.sceditor.theme.css', array('force_current' => true, 'validate' => true,), 'smf_jquery_sceditor_theme'); + + // JS makes the editor go round + loadJavaScriptFile('editor.js', array('minimize' => true), 'smf_editor'); + loadJavaScriptFile('jquery.sceditor.bbcode.min.js', array(), 'smf_sceditor_bbcode'); + loadJavaScriptFile('jquery.sceditor.smf.js', array('minimize' => true), 'smf_sceditor_smf'); + + $scExtraLangs = ' + $.sceditor.locale["' . $txt['lang_dictionary'] . '"] = { + "Width (optional):": "' . $editortxt['width'] . '", + "Height (optional):": "' . $editortxt['height'] . '", + "Insert": "' . $editortxt['insert'] . '", + "Description (optional):": "' . $editortxt['description'] . '", + "Rows:": "' . $editortxt['rows'] . '", + "Cols:": "' . $editortxt['cols'] . '", + "URL:": "' . $editortxt['url'] . '", + "E-mail:": "' . $editortxt['email'] . '", + "Video URL:": "' . $editortxt['video_url'] . '", + "More": "' . $editortxt['more'] . '", + "Close": "' . $editortxt['close'] . '", + dateFormat: "' . $editortxt['dateformat'] . '" + };'; + + addInlineJavaScript($scExtraLangs, true); + + addInlineJavaScript(' + var smf_smileys_url = \'' . $settings['smileys_url'] . '\'; + var bbc_quote_from = \'' . addcslashes($txt['quote_from'], "'") . '\'; + var bbc_quote = \'' . addcslashes($txt['quote'], "'") . '\'; + var bbc_search_on = \'' . addcslashes($txt['search_on'], "'") . '\';'); + + $context['shortcuts_text'] = $txt['shortcuts' . (!empty($context['drafts_save']) ? '_drafts' : '') . (stripos($_SERVER['HTTP_USER_AGENT'], 'Macintosh') !== false ? '_mac' : (isBrowser('is_firefox') ? '_firefox' : ''))]; + + if ($context['show_spellchecking']) + { + loadJavaScriptFile('spellcheck.js', array('minimize' => true), 'smf_spellcheck'); + + // Some hidden information is needed in order to make the spell checking work. + if (!isset($_REQUEST['xml'])) + $context['insert_after_template'] .= ' +
          + +
          '; + } + } + + // The [#] item code for creating list items causes issues with SCEditor, but [+] is a safe equivalent. + $editorOptions['value'] = str_replace('[#]', '[+]', $editorOptions['value']); + // Tabs are not shown in the SCEditor, replace with spaces. + $editorOptions['value'] = str_replace("\t", ' ', $editorOptions['value']); + + // Start off the editor... + $context['controls']['richedit'][$editorOptions['id']] = array( + 'id' => $editorOptions['id'], + 'value' => $editorOptions['value'], + 'rich_value' => $editorOptions['value'], // 2.0 editor compatibility + 'rich_active' => empty($modSettings['disable_wysiwyg']) && (!empty($options['wysiwyg_default']) || !empty($editorOptions['force_rich']) || !empty($_REQUEST[$editorOptions['id'] . '_mode'])), + 'disable_smiley_box' => !empty($editorOptions['disable_smiley_box']), + 'columns' => isset($editorOptions['columns']) ? $editorOptions['columns'] : 60, + 'rows' => isset($editorOptions['rows']) ? $editorOptions['rows'] : 18, + 'width' => isset($editorOptions['width']) ? $editorOptions['width'] : '70%', + 'height' => isset($editorOptions['height']) ? $editorOptions['height'] : '175px', + 'form' => isset($editorOptions['form']) ? $editorOptions['form'] : 'postmodify', + 'bbc_level' => !empty($editorOptions['bbc_level']) ? $editorOptions['bbc_level'] : 'full', + 'preview_type' => isset($editorOptions['preview_type']) ? (int) $editorOptions['preview_type'] : 1, + 'labels' => !empty($editorOptions['labels']) ? $editorOptions['labels'] : array(), + 'locale' => !empty($txt['lang_dictionary']) && $txt['lang_dictionary'] != 'en' ? $txt['lang_dictionary'] : '', + 'required' => !empty($editorOptions['required']), + ); + + if (empty($context['bbc_tags'])) + { + // The below array makes it dead easy to add images to this control. Add it to the array and everything else is done for you! + // Note: 'before' and 'after' are deprecated as of SMF 2.1. Instead, use a separate JS file to configure the functionality of your toolbar buttons. + /* + array( + 'code' => 'b', // Required + 'description' => $editortxt['bold'], // Required + 'image' => 'bold', // Optional + 'before' => '[b]', // Deprecated + 'after' => '[/b]', // Deprecated + ), + */ + $context['bbc_tags'] = array(); + $context['bbc_tags'][] = array( + array( + 'code' => 'bold', + 'description' => $editortxt['bold'], + ), + array( + 'code' => 'italic', + 'description' => $editortxt['italic'], + ), + array( + 'code' => 'underline', + 'description' => $editortxt['underline'] + ), + array( + 'code' => 'strike', + 'description' => $editortxt['strikethrough'] + ), + array( + 'code' => 'superscript', + 'description' => $editortxt['superscript'] + ), + array( + 'code' => 'subscript', + 'description' => $editortxt['subscript'] + ), + array(), + array( + 'code' => 'pre', + 'description' => $editortxt['preformatted_text'] + ), + array( + 'code' => 'left', + 'description' => $editortxt['align_left'] + ), + array( + 'code' => 'center', + 'description' => $editortxt['center'] + ), + array( + 'code' => 'right', + 'description' => $editortxt['align_right'] + ), + array( + 'code' => 'justify', + 'description' => $editortxt['justify'] + ), + array(), + array( + 'code' => 'font', + 'description' => $editortxt['font_name'] + ), + array( + 'code' => 'size', + 'description' => $editortxt['font_size'] + ), + array( + 'code' => 'color', + 'description' => $editortxt['font_color'] + ), + ); + if (empty($modSettings['disable_wysiwyg'])) + { + $context['bbc_tags'][count($context['bbc_tags']) - 1][] = array( + 'code' => 'removeformat', + 'description' => $editortxt['remove_formatting'], + ); + } + $context['bbc_tags'][] = array( + array( + 'code' => 'floatleft', + 'description' => $editortxt['float_left'] + ), + array( + 'code' => 'floatright', + 'description' => $editortxt['float_right'] + ), + array(), + array( + 'code' => 'youtube', + 'description' => $editortxt['insert_youtube_video'] + ), + array( + 'code' => 'image', + 'description' => $editortxt['insert_image'] + ), + array( + 'code' => 'link', + 'description' => $editortxt['insert_link'] + ), + array( + 'code' => 'email', + 'description' => $editortxt['insert_email'] + ), + array(), + array( + 'code' => 'table', + 'description' => $editortxt['insert_table'] + ), + array( + 'code' => 'code', + 'description' => $editortxt['code'] + ), + array( + 'code' => 'quote', + 'description' => $editortxt['insert_quote'] + ), + array(), + array( + 'code' => 'bulletlist', + 'description' => $editortxt['bullet_list'] + ), + array( + 'code' => 'orderedlist', + 'description' => $editortxt['numbered_list'] + ), + array( + 'code' => 'horizontalrule', + 'description' => $editortxt['insert_horizontal_rule'] + ), + array(), + array( + 'code' => 'maximize', + 'description' => $editortxt['maximize'] + ), + ); + if (empty($modSettings['disable_wysiwyg'])) + { + $context['bbc_tags'][count($context['bbc_tags']) - 1][] = array( + 'code' => 'source', + 'description' => $editortxt['view_source'], + ); + } + + $editor_tag_map = array( + 'b' => 'bold', + 'i' => 'italic', + 'u' => 'underline', + 's' => 'strike', + 'img' => 'image', + 'url' => 'link', + 'sup' => 'superscript', + 'sub' => 'subscript', + 'hr' => 'horizontalrule', + ); + + // Define this here so mods can add to it via the hook. + $context['disabled_tags'] = array(); + + // Allow mods to modify BBC buttons. + // Note: passing the array here is not necessary and is deprecated, but it is kept for backward compatibility with 2.0 + call_integration_hook('integrate_bbc_buttons', array(&$context['bbc_tags'], &$editor_tag_map)); + + // Generate a list of buttons that shouldn't be shown - this should be the fastest way to do this. + $disabled_tags = array(); + if (!empty($modSettings['disabledBBC'])) + $disabled_tags = explode(',', $modSettings['disabledBBC']); + + foreach ($disabled_tags as $tag) + { + $tag = trim($tag); + + if ($tag === 'list') + { + $context['disabled_tags']['bulletlist'] = true; + $context['disabled_tags']['orderedlist'] = true; + } + + if ($tag === 'float') + { + $context['disabled_tags']['floatleft'] = true; + $context['disabled_tags']['floatright'] = true; + } + + foreach ($editor_tag_map as $thisTag => $tagNameBBC) + if ($tag === $thisTag) + $context['disabled_tags'][$tagNameBBC] = true; + + $context['disabled_tags'][$tag] = true; + } + + $bbcodes_styles = ''; + $context['bbcodes_handlers'] = ''; + $context['bbc_toolbar'] = array(); + + foreach ($context['bbc_tags'] as $row => $tagRow) + { + if (!isset($context['bbc_toolbar'][$row])) + $context['bbc_toolbar'][$row] = array(); + + $tagsRow = array(); + + foreach ($tagRow as $tag) + { + if (empty($tag['code'])) + { + $context['bbc_toolbar'][$row][] = implode(',', $tagsRow); + $tagsRow = array(); + } + elseif (empty($context['disabled_tags'][$tag['code']])) + { + $tagsRow[] = $tag['code']; + + // If we have a custom button image, set it now. + if (isset($tag['image'])) + { + $bbcodes_styles .= ' + .sceditor-button-' . $tag['code'] . ' div { + background: url(\'' . $settings['default_theme_url'] . '/images/bbc/' . $tag['image'] . '.png\'); + }'; + } + + // Set the tooltip and possibly the command info + $context['bbcodes_handlers'] .= ' + sceditor.command.set(' . JavaScriptEscape($tag['code']) . ', { + tooltip: ' . JavaScriptEscape(isset($tag['description']) ? $tag['description'] : $tag['code']); + + // Legacy support for 2.0 BBC mods + if (isset($tag['before'])) + { + $context['bbcodes_handlers'] .= ', + exec: function () { + this.insertText(' . JavaScriptEscape($tag['before']) . (isset($tag['after']) ? ', ' . JavaScriptEscape($tag['after']) : '') . '); + }, + txtExec: [' . JavaScriptEscape($tag['before']) . (isset($tag['after']) ? ', ' . JavaScriptEscape($tag['after']) : '') . ']'; + } + + $context['bbcodes_handlers'] .= ' + });'; + } + } + + if (!empty($tagsRow)) + $context['bbc_toolbar'][$row][] = implode(',', $tagsRow); + } + + if (!empty($bbcodes_styles)) + addInlineCss($bbcodes_styles); + } + + // Initialize smiley array... if not loaded before. + if (empty($context['smileys']) && empty($editorOptions['disable_smiley_box'])) + { + $context['smileys'] = array( + 'postform' => array(), + 'popup' => array(), + ); + + if ($user_info['smiley_set'] != 'none') + { + // Cache for longer when customized smiley codes aren't enabled + $cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480; + + if (($temp = cache_get_data('posting_smileys_' . $user_info['smiley_set'], $cache_time)) == null) + { + $request = $smcFunc['db_query']('', ' + SELECT s.code, f.filename, s.description, s.smiley_row, s.hidden + FROM {db_prefix}smileys AS s + JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley) + WHERE s.hidden IN (0, 2) + AND f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? ' + AND s.code IN ({array_string:default_codes})' : '') . ' + ORDER BY s.smiley_row, s.smiley_order', + array( + 'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'), + 'smiley_set' => $user_info['smiley_set'], + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $row['description'] = !empty($txt['icon_' . strtolower($row['description'])]) ? $smcFunc['htmlspecialchars']($txt['icon_' . strtolower($row['description'])]) : $smcFunc['htmlspecialchars']($row['description']); + + $context['smileys'][empty($row['hidden']) ? 'postform' : 'popup'][$row['smiley_row']]['smileys'][] = $row; + } + $smcFunc['db_free_result']($request); + + foreach ($context['smileys'] as $section => $smileyRows) + { + foreach ($smileyRows as $rowIndex => $smileys) + $context['smileys'][$section][$rowIndex]['smileys'][count($smileys['smileys']) - 1]['isLast'] = true; + + if (!empty($smileyRows)) + $context['smileys'][$section][count($smileyRows) - 1]['isLast'] = true; + } + + cache_put_data('posting_smileys_' . $user_info['smiley_set'], $context['smileys'], $cache_time); + } + else + $context['smileys'] = $temp; + } + } + + // Set up the SCEditor options + $sce_options = array( + 'width' => isset($editorOptions['width']) ? $editorOptions['width'] : '100%', + 'height' => isset($editorOptions['height']) ? $editorOptions['height'] : '175px', + 'style' => $settings[file_exists($settings['theme_dir'] . '/css/jquery.sceditor.default.css') ? 'theme_url' : 'default_theme_url'] . '/css/jquery.sceditor.default.css' . $context['browser_cache'], + 'emoticonsCompat' => true, + 'colors' => 'black,maroon,brown,green,navy,grey,red,orange,teal,blue,white,hotpink,yellow,limegreen,purple', + 'format' => 'bbcode', + 'plugins' => '', + 'bbcodeTrim' => false, + ); + if (!empty($context['controls']['richedit'][$editorOptions['id']]['locale'])) + $sce_options['locale'] = $context['controls']['richedit'][$editorOptions['id']]['locale']; + if (!empty($context['right_to_left'])) + $sce_options['rtl'] = true; + if ($editorOptions['id'] != 'quickReply') + $sce_options['autofocus'] = true; + + $sce_options['emoticons'] = array(); + $sce_options['emoticonsDescriptions'] = array(); + $sce_options['emoticonsEnabled'] = false; + if ((!empty($context['smileys']['postform']) || !empty($context['smileys']['popup'])) && !$context['controls']['richedit'][$editorOptions['id']]['disable_smiley_box']) + { + $sce_options['emoticonsEnabled'] = true; + $sce_options['emoticons']['dropdown'] = array(); + $sce_options['emoticons']['popup'] = array(); + + $countLocations = count($context['smileys']); + foreach ($context['smileys'] as $location => $smileyRows) + { + $countLocations--; + + unset($smiley_location); + if ($location == 'postform') + $smiley_location = &$sce_options['emoticons']['dropdown']; + elseif ($location == 'popup') + $smiley_location = &$sce_options['emoticons']['popup']; + + $numRows = count($smileyRows); + + // This is needed because otherwise the editor will remove all the duplicate (empty) keys and leave only 1 additional line + $emptyPlaceholder = 0; + foreach ($smileyRows as $smileyRow) + { + foreach ($smileyRow['smileys'] as $smiley) + { + $smiley_location[$smiley['code']] = $settings['smileys_url'] . '/' . $smiley['filename']; + $sce_options['emoticonsDescriptions'][$smiley['code']] = $smiley['description']; + } + + if (empty($smileyRow['isLast']) && $numRows != 1) + $smiley_location['-' . $emptyPlaceholder++] = ''; + } + } + } + + $sce_options['toolbar'] = ''; + $sce_options['parserOptions']['txtVars'] = [ + 'code' => $txt['code'] + ]; + if (!empty($modSettings['enableBBC'])) + { + $count_tags = count($context['bbc_tags']); + foreach ($context['bbc_toolbar'] as $i => $buttonRow) + { + $sce_options['toolbar'] .= implode('|', $buttonRow); + + $count_tags--; + + if (!empty($count_tags)) + $sce_options['toolbar'] .= '||'; + } + } + + // Allow mods to change $sce_options. Usful if, e.g., a mod wants to add an SCEditor plugin. + call_integration_hook('integrate_sceditor_options', array(&$sce_options)); + + $context['controls']['richedit'][$editorOptions['id']]['sce_options'] = $sce_options; +} + +/** + * Create a anti-bot verification control? + * + * @param array &$verificationOptions Options for the verification control + * @param bool $do_test Whether to check to see if the user entered the code correctly + * @return bool|array False if there's nothing to show, true if everything went well or an array containing error indicators if the test failed + */ +function create_control_verification(&$verificationOptions, $do_test = false) +{ + global $modSettings, $smcFunc; + global $context, $user_info, $scripturl, $language; + + // First verification means we need to set up some bits... + if (empty($context['controls']['verification'])) + { + // The template + loadTemplate('GenericControls'); + + // Some javascript ma'am? + if (!empty($verificationOptions['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($verificationOptions['override_visual']))) + loadJavaScriptFile('captcha.js', array('minimize' => true), 'smf_captcha'); + + $context['use_graphic_library'] = in_array('gd', get_loaded_extensions()); + + // Skip I, J, L, O, Q, S and Z. + $context['standard_captcha_range'] = array_merge(range('A', 'H'), array('K', 'M', 'N', 'P', 'R'), range('T', 'Y')); + } + + // Always have an ID. + assert(isset($verificationOptions['id'])); + $isNew = !isset($context['controls']['verification'][$verificationOptions['id']]); + + // Log this into our collection. + if ($isNew) + $context['controls']['verification'][$verificationOptions['id']] = array( + 'id' => $verificationOptions['id'], + 'empty_field' => empty($verificationOptions['no_empty_field']), + 'show_visual' => !empty($verificationOptions['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($verificationOptions['override_visual'])), + 'number_questions' => isset($verificationOptions['override_qs']) ? $verificationOptions['override_qs'] : (!empty($modSettings['qa_verification_number']) ? $modSettings['qa_verification_number'] : 0), + 'max_errors' => isset($verificationOptions['max_errors']) ? $verificationOptions['max_errors'] : 3, + 'image_href' => $scripturl . '?action=verificationcode;vid=' . $verificationOptions['id'] . ';rand=' . md5(mt_rand()), + 'text_value' => '', + 'questions' => array(), + 'can_recaptcha' => !empty($modSettings['recaptcha_enabled']) && !empty($modSettings['recaptcha_site_key']) && !empty($modSettings['recaptcha_secret_key']), + ); + $thisVerification = &$context['controls']['verification'][$verificationOptions['id']]; + + // Add a verification hook, presetup. + call_integration_hook('integrate_create_control_verification_pre', array(&$verificationOptions, $do_test)); + + // Is there actually going to be anything? + if (empty($thisVerification['show_visual']) && empty($thisVerification['number_questions']) && empty($thisVerification['can_recaptcha'])) + return false; + elseif (!$isNew && !$do_test) + return true; + + // Sanitize reCAPTCHA fields? + if ($thisVerification['can_recaptcha']) + { + // Only allow 40 alphanumeric, underscore and dash characters. + $thisVerification['recaptcha_site_key'] = preg_replace('/(0-9a-zA-Z_){40}/', '$1', $modSettings['recaptcha_site_key']); + + // Light or dark theme... + $thisVerification['recaptcha_theme'] = preg_replace('/(light|dark)/', '$1', $modSettings['recaptcha_theme']); + } + + // Add javascript for the object. + if ($context['controls']['verification'][$verificationOptions['id']]['show_visual']) + $context['insert_after_template'] .= ' + '; + + // If we want questions do we have a cache of all the IDs? + if (!empty($thisVerification['number_questions']) && empty($modSettings['question_id_cache'])) + { + if (($modSettings['question_id_cache'] = cache_get_data('verificationQuestions', 300)) == null) + { + $request = $smcFunc['db_query']('', ' + SELECT id_question, lngfile, question, answers + FROM {db_prefix}qanda', + array() + ); + $modSettings['question_id_cache'] = array( + 'questions' => array(), + 'langs' => array(), + ); + // This is like Captain Kirk climbing a mountain in some ways. This is L's fault, mkay? :P + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $id_question = $row['id_question']; + unset ($row['id_question']); + // Make them all lowercase. We can't directly use $smcFunc['strtolower'] with array_walk, so do it manually, eh? + $row['answers'] = (array) $smcFunc['json_decode']($row['answers'], true); + foreach ($row['answers'] as $k => $v) + $row['answers'][$k] = $smcFunc['strtolower']($v); + + $modSettings['question_id_cache']['questions'][$id_question] = $row; + $modSettings['question_id_cache']['langs'][$row['lngfile']][] = $id_question; + } + $smcFunc['db_free_result']($request); + + cache_put_data('verificationQuestions', $modSettings['question_id_cache'], 300); + } + } + + if (!isset($_SESSION[$verificationOptions['id'] . '_vv'])) + $_SESSION[$verificationOptions['id'] . '_vv'] = array(); + + // Do we need to refresh the verification? + if (!$do_test && (!empty($_SESSION[$verificationOptions['id'] . '_vv']['did_pass']) || empty($_SESSION[$verificationOptions['id'] . '_vv']['count']) || $_SESSION[$verificationOptions['id'] . '_vv']['count'] > 3) && empty($verificationOptions['dont_refresh'])) + $force_refresh = true; + else + $force_refresh = false; + + // This can also force a fresh, although unlikely. + if (($thisVerification['show_visual'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['code'])) || ($thisVerification['number_questions'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['q']))) + $force_refresh = true; + + $verification_errors = array(); + // Start with any testing. + if ($do_test) + { + // This cannot happen! + if (!isset($_SESSION[$verificationOptions['id'] . '_vv']['count'])) + fatal_lang_error('no_access', false); + // Hmm, it's requested but not actually declared. This shouldn't happen. + if ($thisVerification['empty_field'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field'])) + fatal_lang_error('no_access', false); + // While we're here, did the user do something bad? + if ($thisVerification['empty_field'] && !empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field']) && !empty($_REQUEST[$_SESSION[$verificationOptions['id'] . '_vv']['empty_field']])) + $verification_errors[] = 'wrong_verification_answer'; + + if ($thisVerification['can_recaptcha']) + { + $reCaptcha = new \ReCaptcha\ReCaptcha($modSettings['recaptcha_secret_key'], new \ReCaptcha\RequestMethod\SocketPost()); + + // Was there a reCAPTCHA response? + if (isset($_POST['g-recaptcha-response'])) + { + $resp = $reCaptcha->verify($_POST['g-recaptcha-response'], $user_info['ip']); + + if (!$resp->isSuccess()) + $verification_errors[] = 'wrong_verification_recaptcha'; + } + else + $verification_errors[] = 'wrong_verification_code'; + } + if ($thisVerification['show_visual'] && (empty($_REQUEST[$verificationOptions['id'] . '_vv']['code']) || empty($_SESSION[$verificationOptions['id'] . '_vv']['code']) || strtoupper($_REQUEST[$verificationOptions['id'] . '_vv']['code']) !== $_SESSION[$verificationOptions['id'] . '_vv']['code'])) + $verification_errors[] = 'wrong_verification_code'; + if ($thisVerification['number_questions']) + { + $incorrectQuestions = array(); + foreach ($_SESSION[$verificationOptions['id'] . '_vv']['q'] as $q) + { + // We don't have this question any more, thus no answers. + if (!isset($modSettings['question_id_cache']['questions'][$q])) + continue; + // This is quite complex. We have our question but it might have multiple answers. + // First, did they actually answer this question? + if (!isset($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) || trim($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) == '') + { + $incorrectQuestions[] = $q; + continue; + } + // Second, is their answer in the list of possible answers? + else + { + $given_answer = trim($smcFunc['htmlspecialchars']($smcFunc['strtolower']($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]))); + if (!in_array($given_answer, $modSettings['question_id_cache']['questions'][$q]['answers'])) + $incorrectQuestions[] = $q; + } + } + + if (!empty($incorrectQuestions)) + $verification_errors[] = 'wrong_verification_answer'; + } + + // Hooks got anything to say about this verification? + call_integration_hook('integrate_create_control_verification_test', array($thisVerification, &$verification_errors)); + } + + // Any errors means we refresh potentially. + if (!empty($verification_errors)) + { + if (empty($_SESSION[$verificationOptions['id'] . '_vv']['errors'])) + $_SESSION[$verificationOptions['id'] . '_vv']['errors'] = 0; + // Too many errors? + elseif ($_SESSION[$verificationOptions['id'] . '_vv']['errors'] > $thisVerification['max_errors']) + $force_refresh = true; + + // Keep a track of these. + $_SESSION[$verificationOptions['id'] . '_vv']['errors']++; + } + + // Are we refreshing then? + if ($force_refresh) + { + // Assume nothing went before. + $_SESSION[$verificationOptions['id'] . '_vv']['count'] = 0; + $_SESSION[$verificationOptions['id'] . '_vv']['errors'] = 0; + $_SESSION[$verificationOptions['id'] . '_vv']['did_pass'] = false; + $_SESSION[$verificationOptions['id'] . '_vv']['q'] = array(); + $_SESSION[$verificationOptions['id'] . '_vv']['code'] = ''; + + // Make our magic empty field. + if ($thisVerification['empty_field']) + { + // We're building a field that lives in the template, that we hope to be empty later. But at least we give it a believable name. + $terms = array('gadget', 'device', 'uid', 'gid', 'guid', 'uuid', 'unique', 'identifier'); + $second_terms = array('hash', 'cipher', 'code', 'key', 'unlock', 'bit', 'value'); + $start = mt_rand(0, 27); + $hash = substr(md5(time()), $start, 4); + $_SESSION[$verificationOptions['id'] . '_vv']['empty_field'] = $terms[array_rand($terms)] . '-' . $second_terms[array_rand($second_terms)] . '-' . $hash; + } + + // Generating a new image. + if ($thisVerification['show_visual']) + { + // Are we overriding the range? + $character_range = !empty($verificationOptions['override_range']) ? $verificationOptions['override_range'] : $context['standard_captcha_range']; + + for ($i = 0; $i < 6; $i++) + $_SESSION[$verificationOptions['id'] . '_vv']['code'] .= $character_range[array_rand($character_range)]; + } + + // Getting some new questions? + if ($thisVerification['number_questions']) + { + // Attempt to try the current page's language, followed by the user's preference, followed by the site default. + $possible_langs = array(); + if (isset($_SESSION['language'])) + $possible_langs[] = strtr($_SESSION['language'], array('-utf8' => '')); + if (!empty($user_info['language'])) + $possible_langs[] = $user_info['language']; + + $possible_langs[] = $language; + + $questionIDs = array(); + foreach ($possible_langs as $lang) + { + $lang = strtr($lang, array('-utf8' => '')); + if (isset($modSettings['question_id_cache']['langs'][$lang])) + { + // If we find questions for this, grab the ids from this language's ones, randomize the array and take just the number we need. + $questionIDs = $modSettings['question_id_cache']['langs'][$lang]; + shuffle($questionIDs); + $questionIDs = array_slice($questionIDs, 0, $thisVerification['number_questions']); + break; + } + } + } + + // Hooks may need to know about this. + call_integration_hook('integrate_create_control_verification_refresh', array($thisVerification)); + } + else + { + // Same questions as before. + $questionIDs = !empty($_SESSION[$verificationOptions['id'] . '_vv']['q']) ? $_SESSION[$verificationOptions['id'] . '_vv']['q'] : array(); + $thisVerification['text_value'] = !empty($_REQUEST[$verificationOptions['id'] . '_vv']['code']) ? $smcFunc['htmlspecialchars']($_REQUEST[$verificationOptions['id'] . '_vv']['code']) : ''; + } + + // If we do have an empty field, it would be nice to hide it from legitimate users who shouldn't be populating it anyway. + if (!empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field'])) + { + if (!isset($context['html_headers'])) + $context['html_headers'] = ''; + $context['html_headers'] .= ''; + } + + // Have we got some questions to load? + if (!empty($questionIDs)) + { + $_SESSION[$verificationOptions['id'] . '_vv']['q'] = array(); + foreach ($questionIDs as $q) + { + // Bit of a shortcut this. + $row = &$modSettings['question_id_cache']['questions'][$q]; + $thisVerification['questions'][] = array( + 'id' => $q, + 'q' => parse_bbc($row['question']), + 'is_error' => !empty($incorrectQuestions) && in_array($q, $incorrectQuestions), + // Remember a previous submission? + 'a' => isset($_REQUEST[$verificationOptions['id'] . '_vv'], $_REQUEST[$verificationOptions['id'] . '_vv']['q'], $_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) ? $smcFunc['htmlspecialchars']($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) : '', + ); + $_SESSION[$verificationOptions['id'] . '_vv']['q'][] = $q; + } + } + + $_SESSION[$verificationOptions['id'] . '_vv']['count'] = empty($_SESSION[$verificationOptions['id'] . '_vv']['count']) ? 1 : $_SESSION[$verificationOptions['id'] . '_vv']['count'] + 1; + + // Let our hooks know that we are done with the verification process. + call_integration_hook('integrate_create_control_verification_post', array(&$verification_errors, $do_test)); + + // Return errors if we have them. + if (!empty($verification_errors)) + return $verification_errors; + // If we had a test that one, make a note. + elseif ($do_test) + $_SESSION[$verificationOptions['id'] . '_vv']['did_pass'] = true; + + // Say that everything went well chaps. + return true; +} + +/** + * This keeps track of all registered handling functions for auto suggest functionality and passes execution to them. + * + * @param bool $checkRegistered If set to something other than null, checks whether the callback function is registered + * @return void|bool Returns whether the callback function is registered if $checkRegistered isn't null + */ +function AutoSuggestHandler($checkRegistered = null) +{ + global $smcFunc, $context; + + // These are all registered types. + $searchTypes = array( + 'member' => 'Member', + 'membergroups' => 'MemberGroups', + 'versions' => 'SMFVersions', + ); + + call_integration_hook('integrate_autosuggest', array(&$searchTypes)); + + // If we're just checking the callback function is registered return true or false. + if ($checkRegistered != null) + return isset($searchTypes[$checkRegistered]) && function_exists('AutoSuggest_Search_' . $checkRegistered); + + checkSession('get'); + loadTemplate('Xml'); + + // Any parameters? + $context['search_param'] = isset($_REQUEST['search_param']) ? $smcFunc['json_decode'](base64_decode($_REQUEST['search_param']), true) : array(); + + if (isset($_REQUEST['suggest_type'], $_REQUEST['search']) && isset($searchTypes[$_REQUEST['suggest_type']])) + { + $function = 'AutoSuggest_Search_' . $searchTypes[$_REQUEST['suggest_type']]; + $context['sub_template'] = 'generic_xml'; + $context['xml_data'] = $function(); + } +} + +/** + * Search for a member - by real_name or member_name by default. + * + * @return array An array of information for displaying the suggestions + */ +function AutoSuggest_Search_Member() +{ + global $user_info, $smcFunc, $context; + + $_REQUEST['search'] = trim($smcFunc['strtolower']($_REQUEST['search'])) . '*'; + $_REQUEST['search'] = strtr($_REQUEST['search'], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '&' => '&')); + + // Find the member. + $request = $smcFunc['db_query']('', ' + SELECT id_member, real_name + FROM {db_prefix}members + WHERE {raw:real_name} LIKE {string:search}' . (!empty($context['search_param']['buddies']) ? ' + AND id_member IN ({array_int:buddy_list})' : '') . ' + AND is_activated IN (1, 11) + LIMIT ' . ($smcFunc['strlen']($_REQUEST['search']) <= 2 ? '100' : '800'), + array( + 'real_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(real_name)' : 'real_name', + 'buddy_list' => $user_info['buddies'], + 'search' => $_REQUEST['search'], + ) + ); + $xml_data = array( + 'items' => array( + 'identifier' => 'item', + 'children' => array(), + ), + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $row['real_name'] = strtr($row['real_name'], array('&' => '&', '<' => '<', '>' => '>', '"' => '"')); + + $xml_data['items']['children'][] = array( + 'attributes' => array( + 'id' => $row['id_member'], + ), + 'value' => $row['real_name'], + ); + } + $smcFunc['db_free_result']($request); + + return $xml_data; +} + +/** + * Search for a membergroup by name + * + * @return array An array of information for displaying the suggestions + */ +function AutoSuggest_Search_MemberGroups() +{ + global $smcFunc; + + $_REQUEST['search'] = trim($smcFunc['strtolower']($_REQUEST['search'])) . '*'; + $_REQUEST['search'] = strtr($_REQUEST['search'], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '&' => '&')); + + // Find the group. + // Only return groups which are not post-based and not "Hidden", but not the "Administrators" or "Moderators" groups. + $request = $smcFunc['db_query']('', ' + SELECT id_group, group_name + FROM {db_prefix}membergroups + WHERE {raw:group_name} LIKE {string:search} + AND min_posts = {int:min_posts} + AND id_group NOT IN ({array_int:invalid_groups}) + AND hidden != {int:hidden}', + array( + 'group_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(group_name)' : 'group_name', + 'min_posts' => -1, + 'invalid_groups' => array(1, 3), + 'hidden' => 2, + 'search' => $_REQUEST['search'], + ) + ); + $xml_data = array( + 'items' => array( + 'identifier' => 'item', + 'children' => array(), + ), + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $row['group_name'] = strtr($row['group_name'], array('&' => '&', '<' => '<', '>' => '>', '"' => '"')); + + $xml_data['items']['children'][] = array( + 'attributes' => array( + 'id' => $row['id_group'], + ), + 'value' => $row['group_name'], + ); + } + $smcFunc['db_free_result']($request); + + return $xml_data; +} + +/** + * Provides a list of possible SMF versions to use in emulation + * + * @return array An array of data for displaying the suggestions + */ +function AutoSuggest_Search_SMFVersions() +{ + global $smcFunc; + + $xml_data = array( + 'items' => array( + 'identifier' => 'item', + 'children' => array(), + ), + ); + + // First try and get it from the database. + $versions = array(); + $request = $smcFunc['db_query']('', ' + SELECT data + FROM {db_prefix}admin_info_files + WHERE filename = {string:latest_versions} + AND path = {string:path}', + array( + 'latest_versions' => 'latest-versions.txt', + 'path' => '/smf/', + ) + ); + if (($smcFunc['db_num_rows']($request) > 0) && ($row = $smcFunc['db_fetch_assoc']($request)) && !empty($row['data'])) + { + // The file can be either Windows or Linux line endings, but let's ensure we clean it as best we can. + $possible_versions = explode("\n", $row['data']); + foreach ($possible_versions as $ver) + { + $ver = trim($ver); + if (strpos($ver, 'SMF') === 0) + $versions[] = $ver; + } + } + $smcFunc['db_free_result']($request); + + // Just in case we don't have ANYthing. + if (empty($versions)) + $versions = array(SMF_FULL_VERSION); + + foreach ($versions as $id => $version) + if (strpos($version, strtoupper($_REQUEST['search'])) !== false) + $xml_data['items']['children'][] = array( + 'attributes' => array( + 'id' => $id, + ), + 'value' => $version, + ); + + return $xml_data; +} + +?> \ No newline at end of file diff --git a/Sources/Subs-Graphics.php b/Sources/Subs-Graphics.php new file mode 100644 index 0000000..404c2be --- /dev/null +++ b/Sources/Subs-Graphics.php @@ -0,0 +1,1222 @@ + $memID)); + + $id_folder = 1; + $avatar_hash = ''; + $attachID = $smcFunc['db_insert']('', + '{db_prefix}attachments', + array( + 'id_member' => 'int', 'attachment_type' => 'int', 'filename' => 'string-255', 'file_hash' => 'string-255', 'fileext' => 'string-8', 'size' => 'int', + 'id_folder' => 'int', + ), + array( + $memID, 1, $destName, $avatar_hash, $ext, 1, + $id_folder, + ), + array('id_attach'), + 1 + ); + + // Retain this globally in case the script wants it. + $modSettings['new_avatar_data'] = array( + 'id' => $attachID, + 'filename' => $destName, + 'type' => 1, + ); + + $destName = $modSettings['custom_avatar_dir'] . '/' . $destName . '.tmp'; + + // Resize it. + if (!empty($modSettings['avatar_download_png'])) + $success = resizeImageFile($url, $destName, $max_width, $max_height, 3); + else + $success = resizeImageFile($url, $destName, $max_width, $max_height); + + // Remove the .tmp extension. + $destName = substr($destName, 0, -4); + + if ($success) + { + // Remove the .tmp extension from the attachment. + if (rename($destName . '.tmp', $destName)) + { + list ($width, $height) = getimagesize($destName); + $mime_type = 'image/' . $ext; + + // Write filesize in the database. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}attachments + SET size = {int:filesize}, width = {int:width}, height = {int:height}, + mime_type = {string:mime_type} + WHERE id_attach = {int:current_attachment}', + array( + 'filesize' => filesize($destName), + 'width' => (int) $width, + 'height' => (int) $height, + 'current_attachment' => $attachID, + 'mime_type' => $mime_type, + ) + ); + return true; + } + else + return false; + } + else + { + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}attachments + WHERE id_attach = {int:current_attachment}', + array( + 'current_attachment' => $attachID, + ) + ); + + @unlink($destName . '.tmp'); + return false; + } +} + +/** + * Create a thumbnail of the given source. + * + * @uses resizeImageFile() function to achieve the resize. + * + * @param string $source The name of the source image + * @param int $max_width The maximum allowed width + * @param int $max_height The maximum allowed height + * @return boolean Whether the thumbnail creation was successful. + */ +function createThumbnail($source, $max_width, $max_height) +{ + global $modSettings; + + $destName = $source . '_thumb.tmp'; + + // Do the actual resize. + if (!empty($modSettings['attachment_thumb_png'])) + $success = resizeImageFile($source, $destName, $max_width, $max_height, 3); + else + $success = resizeImageFile($source, $destName, $max_width, $max_height); + + // Okay, we're done with the temporary stuff. + $destName = substr($destName, 0, -4); + + if ($success && @rename($destName . '.tmp', $destName)) + return true; + else + { + @unlink($destName . '.tmp'); + @touch($destName); + return false; + } +} + +/** + * Used to re-econodes an image to a specified image format + * - creates a copy of the file at the same location as fileName. + * - the file would have the format preferred_format if possible, otherwise the default format is jpeg. + * - the function makes sure that all non-essential image contents are disposed. + * + * @param string $fileName The path to the file + * @param int $preferred_format The preferred format - 0 to automatically determine, 1 for gif, 2 for jpg, 3 for png, 6 for bmp and 15 for wbmp + * @return boolean Whether the reencoding was successful + */ +function reencodeImage($fileName, $preferred_format = 0) +{ + if (!resizeImageFile($fileName, $fileName . '.tmp', null, null, $preferred_format)) + { + if (file_exists($fileName . '.tmp')) + unlink($fileName . '.tmp'); + + return false; + } + + if (!unlink($fileName)) + return false; + + if (!rename($fileName . '.tmp', $fileName)) + return false; + + return true; +} + +/** + * Searches through the file to see if there's potentially harmful non-binary content. + * - if extensiveCheck is true, searches for asp/php short tags as well. + * + * @param string $fileName The path to the file + * @param bool $extensiveCheck Whether to perform extensive checks + * @return bool Whether the image appears to be safe + */ +function checkImageContents($fileName, $extensiveCheck = false) +{ + $fp = fopen($fileName, 'rb'); + if (!$fp) + fatal_lang_error('attach_timeout'); + + $prev_chunk = ''; + while (!feof($fp)) + { + $cur_chunk = fread($fp, 8192); + + // Though not exhaustive lists, better safe than sorry. + if (!empty($extensiveCheck)) + { + // Paranoid check. Use this if you have reason to distrust your host's security config. + // Will result in MANY false positives, and is not suitable for photography sites. + if (preg_match('~(iframe|\\<\\?|\\<%|html|eval|body|script\W|(?-i)[CFZ]WS[\x01-\x0E])~i', $prev_chunk . $cur_chunk) === 1) + { + fclose($fp); + return false; + } + } + else + { + // Check for potential infection - focus on clues for inline php & flash. + // Will result in significantly fewer false positives than the paranoid check. + if (preg_match('~(\\<\\?php\s|(?-i)[CFZ]WS[\x01-\x0E])~i', $prev_chunk . $cur_chunk) === 1) + { + fclose($fp); + return false; + } + } + $prev_chunk = $cur_chunk; + } + fclose($fp); + + return true; +} + +/** + * Sets a global $gd2 variable needed by some functions to determine + * whether the GD2 library is present. + * + * @return bool Whether or not GD1 is available. + */ +function checkGD() +{ + global $gd2; + + // Check to see if GD is installed and what version. + if (($extensionFunctions = get_extension_funcs('gd')) === false) + return false; + + // Also determine if GD2 is installed and store it in a global. + $gd2 = in_array('imagecreatetruecolor', $extensionFunctions) && function_exists('imagecreatetruecolor'); + + return true; +} + +/** + * Checks whether the Imagick class is present. + * + * @return bool Whether or not the Imagick extension is available. + */ +function checkImagick() +{ + return class_exists('Imagick', false); +} + +/** + * Checks whether the MagickWand extension is present. + * + * @return bool Whether or not the MagickWand extension is available. + */ +function checkMagickWand() +{ + return function_exists('newMagickWand'); +} + +/** + * See if we have enough memory to thumbnail an image + * + * @param array $sizes image size + * @return bool Whether we do + */ +function imageMemoryCheck($sizes) +{ + global $modSettings; + + // doing the old 'set it and hope' way? + if (empty($modSettings['attachment_thumb_memory'])) + { + setMemoryLimit('128M'); + return true; + } + + // Determine the memory requirements for this image, note: if you want to use an image formula W x H x bits/8 x channels x Overhead factor + // you will need to account for single bit images as GD expands them to an 8 bit and will greatly overun the calculated value. The 5 is + // simply a shortcut of 8bpp, 3 channels, 1.66 overhead + $needed_memory = ($sizes[0] * $sizes[1] * 5); + + // if we need more, lets try to get it + return setMemoryLimit($needed_memory, true); +} + +/** + * Resizes an image from a remote location or a local file. + * Puts the resized image at the destination location. + * The file would have the format preferred_format if possible, + * otherwise the default format is jpeg. + * + * @param string $source The path to the source image + * @param string $destination The path to the destination image + * @param int $max_width The maximum allowed width + * @param int $max_height The maximum allowed height + * @param int $preferred_format - The preferred format (0 to use jpeg, 1 for gif, 2 to force jpeg, 3 for png, 6 for bmp and 15 for wbmp) + * @return bool Whether it succeeded. + */ +function resizeImageFile($source, $destination, $max_width, $max_height, $preferred_format = 0) +{ + global $sourcedir; + + // Nothing to do without GD or IM/MW + if (!checkGD() && !checkImagick() && !checkMagickWand()) + return false; + + static $default_formats = array( + '1' => 'gif', + '2' => 'jpeg', + '3' => 'png', + '6' => 'bmp', + '15' => 'wbmp' + ); + + // Get the image file, we have to work with something after all + $fp_destination = fopen($destination, 'wb'); + if ($fp_destination && (substr($source, 0, 7) == 'http://' || substr($source, 0, 8) == 'https://')) + { + $fileContents = fetch_web_data($source); + + $mime_valid = check_mime_type($fileContents, implode('|', array_map('image_type_to_mime_type', array_keys($default_formats)))); + if (empty($mime_valid)) + return false; + + fwrite($fp_destination, $fileContents); + fclose($fp_destination); + + $sizes = @getimagesize($destination); + } + elseif ($fp_destination) + { + $mime_valid = check_mime_type($source, implode('|', array_map('image_type_to_mime_type', array_keys($default_formats))), true); + if (empty($mime_valid)) + return false; + + $sizes = @getimagesize($source); + + $fp_source = fopen($source, 'rb'); + if ($fp_source !== false) + { + while (!feof($fp_source)) + fwrite($fp_destination, fread($fp_source, 8192)); + fclose($fp_source); + } + else + $sizes = array(-1, -1, -1); + fclose($fp_destination); + } + + // We can't get to the file. or a previous getimagesize failed. + if (empty($sizes)) + $sizes = array(-1, -1, -1); + + // See if we have -or- can get the needed memory for this operation + // ImageMagick isn't subject to PHP's memory limits :) + if (!(checkIMagick() || checkMagickWand()) && checkGD() && !imageMemoryCheck($sizes)) + return false; + + // A known and supported format? + // @todo test PSD and gif. + if ((checkImagick() || checkMagickWand()) && isset($default_formats[$sizes[2]])) + { + return resizeImage(null, $destination, null, null, $max_width, $max_height, true, $preferred_format); + } + elseif (checkGD() && isset($default_formats[$sizes[2]]) && function_exists('imagecreatefrom' . $default_formats[$sizes[2]])) + { + $imagecreatefrom = 'imagecreatefrom' . $default_formats[$sizes[2]]; + if ($src_img = @$imagecreatefrom($destination)) + { + return resizeImage($src_img, $destination, imagesx($src_img), imagesy($src_img), $max_width === null ? imagesx($src_img) : $max_width, $max_height === null ? imagesy($src_img) : $max_height, true, $preferred_format); + } + } + + return false; +} + +/** + * Resizes src_img proportionally to fit within max_width and max_height limits + * if it is too large. + * If GD2 is present, it'll use it to achieve better quality. + * It saves the new image to destination_filename, as preferred_format + * if possible, default is jpeg. + * + * Uses Imagemagick (IMagick or MagickWand extension) or GD + * + * @param resource $src_img The source image + * @param string $destName The path to the destination image + * @param int $src_width The width of the source image + * @param int $src_height The height of the source image + * @param int $max_width The maximum allowed width + * @param int $max_height The maximum allowed height + * @param bool $force_resize = false Whether to forcibly resize it + * @param int $preferred_format - 1 for gif, 2 for jpeg, 3 for png, 6 for bmp or 15 for wbmp + * @return bool Whether the resize was successful + */ +function resizeImage($src_img, $destName, $src_width, $src_height, $max_width, $max_height, $force_resize = false, $preferred_format = 0) +{ + global $gd2, $modSettings; + + $orientation = 0; + if (function_exists('exif_read_data') && ($exif_data = @exif_read_data($destName)) !== false && !empty($exif_data['Orientation'])) + $orientation = $exif_data['Orientation']; + + if (checkImagick() || checkMagickWand()) + { + static $default_formats = array( + '1' => 'gif', + '2' => 'jpeg', + '3' => 'png', + '6' => 'bmp', + '15' => 'wbmp' + ); + $preferred_format = empty($preferred_format) || !isset($default_formats[$preferred_format]) ? 2 : $preferred_format; + + if (checkImagick()) + { + $imagick = New Imagick($destName); + $src_width = empty($src_width) ? $imagick->getImageWidth() : $src_width; + $src_height = empty($src_height) ? $imagick->getImageHeight() : $src_height; + $dest_width = empty($max_width) ? $src_width : $max_width; + $dest_height = empty($max_height) ? $src_height : $max_height; + + if ($default_formats[$preferred_format] == 'jpeg') + $imagick->setCompressionQuality(!empty($modSettings['avatar_jpeg_quality']) ? $modSettings['avatar_jpeg_quality'] : 82); + + $imagick->setImageFormat($default_formats[$preferred_format]); + $imagick->resizeImage($dest_width, $dest_height, Imagick::FILTER_LANCZOS, 1, true); + + if ($orientation > 1 && $preferred_format == 3) + { + if (in_array($orientation, [3, 4])) + $imagick->rotateImage('#00000000', 180); + elseif (in_array($orientation, [5, 6])) + $imagick->rotateImage('#00000000', 90); + elseif (in_array($orientation, [7, 8])) + $imagick->rotateImage('#00000000', 270); + + if (in_array($orientation, [2, 4, 5, 7])) + $imagick->flopImage(); + } + $success = $imagick->writeImage($destName); + } + else + { + $magick_wand = newMagickWand(); + MagickReadImage($magick_wand, $destName); + $src_width = empty($src_width) ? MagickGetImageWidth($magick_wand) : $src_width; + $src_height = empty($src_height) ? MagickGetImageSize($magick_wand) : $src_height; + $dest_width = empty($max_width) ? $src_width : $max_width; + $dest_height = empty($max_height) ? $src_height : $max_height; + + if ($default_formats[$preferred_format] == 'jpeg') + MagickSetCompressionQuality($magick_wand, !empty($modSettings['avatar_jpeg_quality']) ? $modSettings['avatar_jpeg_quality'] : 82); + + MagickSetImageFormat($magick_wand, $default_formats[$preferred_format]); + MagickResizeImage($magick_wand, $dest_width, $dest_height, MW_LanczosFilter, 1, true); + + if ($orientation > 1) + { + if (in_array($orientation, [3, 4])) + MagickResizeImage($magick_wand, NewPixelWand('white'), 180); + elseif (in_array($orientation, [5, 6])) + MagickResizeImage($magick_wand, NewPixelWand('white'), 90); + elseif (in_array($orientation, [7, 8])) + MagickResizeImage($magick_wand, NewPixelWand('white'), 270); + + if (in_array($orientation, [2, 4, 5, 7])) + MagickFlopImage($magick_wand); + } + $success = MagickWriteImage($magick_wand, $destName); + } + + return !empty($success); + } + elseif (checkGD()) + { + $success = false; + + // Determine whether to resize to max width or to max height (depending on the limits.) + if (!empty($max_width) || !empty($max_height)) + { + if (!empty($max_width) && (empty($max_height) || round($src_height * $max_width / $src_width) <= $max_height)) + { + $dst_width = $max_width; + $dst_height = round($src_height * $max_width / $src_width); + } + elseif (!empty($max_height)) + { + $dst_width = round($src_width * $max_height / $src_height); + $dst_height = $max_height; + } + + // Don't bother resizing if it's already smaller... + if (!empty($dst_width) && !empty($dst_height) && ($dst_width < $src_width || $dst_height < $src_height || $force_resize)) + { + // (make a true color image, because it just looks better for resizing.) + if ($gd2) + { + $dst_img = imagecreatetruecolor($dst_width, $dst_height); + + // Deal nicely with a PNG - because we can. + if ((!empty($preferred_format)) && ($preferred_format == 3)) + { + imagealphablending($dst_img, false); + if (function_exists('imagesavealpha')) + imagesavealpha($dst_img, true); + } + } + else + $dst_img = imagecreate($dst_width, $dst_height); + + // Resize it! + if ($gd2) + imagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $dst_width, $dst_height, $src_width, $src_height); + else + imagecopyresamplebicubic($dst_img, $src_img, 0, 0, 0, 0, $dst_width, $dst_height, $src_width, $src_height); + } + else + $dst_img = $src_img; + } + else + $dst_img = $src_img; + + if ($orientation > 1) + { + if (in_array($orientation, [3, 4])) + $dst_img = imagerotate($dst_img, 180, 0); + elseif (in_array($orientation, [5, 6])) + $dst_img = imagerotate($dst_img, 270, 0); + elseif (in_array($orientation, [7, 8])) + $dst_img = imagerotate($dst_img, 90, 0); + + if (in_array($orientation, [2, 4, 5, 7])) + imageflip($dst_img, IMG_FLIP_HORIZONTAL); + } + + // Save the image as ... + if (!empty($preferred_format) && ($preferred_format == 3) && function_exists('imagepng')) + $success = imagepng($dst_img, $destName); + elseif (!empty($preferred_format) && ($preferred_format == 1) && function_exists('imagegif')) + $success = imagegif($dst_img, $destName); + elseif (!empty($preferred_format) && ($preferred_format == 6) && function_exists('imagebmp')) + $success = imagebmp($dst_img, $destName); + elseif (!empty($preferred_format) && ($preferred_format == 15) && function_exists('imagewbmp')) + $success = imagewbmp($dst_img, $destName); + elseif (function_exists('imagejpeg')) + $success = imagejpeg($dst_img, $destName, !empty($modSettings['avatar_jpeg_quality']) ? $modSettings['avatar_jpeg_quality'] : 82); + + // Free the memory. + imagedestroy($src_img); + if ($dst_img != $src_img) + imagedestroy($dst_img); + + return $success; + } + else + // Without GD, no image resizing at all. + return false; +} + +/** + * Copy image. + * Used when imagecopyresample() is not available. + * + * @param resource $dst_img The destination image - a GD image resource + * @param resource $src_img The source image - a GD image resource + * @param int $dst_x The "x" coordinate of the destination image + * @param int $dst_y The "y" coordinate of the destination image + * @param int $src_x The "x" coordinate of the source image + * @param int $src_y The "y" coordinate of the source image + * @param int $dst_w The width of the destination image + * @param int $dst_h The height of the destination image + * @param int $src_w The width of the destination image + * @param int $src_h The height of the destination image + */ +function imagecopyresamplebicubic($dst_img, $src_img, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h) +{ + $palsize = imagecolorstotal($src_img); + for ($i = 0; $i < $palsize; $i++) + { + $colors = imagecolorsforindex($src_img, $i); + imagecolorallocate($dst_img, $colors['red'], $colors['green'], $colors['blue']); + } + + $scaleX = ($src_w - 1) / $dst_w; + $scaleY = ($src_h - 1) / $dst_h; + + $scaleX2 = (int) $scaleX / 2; + $scaleY2 = (int) $scaleY / 2; + + for ($j = $src_y; $j < $dst_h; $j++) + { + $sY = (int) $j * $scaleY; + $y13 = $sY + $scaleY2; + + for ($i = $src_x; $i < $dst_w; $i++) + { + $sX = (int) $i * $scaleX; + $x34 = $sX + $scaleX2; + + $color1 = imagecolorsforindex($src_img, imagecolorat($src_img, $sX, $y13)); + $color2 = imagecolorsforindex($src_img, imagecolorat($src_img, $sX, $sY)); + $color3 = imagecolorsforindex($src_img, imagecolorat($src_img, $x34, $y13)); + $color4 = imagecolorsforindex($src_img, imagecolorat($src_img, $x34, $sY)); + + $red = ($color1['red'] + $color2['red'] + $color3['red'] + $color4['red']) / 4; + $green = ($color1['green'] + $color2['green'] + $color3['green'] + $color4['green']) / 4; + $blue = ($color1['blue'] + $color2['blue'] + $color3['blue'] + $color4['blue']) / 4; + + $color = imagecolorresolve($dst_img, $red, $green, $blue); + if ($color == -1) + { + if ($palsize++ < 256) + imagecolorallocate($dst_img, $red, $green, $blue); + $color = imagecolorclosest($dst_img, $red, $green, $blue); + } + + imagesetpixel($dst_img, $i + $dst_x - $src_x, $j + $dst_y - $src_y, $color); + } + } +} + +if (!function_exists('imagecreatefrombmp')) +{ + /** + * It is set only if it doesn't already exist (for forwards compatiblity.) + * It only supports uncompressed bitmaps. + * + * @param string $filename The name of the file + * @return resource An image identifier representing the bitmap image + * obtained from the given filename. + */ + function imagecreatefrombmp($filename) + { + global $gd2; + + $fp = fopen($filename, 'rb'); + + $errors = error_reporting(0); + + $header = unpack('vtype/Vsize/Vreserved/Voffset', fread($fp, 14)); + $info = unpack('Vsize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vncolor/Vcolorimportant', fread($fp, 40)); + + if ($header['type'] != 0x4D42) + return false; + + if ($gd2) + $dst_img = imagecreatetruecolor($info['width'], $info['height']); + else + $dst_img = imagecreate($info['width'], $info['height']); + + $palette_size = $header['offset'] - 54; + $info['ncolor'] = $palette_size / 4; + + $palette = array(); + + $palettedata = fread($fp, $palette_size); + $n = 0; + for ($j = 0; $j < $palette_size; $j++) + { + $b = ord($palettedata[$j++]); + $g = ord($palettedata[$j++]); + $r = ord($palettedata[$j++]); + + $palette[$n++] = imagecolorallocate($dst_img, $r, $g, $b); + } + + $scan_line_size = ($info['bits'] * $info['width'] + 7) >> 3; + $scan_line_align = $scan_line_size & 3 ? 4 - ($scan_line_size & 3) : 0; + + for ($y = 0, $l = $info['height'] - 1; $y < $info['height']; $y++, $l--) + { + fseek($fp, $header['offset'] + ($scan_line_size + $scan_line_align) * $l); + $scan_line = fread($fp, $scan_line_size); + + if (strlen($scan_line) < $scan_line_size) + continue; + + if ($info['bits'] == 32) + { + $x = 0; + for ($j = 0; $j < $scan_line_size; $x++) + { + $b = ord($scan_line[$j++]); + $g = ord($scan_line[$j++]); + $r = ord($scan_line[$j++]); + $j++; + + $color = imagecolorexact($dst_img, $r, $g, $b); + if ($color == -1) + { + $color = imagecolorallocate($dst_img, $r, $g, $b); + + // Gah! Out of colors? Stupid GD 1... try anyhow. + if ($color == -1) + $color = imagecolorclosest($dst_img, $r, $g, $b); + } + + imagesetpixel($dst_img, $x, $y, $color); + } + } + elseif ($info['bits'] == 24) + { + $x = 0; + for ($j = 0; $j < $scan_line_size; $x++) + { + $b = ord($scan_line[$j++]); + $g = ord($scan_line[$j++]); + $r = ord($scan_line[$j++]); + + $color = imagecolorexact($dst_img, $r, $g, $b); + if ($color == -1) + { + $color = imagecolorallocate($dst_img, $r, $g, $b); + + // Gah! Out of colors? Stupid GD 1... try anyhow. + if ($color == -1) + $color = imagecolorclosest($dst_img, $r, $g, $b); + } + + imagesetpixel($dst_img, $x, $y, $color); + } + } + elseif ($info['bits'] == 16) + { + $x = 0; + for ($j = 0; $j < $scan_line_size; $x++) + { + $b1 = ord($scan_line[$j++]); + $b2 = ord($scan_line[$j++]); + + $word = $b2 * 256 + $b1; + + $b = (($word & 31) * 255) / 31; + $g = ((($word >> 5) & 31) * 255) / 31; + $r = ((($word >> 10) & 31) * 255) / 31; + + // Scale the image colors up properly. + $color = imagecolorexact($dst_img, $r, $g, $b); + if ($color == -1) + { + $color = imagecolorallocate($dst_img, $r, $g, $b); + + // Gah! Out of colors? Stupid GD 1... try anyhow. + if ($color == -1) + $color = imagecolorclosest($dst_img, $r, $g, $b); + } + + imagesetpixel($dst_img, $x, $y, $color); + } + } + elseif ($info['bits'] == 8) + { + $x = 0; + for ($j = 0; $j < $scan_line_size; $x++) + imagesetpixel($dst_img, $x, $y, $palette[ord($scan_line[$j++])]); + } + elseif ($info['bits'] == 4) + { + $x = 0; + for ($j = 0; $j < $scan_line_size; $x++) + { + $byte = ord($scan_line[$j++]); + + imagesetpixel($dst_img, $x, $y, $palette[(int) ($byte / 16)]); + + if (++$x < $info['width']) + imagesetpixel($dst_img, $x, $y, $palette[$byte & 15]); + } + } + elseif ($info['bits'] == 1) + { + $x = 0; + for ($j = 0; $j < $scan_line_size; $x++) + { + $byte = ord($scan_line[$j++]); + + imagesetpixel($dst_img, $x, $y, $palette[(($byte) & 128) != 0]); + + for ($shift = 1; $shift < 8; $shift++) + { + if (++$x < $info['width']) + imagesetpixel($dst_img, $x, $y, $palette[(($byte << $shift) & 128) != 0]); + } + } + } + } + + fclose($fp); + + error_reporting($errors); + + return $dst_img; + } +} + +/** + * Writes a gif file to disk as a png file. + * + * @param gif_file $gif A gif image resource + * @param string $lpszFileName The name of the file + * @param int $background_color The background color + * @return bool Whether the operation was successful + */ +function gif_outputAsPng($gif, $lpszFileName, $background_color = -1) +{ + if (!is_a($gif, 'gif_file') || $lpszFileName == '') + return false; + + if (($fd = $gif->get_png_data($background_color)) === false) + return false; + + if (($fh = @fopen($lpszFileName, 'wb')) === false) + return false; + + @fwrite($fh, $fd, strlen($fd)); + @fflush($fh); + @fclose($fh); + + return true; +} + +/** + * Show an image containing the visual verification code for registration. + * Requires the GD extension. + * Uses a random font for each letter from default_theme_dir/fonts. + * Outputs a gif or a png (depending on whether gif ix supported). + * + * @param string $code The code to display + * @return void|false False if something goes wrong. + */ +function showCodeImage($code) +{ + global $gd2, $settings, $user_info, $modSettings; + + // Note: The higher the value of visual_verification_type the harder the verification is - from 0 as disabled through to 4 as "Very hard". + + // What type are we going to be doing? + $imageType = $modSettings['visual_verification_type']; + // Special case to allow the admin center to show samples. + if ($user_info['is_admin'] && isset($_GET['type'])) + $imageType = (int) $_GET['type']; + + // Some quick references for what we do. + // Do we show no, low or high noise? + $noiseType = $imageType == 3 ? 'low' : ($imageType == 4 ? 'high' : ($imageType == 5 ? 'extreme' : 'none')); + // Can we have more than one font in use? + $varyFonts = $imageType > 3 ? true : false; + // Just a plain white background? + $simpleBGColor = $imageType < 3 ? true : false; + // Plain black foreground? + $simpleFGColor = $imageType == 0 ? true : false; + // High much to rotate each character. + $rotationType = $imageType == 1 ? 'none' : ($imageType > 3 ? 'low' : 'high'); + // Do we show some characters inversed? + $showReverseChars = $imageType > 3 ? true : false; + // Special case for not showing any characters. + $disableChars = $imageType == 0 ? true : false; + // What do we do with the font colors. Are they one color, close to one color or random? + $fontColorType = $imageType == 1 ? 'plain' : ($imageType > 3 ? 'random' : 'cyclic'); + // Are the fonts random sizes? + $fontSizeRandom = $imageType > 3 ? true : false; + // How much space between characters? + $fontHorSpace = $imageType > 3 ? 'high' : ($imageType == 1 ? 'medium' : 'minus'); + // Where do characters sit on the image? (Fixed position or random/very random) + $fontVerPos = $imageType == 1 ? 'fixed' : ($imageType > 3 ? 'vrandom' : 'random'); + // Make font semi-transparent? + $fontTrans = $imageType == 2 || $imageType == 3 ? true : false; + // Give the image a border? + $hasBorder = $simpleBGColor; + + // The amount of pixels inbetween characters. + $character_spacing = 1; + + // What color is the background - generally white unless we're on "hard". + if ($simpleBGColor) + $background_color = array(255, 255, 255); + else + $background_color = isset($settings['verification_background']) ? $settings['verification_background'] : array(236, 237, 243); + + // The color of the characters shown (red, green, blue). + if ($simpleFGColor) + $foreground_color = array(0, 0, 0); + else + { + $foreground_color = array(64, 101, 136); + + // Has the theme author requested a custom color? + if (isset($settings['verification_foreground'])) + $foreground_color = $settings['verification_foreground']; + } + + if (!is_dir($settings['default_theme_dir'] . '/fonts')) + return false; + + // Get a list of the available fonts. + $font_dir = dir($settings['default_theme_dir'] . '/fonts'); + $font_list = array(); + $ttfont_list = array(); + $endian = unpack('v', pack('S', 0x00FF)) === 0x00FF; + while ($entry = $font_dir->read()) + { + if (preg_match('~^(.+)\.gdf$~', $entry, $matches) === 1) + { + if ($endian ^ (strpos($entry, '_end.gdf') === false)) + $font_list[] = $entry; + } + elseif (preg_match('~^(.+)\.ttf$~', $entry, $matches) === 1) + $ttfont_list[] = $entry; + } + + if (empty($font_list)) + return false; + + // For non-hard things don't even change fonts. + if (!$varyFonts) + { + $font_list = array($font_list[0]); + // Try use Screenge if we can - it looks good! + if (in_array('AnonymousPro.ttf', $ttfont_list)) + $ttfont_list = array('AnonymousPro.ttf'); + else + $ttfont_list = empty($ttfont_list) ? array() : array($ttfont_list[0]); + } + + // Create a list of characters to be shown. + $characters = array(); + $loaded_fonts = array(); + for ($i = 0; $i < strlen($code); $i++) + { + $characters[$i] = array( + 'id' => $code[$i], + 'font' => array_rand($font_list), + ); + + $loaded_fonts[$characters[$i]['font']] = null; + } + + // Load all fonts and determine the maximum font height. + foreach ($loaded_fonts as $font_index => $dummy) + $loaded_fonts[$font_index] = imageloadfont($settings['default_theme_dir'] . '/fonts/' . $font_list[$font_index]); + + // Determine the dimensions of each character. + if ($imageType == 4 || $imageType == 5) + $extra = 80; + else + $extra = 45; + + $total_width = $character_spacing * strlen($code) + $extra; + $max_height = 0; + foreach ($characters as $char_index => $character) + { + $characters[$char_index]['width'] = imagefontwidth($loaded_fonts[$character['font']]); + $characters[$char_index]['height'] = imagefontheight($loaded_fonts[$character['font']]); + + $max_height = max($characters[$char_index]['height'] + 5, $max_height); + $total_width += $characters[$char_index]['width']; + } + + // Create an image. + $code_image = $gd2 ? imagecreatetruecolor($total_width, $max_height) : imagecreate($total_width, $max_height); + + // Draw the background. + $bg_color = imagecolorallocate($code_image, $background_color[0], $background_color[1], $background_color[2]); + imagefilledrectangle($code_image, 0, 0, $total_width - 1, $max_height - 1, $bg_color); + + // Randomize the foreground color a little. + for ($i = 0; $i < 3; $i++) + $foreground_color[$i] = mt_rand(max($foreground_color[$i] - 3, 0), min($foreground_color[$i] + 3, 255)); + $fg_color = imagecolorallocate($code_image, $foreground_color[0], $foreground_color[1], $foreground_color[2]); + + // Color for the dots. + for ($i = 0; $i < 3; $i++) + $dotbgcolor[$i] = $background_color[$i] < $foreground_color[$i] ? mt_rand(0, max($foreground_color[$i] - 20, 0)) : mt_rand(min($foreground_color[$i] + 20, 255), 255); + $randomness_color = imagecolorallocate($code_image, $dotbgcolor[0], $dotbgcolor[1], $dotbgcolor[2]); + + // Some squares/rectanges for new extreme level + if ($noiseType == 'extreme') + { + for ($i = 0; $i < mt_rand(1, 5); $i++) + { + $x1 = mt_rand(0, $total_width / 4); + $x2 = $x1 + round(rand($total_width / 4, $total_width)); + $y1 = mt_rand(0, $max_height); + $y2 = $y1 + round(rand(0, $max_height / 3)); + imagefilledrectangle($code_image, $x1, $y1, $x2, $y2, mt_rand(0, 1) ? $fg_color : $randomness_color); + } + } + + // Fill in the characters. + if (!$disableChars) + { + $cur_x = 0; + foreach ($characters as $char_index => $character) + { + // Can we use true type fonts? + $can_do_ttf = function_exists('imagettftext'); + + // How much rotation will we give? + if ($rotationType == 'none') + $angle = 0; + else + $angle = mt_rand(-100, 100) / ($rotationType == 'high' ? 6 : 10); + + // What color shall we do it? + if ($fontColorType == 'cyclic') + { + // Here we'll pick from a set of acceptance types. + $colors = array( + array(10, 120, 95), + array(46, 81, 29), + array(4, 22, 154), + array(131, 9, 130), + array(0, 0, 0), + array(143, 39, 31), + ); + if (!isset($last_index)) + $last_index = -1; + $new_index = $last_index; + while ($last_index == $new_index) + $new_index = mt_rand(0, count($colors) - 1); + $char_fg_color = $colors[$new_index]; + $last_index = $new_index; + } + elseif ($fontColorType == 'random') + $char_fg_color = array(mt_rand(max($foreground_color[0] - 2, 0), $foreground_color[0]), mt_rand(max($foreground_color[1] - 2, 0), $foreground_color[1]), mt_rand(max($foreground_color[2] - 2, 0), $foreground_color[2])); + else + $char_fg_color = array($foreground_color[0], $foreground_color[1], $foreground_color[2]); + + if (!empty($can_do_ttf)) + { + // GD2 handles font size differently. + if ($fontSizeRandom) + $font_size = $gd2 ? mt_rand(17, 19) : mt_rand(18, 25); + else + $font_size = $gd2 ? 18 : 24; + + // Work out the sizes - also fix the character width cause TTF not quite so wide! + $font_x = $fontHorSpace == 'minus' && $cur_x > 0 ? $cur_x - 3 : $cur_x + 5; + $font_y = $max_height - ($fontVerPos == 'vrandom' ? mt_rand(2, 8) : ($fontVerPos == 'random' ? mt_rand(3, 5) : 5)); + + // What font face? + if (!empty($ttfont_list)) + $fontface = $settings['default_theme_dir'] . '/fonts/' . $ttfont_list[mt_rand(0, count($ttfont_list) - 1)]; + + // What color are we to do it in? + $is_reverse = $showReverseChars ? mt_rand(0, 1) : false; + $char_color = function_exists('imagecolorallocatealpha') && $fontTrans ? imagecolorallocatealpha($code_image, $char_fg_color[0], $char_fg_color[1], $char_fg_color[2], 50) : imagecolorallocate($code_image, $char_fg_color[0], $char_fg_color[1], $char_fg_color[2]); + + $fontcord = @imagettftext($code_image, $font_size, $angle, $font_x, $font_y, $char_color, $fontface, $character['id']); + if (empty($fontcord)) + $can_do_ttf = false; + elseif ($is_reverse) + { + imagefilledpolygon($code_image, $fontcord, 4, $fg_color); + // Put the character back! + imagettftext($code_image, $font_size, $angle, $font_x, $font_y, $randomness_color, $fontface, $character['id']); + } + + if ($can_do_ttf) + $cur_x = max($fontcord[2], $fontcord[4]) + ($angle == 0 ? 0 : 3); + } + + if (!$can_do_ttf) + { + // Rotating the characters a little... + if (function_exists('imagerotate')) + { + $char_image = $gd2 ? imagecreatetruecolor($character['width'], $character['height']) : imagecreate($character['width'], $character['height']); + $char_bgcolor = imagecolorallocate($char_image, $background_color[0], $background_color[1], $background_color[2]); + imagefilledrectangle($char_image, 0, 0, $character['width'] - 1, $character['height'] - 1, $char_bgcolor); + imagechar($char_image, $loaded_fonts[$character['font']], 0, 0, $character['id'], imagecolorallocate($char_image, $char_fg_color[0], $char_fg_color[1], $char_fg_color[2])); + $rotated_char = imagerotate($char_image, mt_rand(-100, 100) / 10, $char_bgcolor); + imagecopy($code_image, $rotated_char, $cur_x, 0, 0, 0, $character['width'], $character['height']); + imagedestroy($rotated_char); + imagedestroy($char_image); + } + + // Sorry, no rotation available. + else + imagechar($code_image, $loaded_fonts[$character['font']], $cur_x, floor(($max_height - $character['height']) / 2), $character['id'], imagecolorallocate($code_image, $char_fg_color[0], $char_fg_color[1], $char_fg_color[2])); + $cur_x += $character['width'] + $character_spacing; + } + } + } + // If disabled just show a cross. + else + { + imageline($code_image, 0, 0, $total_width, $max_height, $fg_color); + imageline($code_image, 0, $max_height, $total_width, 0, $fg_color); + } + + // Make the background color transparent on the hard image. + if (!$simpleBGColor) + imagecolortransparent($code_image, $bg_color); + if ($hasBorder) + imagerectangle($code_image, 0, 0, $total_width - 1, $max_height - 1, $fg_color); + + // Add some noise to the background? + if ($noiseType != 'none') + { + for ($i = mt_rand(0, 2); $i < $max_height; $i += mt_rand(1, 2)) + for ($j = mt_rand(0, 10); $j < $total_width; $j += mt_rand(1, 10)) + imagesetpixel($code_image, $j, $i, mt_rand(0, 1) ? $fg_color : $randomness_color); + + // Put in some lines too? + if ($noiseType != 'extreme') + { + $num_lines = $noiseType == 'high' ? mt_rand(3, 7) : mt_rand(2, 5); + for ($i = 0; $i < $num_lines; $i++) + { + if (mt_rand(0, 1)) + { + $x1 = mt_rand(0, $total_width); + $x2 = mt_rand(0, $total_width); + $y1 = 0; + $y2 = $max_height; + } + else + { + $y1 = mt_rand(0, $max_height); + $y2 = mt_rand(0, $max_height); + $x1 = 0; + $x2 = $total_width; + } + imagesetthickness($code_image, mt_rand(1, 2)); + imageline($code_image, $x1, $y1, $x2, $y2, mt_rand(0, 1) ? $fg_color : $randomness_color); + } + } + else + { + // Put in some ellipse + $num_ellipse = $noiseType == 'extreme' ? mt_rand(6, 12) : mt_rand(2, 6); + for ($i = 0; $i < $num_ellipse; $i++) + { + $x1 = round(rand(($total_width / 4) * -1, $total_width + ($total_width / 4))); + $x2 = round(rand($total_width / 2, 2 * $total_width)); + $y1 = round(rand(($max_height / 4) * -1, $max_height + ($max_height / 4))); + $y2 = round(rand($max_height / 2, 2 * $max_height)); + imageellipse($code_image, $x1, $y1, $x2, $y2, mt_rand(0, 1) ? $fg_color : $randomness_color); + } + } + } + + // Show the image. + if (function_exists('imagegif')) + { + header('content-type: image/gif'); + imagegif($code_image); + } + else + { + header('content-type: image/png'); + imagepng($code_image); + } + + // Bail out. + imagedestroy($code_image); + die(); +} + +/** + * Show a letter for the visual verification code. + * Alternative function for showCodeImage() in case GD is missing. + * Includes an image from a random sub directory of default_theme_dir/fonts. + * + * @param string $letter A letter to show as an image + * @return void|false False if something went wrong + */ +function showLetterImage($letter) +{ + global $settings; + + if (!is_dir($settings['default_theme_dir'] . '/fonts')) + return false; + + // Get a list of the available font directories. + $font_dir = dir($settings['default_theme_dir'] . '/fonts'); + $font_list = array(); + while ($entry = $font_dir->read()) + if ($entry[0] !== '.' && is_dir($settings['default_theme_dir'] . '/fonts/' . $entry) && file_exists($settings['default_theme_dir'] . '/fonts/' . $entry . '.gdf')) + $font_list[] = $entry; + + if (empty($font_list)) + return false; + + // Pick a random font. + $random_font = $font_list[array_rand($font_list)]; + + // Check if the given letter exists. + if (!file_exists($settings['default_theme_dir'] . '/fonts/' . $random_font . '/' . strtoupper($letter) . '.png')) + return false; + + // Include it! + header('content-type: image/png'); + include($settings['default_theme_dir'] . '/fonts/' . $random_font . '/' . strtoupper($letter) . '.png'); + + // Nothing more to come. + die(); +} + +?> \ No newline at end of file diff --git a/Sources/Subs-List.php b/Sources/Subs-List.php new file mode 100644 index 0000000..9a6a3d1 --- /dev/null +++ b/Sources/Subs-List.php @@ -0,0 +1,273 @@ + $_REQUEST[$request_var_sort], + 'desc' => isset($_REQUEST[$request_var_desc]) && isset($listOptions['columns'][$_REQUEST[$request_var_sort]]['sort']['reverse']), + ); + else + $list_context['sort'] = array( + 'id' => $listOptions['default_sort_col'], + 'desc' => (!empty($listOptions['default_sort_dir']) && $listOptions['default_sort_dir'] == 'desc') || (!empty($listOptions['columns'][$listOptions['default_sort_col']]['sort']['default']) && substr($listOptions['columns'][$listOptions['default_sort_col']]['sort']['default'], -4, 4) == 'desc') ? true : false, + ); + + // Set the database column sort. + $sort = $listOptions['columns'][$list_context['sort']['id']]['sort'][$list_context['sort']['desc'] ? 'reverse' : 'default']; + } + + $list_context['start_var_name'] = isset($listOptions['start_var_name']) ? $listOptions['start_var_name'] : 'start'; + // In some cases the full list must be shown, regardless of the amount of items. + if (empty($listOptions['items_per_page'])) + { + $list_context['start'] = 0; + $list_context['items_per_page'] = 0; + } + // With items per page set, calculate total number of items and page index. + else + { + // First get an impression of how many items to expect. + if (isset($listOptions['get_count']['value']) && (is_int($listOptions['get_count']['value']) || ctype_digit($listOptions['get_count']['value']))) + $list_context['total_num_items'] = $listOptions['get_count']['value']; + + else + { + if (isset($listOptions['get_count']['file'])) + require_once($listOptions['get_count']['file']); + + $call = call_helper($listOptions['get_count']['function'], true); + $list_context['total_num_items'] = call_user_func_array($call, empty($listOptions['get_count']['params']) ? + array() : array_values($listOptions['get_count']['params'])); + } + + // Default the start to the beginning...sounds logical. + $list_context['start'] = isset($_REQUEST[$list_context['start_var_name']]) ? (int) $_REQUEST[$list_context['start_var_name']] : 0; + $list_context['items_per_page'] = $listOptions['items_per_page']; + + // Then create a page index. + if ($list_context['total_num_items'] > $list_context['items_per_page']) + $list_context['page_index'] = constructPageIndex($listOptions['base_href'] . (empty($list_context['sort']) ? '' : ';' . $request_var_sort . '=' . $list_context['sort']['id'] . ($list_context['sort']['desc'] ? ';' . $request_var_desc : '')) . ($list_context['start_var_name'] != 'start' ? ';' . $list_context['start_var_name'] . '=%1$d' : ''), $list_context['start'], $list_context['total_num_items'], $list_context['items_per_page'], $list_context['start_var_name'] != 'start'); + } + + // Prepare the headers of the table. + $list_context['headers'] = array(); + foreach ($listOptions['columns'] as $column_id => $column) + $list_context['headers'][] = array( + 'id' => $column_id, + 'label' => isset($column['header']['eval']) ? eval($column['header']['eval']) : (isset($column['header']['value']) ? $column['header']['value'] : ''), + 'href' => empty($listOptions['default_sort_col']) || empty($column['sort']) ? '' : $listOptions['base_href'] . ';' . $request_var_sort . '=' . $column_id . ($column_id === $list_context['sort']['id'] && !$list_context['sort']['desc'] && isset($column['sort']['reverse']) ? ';' . $request_var_desc : '') . (empty($list_context['start']) ? '' : ';' . $list_context['start_var_name'] . '=' . $list_context['start']), + 'sort_image' => empty($listOptions['default_sort_col']) || empty($column['sort']) || $column_id !== $list_context['sort']['id'] ? null : ($list_context['sort']['desc'] ? 'down' : 'up'), + 'class' => isset($column['header']['class']) ? $column['header']['class'] : '', + 'style' => isset($column['header']['style']) ? $column['header']['style'] : '', + 'colspan' => isset($column['header']['colspan']) ? $column['header']['colspan'] : '', + ); + + // We know the amount of columns, might be useful for the template. + $list_context['num_columns'] = count($listOptions['columns']); + $list_context['width'] = isset($listOptions['width']) ? $listOptions['width'] : '0'; + + // Call the function and include which items we want and in what order. + if (!empty($listOptions['get_items']['value']) && is_array($listOptions['get_items']['value'])) + $list_items = $listOptions['get_items']['value']; + + else + { + // Get the file with the function for the item list. + if (isset($listOptions['get_items']['file'])) + require_once($listOptions['get_items']['file']); + + $call = call_helper($listOptions['get_items']['function'], true); + $list_items = call_user_func_array($call, array_merge(array($list_context['start'], $list_context['items_per_page'], $sort), empty($listOptions['get_items']['params']) ? array() : $listOptions['get_items']['params'])); + $list_items = empty($list_items) ? array() : $list_items; + } + + // Loop through the list items to be shown and construct the data values. + $list_context['rows'] = array(); + foreach ($list_items as $item_id => $list_item) + { + $cur_row = array(); + foreach ($listOptions['columns'] as $column_id => $column) + { + $cur_data = array(); + + // A value straight from the database? + if (isset($column['data']['db'])) + $cur_data['value'] = $list_item[$column['data']['db']]; + + // Take the value from the database and make it HTML safe. + elseif (isset($column['data']['db_htmlsafe'])) + $cur_data['value'] = $smcFunc['htmlspecialchars']($list_item[$column['data']['db_htmlsafe']]); + + // Using sprintf is probably the most readable way of injecting data. + elseif (isset($column['data']['sprintf'])) + { + $params = array(); + foreach ($column['data']['sprintf']['params'] as $sprintf_param => $htmlsafe) + $params[] = $htmlsafe ? $smcFunc['htmlspecialchars']($list_item[$sprintf_param]) : $list_item[$sprintf_param]; + $cur_data['value'] = vsprintf($column['data']['sprintf']['format'], $params); + } + + // The most flexible way probably is applying a custom function. + elseif (isset($column['data']['function'])) + $cur_data['value'] = call_user_func_array($column['data']['function'], array($list_item)); + + // A modified value (inject the database values). + elseif (isset($column['data']['eval'])) + $cur_data['value'] = eval(preg_replace('~%([a-zA-Z0-9\-_]+)%~', '$list_item[\'$1\']', $column['data']['eval'])); + + // A literal value. + elseif (isset($column['data']['value'])) + $cur_data['value'] = $column['data']['value']; + + // Empty value. + else + $cur_data['value'] = ''; + + // Allow for basic formatting. + if (!empty($column['data']['comma_format'])) + $cur_data['value'] = comma_format($cur_data['value']); + elseif (!empty($column['data']['timeformat'])) + $cur_data['value'] = timeformat($cur_data['value']); + + // Set a style class for this column? + if (isset($column['data']['class'])) + $cur_data['class'] = $column['data']['class']; + + // Fully customized styling for the cells in this column only. + if (isset($column['data']['style'])) + $cur_data['style'] = $column['data']['style']; + + // Add the data cell properties to the current row. + $cur_row[$column_id] = $cur_data; + } + + // Maybe we wat set a custom class for the row based on the data in the row itself + if (isset($listOptions['data_check'])) + { + if (isset($listOptions['data_check']['class'])) + $list_context['rows'][$item_id]['class'] = $listOptions['data_check']['class']($list_item); + if (isset($listOptions['data_check']['style'])) + $list_context['rows'][$item_id]['style'] = $listOptions['data_check']['style']($list_item); + } + + // Insert the row into the list. + $list_context['rows'][$item_id]['data'] = $cur_row; + } + + // The title is currently optional. + if (isset($listOptions['title'])) + $list_context['title'] = $listOptions['title']; + + // In case there's a form, share it with the template context. + if (isset($listOptions['form'])) + { + $list_context['form'] = $listOptions['form']; + + if (!isset($list_context['form']['hidden_fields'])) + $list_context['form']['hidden_fields'] = array(); + + // Always add a session check field. + $list_context['form']['hidden_fields'][$context['session_var']] = $context['session_id']; + + // Will this do a token check? + if (isset($listOptions['form']['token'])) + $list_context['form']['hidden_fields'][$context[$listOptions['form']['token'] . '_token_var']] = $context[$listOptions['form']['token'] . '_token']; + + // Include the starting page as hidden field? + if (!empty($list_context['form']['include_start']) && !empty($list_context['start'])) + $list_context['form']['hidden_fields'][$list_context['start_var_name']] = $list_context['start']; + + // If sorting needs to be the same after submitting, add the parameter. + if (!empty($list_context['form']['include_sort']) && !empty($list_context['sort'])) + { + $list_context['form']['hidden_fields']['sort'] = $list_context['sort']['id']; + if ($list_context['sort']['desc']) + $list_context['form']['hidden_fields']['desc'] = 1; + } + } + + // Wanna say something nice in case there are no items? + if (isset($listOptions['no_items_label'])) + { + $list_context['no_items_label'] = $listOptions['no_items_label']; + $list_context['no_items_align'] = isset($listOptions['no_items_align']) ? $listOptions['no_items_align'] : ''; + } + + // A list can sometimes need a few extra rows above and below. + if (isset($listOptions['additional_rows'])) + { + $list_context['additional_rows'] = array(); + foreach ($listOptions['additional_rows'] as $row) + { + if (empty($row)) + continue; + + // Supported row positions: top_of_list, after_title, + // above_column_headers, below_table_data, bottom_of_list. + if (!isset($list_context['additional_rows'][$row['position']])) + $list_context['additional_rows'][$row['position']] = array(); + $list_context['additional_rows'][$row['position']][] = $row; + } + } + + // Add an option for inline JavaScript. + if (isset($listOptions['javascript'])) + $list_context['javascript'] = $listOptions['javascript']; + + // Make sure the template is loaded. + loadTemplate('GenericList'); +} + +?> \ No newline at end of file diff --git a/Sources/Subs-Membergroups.php b/Sources/Subs-Membergroups.php new file mode 100644 index 0000000..b5332a3 --- /dev/null +++ b/Sources/Subs-Membergroups.php @@ -0,0 +1,902 @@ + $value) + $groups[$key] = (int) $value; + } + + // Some groups are protected (guests, administrators, moderators, newbies). + $protected_groups = array(-1, 0, 1, 3, 4); + + // There maybe some others as well. + if (!allowedTo('admin_forum')) + { + $request = $smcFunc['db_query']('', ' + SELECT id_group + FROM {db_prefix}membergroups + WHERE group_type = {int:is_protected}', + array( + 'is_protected' => 1, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $protected_groups[] = $row['id_group']; + $smcFunc['db_free_result']($request); + } + + // Make sure they don't delete protected groups! + $groups = array_diff($groups, array_unique($protected_groups)); + if (empty($groups)) + return 'no_group_found'; + + // Make sure they don't try to delete a group attached to a paid subscription. + $subscriptions = array(); + $request = $smcFunc['db_query']('', ' + SELECT id_subscribe, name, id_group, add_groups + FROM {db_prefix}subscriptions + ORDER BY name'); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (in_array($row['id_group'], $groups)) + $subscriptions[] = $row['name']; + else + { + $add_groups = explode(',', $row['add_groups']); + if (count(array_intersect($add_groups, $groups)) != 0) + $subscriptions[] = $row['name']; + } + } + $smcFunc['db_free_result']($request); + if (!empty($subscriptions)) + { + // Uh oh. But before we return, we need to update a language string because we want the names of the groups. + loadLanguage('ManageMembers'); + $txt['membergroups_cannot_delete_paid'] = sprintf($txt['membergroups_cannot_delete_paid'], implode(', ', $subscriptions)); + return 'group_cannot_delete_sub'; + } + + // Log the deletion. + $request = $smcFunc['db_query']('', ' + SELECT group_name + FROM {db_prefix}membergroups + WHERE id_group IN ({array_int:group_list})', + array( + 'group_list' => $groups, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + logAction('delete_group', array('group' => $row['group_name']), 'admin'); + $smcFunc['db_free_result']($request); + + call_integration_hook('integrate_delete_membergroups', array($groups)); + + // Remove the membergroups themselves. + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}membergroups + WHERE id_group IN ({array_int:group_list})', + array( + 'group_list' => $groups, + ) + ); + + // Remove the permissions of the membergroups. + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}permissions + WHERE id_group IN ({array_int:group_list})', + array( + 'group_list' => $groups, + ) + ); + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}board_permissions + WHERE id_group IN ({array_int:group_list})', + array( + 'group_list' => $groups, + ) + ); + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}group_moderators + WHERE id_group IN ({array_int:group_list})', + array( + 'group_list' => $groups, + ) + ); + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}moderator_groups + WHERE id_group IN ({array_int:group_list})', + array( + 'group_list' => $groups, + ) + ); + + // Delete any outstanding requests. + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_group_requests + WHERE id_group IN ({array_int:group_list})', + array( + 'group_list' => $groups, + ) + ); + + // Update the primary groups of members. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}members + SET id_group = {int:regular_group} + WHERE id_group IN ({array_int:group_list})', + array( + 'group_list' => $groups, + 'regular_group' => 0, + ) + ); + + // Update any inherited groups (Lose inheritance). + $smcFunc['db_query']('', ' + UPDATE {db_prefix}membergroups + SET id_parent = {int:uninherited} + WHERE id_parent IN ({array_int:group_list})', + array( + 'group_list' => $groups, + 'uninherited' => -2, + ) + ); + + // Update the additional groups of members. + $request = $smcFunc['db_query']('', ' + SELECT id_member, additional_groups + FROM {db_prefix}members + WHERE FIND_IN_SET({raw:additional_groups_explode}, additional_groups) != 0', + array( + 'additional_groups_explode' => implode(', additional_groups) != 0 OR FIND_IN_SET(', $groups), + ) + ); + $updates = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $updates[$row['additional_groups']][] = $row['id_member']; + $smcFunc['db_free_result']($request); + + foreach ($updates as $additional_groups => $memberArray) + updateMemberData($memberArray, array('additional_groups' => implode(',', array_diff(explode(',', $additional_groups), $groups)))); + + // No boards can provide access to these membergroups anymore. + $request = $smcFunc['db_query']('', ' + SELECT id_board, member_groups + FROM {db_prefix}boards + WHERE FIND_IN_SET({raw:member_groups_explode}, member_groups) != 0', + array( + 'member_groups_explode' => implode(', member_groups) != 0 OR FIND_IN_SET(', $groups), + ) + ); + $updates = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $updates[$row['member_groups']][] = $row['id_board']; + $smcFunc['db_free_result']($request); + + foreach ($updates as $member_groups => $boardArray) + $smcFunc['db_query']('', ' + UPDATE {db_prefix}boards + SET member_groups = {string:member_groups} + WHERE id_board IN ({array_int:board_lists})', + array( + 'board_lists' => $boardArray, + 'member_groups' => implode(',', array_diff(explode(',', $member_groups), $groups)), + ) + ); + + // Recalculate the post groups, as they likely changed. + updateStats('postgroups'); + + // Make a note of the fact that the cache may be wrong. + $settings_update = array('settings_updated' => time()); + // Have we deleted the spider group? + if (isset($modSettings['spider_group']) && in_array($modSettings['spider_group'], $groups)) + $settings_update['spider_group'] = 0; + + updateSettings($settings_update); + + // It was a success. + return true; +} + +/** + * Remove one or more members from one or more membergroups. + * Requires the manage_membergroups permission. + * Function includes a protection against removing from implicit groups. + * Non-admins are not able to remove members from the admin group. + * + * @param int|array $members The ID of a member or an array of member IDs + * @param null|array The groups to remove the member(s) from. If null, the specified members are stripped from all their membergroups. + * @param bool $permissionCheckDone Whether we've already checked permissions prior to calling this function + * @param bool $ignoreProtected Whether to ignore protected groups + * @return bool Whether the operation was successful + */ +function removeMembersFromGroups($members, $groups = null, $permissionCheckDone = false, $ignoreProtected = false) +{ + global $smcFunc, $modSettings, $sourcedir; + + // You're getting nowhere without this permission, unless of course you are the group's moderator. + if (!$permissionCheckDone) + isAllowedTo('manage_membergroups'); + + // Assume something will happen. + updateSettings(array('settings_updated' => time())); + + // Cleaning the input. + if (!is_array($members)) + $members = array((int) $members); + else + { + $members = array_unique($members); + + // Cast the members to integer. + foreach ($members as $key => $value) + $members[$key] = (int) $value; + } + + // Before we get started, let's check we won't leave the admin group empty! + if ($groups === null || $groups == 1 || (is_array($groups) && in_array(1, $groups))) + { + $admins = array(); + listMembergroupMembers_Href($admins, 1); + + // Remove any admins if there are too many. + $non_changing_admins = array_diff(array_keys($admins), $members); + + if (empty($non_changing_admins)) + $members = array_diff($members, array_keys($admins)); + } + + // Just in case. + if (empty($members)) + return false; + elseif ($groups === null) + { + // Wanna remove all groups from these members? That's easy. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}members + SET + id_group = {int:regular_member}, + additional_groups = {string:blank_string} + WHERE id_member IN ({array_int:member_list})' . (allowedTo('admin_forum') ? '' : ' + AND id_group != {int:admin_group} + AND FIND_IN_SET({int:admin_group}, additional_groups) = 0'), + array( + 'member_list' => $members, + 'regular_member' => 0, + 'admin_group' => 1, + 'blank_string' => '', + ) + ); + + updateStats('postgroups', $members); + + // Log what just happened. + foreach ($members as $member) + logAction('removed_all_groups', array('member' => $member), 'admin'); + + return true; + } + elseif (!is_array($groups)) + $groups = array((int) $groups); + else + { + $groups = array_unique($groups); + + // Make sure all groups are integer. + foreach ($groups as $key => $value) + $groups[$key] = (int) $value; + } + + // Fetch a list of groups members cannot be assigned to explicitly, and the group names of the ones we want. + $implicitGroups = array(-1, 0, 3); + $request = $smcFunc['db_query']('', ' + SELECT id_group, group_name, min_posts + FROM {db_prefix}membergroups + WHERE id_group IN ({array_int:group_list})', + array( + 'group_list' => $groups, + ) + ); + $group_names = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if ($row['min_posts'] != -1) + $implicitGroups[] = $row['id_group']; + else + $group_names[$row['id_group']] = $row['group_name']; + } + $smcFunc['db_free_result']($request); + + // Now get rid of those groups. + $groups = array_diff($groups, $implicitGroups); + + // Don't forget the protected groups. + if (!allowedTo('admin_forum') && !$ignoreProtected) + { + $request = $smcFunc['db_query']('', ' + SELECT id_group + FROM {db_prefix}membergroups + WHERE group_type = {int:is_protected}', + array( + 'is_protected' => 1, + ) + ); + $protected_groups = array(1); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $protected_groups[] = $row['id_group']; + $smcFunc['db_free_result']($request); + + // If you're not an admin yourself, you can't touch protected groups! + $groups = array_diff($groups, array_unique($protected_groups)); + } + + // Only continue if there are still groups and members left. + if (empty($groups) || empty($members)) + return false; + + // First, reset those who have this as their primary group - this is the easy one. + $log_inserts = array(); + $request = $smcFunc['db_query']('', ' + SELECT id_member, id_group + FROM {db_prefix}members AS members + WHERE id_group IN ({array_int:group_list}) + AND id_member IN ({array_int:member_list})', + array( + 'group_list' => $groups, + 'member_list' => $members, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $log_inserts[] = array('group' => $group_names[$row['id_group']], 'member' => $row['id_member']); + $smcFunc['db_free_result']($request); + + $smcFunc['db_query']('', ' + UPDATE {db_prefix}members + SET id_group = {int:regular_member} + WHERE id_group IN ({array_int:group_list}) + AND id_member IN ({array_int:member_list})', + array( + 'group_list' => $groups, + 'member_list' => $members, + 'regular_member' => 0, + ) + ); + + // Those who have it as part of their additional group must be updated the long way... sadly. + $request = $smcFunc['db_query']('', ' + SELECT id_member, additional_groups + FROM {db_prefix}members + WHERE (FIND_IN_SET({raw:additional_groups_implode}, additional_groups) != 0) + AND id_member IN ({array_int:member_list}) + LIMIT {int:limit}', + array( + 'member_list' => $members, + 'additional_groups_implode' => implode(', additional_groups) != 0 OR FIND_IN_SET(', $groups), + 'limit' => count($members), + ) + ); + $updates = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // What log entries must we make for this one, eh? + foreach (explode(',', $row['additional_groups']) as $group) + if (in_array($group, $groups)) + $log_inserts[] = array('group' => $group_names[$group], 'member' => $row['id_member']); + + $updates[$row['additional_groups']][] = $row['id_member']; + } + $smcFunc['db_free_result']($request); + + foreach ($updates as $additional_groups => $memberArray) + $smcFunc['db_query']('', ' + UPDATE {db_prefix}members + SET additional_groups = {string:additional_groups} + WHERE id_member IN ({array_int:member_list})', + array( + 'member_list' => $memberArray, + 'additional_groups' => implode(',', array_diff(explode(',', $additional_groups), $groups)), + ) + ); + + // Their post groups may have changed now... + updateStats('postgroups', $members); + + // Do the log. + if (!empty($log_inserts) && !empty($modSettings['modlog_enabled'])) + { + require_once($sourcedir . '/Logging.php'); + foreach ($log_inserts as $extra) + logAction('removed_from_group', $extra, 'admin'); + } + + // Mission successful. + return true; +} + +/** + * Add one or more members to a membergroup + * + * Requires the manage_membergroups permission. + * Function has protection against adding members to implicit groups. + * Non-admins are not able to add members to the admin group. + * + * @param int|array $members A single member or an array containing the IDs of members + * @param int $group The group to add them to + * @param string $type Specifies whether the group is added as primary or as additional group. + * Supported types: + * - only_primary - Assigns a membergroup as primary membergroup, but only + * if a member has not yet a primary membergroup assigned, + * unless the member is already part of the membergroup. + * - only_additional - Assigns a membergroup to the additional membergroups, + * unless the member is already part of the membergroup. + * - force_primary - Assigns a membergroup as primary membergroup no matter + * what the previous primary membergroup was. + * - auto - Assigns a membergroup to the primary group if it's still + * available. If not, assign it to the additional group. + * @param bool $permissionCheckDone Whether we've already done a permission check + * @param bool $ignoreProtected Whether to ignore protected groups + * @return bool Whether the operation was successful + */ +function addMembersToGroup($members, $group, $type = 'auto', $permissionCheckDone = false, $ignoreProtected = false) +{ + global $smcFunc, $sourcedir, $txt; + + // Show your licence, but only if it hasn't been done yet. + if (!$permissionCheckDone) + isAllowedTo('manage_membergroups'); + + // Make sure we don't keep old stuff cached. + updateSettings(array('settings_updated' => time())); + + if (!is_array($members)) + $members = array((int) $members); + else + { + $members = array_unique($members); + + // Make sure all members are integer. + foreach ($members as $key => $value) + $members[$key] = (int) $value; + } + $group = (int) $group; + + // Some groups just don't like explicitly having members. + $implicitGroups = array(-1, 0, 3); + $request = $smcFunc['db_query']('', ' + SELECT id_group, group_name, min_posts + FROM {db_prefix}membergroups + WHERE id_group = {int:current_group}', + array( + 'current_group' => $group, + ) + ); + $group_names = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if ($row['min_posts'] != -1) + $implicitGroups[] = $row['id_group']; + else + $group_names[$row['id_group']] = $row['group_name']; + } + $smcFunc['db_free_result']($request); + + // Sorry, you can't join an implicit group. + if (in_array($group, $implicitGroups) || empty($members)) + return false; + + // Only admins can add admins... + if (!allowedTo('admin_forum') && $group == 1) + return false; + // ... and assign protected groups! + elseif (!allowedTo('admin_forum') && !$ignoreProtected) + { + $request = $smcFunc['db_query']('', ' + SELECT group_type + FROM {db_prefix}membergroups + WHERE id_group = {int:current_group} + LIMIT {int:limit}', + array( + 'current_group' => $group, + 'limit' => 1, + ) + ); + list ($is_protected) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // Is it protected? + if ($is_protected == 1) + return false; + } + + // Do the actual updates. + if ($type == 'only_additional') + $smcFunc['db_query']('', ' + UPDATE {db_prefix}members + SET additional_groups = CASE WHEN additional_groups = {string:blank_string} THEN {string:id_group_string} ELSE CONCAT(additional_groups, {string:id_group_string_extend}) END + WHERE id_member IN ({array_int:member_list}) + AND id_group != {int:id_group} + AND FIND_IN_SET({int:id_group}, additional_groups) = 0', + array( + 'member_list' => $members, + 'id_group' => $group, + 'id_group_string' => (string) $group, + 'id_group_string_extend' => ',' . $group, + 'blank_string' => '', + ) + ); + elseif ($type == 'only_primary' || $type == 'force_primary') + $smcFunc['db_query']('', ' + UPDATE {db_prefix}members + SET id_group = {int:id_group} + WHERE id_member IN ({array_int:member_list})' . ($type == 'force_primary' ? '' : ' + AND id_group = {int:regular_group} + AND FIND_IN_SET({int:id_group}, additional_groups) = 0'), + array( + 'member_list' => $members, + 'id_group' => $group, + 'regular_group' => 0, + ) + ); + elseif ($type == 'auto') + $smcFunc['db_query']('', ' + UPDATE {db_prefix}members + SET + id_group = CASE WHEN id_group = {int:regular_group} THEN {int:id_group} ELSE id_group END, + additional_groups = CASE WHEN id_group = {int:id_group} THEN additional_groups + WHEN additional_groups = {string:blank_string} THEN {string:id_group_string} + ELSE CONCAT(additional_groups, {string:id_group_string_extend}) END + WHERE id_member IN ({array_int:member_list}) + AND id_group != {int:id_group} + AND FIND_IN_SET({int:id_group}, additional_groups) = 0', + array( + 'member_list' => $members, + 'regular_group' => 0, + 'id_group' => $group, + 'blank_string' => '', + 'id_group_string' => (string) $group, + 'id_group_string_extend' => ',' . $group, + ) + ); + // Ack!!? What happened? + else + { + loadLanguage('Errors'); + trigger_error(sprintf($txt['add_members_to_group_invalid_type'], $type), E_USER_WARNING); + } + + call_integration_hook('integrate_add_members_to_group', array($members, $group, &$group_names)); + + // Update their postgroup statistics. + updateStats('postgroups', $members); + + // Log the data. + require_once($sourcedir . '/Logging.php'); + foreach ($members as $member) + logAction('added_to_group', array('group' => $group_names[$group], 'member' => $member), 'admin'); + + return true; +} + +/** + * Gets the members of a supplied membergroup + * Returns them as a link for display + * + * @param array &$members The IDs of the members + * @param int $membergroup The ID of the group + * @param int $limit How many members to show (null for no limit) + * @return bool True if there are more members to display, false otherwise + */ +function listMembergroupMembers_Href(&$members, $membergroup, $limit = null) +{ + global $scripturl, $smcFunc; + + $request = $smcFunc['db_query']('', ' + SELECT id_member, real_name + FROM {db_prefix}members + WHERE id_group = {int:id_group} OR FIND_IN_SET({int:id_group}, additional_groups) != 0' . ($limit === null ? '' : ' + LIMIT ' . ($limit + 1)), + array( + 'id_group' => $membergroup, + ) + ); + $members = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $members[$row['id_member']] = '' . $row['real_name'] . ''; + $smcFunc['db_free_result']($request); + + // If there are more than $limit members, add a 'more' link. + if ($limit !== null && count($members) > $limit) + { + array_pop($members); + return true; + } + else + return false; +} + +/** + * Retrieve a list of (visible) membergroups used by the cache. + * + * @return array An array of information about the cache + */ +function cache_getMembergroupList() +{ + global $scripturl, $smcFunc; + + $request = $smcFunc['db_query']('', ' + SELECT id_group, group_name, online_color + FROM {db_prefix}membergroups + WHERE min_posts = {int:min_posts} + AND hidden = {int:not_hidden} + AND id_group != {int:mod_group} + ORDER BY group_name', + array( + 'min_posts' => -1, + 'not_hidden' => 0, + 'mod_group' => 3, + ) + ); + $groupCache = array(); + $group = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $group[$row['id_group']] = $row; + $groupCache[$row['id_group']] = '' . $row['group_name'] . ''; + } + $smcFunc['db_free_result']($request); + + call_integration_hook('integrate_getMembergroupList', array(&$groupCache, $group)); + + return array( + 'data' => $groupCache, + 'expires' => time() + 3600, + 'refresh_eval' => 'return $GLOBALS[\'modSettings\'][\'settings_updated\'] > ' . time() . ';', + ); +} + +/** + * Helper function to generate a list of membergroups for display + * + * @param int $start What item to start with (not used here) + * @param int $items_per_page How many items to show on each page (not used here) + * @param string $sort An SQL query indicating how to sort the results + * @param string $membergroup_type Should be 'post_count' for post groups or anything else for regular groups + * @return array An array of group member info for the list + */ +function list_getMembergroups($start, $items_per_page, $sort, $membergroup_type) +{ + global $scripturl, $context, $settings, $smcFunc, $user_info; + + $request = $smcFunc['db_query']('substring_membergroups', ' + SELECT mg.id_group, mg.group_name, mg.min_posts, mg.description, mg.group_type, mg.online_color, mg.hidden, + mg.icons, COALESCE(gm.id_member, 0) AS can_moderate, 0 AS num_members + FROM {db_prefix}membergroups AS mg + LEFT JOIN {db_prefix}group_moderators AS gm ON (gm.id_group = mg.id_group AND gm.id_member = {int:current_member}) + WHERE mg.min_posts {raw:min_posts}' . (allowedTo('admin_forum') ? '' : ' + AND mg.id_group != {int:mod_group}') . ' + ORDER BY {raw:sort}', + array( + 'current_member' => $user_info['id'], + 'min_posts' => ($membergroup_type === 'post_count' ? '!= ' : '= ') . -1, + 'mod_group' => 3, + 'sort' => $sort, + ) + ); + + // Start collecting the data. + $groups = array(); + $group_ids = array(); + $context['can_moderate'] = allowedTo('manage_membergroups'); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // We only list the groups they can see. + if ($row['hidden'] && !$row['can_moderate'] && !allowedTo('manage_membergroups')) + continue; + + $row['icons'] = explode('#', $row['icons']); + + $groups[$row['id_group']] = array( + 'id_group' => $row['id_group'], + 'group_name' => $row['group_name'], + 'min_posts' => $row['min_posts'], + 'desc' => parse_bbc($row['description'], false, '', $context['description_allowed_tags']), + 'online_color' => $row['online_color'], + 'type' => $row['group_type'], + 'num_members' => $row['num_members'], + 'moderators' => array(), + 'icons' => !empty($row['icons'][0]) && !empty($row['icons'][1]) ? str_repeat('*', $row['icons'][0]) : '', + ); + + $context['can_moderate'] |= $row['can_moderate']; + $group_ids[] = $row['id_group']; + } + $smcFunc['db_free_result']($request); + + // If we found any membergroups, get the amount of members in them. + if (!empty($group_ids)) + { + if ($membergroup_type === 'post_count') + { + $query = $smcFunc['db_query']('', ' + SELECT id_post_group AS id_group, COUNT(*) AS num_members + FROM {db_prefix}members + WHERE id_post_group IN ({array_int:group_list}) + GROUP BY id_post_group', + array( + 'group_list' => $group_ids, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($query)) + $groups[$row['id_group']]['num_members'] += $row['num_members']; + $smcFunc['db_free_result']($query); + } + + else + { + $query = $smcFunc['db_query']('', ' + SELECT id_group, COUNT(*) AS num_members + FROM {db_prefix}members + WHERE id_group IN ({array_int:group_list}) + GROUP BY id_group', + array( + 'group_list' => $group_ids, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($query)) + $groups[$row['id_group']]['num_members'] += $row['num_members']; + $smcFunc['db_free_result']($query); + + // Only do additional groups if we can moderate... + if ($context['can_moderate']) + { + $query = $smcFunc['db_query']('', ' + SELECT mg.id_group, COUNT(*) AS num_members + FROM {db_prefix}membergroups AS mg + INNER JOIN {db_prefix}members AS mem ON (mem.additional_groups != {string:blank_string} + AND mem.id_group != mg.id_group + AND FIND_IN_SET(mg.id_group, mem.additional_groups) != 0) + WHERE mg.id_group IN ({array_int:group_list}) + GROUP BY mg.id_group', + array( + 'group_list' => $group_ids, + 'blank_string' => '', + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($query)) + $groups[$row['id_group']]['num_members'] += $row['num_members']; + $smcFunc['db_free_result']($query); + } + } + + $query = $smcFunc['db_query']('', ' + SELECT mods.id_group, mods.id_member, mem.member_name, mem.real_name + FROM {db_prefix}group_moderators AS mods + INNER JOIN {db_prefix}members AS mem ON (mem.id_member = mods.id_member) + WHERE mods.id_group IN ({array_int:group_list})', + array( + 'group_list' => $group_ids, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($query)) + $groups[$row['id_group']]['moderators'][] = '' . $row['real_name'] . ''; + $smcFunc['db_free_result']($query); + } + + // Apply manual sorting if the 'number of members' column is selected. + if (substr($sort, 0, 1) == '1' || strpos($sort, ', 1') !== false) + { + $sort_ascending = strpos($sort, 'DESC') === false; + + foreach ($groups as $group) + $sort_array[] = $group['id_group'] != 3 ? (int) $group['num_members'] : -1; + + array_multisort($sort_array, $sort_ascending ? SORT_ASC : SORT_DESC, SORT_REGULAR, $groups); + } + + return $groups; +} + +/** + * Retrieves a list of membergroups with the given permissions. + * + * @param array $group_permissions + * @param array $board_permissions + * @param int $profile_id + * + * @return array An array containing two arrays - 'allowed', which has which groups are allowed to do it and 'denied' which has the groups that are denied + */ +function getGroupsWithPermissions(array $group_permissions = array(), array $board_permissions = array(), $profile_id = 1) +{ + global $smcFunc; + + $member_groups = array(); + if (!empty($group_permissions)) + { + foreach ($group_permissions as $group_permission) + // Admins are allowed to do anything. + $member_groups[$group_permission] = array( + 'allowed' => array(1), + 'denied' => array(), + ); + + $request = $smcFunc['db_query']('', ' + SELECT id_group, permission, add_deny + FROM {db_prefix}permissions + WHERE permission IN ({array_string:group_permissions})', + array( + 'group_permissions' => $group_permissions, + ) + ); + while (list ($id_group, $permission, $add_deny) = $smcFunc['db_fetch_row']($request)) + $member_groups[$permission][$add_deny === '1' ? 'allowed' : 'denied'][] = $id_group; + $smcFunc['db_free_result']($request); + } + + if (!empty($board_permissions)) + { + foreach ($board_permissions as $board_permission) + $member_groups[$board_permission] = array( + 'allowed' => array(1), + 'denied' => array(), + ); + + $request = $smcFunc['db_query']('', ' + SELECT id_group, permission, add_deny + FROM {db_prefix}board_permissions + WHERE permission IN ({array_string:board_permissions}) + AND id_profile = {int:profile_id}', + array( + 'profile_id' => $profile_id, + 'board_permissions' => $board_permissions, + ) + ); + while (list ($id_group, $permission, $add_deny) = $smcFunc['db_fetch_row']($request)) + $member_groups[$permission][$add_deny === '1' ? 'allowed' : 'denied'][] = $id_group; + $smcFunc['db_free_result']($request); + } + + // Denied is never allowed. + foreach ($member_groups as $permission => $groups) + $member_groups[$permission]['allowed'] = array_diff($member_groups[$permission]['allowed'], $member_groups[$permission]['denied']); + + return $member_groups; +} + +?> \ No newline at end of file diff --git a/Sources/Subs-Members.php b/Sources/Subs-Members.php new file mode 100644 index 0000000..b1d3352 --- /dev/null +++ b/Sources/Subs-Members.php @@ -0,0 +1,1565 @@ + $v) + $users[$k] = (int) $v; + + // Deleting more than one? You can't have more than one account... + isAllowedTo('profile_remove_any'); + } + + // Get their names for logging purposes. + $request = $smcFunc['db_query']('', ' + SELECT id_member, member_name, CASE WHEN id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0 THEN 1 ELSE 0 END AS is_admin + FROM {db_prefix}members + WHERE id_member IN ({array_int:user_list}) + LIMIT {int:limit}', + array( + 'user_list' => $users, + 'admin_group' => 1, + 'limit' => count($users), + ) + ); + $admins = array(); + $user_log_details = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if ($row['is_admin']) + $admins[] = $row['id_member']; + $user_log_details[$row['id_member']] = array($row['id_member'], $row['member_name']); + } + $smcFunc['db_free_result']($request); + + if (empty($user_log_details)) + return; + + // Make sure they aren't trying to delete administrators if they aren't one. But don't bother checking if it's just themself. + if (!empty($admins) && ($check_not_admin || (!allowedTo('admin_forum') && (count($users) != 1 || $users[0] != $user_info['id'])))) + { + $users = array_diff($users, $admins); + foreach ($admins as $id) + unset($user_log_details[$id]); + } + + // No one left? + if (empty($users)) + return; + + // Log the action - regardless of who is deleting it. + $log_changes = array(); + foreach ($user_log_details as $user) + { + $log_changes[] = array( + 'action' => 'delete_member', + 'log_type' => 'admin', + 'extra' => array( + 'member' => $user[0], + 'name' => $user[1], + 'member_acted' => $user_info['name'], + ), + ); + + // Remove any cached data if enabled. + if (!empty($cache_enable) && $cache_enable >= 2) + cache_put_data('user_settings-' . $user[0], null, 60); + } + + // Make these peoples' posts guest posts. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}messages + SET id_member = {int:guest_id}' . (!empty($modSettings['deleteMembersRemovesEmail']) ? ', + poster_email = {string:blank_email}' : '') . ' + WHERE id_member IN ({array_int:users})', + array( + 'guest_id' => 0, + 'blank_email' => '', + 'users' => $users, + ) + ); + $smcFunc['db_query']('', ' + UPDATE {db_prefix}polls + SET id_member = {int:guest_id} + WHERE id_member IN ({array_int:users})', + array( + 'guest_id' => 0, + 'users' => $users, + ) + ); + + // Make these peoples' posts guest first posts and last posts. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}topics + SET id_member_started = {int:guest_id} + WHERE id_member_started IN ({array_int:users})', + array( + 'guest_id' => 0, + 'users' => $users, + ) + ); + $smcFunc['db_query']('', ' + UPDATE {db_prefix}topics + SET id_member_updated = {int:guest_id} + WHERE id_member_updated IN ({array_int:users})', + array( + 'guest_id' => 0, + 'users' => $users, + ) + ); + + $smcFunc['db_query']('', ' + UPDATE {db_prefix}log_actions + SET id_member = {int:guest_id} + WHERE id_member IN ({array_int:users})', + array( + 'guest_id' => 0, + 'users' => $users, + ) + ); + + $smcFunc['db_query']('', ' + UPDATE {db_prefix}log_banned + SET id_member = {int:guest_id} + WHERE id_member IN ({array_int:users})', + array( + 'guest_id' => 0, + 'users' => $users, + ) + ); + + $smcFunc['db_query']('', ' + UPDATE {db_prefix}log_errors + SET id_member = {int:guest_id} + WHERE id_member IN ({array_int:users})', + array( + 'guest_id' => 0, + 'users' => $users, + ) + ); + + // Delete the member. + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}members + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + + // Delete any drafts... + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}user_drafts + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + + // Delete anything they liked. + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}user_likes + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + + // Delete their mentions + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}mentions + WHERE id_member IN ({array_int:members})', + array( + 'members' => $users, + ) + ); + + // Delete the logs... + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_actions + WHERE id_log = {int:log_type} + AND id_member IN ({array_int:users})', + array( + 'log_type' => 2, + 'users' => $users, + ) + ); + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_boards + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_comments + WHERE id_recipient IN ({array_int:users}) + AND comment_type = {string:warntpl}', + array( + 'users' => $users, + 'warntpl' => 'warntpl', + ) + ); + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_group_requests + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_mark_read + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_notify + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_online + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_subscribed + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_topics + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + + // Make their votes appear as guest votes - at least it keeps the totals right. + // @todo Consider adding back in cookie protection. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}log_polls + SET id_member = {int:guest_id} + WHERE id_member IN ({array_int:users})', + array( + 'guest_id' => 0, + 'users' => $users, + ) + ); + + // Delete personal messages. + require_once($sourcedir . '/PersonalMessage.php'); + deleteMessages(null, null, $users); + + $smcFunc['db_query']('', ' + UPDATE {db_prefix}personal_messages + SET id_member_from = {int:guest_id} + WHERE id_member_from IN ({array_int:users})', + array( + 'guest_id' => 0, + 'users' => $users, + ) + ); + + // They no longer exist, so we don't know who it was sent to. + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}pm_recipients + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + + // Delete avatar. + require_once($sourcedir . '/ManageAttachments.php'); + removeAttachments(array('id_member' => $users)); + + // It's over, no more moderation for you. + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}moderators + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}group_moderators + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + + // If you don't exist we can't ban you. + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}ban_items + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + + // Remove individual theme settings. + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}themes + WHERE id_member IN ({array_int:users})', + array( + 'users' => $users, + ) + ); + + // These users are nobody's buddy nomore. + $request = $smcFunc['db_query']('', ' + SELECT id_member, pm_ignore_list, buddy_list + FROM {db_prefix}members + WHERE FIND_IN_SET({raw:pm_ignore_list}, pm_ignore_list) != 0 OR FIND_IN_SET({raw:buddy_list}, buddy_list) != 0', + array( + 'pm_ignore_list' => implode(', pm_ignore_list) != 0 OR FIND_IN_SET(', $users), + 'buddy_list' => implode(', buddy_list) != 0 OR FIND_IN_SET(', $users), + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $smcFunc['db_query']('', ' + UPDATE {db_prefix}members + SET + pm_ignore_list = {string:pm_ignore_list}, + buddy_list = {string:buddy_list} + WHERE id_member = {int:id_member}', + array( + 'id_member' => $row['id_member'], + 'pm_ignore_list' => implode(',', array_diff(explode(',', $row['pm_ignore_list']), $users)), + 'buddy_list' => implode(',', array_diff(explode(',', $row['buddy_list']), $users)), + ) + ); + $smcFunc['db_free_result']($request); + + // Make sure no member's birthday is still sticking in the calendar... + updateSettings(array( + 'calendar_updated' => time(), + )); + + // Integration rocks! + call_integration_hook('integrate_delete_members', array($users)); + + updateStats('member'); + + require_once($sourcedir . '/Logging.php'); + logActions($log_changes); +} + +/** + * Registers a member to the forum. + * Allows two types of interface: 'guest' and 'admin'. The first + * includes hammering protection, the latter can perform the + * registration silently. + * The strings used in the options array are assumed to be escaped. + * Allows to perform several checks on the input, e.g. reserved names. + * The function will adjust member statistics. + * If an error is detected will fatal error on all errors unless return_errors is true. + * + * @param array $regOptions An array of registration options + * @param bool $return_errors Whether to return the errors + * @return int|array The ID of the newly registered user or an array of error info if $return_errors is true + */ +function registerMember(&$regOptions, $return_errors = false) +{ + global $scripturl, $txt, $modSettings, $context, $sourcedir; + global $user_info, $smcFunc; + + loadLanguage('Login'); + + // We'll need some external functions. + require_once($sourcedir . '/Subs-Auth.php'); + require_once($sourcedir . '/Subs-Post.php'); + + // Put any errors in here. + $reg_errors = array(); + + // Registration from the admin center, let them sweat a little more. + if ($regOptions['interface'] == 'admin') + { + is_not_guest(); + isAllowedTo('moderate_forum'); + } + // If you're an admin, you're special ;). + elseif ($regOptions['interface'] == 'guest') + { + // You cannot register twice... + if (empty($user_info['is_guest'])) + redirectexit(); + + // Make sure they didn't just register with this session. + if (!empty($_SESSION['just_registered']) && empty($modSettings['disableRegisterCheck'])) + fatal_lang_error('register_only_once', false); + } + + // Spaces and other odd characters are evil... + $regOptions['username'] = trim(normalize_spaces(sanitize_chars($regOptions['username'], 1, ' '), true, true, array('no_breaks' => true, 'replace_tabs' => true, 'collapse_hspace' => true))); + + // Convert character encoding for non-utf8mb4 database + $regOptions['username'] = $smcFunc['htmlspecialchars']($regOptions['username']); + + // @todo Separate the sprintf? + if (empty($regOptions['email']) || !filter_var($regOptions['email'], FILTER_VALIDATE_EMAIL) || strlen($regOptions['email']) > 255) + $reg_errors[] = array('lang', 'profile_error_bad_email'); + + $username_validation_errors = validateUsername(0, $regOptions['username'], true, !empty($regOptions['check_reserved_name'])); + if (!empty($username_validation_errors)) + $reg_errors = array_merge($reg_errors, $username_validation_errors); + + // Generate a validation code if it's supposed to be emailed. + $validation_code = ''; + if ($regOptions['require'] == 'activation') + $validation_code = generateValidationCode(); + + // If you haven't put in a password generate one. + if ($regOptions['interface'] == 'admin' && $regOptions['password'] == '') + { + mt_srand(time() + 1277); + $regOptions['password'] = generateValidationCode(); + $regOptions['password_check'] = $regOptions['password']; + } + // Does the first password match the second? + elseif ($regOptions['password'] != $regOptions['password_check']) + $reg_errors[] = array('lang', 'passwords_dont_match'); + + // That's kind of easy to guess... + if ($regOptions['password'] == '') + { + $reg_errors[] = array('lang', 'no_password'); + } + + // Now perform hard password validation as required. + if (!empty($regOptions['check_password_strength']) && $regOptions['password'] != '') + { + $passwordError = validatePassword($regOptions['password'], $regOptions['username'], array($regOptions['email'])); + + // Password isn't legal? + if ($passwordError != null) + { + $errorCode = array('lang', 'profile_error_password_' . $passwordError, false); + if ($passwordError == 'short') + $errorCode[] = array(empty($modSettings['password_strength']) ? 4 : 8); + $reg_errors[] = $errorCode; + } + } + + // You may not be allowed to register this email. + if (!empty($regOptions['check_email_ban'])) + isBannedEmail($regOptions['email'], 'cannot_register', $txt['ban_register_prohibited']); + + // Check if the email address is in use. + $request = $smcFunc['db_query']('', ' + SELECT id_member + FROM {db_prefix}members + WHERE email_address = {string:email_address} + OR email_address = {string:username} + LIMIT 1', + array( + 'email_address' => $regOptions['email'], + 'username' => $regOptions['username'], + ) + ); + // @todo Separate the sprintf? + if ($smcFunc['db_num_rows']($request) != 0) + $reg_errors[] = array('lang', 'email_in_use', false, array($smcFunc['htmlspecialchars']($regOptions['email']))); + + $smcFunc['db_free_result']($request); + + // Perhaps someone else wants to check this user. + call_integration_hook('integrate_register_check', array(&$regOptions, &$reg_errors)); + + // If we found any errors we need to do something about it right away! + foreach ($reg_errors as $key => $error) + { + /* Note for each error: + 0 = 'lang' if it's an index, 'done' if it's clear text. + 1 = The text/index. + 2 = Whether to log. + 3 = sprintf data if necessary. */ + if ($error[0] == 'lang') + loadLanguage('Errors'); + $message = $error[0] == 'lang' ? (empty($error[3]) ? $txt[$error[1]] : vsprintf($txt[$error[1]], (array) $error[3])) : $error[1]; + + // What to do, what to do, what to do. + if ($return_errors) + { + if (!empty($error[2])) + log_error($message, $error[2]); + $reg_errors[$key] = $message; + } + else + fatal_error($message, empty($error[2]) ? false : $error[2]); + } + + // If there's any errors left return them at once! + if (!empty($reg_errors)) + return $reg_errors; + + $reservedVars = array( + 'actual_theme_url', + 'actual_images_url', + 'base_theme_dir', + 'base_theme_url', + 'default_images_url', + 'default_theme_dir', + 'default_theme_url', + 'default_template', + 'images_url', + 'number_recent_posts', + 'smiley_sets_default', + 'theme_dir', + 'theme_id', + 'theme_layers', + 'theme_templates', + 'theme_url', + ); + + // Can't change reserved vars. + if (isset($regOptions['theme_vars']) && count(array_intersect(array_keys($regOptions['theme_vars']), $reservedVars)) != 0) + fatal_lang_error('no_theme'); + + // Some of these might be overwritten. (the lower ones that are in the arrays below.) + $regOptions['register_vars'] = array( + 'member_name' => $regOptions['username'], + 'email_address' => $regOptions['email'], + 'passwd' => hash_password($regOptions['username'], $regOptions['password']), + 'password_salt' => bin2hex($smcFunc['random_bytes'](16)), + 'posts' => 0, + 'date_registered' => time(), + 'member_ip' => $regOptions['interface'] == 'admin' ? '127.0.0.1' : $user_info['ip'], + 'member_ip2' => $regOptions['interface'] == 'admin' ? '127.0.0.1' : $_SERVER['BAN_CHECK_IP'], + 'validation_code' => $validation_code, + 'real_name' => $regOptions['username'], + 'personal_text' => $modSettings['default_personal_text'], + 'id_theme' => 0, + 'id_post_group' => 4, + 'lngfile' => '', + 'buddy_list' => '', + 'pm_ignore_list' => '', + 'website_title' => '', + 'website_url' => '', + 'time_format' => '', + 'signature' => '', + 'avatar' => '', + 'usertitle' => '', + 'secret_question' => '', + 'secret_answer' => '', + 'additional_groups' => '', + 'ignore_boards' => '', + 'smiley_set' => '', + 'timezone' => empty($modSettings['default_timezone']) || !array_key_exists($modSettings['default_timezone'], smf_list_timezones()) ? 'UTC' : $modSettings['default_timezone'], + ); + + // Setup the activation status on this new account so it is correct - firstly is it an under age account? + if ($regOptions['require'] == 'coppa') + { + $regOptions['register_vars']['is_activated'] = 5; + // @todo This should be changed. To what should be it be changed?? + $regOptions['register_vars']['validation_code'] = ''; + } + // Maybe it can be activated right away? + elseif ($regOptions['require'] == 'nothing') + $regOptions['register_vars']['is_activated'] = 1; + // Maybe it must be activated by email? + elseif ($regOptions['require'] == 'activation') + $regOptions['register_vars']['is_activated'] = 0; + // Otherwise it must be awaiting approval! + else + $regOptions['register_vars']['is_activated'] = 3; + + if (isset($regOptions['memberGroup'])) + { + // Make sure the id_group will be valid, if this is an administrator. + $regOptions['register_vars']['id_group'] = $regOptions['memberGroup'] == 1 && !allowedTo('admin_forum') ? 0 : $regOptions['memberGroup']; + + // Check if this group is assignable. + $unassignableGroups = array(-1, 3); + $request = $smcFunc['db_query']('', ' + SELECT id_group + FROM {db_prefix}membergroups + WHERE min_posts != {int:min_posts}' . (allowedTo('admin_forum') ? '' : ' + OR group_type = {int:is_protected}'), + array( + 'min_posts' => -1, + 'is_protected' => 1, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $unassignableGroups[] = $row['id_group']; + $smcFunc['db_free_result']($request); + + if (in_array($regOptions['register_vars']['id_group'], $unassignableGroups)) + $regOptions['register_vars']['id_group'] = 0; + } + + // Verify that timezone is correct, if provided. + if (!empty($regOptions['extra_register_vars']) && !empty($regOptions['extra_register_vars']['timezone']) && + !array_key_exists($regOptions['extra_register_vars']['timezone'], smf_list_timezones())) + unset($regOptions['extra_register_vars']['timezone']); + + // Integrate optional member settings to be set. + if (!empty($regOptions['extra_register_vars'])) + foreach ($regOptions['extra_register_vars'] as $var => $value) + $regOptions['register_vars'][$var] = $value; + + // Integrate optional user theme options to be set. + $theme_vars = array(); + if (!empty($regOptions['theme_vars'])) + foreach ($regOptions['theme_vars'] as $var => $value) + $theme_vars[$var] = $value; + + // Right, now let's prepare for insertion. + $knownInts = array( + 'date_registered', 'posts', 'id_group', 'last_login', 'instant_messages', 'unread_messages', + 'new_pm', 'pm_prefs', 'show_online', + 'id_theme', 'is_activated', 'id_msg_last_visit', 'id_post_group', 'total_time_logged_in', 'warning', + ); + $knownFloats = array( + 'time_offset', + ); + $knownInets = array( + 'member_ip', 'member_ip2', + ); + + // Call an optional function to validate the users' input. + call_integration_hook('integrate_register', array(&$regOptions, &$theme_vars, &$knownInts, &$knownFloats)); + + $column_names = array(); + $values = array(); + foreach ($regOptions['register_vars'] as $var => $val) + { + $type = 'string'; + if (in_array($var, $knownInts)) + $type = 'int'; + elseif (in_array($var, $knownFloats)) + $type = 'float'; + elseif (in_array($var, $knownInets)) + $type = 'inet'; + elseif ($var == 'birthdate') + $type = 'date'; + + $column_names[$var] = $type; + $values[$var] = $val; + } + + // Register them into the database. + $memberID = $smcFunc['db_insert']('', + '{db_prefix}members', + $column_names, + $values, + array('id_member'), + 1 + ); + + // Call an optional function as notification of registration. + call_integration_hook('integrate_post_register', array(&$regOptions, &$theme_vars, &$memberID)); + + // Update the number of members and latest member's info - and pass the name, but remove the 's. + if ($regOptions['register_vars']['is_activated'] == 1) + updateStats('member', $memberID, $regOptions['register_vars']['real_name']); + else + updateStats('member'); + + // Theme variables too? + if (!empty($theme_vars)) + { + $inserts = array(); + foreach ($theme_vars as $var => $val) + $inserts[] = array($memberID, $var, $val); + $smcFunc['db_insert']('insert', + '{db_prefix}themes', + array('id_member' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'), + $inserts, + array('id_member', 'variable') + ); + } + + // Log their acceptance of the agreement and privacy policy, for future reference. + foreach (array('agreement_accepted', 'policy_accepted') as $key) + if (!empty($theme_vars[$key])) + logAction($key, array('member_affected' => $memberID, 'applicator' => $memberID), 'user'); + + // If it's enabled, increase the registrations for today. + trackStats(array('registers' => '+')); + + // Administrative registrations are a bit different... + if ($regOptions['interface'] == 'admin') + { + if ($regOptions['require'] == 'activation') + $email_message = 'admin_register_activate'; + elseif (!empty($regOptions['send_welcome_email'])) + $email_message = 'admin_register_immediate'; + + if (isset($email_message)) + { + $replacements = array( + 'REALNAME' => $regOptions['register_vars']['real_name'], + 'USERNAME' => $regOptions['username'], + 'PASSWORD' => $regOptions['password'], + 'FORGOTPASSWORDLINK' => $scripturl . '?action=reminder', + 'ACTIVATIONLINK' => $scripturl . '?action=activate;u=' . $memberID . ';code=' . $validation_code, + 'ACTIVATIONLINKWITHOUTCODE' => $scripturl . '?action=activate;u=' . $memberID, + 'ACTIVATIONCODE' => $validation_code, + ); + + $emaildata = loadEmailTemplate($email_message, $replacements); + + sendmail($regOptions['email'], $emaildata['subject'], $emaildata['body'], null, $email_message . $memberID, $emaildata['is_html'], 0); + } + + // All admins are finished here. + return $memberID; + } + + // Can post straight away - welcome them to your fantastic community... + if ($regOptions['require'] == 'nothing') + { + if (!empty($regOptions['send_welcome_email'])) + { + $replacements = array( + 'REALNAME' => $regOptions['register_vars']['real_name'], + 'USERNAME' => $regOptions['username'], + 'PASSWORD' => $regOptions['password'], + 'FORGOTPASSWORDLINK' => $scripturl . '?action=reminder', + ); + $emaildata = loadEmailTemplate('register_immediate', $replacements); + sendmail($regOptions['email'], $emaildata['subject'], $emaildata['body'], null, 'register', $emaildata['is_html'], 0); + } + + // Send admin their notification. + adminNotify('standard', $memberID, $regOptions['username']); + } + // Need to activate their account - or fall under COPPA. + elseif ($regOptions['require'] == 'activation' || $regOptions['require'] == 'coppa') + { + $replacements = array( + 'REALNAME' => $regOptions['register_vars']['real_name'], + 'USERNAME' => $regOptions['username'], + 'PASSWORD' => $regOptions['password'], + 'FORGOTPASSWORDLINK' => $scripturl . '?action=reminder', + ); + + if ($regOptions['require'] == 'activation') + $replacements += array( + 'ACTIVATIONLINK' => $scripturl . '?action=activate;u=' . $memberID . ';code=' . $validation_code, + 'ACTIVATIONLINKWITHOUTCODE' => $scripturl . '?action=activate;u=' . $memberID, + 'ACTIVATIONCODE' => $validation_code, + ); + + else + $replacements += array( + 'COPPALINK' => $scripturl . '?action=coppa;member=' . $memberID, + ); + + $emaildata = loadEmailTemplate('register_' . ($regOptions['require'] == 'activation' ? 'activate' : 'coppa'), $replacements); + + sendmail($regOptions['email'], $emaildata['subject'], $emaildata['body'], null, 'reg_' . $regOptions['require'] . $memberID, $emaildata['is_html'], 0); + } + // Must be awaiting approval. + else + { + $replacements = array( + 'REALNAME' => $regOptions['register_vars']['real_name'], + 'USERNAME' => $regOptions['username'], + 'PASSWORD' => $regOptions['password'], + 'FORGOTPASSWORDLINK' => $scripturl . '?action=reminder', + ); + + $emaildata = loadEmailTemplate('register_pending', $replacements); + + sendmail($regOptions['email'], $emaildata['subject'], $emaildata['body'], null, 'reg_pending', $emaildata['is_html'], 0); + + // Admin gets informed here... + adminNotify('approval', $memberID, $regOptions['username']); + } + + // Okay, they're for sure registered... make sure the session is aware of this for security. (Just married :P!) + $_SESSION['just_registered'] = 1; + + // If they are for sure registered, let other people to know about it + call_integration_hook('integrate_register_after', array($regOptions, $memberID)); + + return $memberID; +} + +/** + * Check if a name is in the reserved words list. + * (name, current member id, name/username?.) + * - checks if name is a reserved name or username. + * - if is_name is false, the name is assumed to be a username. + * - the id_member variable is used to ignore duplicate matches with the + * current member. + * + * @param string $name The name to check + * @param int $current_id_member The ID of the current member (to avoid false positives with the current member) + * @param bool $is_name Whether we're checking against reserved names or just usernames + * @param bool $fatal Whether to die with a fatal error if the name is reserved + * @return bool|void False if name is not reserved, otherwise true if $fatal is false or dies with a fatal_lang_error if $fatal is true + */ +function isReservedName($name, $current_id_member = 0, $is_name = true, $fatal = true) +{ + global $modSettings, $smcFunc; + + $name = preg_replace_callback('~(&#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'replaceEntities__callback', $name); + $checkName = $smcFunc['strtolower']($name); + + // Administrators are never restricted ;). + if (!allowedTo('moderate_forum') && ((!empty($modSettings['reserveName']) && $is_name) || !empty($modSettings['reserveUser']) && !$is_name)) + { + $reservedNames = explode("\n", $modSettings['reserveNames']); + // Case sensitive check? + $checkMe = empty($modSettings['reserveCase']) ? $checkName : $name; + + // Check each name in the list... + foreach ($reservedNames as $reserved) + { + if ($reserved == '') + continue; + + // The admin might've used entities too, level the playing field. + $reservedCheck = preg_replace('~(&#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'replaceEntities__callback', $reserved); + + // Case sensitive name? + if (empty($modSettings['reserveCase'])) + $reservedCheck = $smcFunc['strtolower']($reservedCheck); + + // If it's not just entire word, check for it in there somewhere... + if ($checkMe == $reservedCheck || ($smcFunc['strpos']($checkMe, $reservedCheck) !== false && empty($modSettings['reserveWord']))) + if ($fatal) + fatal_lang_error('username_reserved', 'password', array($reserved)); + else + return true; + } + + $censor_name = $name; + if (censorText($censor_name) != $name) + if ($fatal) + fatal_lang_error('name_censored', 'password', array($name)); + else + return true; + } + + // Characters we just shouldn't allow, regardless. + foreach (array('*') as $char) + if (strpos($checkName, $char) !== false) + if ($fatal) + fatal_lang_error('username_reserved', 'password', array($char)); + else + return true; + + $request = $smcFunc['db_query']('', ' + SELECT id_member + FROM {db_prefix}members + WHERE ' . (empty($current_id_member) ? '' : 'id_member != {int:current_member} + AND ') . '({raw:real_name} {raw:operator} LOWER({string:check_name}) OR {raw:member_name} {raw:operator} LOWER({string:check_name})) + LIMIT 1', + array( + 'real_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(real_name)' : 'real_name', + 'member_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(member_name)' : 'member_name', + 'current_member' => $current_id_member, + 'check_name' => $checkName, + 'operator' => strpos($checkName, '%') || strpos($checkName, '_') ? 'LIKE' : '=', + ) + ); + if ($smcFunc['db_num_rows']($request) > 0) + { + $smcFunc['db_free_result']($request); + return true; + } + + // Does name case insensitive match a member group name? + $request = $smcFunc['db_query']('', ' + SELECT id_group + FROM {db_prefix}membergroups + WHERE {raw:group_name} LIKE {string:check_name} + LIMIT 1', + array( + 'group_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(group_name)' : 'group_name', + 'check_name' => $checkName, + ) + ); + if ($smcFunc['db_num_rows']($request) > 0) + { + $smcFunc['db_free_result']($request); + return true; + } + + // Okay, they passed. + $is_reserved = false; + + // Maybe a mod wants to perform further checks? + call_integration_hook('integrate_check_name', array($checkName, &$is_reserved, $current_id_member, $is_name)); + + return $is_reserved; +} + +/** + * Retrieves a list of membergroups that have the given permission, either on + * a given board or in general. + * + * If board_id is not null, a board permission is assumed. + * The function takes different permission settings into account. + * + * @param string $permission The permission to check + * @param int $board_id = null If set, checks permissions for the specified board + * @return array An array containing two arrays - 'allowed', which has which groups are allowed to do it and 'denied' which has the groups that are denied + */ +function groupsAllowedTo($permission, $board_id = null) +{ + global $board_info, $smcFunc; + + // Admins are allowed to do anything. + $member_groups = array( + 'allowed' => array(1), + 'denied' => array(), + ); + + // Assume we're dealing with regular permissions (like profile_view). + if ($board_id === null) + { + $request = $smcFunc['db_query']('', ' + SELECT id_group, add_deny + FROM {db_prefix}permissions + WHERE permission = {string:permission}', + array( + 'permission' => $permission, + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $member_groups[$row['add_deny'] === '1' ? 'allowed' : 'denied'][] = $row['id_group']; + } + + $smcFunc['db_free_result']($request); + } + + // Otherwise it's time to look at the board. + else + { + $board_id = (int) $board_id; + + // First get the profile of the given board. + if (isset($board_info['id']) && $board_info['id'] == $board_id) + { + $profile_id = $board_info['profile']; + } + elseif ($board_id !== 0) + { + $request = $smcFunc['db_query']('', ' + SELECT id_profile + FROM {db_prefix}boards + WHERE id_board = {int:id_board} + LIMIT 1', + array( + 'id_board' => $board_id, + ) + ); + + if ($smcFunc['db_num_rows']($request) == 0) + { + fatal_lang_error('no_board'); + } + + list ($profile_id) = $smcFunc['db_fetch_row']($request); + + $smcFunc['db_free_result']($request); + } + else + $profile_id = 1; + + $request = $smcFunc['db_query']('', ' + SELECT bp.id_group, bp.add_deny + FROM {db_prefix}board_permissions AS bp + WHERE bp.permission = {string:permission} + AND bp.id_profile = {int:profile_id}', + array( + 'profile_id' => $profile_id, + 'permission' => $permission, + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $member_groups[$row['add_deny'] === '1' ? 'allowed' : 'denied'][] = $row['id_group']; + } + + $smcFunc['db_free_result']($request); + + $moderator_groups = array(); + + // "Inherit" any moderator permissions as needed + if (isset($board_info['moderator_groups'])) + { + $moderator_groups = array_keys($board_info['moderator_groups']); + } + elseif ($board_id !== 0) + { + // Get the groups that can moderate this board + $request = $smcFunc['db_query']('', ' + SELECT id_group + FROM {db_prefix}moderator_groups + WHERE id_board = {int:board_id}', + array( + 'board_id' => $board_id, + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $moderator_groups[] = $row['id_group']; + } + + $smcFunc['db_free_result']($request); + } + + // "Inherit" any additional permissions from the "Moderators" group + foreach ($moderator_groups as $mod_group) + { + // If they're not specifically allowed, but the moderator group is, then allow it + if (in_array(3, $member_groups['allowed']) && !in_array($mod_group, $member_groups['allowed'])) + { + $member_groups['allowed'][] = $mod_group; + } + + // They're not denied, but the moderator group is, so deny it + if (in_array(3, $member_groups['denied']) && !in_array($mod_group, $member_groups['denied'])) + { + $member_groups['denied'][] = $mod_group; + } + } + } + + // Maybe a mod needs to tweak the list of allowed groups on the fly? + call_integration_hook('integrate_groups_allowed_to', array(&$member_groups, $permission, $board_id)); + + // Denied is never allowed. + $member_groups['allowed'] = array_diff($member_groups['allowed'], $member_groups['denied']); + + return $member_groups; +} + +/** + * Retrieves a list of members that have a given permission + * (on a given board). + * If board_id is not null, a board permission is assumed. + * Takes different permission settings into account. + * Takes possible moderators (on board 'board_id') into account. + * + * @param string $permission The permission to check + * @param int $board_id If set, checks permission for that specific board + * @return array An array containing the IDs of the members having that permission + */ +function membersAllowedTo($permission, $board_id = null) +{ + global $smcFunc; + + $member_groups = groupsAllowedTo($permission, $board_id); + + $all_groups = array_merge($member_groups['allowed'], $member_groups['denied']); + + $include_moderators = in_array(3, $member_groups['allowed']) && $board_id !== null; + $member_groups['allowed'] = array_diff($member_groups['allowed'], array(3)); + + $exclude_moderators = in_array(3, $member_groups['denied']) && $board_id !== null; + $member_groups['denied'] = array_diff($member_groups['denied'], array(3)); + + $request = $smcFunc['db_query']('', ' + SELECT mem.id_member + FROM {db_prefix}members AS mem' . ($include_moderators || $exclude_moderators ? ' + LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_member = mem.id_member AND mods.id_board = {int:board_id})' : '') . ' + WHERE (' . ($include_moderators ? 'mods.id_member IS NOT NULL OR ' : '') . 'mem.id_group IN ({array_int:member_groups_allowed}) OR FIND_IN_SET({raw:member_group_allowed_implode}, mem.additional_groups) != 0 OR mem.id_post_group IN ({array_int:member_groups_allowed}))' . (empty($member_groups['denied']) ? '' : ' + AND NOT (' . ($exclude_moderators ? 'mods.id_member IS NOT NULL OR ' : '') . 'mem.id_group IN ({array_int:member_groups_denied}) OR FIND_IN_SET({raw:member_group_denied_implode}, mem.additional_groups) != 0 OR mem.id_post_group IN ({array_int:member_groups_denied}))'), + array( + 'member_groups_allowed' => $member_groups['allowed'], + 'member_groups_denied' => $member_groups['denied'], + 'all_member_groups' => $all_groups, + 'board_id' => $board_id, + 'member_group_allowed_implode' => implode(', mem.additional_groups) != 0 OR FIND_IN_SET(', $member_groups['allowed']), + 'member_group_denied_implode' => implode(', mem.additional_groups) != 0 OR FIND_IN_SET(', $member_groups['denied']), + ) + ); + $members = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $members[] = $row['id_member']; + $smcFunc['db_free_result']($request); + + return $members; +} + +/** + * This function is used to reassociate members with relevant posts. + * Reattribute guest posts to a specified member. + * Does not check for any permissions. + * If add_to_post_count is set, the member's post count is increased. + * + * @param int $memID The ID of the original poster + * @param bool|string $email If set, should be the email of the poster + * @param bool|string $membername If set, the membername of the poster + * @param bool $post_count Whether to adjust post counts + * @return array An array containing the number of messages, topics and reports updated + */ +function reattributePosts($memID, $email = false, $membername = false, $post_count = false) +{ + global $smcFunc, $modSettings; + + $updated = array( + 'messages' => 0, + 'topics' => 0, + 'reports' => 0, + ); + + // Firstly, if email and username aren't passed find out the members email address and name. + if ($email === false && $membername === false) + { + $request = $smcFunc['db_query']('', ' + SELECT email_address, member_name + FROM {db_prefix}members + WHERE id_member = {int:memID} + LIMIT 1', + array( + 'memID' => $memID, + ) + ); + list ($email, $membername) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + } + + // If they want the post count restored then we need to do some research. + if ($post_count) + { + $recycle_board = !empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board']) ? (int) $modSettings['recycle_board'] : 0; + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}messages AS m + INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND b.count_posts = {int:count_posts}) + WHERE m.id_member = {int:guest_id} + AND m.approved = {int:is_approved}' . (!empty($recycle_board) ? ' + AND m.id_board != {int:recycled_board}' : '') . (empty($email) ? '' : ' + AND m.poster_email = {string:email_address}') . (empty($membername) ? '' : ' + AND m.poster_name = {string:member_name}'), + array( + 'count_posts' => 0, + 'guest_id' => 0, + 'email_address' => $email, + 'member_name' => $membername, + 'is_approved' => 1, + 'recycled_board' => $recycle_board, + ) + ); + list ($messageCount) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + updateMemberData($memID, array('posts' => 'posts + ' . $messageCount)); + } + + $query_parts = array(); + if (!empty($email)) + $query_parts[] = 'poster_email = {string:email_address}'; + if (!empty($membername)) + $query_parts[] = 'poster_name = {string:member_name}'; + $query = implode(' AND ', $query_parts); + + // Finally, update the posts themselves! + $smcFunc['db_query']('', ' + UPDATE {db_prefix}messages + SET id_member = {int:memID} + WHERE ' . $query, + array( + 'memID' => $memID, + 'email_address' => $email, + 'member_name' => $membername, + ) + ); + $updated['messages'] = $smcFunc['db_affected_rows'](); + + // Did we update any messages? + if ($updated['messages'] > 0) + { + // First, check for updated topics. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}topics AS t + SET id_member_started = {int:memID} + WHERE t.id_first_msg = ( + SELECT m.id_msg + FROM {db_prefix}messages m + WHERE m.id_member = {int:memID} + AND m.id_msg = t.id_first_msg + AND ' . $query . ' + )', + array( + 'memID' => $memID, + 'email_address' => $email, + 'member_name' => $membername, + ) + ); + $updated['topics'] = $smcFunc['db_affected_rows'](); + + // Second, check for updated reports. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}log_reported AS lr + SET id_member = {int:memID} + WHERE lr.id_msg = ( + SELECT m.id_msg + FROM {db_prefix}messages m + WHERE m.id_member = {int:memID} + AND m.id_msg = lr.id_msg + AND ' . $query . ' + )', + array( + 'memID' => $memID, + 'email_address' => $email, + 'member_name' => $membername, + ) + ); + $updated['reports'] = $smcFunc['db_affected_rows'](); + } + + // Allow mods with their own post tables to reattribute posts as well :) + call_integration_hook('integrate_reattribute_posts', array($memID, $email, $membername, $post_count, &$updated)); + + return $updated; +} + +/** + * This simple function adds/removes the passed user from the current users buddy list. + * Requires profile_identity_own permission. + * Called by ?action=buddy;u=x;session_id=y. + * Redirects to ?action=profile;u=x. + */ +function BuddyListToggle() +{ + global $user_info, $smcFunc; + + checkSession('get'); + + isAllowedTo('profile_extra_own'); + is_not_guest(); + + $userReceiver = (int) !empty($_REQUEST['u']) ? $_REQUEST['u'] : 0; + + if (empty($userReceiver)) + fatal_lang_error('no_access', false); + + // Remove if it's already there... + if (in_array($userReceiver, $user_info['buddies'])) + $user_info['buddies'] = array_diff($user_info['buddies'], array($userReceiver)); + + // ...or add if it's not and if it's not you. + elseif ($user_info['id'] != $userReceiver) + { + $user_info['buddies'][] = $userReceiver; + + // And add a nice alert. Don't abuse though! + if ((cache_get_data('Buddy-sent-' . $user_info['id'] . '-' . $userReceiver, 86400)) == null) + { + $smcFunc['db_insert']('insert', + '{db_prefix}background_tasks', + array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'), + array('$sourcedir/tasks/Buddy-Notify.php', 'Buddy_Notify_Background', $smcFunc['json_encode'](array( + 'receiver_id' => $userReceiver, + 'id_member' => $user_info['id'], + 'member_name' => $user_info['username'], + 'time' => time(), + )), 0), + array('id_task') + ); + + // Store this in a cache entry to avoid creating multiple alerts. Give it a long life cycle. + cache_put_data('Buddy-sent-' . $user_info['id'] . '-' . $userReceiver, '1', 86400); + } + } + + // Update the settings. + updateMemberData($user_info['id'], array('buddy_list' => implode(',', $user_info['buddies']))); + + // Redirect back to the profile + redirectexit('action=profile;u=' . $userReceiver); +} + +/** + * Callback for createList(). + * + * @param int $start Which item to start with (for pagination purposes) + * @param int $items_per_page How many items to show per page + * @param string $sort An SQL query indicating how to sort the results + * @param string $where An SQL query used to filter the results + * @param array $where_params An array of parameters for $where + * @param bool $get_duplicates Whether to get duplicates (used for the admin member list) + * @return array An array of information for displaying the list of members + */ +function list_getMembers($start, $items_per_page, $sort, $where, $where_params = array(), $get_duplicates = false) +{ + global $smcFunc; + + $request = $smcFunc['db_query']('', ' + SELECT + mem.id_member, mem.member_name, mem.real_name, mem.email_address, mem.member_ip, mem.member_ip2, mem.last_login, + mem.posts, mem.is_activated, mem.date_registered, mem.id_group, mem.additional_groups, mg.group_name + FROM {db_prefix}members AS mem + LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = mem.id_group) + WHERE ' . ($where == '1' ? '1=1' : $where) . ' + ORDER BY {raw:sort} + LIMIT {int:start}, {int:per_page}', + array_merge($where_params, array( + 'sort' => $sort, + 'start' => $start, + 'per_page' => $items_per_page, + )) + ); + + $members = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $row['member_ip'] = inet_dtop($row['member_ip']); + $row['member_ip2'] = inet_dtop($row['member_ip2']); + $members[] = $row; + } + $smcFunc['db_free_result']($request); + + // If we want duplicates pass the members array off. + if ($get_duplicates) + populateDuplicateMembers($members); + + return $members; +} + +/** + * Callback for createList(). + * + * @param string $where An SQL query to filter the results + * @param array $where_params An array of parameters for $where + * @return int The number of members matching the given situation + */ +function list_getNumMembers($where, $where_params = array()) +{ + global $smcFunc, $modSettings; + + // We know how many members there are in total. + if (empty($where) || $where == '1=1') + $num_members = $modSettings['totalMembers']; + + // The database knows the amount when there are extra conditions. + else + { + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}members AS mem + WHERE ' . $where, + array_merge($where_params, array( + )) + ); + list ($num_members) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + } + + return $num_members; +} + +/** + * Find potential duplicate registration members based on the same IP address + * + * @param array $members An array of members + */ +function populateDuplicateMembers(&$members) +{ + global $smcFunc; + + // This will hold all the ip addresses. + $ips = array(); + foreach ($members as $key => $member) + { + // Create the duplicate_members element. + $members[$key]['duplicate_members'] = array(); + + // Store the IPs. + if (!empty($member['member_ip'])) + $ips[] = $member['member_ip']; + if (!empty($member['member_ip2'])) + $ips[] = $member['member_ip2']; + } + + $ips = array_unique($ips); + + if (empty($ips)) + return false; + + // Fetch all members with this IP address, we'll filter out the current ones in a sec. + $request = $smcFunc['db_query']('', ' + SELECT + id_member, member_name, email_address, member_ip, member_ip2, is_activated + FROM {db_prefix}members + WHERE member_ip IN ({array_inet:ips}) + OR member_ip2 IN ({array_inet:ips})', + array( + 'ips' => $ips, + ) + ); + $duplicate_members = array(); + $duplicate_ids = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + //$duplicate_ids[] = $row['id_member']; + $row['member_ip'] = inet_dtop($row['member_ip']); + $row['member_ip2'] = inet_dtop($row['member_ip2']); + + $member_context = array( + 'id' => $row['id_member'], + 'name' => $row['member_name'], + 'email' => $row['email_address'], + 'is_banned' => $row['is_activated'] > 10, + 'ip' => $row['member_ip'], + 'ip2' => $row['member_ip2'], + ); + + if (in_array($row['member_ip'], $ips)) + $duplicate_members[$row['member_ip']][] = $member_context; + if ($row['member_ip'] != $row['member_ip2'] && in_array($row['member_ip2'], $ips)) + $duplicate_members[$row['member_ip2']][] = $member_context; + } + $smcFunc['db_free_result']($request); + + // Also try to get a list of messages using these ips. + $request = $smcFunc['db_query']('', ' + SELECT + m.poster_ip, mem.id_member, mem.member_name, mem.email_address, mem.is_activated + FROM {db_prefix}messages AS m + INNER JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member) + WHERE m.id_member != 0 + ' . (!empty($duplicate_ids) ? 'AND m.id_member NOT IN ({array_int:duplicate_ids})' : '') . ' + AND m.poster_ip IN ({array_inet:ips})', + array( + 'duplicate_ids' => $duplicate_ids, + 'ips' => $ips, + ) + ); + + $had_ips = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $row['poster_ip'] = inet_dtop($row['poster_ip']); + + // Don't collect lots of the same. + if (isset($had_ips[$row['poster_ip']]) && in_array($row['id_member'], $had_ips[$row['poster_ip']])) + continue; + $had_ips[$row['poster_ip']][] = $row['id_member']; + + $duplicate_members[$row['poster_ip']][] = array( + 'id' => $row['id_member'], + 'name' => $row['member_name'], + 'email' => $row['email_address'], + 'is_banned' => $row['is_activated'] > 10, + 'ip' => $row['poster_ip'], + 'ip2' => $row['poster_ip'], + ); + } + $smcFunc['db_free_result']($request); + + // Now we have all the duplicate members, stick them with their respective member in the list. + if (!empty($duplicate_members)) + foreach ($members as $key => $member) + { + if (isset($duplicate_members[$member['member_ip']])) + $members[$key]['duplicate_members'] = $duplicate_members[$member['member_ip']]; + if ($member['member_ip'] != $member['member_ip2'] && isset($duplicate_members[$member['member_ip2']])) + $members[$key]['duplicate_members'] = array_merge($member['duplicate_members'], $duplicate_members[$member['member_ip2']]); + + // Check we don't have lots of the same member. + $member_track = array($member['id_member']); + foreach ($members[$key]['duplicate_members'] as $duplicate_id_member => $duplicate_member) + { + if (in_array($duplicate_member['id'], $member_track)) + { + unset($members[$key]['duplicate_members'][$duplicate_id_member]); + continue; + } + + $member_track[] = $duplicate_member['id']; + } + } +} + +/** + * Generate a random validation code. + * + * @return string A random validation code + */ +function generateValidationCode() +{ + global $smcFunc; + + return bin2hex($smcFunc['random_bytes'](5)); +} + +?> \ No newline at end of file diff --git a/Sources/Subs-MembersOnline.php b/Sources/Subs-MembersOnline.php new file mode 100644 index 0000000..46c5611 --- /dev/null +++ b/Sources/Subs-MembersOnline.php @@ -0,0 +1,269 @@ + array(), + 'list_users_online' => array(), + 'online_groups' => array(), + 'num_guests' => 0, + 'num_spiders' => 0, + 'num_buddies' => 0, + 'num_users_hidden' => 0, + 'num_users_online' => 0, + ); + + // Get any spiders if enabled. + $spiders = array(); + $spider_finds = array(); + if (!empty($modSettings['show_spider_online']) && ($modSettings['show_spider_online'] < 3 || allowedTo('admin_forum')) && !empty($modSettings['spider_name_cache'])) + $spiders = $smcFunc['json_decode']($modSettings['spider_name_cache'], true); + + // Load the users online right now. + $request = $smcFunc['db_query']('', ' + SELECT + lo.id_member, lo.log_time, lo.id_spider, mem.real_name, mem.member_name, mem.show_online, + mg.online_color, mg.id_group, mg.group_name, mg.hidden, mg.group_type, mg.id_parent + FROM {db_prefix}log_online AS lo + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lo.id_member) + LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = CASE WHEN mem.id_group = {int:reg_mem_group} THEN mem.id_post_group ELSE mem.id_group END)', + array( + 'reg_mem_group' => 0, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (empty($row['real_name'])) + { + // Do we think it's a spider? + if ($row['id_spider'] && isset($spiders[$row['id_spider']])) + { + $spider_finds[$row['id_spider']] = isset($spider_finds[$row['id_spider']]) ? $spider_finds[$row['id_spider']] + 1 : 1; + $membersOnlineStats['num_spiders']++; + } + // Guests are only nice for statistics. + $membersOnlineStats['num_guests']++; + + continue; + } + + elseif (empty($row['show_online']) && empty($membersOnlineOptions['show_hidden'])) + { + // Just increase the stats and don't add this hidden user to any list. + $membersOnlineStats['num_users_hidden']++; + continue; + } + + // Some basic color coding... + if (!empty($row['online_color'])) + $link = '' . $row['real_name'] . ''; + else + $link = '' . $row['real_name'] . ''; + + // Buddies get counted and highlighted. + $is_buddy = in_array($row['id_member'], $user_info['buddies']); + if ($is_buddy) + { + $membersOnlineStats['num_buddies']++; + $link = '' . $link . ''; + } + + // A lot of useful information for each member. + $membersOnlineStats['users_online'][$row[$membersOnlineOptions['sort']] . '_' . $row['member_name']] = array( + 'id' => $row['id_member'], + 'username' => $row['member_name'], + 'name' => $row['real_name'], + 'group' => $row['id_group'], + 'href' => $scripturl . '?action=profile;u=' . $row['id_member'], + 'link' => $link, + 'is_buddy' => $is_buddy, + 'hidden' => empty($row['show_online']), + 'is_last' => false, + ); + + // This is the compact version, simply implode it to show. + $membersOnlineStats['list_users_online'][$row[$membersOnlineOptions['sort']] . '_' . $row['member_name']] = empty($row['show_online']) ? '' . $link . '' : $link; + + // Store all distinct (primary) membergroups that are shown. + if (!isset($membersOnlineStats['online_groups'][$row['id_group']])) + $membersOnlineStats['online_groups'][$row['id_group']] = array( + 'id' => $row['id_group'], + 'name' => $row['group_name'], + 'color' => $row['online_color'], + 'hidden' => $row['hidden'], + 'type' => $row['group_type'], + 'parent' => $row['id_parent'], + ); + } + $smcFunc['db_free_result']($request); + + // If there are spiders only and we're showing the detail, add them to the online list - at the bottom. + if (!empty($spider_finds) && $modSettings['show_spider_online'] > 1) + { + $sort = $membersOnlineOptions['sort'] === 'log_time' && $membersOnlineOptions['reverse_sort'] ? 0 : 'zzz_'; + foreach ($spider_finds as $id => $count) + { + $link = $spiders[$id] . ($count > 1 ? ' (' . $count . ')' : ''); + $membersOnlineStats['users_online'][$sort . '_' . $spiders[$id]] = array( + 'id' => 0, + 'username' => $spiders[$id], + 'name' => $link, + 'group' => $txt['spiders'], + 'href' => '', + 'link' => $link, + 'is_buddy' => false, + 'hidden' => false, + 'is_last' => false, + ); + $membersOnlineStats['list_users_online'][$sort . '_' . $spiders[$id]] = $link; + } + } + + // Time to sort the list a bit. + if (!empty($membersOnlineStats['users_online'])) + { + // Determine the sort direction. + $sortFunction = empty($membersOnlineOptions['reverse_sort']) ? 'ksort' : 'krsort'; + + // Sort the two lists. + $sortFunction($membersOnlineStats['users_online']); + $sortFunction($membersOnlineStats['list_users_online']); + + // Mark the last list item as 'is_last'. + $userKeys = array_keys($membersOnlineStats['users_online']); + $membersOnlineStats['users_online'][end($userKeys)]['is_last'] = true; + } + + // Also sort the membergroups. + ksort($membersOnlineStats['online_groups']); + + // Hidden and non-hidden members make up all online members. + $membersOnlineStats['num_users_online'] = count($membersOnlineStats['users_online']) + $membersOnlineStats['num_users_hidden'] - (isset($modSettings['show_spider_online']) && $modSettings['show_spider_online'] > 1 ? count($spider_finds) : 0); + + call_integration_hook('integrate_online_stats', array(&$membersOnlineStats)); + + return $membersOnlineStats; +} + +/** + * Check if the number of users online is a record and store it. + * + * @param int $total_users_online The total number of members online + */ +function trackStatsUsersOnline($total_users_online) +{ + global $modSettings, $smcFunc; + + $settingsToUpdate = array(); + + // More members on now than ever were? Update it! + if (!isset($modSettings['mostOnline']) || $total_users_online >= $modSettings['mostOnline']) + $settingsToUpdate = array( + 'mostOnline' => $total_users_online, + 'mostDate' => time() + ); + + $date = smf_strftime('%Y-%m-%d', time()); + + // No entry exists for today yet? + if (!isset($modSettings['mostOnlineUpdated']) || $modSettings['mostOnlineUpdated'] != $date) + { + $request = $smcFunc['db_query']('', ' + SELECT most_on + FROM {db_prefix}log_activity + WHERE date = {date:date} + LIMIT 1', + array( + 'date' => $date, + ) + ); + + // The log_activity hasn't got an entry for today? + if ($smcFunc['db_num_rows']($request) === 0) + { + $smcFunc['db_insert']('ignore', + '{db_prefix}log_activity', + array('date' => 'date', 'most_on' => 'int'), + array($date, $total_users_online), + array('date') + ); + } + // There's an entry in log_activity on today... + else + { + list ($modSettings['mostOnlineToday']) = $smcFunc['db_fetch_row']($request); + + if ($total_users_online > $modSettings['mostOnlineToday']) + trackStats(array('most_on' => $total_users_online)); + + $total_users_online = max($total_users_online, $modSettings['mostOnlineToday']); + } + $smcFunc['db_free_result']($request); + + $settingsToUpdate['mostOnlineUpdated'] = $date; + $settingsToUpdate['mostOnlineToday'] = $total_users_online; + } + + // Highest number of users online today? + elseif ($total_users_online > $modSettings['mostOnlineToday']) + { + trackStats(array('most_on' => $total_users_online)); + $settingsToUpdate['mostOnlineToday'] = $total_users_online; + } + + if (!empty($settingsToUpdate)) + updateSettings($settingsToUpdate); +} + +?> \ No newline at end of file diff --git a/Sources/Subs-Menu.php b/Sources/Subs-Menu.php new file mode 100644 index 0000000..972a442 --- /dev/null +++ b/Sources/Subs-Menu.php @@ -0,0 +1,364 @@ + $value) + $menu_context['extra_parameters'] .= ';' . $key . '=' . $value; + + // Only include the session ID in the URL if it's strictly necessary. + if (empty($menuOptions['disable_url_session_check'])) + $menu_context['extra_parameters'] .= ';' . $context['session_var'] . '=' . $context['session_id']; + + $include_data = array(); + $menu_context['sections'] = array(); + + // Now setup the context correctly. + foreach ($menuData as $section_id => $section) + { + // Is this enabled - or has as permission check - which fails? + if ((isset($section['enabled']) && $section['enabled'] == false) || (isset($section['permission']) && !allowedTo($section['permission']))) + continue; + + // Now we cycle through the sections to pick the right area. + foreach ($section['areas'] as $area_id => $area) + { + // Can we do this? + if ((!isset($area['enabled']) || $area['enabled'] != false) && (empty($area['permission']) || allowedTo($area['permission']))) + { + // Add it to the context... if it has some form of name! + if (isset($area['label']) || (isset($txt[$area_id]) && !isset($area['select']))) + { + // If we haven't got an area then the first valid one is our choice. + if (!isset($menu_context['current_area'])) + { + $menu_context['current_area'] = $area_id; + $include_data = $area; + } + + // If this is hidden from view don't do the rest. + if (empty($area['hidden'])) + { + // First time this section? + if (!isset($menu_context['sections'][$section_id])) + $menu_context['sections'][$section_id]['title'] = $section['title']; + + // Is there a counter amount to show for this section? + if (!empty($section['amt'])) + $menu_context['sections'][$section_id]['amt'] = $section['amt']; + + $menu_context['sections'][$section_id]['areas'][$area_id] = array('label' => isset($area['label']) ? $area['label'] : $txt[$area_id]); + // We'll need the ID as well... + $menu_context['sections'][$section_id]['id'] = $section_id; + // Does it have a custom URL? + if (isset($area['custom_url'])) + $menu_context['sections'][$section_id]['areas'][$area_id]['url'] = $area['custom_url']; + + // Is there a counter amount to show for this area? + if (!empty($area['amt'])) + $menu_context['sections'][$section_id]['areas'][$area_id]['amt'] = $area['amt']; + + // Does this area have its own icon? + if (!isset($area['force_menu_into_arms_of_another_menu']) && $user_info['name'] == 'iamanoompaloompa') + { + $menu_context['sections'][$section_id]['areas'][$area_id] = $smcFunc['json_decode'](base64_decode('eyJsYWJlbCI6Ik9vbXBhIExvb21wYSIsInVybCI6Imh0dHBzOlwvXC9lbi53aWtpcGVkaWEub3JnXC93aWtpXC9Pb21wYV9Mb29tcGFzPyIsImljb24iOiI8aW1nIHNyYz1cImh0dHBzOlwvXC93d3cuc2ltcGxlbWFjaGluZXMub3JnXC9pbWFnZXNcL29vbXBhLmdpZlwiIGFsdD1cIkknbSBhbiBPb21wYSBMb29tcGFcIiBcLz4ifQ=='), true); + } + elseif (isset($area['icon'])) + { + if (file_exists($settings['theme_dir'] . '/images/admin/' . $area['icon'])) + { + $menu_context['sections'][$section_id]['areas'][$area_id]['icon'] = ''; + } + elseif (file_exists($settings['default_theme_dir'] . '/images/admin/' . $area['icon'])) + { + $menu_context['sections'][$section_id]['areas'][$area_id]['icon'] = ''; + } + else + $menu_context['sections'][$section_id]['areas'][$area_id]['icon'] = ''; + } + else + $menu_context['sections'][$section_id]['areas'][$area_id]['icon'] = ''; + + if (isset($area['icon_class']) && empty($menu_context['sections'][$section_id]['areas'][$area_id]['icon'])) + { + $menu_context['sections'][$section_id]['areas'][$area_id]['icon_class'] = $menu_context['current_action'] . '_menu_icon ' . $area['icon_class']; + } + elseif (isset($area['icon'])) + { + if (substr($area['icon'], -4) === '.png' || substr($area['icon'], -4) === '.gif') + { + if (file_exists($settings['theme_dir'] . '/images/admin/big/' . $area['icon'])) + { + $menu_context['sections'][$section_id]['areas'][$area_id]['icon_file'] = $settings['theme_url'] . '/images/admin/big/' . $area['icon']; + } + elseif (file_exists($settings['default_theme_dir'] . '/images/admin/big/' . $area['icon'])) + { + $menu_context['sections'][$section_id]['areas'][$area_id]['icon_file'] = $settings['default_theme_url'] . '/images/admin/big/' . $area['icon']; + } + } + + $menu_context['sections'][$section_id]['areas'][$area_id]['icon_class'] = $menu_context['current_action'] . '_menu_icon ' . str_replace(array('.png', '.gif'), '', $area['icon']); + } + else + $menu_context['sections'][$section_id]['areas'][$area_id]['icon_class'] = $menu_context['current_action'] . '_menu_icon ' . str_replace(array('.png', '.gif'), '', $area_id); + + // This is a shortcut for Font-Icon users so they don't have to re-do whole CSS. + $menu_context['sections'][$section_id]['areas'][$area_id]['plain_class'] = !empty($area['icon']) ? $area['icon'] : ''; + + // Some areas may be listed but not active, which we show as greyed out. + $menu_context['sections'][$section_id]['areas'][$area_id]['inactive'] = !empty($area['inactive']); + + // Did it have subsections? + if (!empty($area['subsections'])) + { + $menu_context['sections'][$section_id]['areas'][$area_id]['subsections'] = array(); + $first_sa = $last_sa = null; + foreach ($area['subsections'] as $sa => $sub) + { + if ((empty($sub[1]) || allowedTo($sub[1])) && (!isset($sub['enabled']) || !empty($sub['enabled']))) + { + if ($first_sa == null) + $first_sa = $sa; + + $menu_context['sections'][$section_id]['areas'][$area_id]['subsections'][$sa] = array('label' => $sub[0]); + // Custom URL? + if (isset($sub['url'])) + $menu_context['sections'][$section_id]['areas'][$area_id]['subsections'][$sa]['url'] = $sub['url']; + + // Is there a counter amount to show for this subsection? + if (!empty($sub['amt'])) + $menu_context['sections'][$section_id]['areas'][$area_id]['subsections'][$sa]['amt'] = $sub['amt']; + + // A bit complicated - but is this set? + if ($menu_context['current_area'] == $area_id) + { + // Save which is the first... + if (empty($first_sa)) + $first_sa = $sa; + + // Is this the current subsection? + if (isset($_REQUEST['sa']) && $_REQUEST['sa'] == $sa) + $menu_context['current_subsection'] = $sa; + // Otherwise is it the default? + elseif (!isset($menu_context['current_subsection']) && !empty($sub[2])) + $menu_context['current_subsection'] = $sa; + } + + // Let's assume this is the last, for now. + $last_sa = $sa; + } + // Mark it as disabled... + else + $menu_context['sections'][$section_id]['areas'][$area_id]['subsections'][$sa]['disabled'] = true; + } + + // Set which one is first, last and selected in the group. + if (!empty($menu_context['sections'][$section_id]['areas'][$area_id]['subsections'])) + { + $menu_context['sections'][$section_id]['areas'][$area_id]['subsections'][$context['right_to_left'] ? $last_sa : $first_sa]['is_first'] = true; + $menu_context['sections'][$section_id]['areas'][$area_id]['subsections'][$context['right_to_left'] ? $first_sa : $last_sa]['is_last'] = true; + + if ($menu_context['current_area'] == $area_id && !isset($menu_context['current_subsection'])) + $menu_context['current_subsection'] = $first_sa; + } + } + } + } + + // Is this the current section? + if ($menu_context['current_area'] == $area_id && empty($found_section)) + { + // Only do this once? + $found_section = true; + + // Update the context if required - as we can have areas pretending to be others. ;) + $menu_context['current_section'] = $section_id; + $menu_context['current_area'] = isset($area['select']) ? $area['select'] : $area_id; + + // This will be the data we return. + $include_data = $area; + } + // Make sure we have something in case it's an invalid area. + elseif (empty($found_section) && empty($include_data)) + { + $menu_context['current_section'] = $section_id; + $backup_area = isset($area['select']) ? $area['select'] : $area_id; + $include_data = $area; + } + } + } + } + + foreach ($menu_context['sections'] as $section_id => $section) + { + if (!empty($section['areas'])) + { + foreach ($section['areas'] as $area_id => $area) + { + if (!empty($area['subsections'])) + { + $menu_context['sections'][$section_id]['areas'][$area_id]['hide_subsections'] = true; + + foreach ($area['subsections'] as $sa => $sub) + $menu_context['sections'][$section_id]['areas'][$area_id]['hide_subsections'] &= !empty($sub['disabled']); + } + } + } + } + + // Should we use a custom base url, or use the default? + $menu_context['base_url'] = isset($menuOptions['base_url']) ? $menuOptions['base_url'] : $scripturl . '?action=' . $menu_context['current_action']; + + // If we didn't find the area we were looking for go to a default one. + if (isset($backup_area) && empty($found_section)) + $menu_context['current_area'] = $backup_area; + + // If there are sections quickly goes through all the sections to check if the base menu has an url + if (!empty($menu_context['current_section'])) + { + $menu_context['sections'][$menu_context['current_section']]['selected'] = true; + $menu_context['sections'][$menu_context['current_section']]['areas'][$menu_context['current_area']]['selected'] = true; + if (!empty($menu_context['sections'][$menu_context['current_section']]['areas'][$menu_context['current_area']]['subsections'][$context['current_subaction']])) + $menu_context['sections'][$menu_context['current_section']]['areas'][$menu_context['current_area']]['subsections'][$context['current_subaction']]['selected'] = true; + + foreach ($menu_context['sections'] as $section_id => $section) + foreach ($section['areas'] as $area_id => $area) + { + if (!isset($menu_context['sections'][$section_id]['url'])) + { + $menu_context['sections'][$section_id]['url'] = isset($area['url']) ? $area['url'] : $menu_context['base_url'] . ';area=' . $area_id; + break; + } + } + } + + // If still no data then return - nothing to show! + if (empty($menu_context['sections'])) + { + // Never happened! + $context['max_menu_id']--; + if ($context['max_menu_id'] == 0) + unset($context['max_menu_id']); + + return false; + } + + // Almost there - load the template and add to the template layers. + loadTemplate(isset($menuOptions['template_name']) ? $menuOptions['template_name'] : 'GenericMenu'); + $menu_context['layer_name'] = (isset($menuOptions['layer_name']) ? $menuOptions['layer_name'] : 'generic_menu') . '_dropdown'; + $context['template_layers'][] = $menu_context['layer_name']; + + // Check we had something - for sanity sake. + if (empty($include_data)) + return false; + + // Finally - return information on the selected item. + $include_data += array( + 'current_action' => $menu_context['current_action'], + 'current_area' => $menu_context['current_area'], + 'current_section' => $menu_context['current_section'], + 'current_subsection' => !empty($menu_context['current_subsection']) ? $menu_context['current_subsection'] : '', + ); + + return $include_data; +} + +/** + * Delete a menu. + * + * @param string $menu_id The ID of the menu to destroy or 'last' for the most recent one + * @return bool|void False if the menu doesn't exist, nothing otherwise + */ +function destroyMenu($menu_id = 'last') +{ + global $context; + + $menu_name = $menu_id == 'last' && isset($context['max_menu_id']) && isset($context['menu_data_' . $context['max_menu_id']]) ? 'menu_data_' . $context['max_menu_id'] : 'menu_data_' . $menu_id; + if (!isset($context[$menu_name])) + return false; + + $layer_index = array_search($context[$menu_name]['layer_name'], $context['template_layers']); + if ($layer_index !== false) + unset($context['template_layers'][$layer_index]); + + unset($context[$menu_name]); +} + +?> \ No newline at end of file diff --git a/Sources/Subs-MessageIndex.php b/Sources/Subs-MessageIndex.php new file mode 100644 index 0000000..18d0833 --- /dev/null +++ b/Sources/Subs-MessageIndex.php @@ -0,0 +1,98 @@ + $row['id_cat'], + 'name' => $row['cat_name'], + 'boards' => array(), + ); + + $return_value[$row['id_cat']]['boards'][$row['id_board']] = array( + 'id' => $row['id_board'], + 'name' => $row['board_name'], + 'child_level' => $row['child_level'], + 'redirect' => $row['redirect'], + 'selected' => isset($boardListOptions['selected_board']) && $boardListOptions['selected_board'] == $row['id_board'], + ); + } + } + $smcFunc['db_free_result']($request); + + require_once($sourcedir . '/Subs-Boards.php'); + sortCategories($return_value); + + return $return_value; +} + +?> \ No newline at end of file diff --git a/Sources/Subs-Notify.php b/Sources/Subs-Notify.php new file mode 100644 index 0000000..7835f89 --- /dev/null +++ b/Sources/Subs-Notify.php @@ -0,0 +1,183 @@ + array (pref name -> value), with user id 0 representing the defaults + */ +function getNotifyPrefs($members, $prefs = '', $process_default = false) +{ + global $smcFunc; + + // We want this as an array whether it is or not. + $members = is_array($members) ? $members : (array) $members; + + if (!empty($prefs)) + $prefs = is_array($prefs) ? $prefs : (array) $prefs; + + $result = array(); + + // We want to now load the default, which is stored with a member id of 0. + $members[] = 0; + + $request = $smcFunc['db_query']('', ' + SELECT id_member, alert_pref, alert_value + FROM {db_prefix}user_alerts_prefs + WHERE id_member IN ({array_int:members})' . (!empty($prefs) ? ' + AND alert_pref IN ({array_string:prefs})' : ''), + array( + 'members' => $members, + 'prefs' => $prefs, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $result[$row['id_member']][$row['alert_pref']] = $row['alert_value']; + + // We may want to keep the default values separate from a given user's. Or we might not. + if ($process_default && isset($result[0])) + { + foreach ($members as $member) + if (isset($result[$member])) + $result[$member] += $result[0]; + else + $result[$member] = $result[0]; + + unset ($result[0]); + } + + return $result; +} + +/** + * Sets the list of preferences for a single user. + * + * @param int $memID The user whose preferences you are setting + * @param array $prefs An array key of pref -> value + */ +function setNotifyPrefs($memID, $prefs = array()) +{ + global $smcFunc; + + if (empty($prefs) || !is_int($memID)) + return; + + $update_rows = array(); + foreach ($prefs as $k => $v) + $update_rows[] = array($memID, $k, min(max((int) $v, -128), 127)); + + $smcFunc['db_insert']('replace', + '{db_prefix}user_alerts_prefs', + array('id_member' => 'int', 'alert_pref' => 'string', 'alert_value' => 'int'), + $update_rows, + array('id_member', 'alert_pref') + ); +} + +/** + * Deletes notification preference + * + * @param int $memID The user whose preference you're setting + * @param array $prefs The preferences to delete + */ +function deleteNotifyPrefs($memID, array $prefs) +{ + global $smcFunc; + + if (empty($prefs) || empty($memID)) + return; + + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}user_alerts_prefs + WHERE id_member = {int:member} + AND alert_pref IN ({array_string:prefs})', + array( + 'member' => $memID, + 'prefs' => $prefs, + ) + ); +} + +/** + * Verifies a member's unsubscribe token, then returns some member info + * + * @param string $type The type of notification the token is for (e.g. 'board', 'topic', etc.) + * @return array The id and email address of the specified member + */ +function getMemberWithToken($type) +{ + global $smcFunc, $board, $topic, $modSettings; + + // Keep it sanitary, folks + $id_member = !empty($_REQUEST['u']) ? (int) $_REQUEST['u'] : 0; + + // We can't do anything without these + if (empty($id_member) || empty($_REQUEST['token'])) + fatal_lang_error('unsubscribe_invalid', false); + + // Get the user info we need + $request = $smcFunc['db_query']('', ' + SELECT id_member AS id, email_address AS email + FROM {db_prefix}members + WHERE id_member = {int:id_member}', + array( + 'id_member' => $id_member, + ) + ); + if ($smcFunc['db_num_rows']($request) == 0) + fatal_lang_error('unsubscribe_invalid', false); + $member_info = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + // What token are we expecting? + $expected_token = createUnsubscribeToken($member_info['id'], $member_info['email'], $type, in_array($type, array('board', 'topic')) && !empty($$type) ? $$type : 0); + + // Don't do anything if the token they gave is wrong + if ($_REQUEST['token'] !== $expected_token) + fatal_lang_error('unsubscribe_invalid', false); + + // At this point, we know we have a legitimate unsubscribe request + return $member_info; +} + +/** + * Builds an unsubscribe token + * + * @param int $memID The id of the member that this token is for + * @param string $email The member's email address + * @param string $type The type of notification the token is for (e.g. 'board', 'topic', etc.) + * @param int $itemID The id of the notification item, if applicable. + * @return string The unsubscribe token + */ +function createUnsubscribeToken($memID, $email, $type = '', $itemID = 0) +{ + $token_items = implode(' ', array($memID, $email, $type, $itemID)); + + // When the message is public and the key is secret, an HMAC is the appropriate tool. + $token = hash_hmac('sha256', $token_items, get_auth_secret(), true); + + // When using an HMAC, 80 bits (10 bytes) is plenty for security. + $token = substr($token, 0, 10); + + // Use base64 (with URL-friendly characters) to make the token shorter. + return strtr(base64_encode($token), array('+' => '_', '/' => '-', '=' => '')); +} + +?> \ No newline at end of file diff --git a/Sources/Subs-Package.php b/Sources/Subs-Package.php new file mode 100644 index 0000000..529b252 --- /dev/null +++ b/Sources/Subs-Package.php @@ -0,0 +1,3180 @@ + $v) + { + if (in_array($k, $octdec)) + $current[$k] = octdec(trim($v)); + else + $current[$k] = trim($v); + } + + if ($current['type'] == '5' && substr($current['filename'], -1) != '/') + $current['filename'] .= '/'; + + $checksum = 256; + for ($i = 0; $i < 148; $i++) + $checksum += ord($header[$i]); + for ($i = 156; $i < 512; $i++) + $checksum += ord($header[$i]); + + if ($current['checksum'] != $checksum) + break; + + $size = ceil($current['size'] / 512); + $current['data'] = substr($data, ++$offset << 9, $current['size']); + $offset += $size; + + // Not a directory and doesn't exist already... + if (substr($current['filename'], -1, 1) != '/' && $destination !== null && !file_exists($destination . '/' . $current['filename'])) + $write_this = true; + // File exists... check if it is newer. + elseif (substr($current['filename'], -1, 1) != '/') + $write_this = $overwrite || ($destination !== null && filemtime($destination . '/' . $current['filename']) < $current['mtime']); + // Folder... create. + elseif ($destination !== null && !$single_file) + { + // Protect from accidental parent directory writing... + $current['filename'] = strtr($current['filename'], array('../' => '', '/..' => '')); + + if (!file_exists($destination . '/' . $current['filename'])) + mktree($destination . '/' . $current['filename'], 0777); + $write_this = false; + } + else + $write_this = false; + + if ($write_this && $destination !== null) + { + if (strpos($current['filename'], '/') !== false && !$single_file) + mktree($destination . '/' . dirname($current['filename']), 0777); + + // Is this the file we're looking for? + if ($single_file && ($destination == $current['filename'] || $destination == '*/' . basename($current['filename']))) + return $current['data']; + // If we're looking for another file, keep going. + elseif ($single_file) + continue; + // Looking for restricted files? + elseif ($files_to_extract !== null && !in_array($current['filename'], $files_to_extract)) + continue; + + package_put_contents($destination . '/' . $current['filename'], $current['data']); + } + + if (substr($current['filename'], -1, 1) != '/') + $return[] = array( + 'filename' => $current['filename'], + 'md5' => md5($current['data']), + 'preview' => substr($current['data'], 0, 100), + 'size' => $current['size'], + 'skipped' => false + ); + } + + if ($destination !== null && !$single_file) + package_flush_cache(); + + if ($single_file) + return false; + else + return $return; +} + +/** + * Extract zip data. + * + * If single_file is true, destination can start with * and / to signify that the file may come from any directory. + * Destination should not begin with a / if single_file is true. + * + * @param string $data ZIP data + * @param string $destination Null to display a listing of files in the archive, the destination for the files in the archive or the name of a single file to display (if $single_file is true) + * @param boolean $single_file If true, returns the contents of the file specified by destination or false if the file can't be found (default value is false). + * @param boolean $overwrite If true, will overwrite files with newer modication times. Default is false. + * @param array $files_to_extract + * @return mixed If destination is null, return a short array of a few file details optionally delimited by $files_to_extract. If $single_file is true, return contents of a file as a string; false otherwise + */ +function read_zip_data($data, $destination, $single_file = false, $overwrite = false, $files_to_extract = null) +{ + umask(0); + if ($destination !== null && !file_exists($destination) && !$single_file) + mktree($destination, 0777); + + // Search for the end of directory signature 0x06054b50. + if (($data_ecr = strrpos($data, "\x50\x4b\x05\x06")) === false) + return false; + $return = array(); + + // End of central directory record (EOCD) + $cdir = unpack('vdisk/@4/vdisk_entries/ventries/@12/Voffset', substr($data, $data_ecr + 4, 16)); + + // We only support a single disk. + if ($cdir['disk_entries'] != $cdir['entries']) + return false; + + // First central file directory + $pos_entry = $cdir['offset']; + + for ($i = 0; $i < $cdir['entries']; $i++) + { + // Central directory file header + $header = unpack('Vcompressed_size/@8/vlen1/vlen2/vlen3/vdisk/@22/Voffset', substr($data, $pos_entry + 20, 26)); + + // Sanity check: same disk? + if ($header['disk'] != $cdir['disk']) + continue; + + // Next central file directory + $pos_entry += 46 + $header['len1'] + $header['len2'] + $header['len3']; + + // Local file header (so called because it is in the same file as the data in multi-part archives) + $file_info = unpack( + 'vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len', + substr($data, $header['offset'] + 6, 24) + ); + + $file_info['filename'] = substr($data, $header['offset'] + 30, $file_info['filename_len']); + $is_file = substr($file_info['filename'], -1) != '/'; + + /* + * If the bit at offset 3 (0x08) of the general-purpose flags field + * is set, then the CRC-32 and file sizes are not known when the header + * is written. The fields in the local header are filled with zero, and + * the CRC-32 and size are appended in a 12-byte structure (optionally + * preceded by a 4-byte signature) immediately after the compressed data: + */ + if ($file_info['flag'] & 0x08) + { + $gplen = $header['offset'] + 30 + $file_info['filename_len'] + $file_info['extra_len'] + $header['compressed_size']; + + // The spec allows for an optional header in the general purpose record + if (substr($data, $gplen, 4) === "\x50\x4b\x07\x08") + $gplen += 4; + + if (($general_purpose = unpack('Vcrc/Vcompressed_size/Vsize', substr($data, $gplen, 12))) !== false) + $file_info = $general_purpose + $file_info; + } + + $write_this = false; + if ($destination !== null) + { + // If this is a file, and it doesn't exist.... happy days! + if ($is_file) + $write_this = !file_exists($destination . '/' . $file_info['filename']) || $overwrite; + // This is a directory, so we're gonna want to create it. (probably...) + elseif (!$single_file) + { + $file_info['filename'] = strtr($file_info['filename'], array('../' => '', '/..' => '')); + + if (!file_exists($destination . '/' . $file_info['filename'])) + mktree($destination . '/' . $file_info['filename'], 0777); + } + } + + // Get the actual compressed data. + $file_info['data'] = substr( + $data, + $header['offset'] + 30 + $file_info['filename_len'] + $file_info['extra_len'], + $file_info['compressed_size'] + ); + + // Only for the deflate method (the most common) + if ($file_info['compression'] == 8) + $file_info['data'] = gzinflate($file_info['data']); + // We do not support any other compresion methods. + elseif ($file_info['compression'] != 0) + continue; + + // PKZip/ITU-T V.42 CRC-32 + if (hash('crc32b', $file_info['data']) !== sprintf('%08x', $file_info['crc'])) + continue; + + // Okay! We can write this file, looks good from here... + if ($write_this) + { + // If we're looking for a specific file, and this is it... ka-bam, baby. + if ($single_file && ($destination == $file_info['filename'] || $destination == '*/' . basename($file_info['filename']))) + return $file_info['data']; + // Oh, another file? Fine. You don't like this file, do you? I know how it is. Yeah... just go away. No, don't apologize. I know this file's just not *good enough* for you. + elseif ($single_file || ($files_to_extract !== null && !in_array($file_info['filename'], $files_to_extract))) + continue; + + if (!$single_file && strpos($file_info['filename'], '/') !== false) + mktree($destination . '/' . dirname($file_info['filename']), 0777); + + package_put_contents($destination . '/' . $file_info['filename'], $file_info['data']); + } + + if ($is_file) + $return[] = array( + 'filename' => $file_info['filename'], + 'md5' => md5($file_info['data']), + 'preview' => substr($file_info['data'], 0, 100), + 'size' => $file_info['size'], + 'skipped' => false + ); + } + + if ($destination !== null && !$single_file) + package_flush_cache(); + + return $single_file ? false : $return; +} + +/** + * Checks the existence of a remote file since file_exists() does not do remote. + * will return false if the file is "moved permanently" or similar. + * + * @param string $url The URL to parse + * @return bool Whether the specified URL exists + */ +function url_exists($url) +{ + $a_url = parse_iri($url); + + if (!isset($a_url['scheme'])) + return false; + + // Attempt to connect... + $temp = ''; + $fid = fsockopen($a_url['host'], !isset($a_url['port']) ? 80 : $a_url['port'], $temp, $temp, 8); + if (!$fid) + return false; + + fputs($fid, 'HEAD ' . $a_url['path'] . ' HTTP/1.0' . "\r\n" . 'Host: ' . $a_url['host'] . "\r\n\r\n"); + $head = fread($fid, 1024); + fclose($fid); + + return preg_match('~^HTTP/.+\s+(20[01]|30[127])~i', $head) == 1; +} + +/** + * Loads and returns an array of installed packages. + * + * default sort order is package_installed time + * + * @return array An array of info about installed packages + */ +function loadInstalledPackages() +{ + global $smcFunc; + + // Load the packages from the database - note this is ordered by install time to ensure latest package uninstalled first. + $request = $smcFunc['db_query']('', ' + SELECT id_install, package_id, filename, name, version, time_installed + FROM {db_prefix}log_packages + WHERE install_state != {int:not_installed} + ORDER BY time_installed DESC', + array( + 'not_installed' => 0, + ) + ); + $installed = array(); + $found = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // Already found this? If so don't add it twice! + if (in_array($row['package_id'], $found)) + continue; + + $found[] = $row['package_id']; + + $row = htmlspecialchars__recursive($row); + + $installed[] = array( + 'id' => $row['id_install'], + 'name' => $smcFunc['htmlspecialchars']($row['name']), + 'filename' => $row['filename'], + 'package_id' => $row['package_id'], + 'version' => $smcFunc['htmlspecialchars']($row['version']), + 'time_installed' => !empty($row['time_installed']) ? $row['time_installed'] : 0, + ); + } + $smcFunc['db_free_result']($request); + + return $installed; +} + +/** + * Loads a package's information and returns a representative array. + * - expects the file to be a package in Packages/. + * - returns a error string if the package-info is invalid. + * - otherwise returns a basic array of id, version, filename, and similar information. + * - an xmlArray is available in 'xml'. + * + * @param string $gzfilename The path to the file + * @return array|string An array of info about the file or a string indicating an error + */ +function getPackageInfo($gzfilename) +{ + global $sourcedir, $packagesdir; + + // Extract package-info.xml from downloaded file. (*/ is used because it could be in any directory.) + if (strpos($gzfilename, 'http://') !== false || strpos($gzfilename, 'https://') !== false) + $packageInfo = read_tgz_data($gzfilename, 'package-info.xml', true); + else + { + if (!file_exists($packagesdir . '/' . $gzfilename)) + return 'package_get_error_not_found'; + + if (is_file($packagesdir . '/' . $gzfilename)) + $packageInfo = read_tgz_file($packagesdir . '/' . $gzfilename, '*/package-info.xml', true); + elseif (file_exists($packagesdir . '/' . $gzfilename . '/package-info.xml')) + $packageInfo = file_get_contents($packagesdir . '/' . $gzfilename . '/package-info.xml'); + else + return 'package_get_error_missing_xml'; + } + + // Nothing? + if (empty($packageInfo)) + { + // Perhaps they are trying to install a theme, lets tell them nicely this is the wrong function + $packageInfo = read_tgz_file($packagesdir . '/' . $gzfilename, '*/theme_info.xml', true); + if (!empty($packageInfo)) + return 'package_get_error_is_theme'; + else + return 'package_get_error_is_zero'; + } + + // Parse package-info.xml into an xmlArray. + require_once($sourcedir . '/Class-Package.php'); + $packageInfo = new xmlArray($packageInfo); + + // @todo Error message of some sort? + if (!$packageInfo->exists('package-info[0]')) + return 'package_get_error_packageinfo_corrupt'; + + $packageInfo = $packageInfo->path('package-info[0]'); + + $package = $packageInfo->to_array(); + $package = htmlspecialchars__recursive($package); + $package['xml'] = $packageInfo; + $package['filename'] = $gzfilename; + + // Don't want to mess with code... + $types = array('install', 'uninstall', 'upgrade'); + foreach ($types as $type) + { + if (isset($package[$type]['code'])) + { + $package[$type]['code'] = un_htmlspecialchars($package[$type]['code']); + } + } + + if (!isset($package['type'])) + $package['type'] = 'modification'; + + return $package; +} + +/** + * Create a chmod control for chmoding files. + * + * @param array $chmodFiles Which files to chmod + * @param array $chmodOptions Options for chmod + * @param bool $restore_write_status Whether to restore write status + * @return array An array of file info + */ +function create_chmod_control($chmodFiles = array(), $chmodOptions = array(), $restore_write_status = false) +{ + global $context, $modSettings, $package_ftp, $boarddir, $txt, $sourcedir, $scripturl; + + // If we're restoring the status of existing files prepare the data. + if ($restore_write_status && isset($_SESSION['pack_ftp']) && !empty($_SESSION['pack_ftp']['original_perms'])) + { + /** + * Get a listing of files that will need to be set back to the original state + * + * @param null $dummy1 + * @param null $dummy2 + * @param null $dummy3 + * @param bool $do_change + * @return array An array of info about the files that need to be restored back to their original state + */ + function list_restoreFiles($dummy1, $dummy2, $dummy3, $do_change) + { + global $txt; + + $restore_files = array(); + foreach ($_SESSION['pack_ftp']['original_perms'] as $file => $perms) + { + // Check the file still exists, and the permissions were indeed different than now. + $file_permissions = @fileperms($file); + if (!file_exists($file) || $file_permissions == $perms) + { + unset($_SESSION['pack_ftp']['original_perms'][$file]); + continue; + } + + // Are we wanting to change the permission? + if ($do_change && isset($_POST['restore_files']) && in_array($file, $_POST['restore_files'])) + { + // Use FTP if we have it. + // @todo where does $package_ftp get set? + if (!empty($package_ftp)) + { + $ftp_file = strtr($file, array($_SESSION['pack_ftp']['root'] => '')); + $package_ftp->chmod($ftp_file, $perms); + } + else + smf_chmod($file, $perms); + + $new_permissions = @fileperms($file); + $result = $new_permissions == $perms ? 'success' : 'failure'; + unset($_SESSION['pack_ftp']['original_perms'][$file]); + } + elseif ($do_change) + { + $new_permissions = ''; + $result = 'skipped'; + unset($_SESSION['pack_ftp']['original_perms'][$file]); + } + + // Record the results! + $restore_files[] = array( + 'path' => $file, + 'old_perms_raw' => $perms, + 'old_perms' => substr(sprintf('%o', $perms), -4), + 'cur_perms' => substr(sprintf('%o', $file_permissions), -4), + 'new_perms' => isset($new_permissions) ? substr(sprintf('%o', $new_permissions), -4) : '', + 'result' => isset($result) ? $result : '', + 'writable_message' => '' . (@is_writable($file) ? $txt['package_file_perms_writable'] : $txt['package_file_perms_not_writable']) . '', + ); + } + + return $restore_files; + } + + $listOptions = array( + 'id' => 'restore_file_permissions', + 'title' => $txt['package_restore_permissions'], + 'get_items' => array( + 'function' => 'list_restoreFiles', + 'params' => array( + !empty($_POST['restore_perms']), + ), + ), + 'columns' => array( + 'path' => array( + 'header' => array( + 'value' => $txt['package_restore_permissions_filename'], + ), + 'data' => array( + 'db' => 'path', + 'class' => 'smalltext', + ), + ), + 'old_perms' => array( + 'header' => array( + 'value' => $txt['package_restore_permissions_orig_status'], + ), + 'data' => array( + 'db' => 'old_perms', + 'class' => 'smalltext', + ), + ), + 'cur_perms' => array( + 'header' => array( + 'value' => $txt['package_restore_permissions_cur_status'], + ), + 'data' => array( + 'function' => function($rowData) use ($txt) + { + $formatTxt = $rowData['result'] == '' || $rowData['result'] == 'skipped' ? $txt['package_restore_permissions_pre_change'] : $txt['package_restore_permissions_post_change']; + return sprintf($formatTxt, $rowData['cur_perms'], $rowData['new_perms'], $rowData['writable_message']); + }, + 'class' => 'smalltext', + ), + ), + 'check' => array( + 'header' => array( + 'value' => '', + 'class' => 'centercol', + ), + 'data' => array( + 'sprintf' => array( + 'format' => '', + 'params' => array( + 'path' => false, + ), + ), + 'class' => 'centercol', + ), + ), + 'result' => array( + 'header' => array( + 'value' => $txt['package_restore_permissions_result'], + ), + 'data' => array( + 'function' => function($rowData) use ($txt) + { + return $txt['package_restore_permissions_action_' . $rowData['result']]; + }, + 'class' => 'smalltext', + ), + ), + ), + 'form' => array( + 'href' => !empty($chmodOptions['destination_url']) ? $chmodOptions['destination_url'] : $scripturl . '?action=admin;area=packages;sa=perms;restore;' . $context['session_var'] . '=' . $context['session_id'], + ), + 'additional_rows' => array( + array( + 'position' => 'below_table_data', + 'value' => '', + 'class' => 'titlebg', + ), + array( + 'position' => 'after_title', + 'value' => '' . $txt['package_restore_permissions_desc'] . '', + 'class' => 'windowbg', + ), + ), + ); + + // Work out what columns and the like to show. + if (!empty($_POST['restore_perms'])) + { + $listOptions['additional_rows'][1]['value'] = sprintf($txt['package_restore_permissions_action_done'], $scripturl . '?action=admin;area=packages;sa=perms;' . $context['session_var'] . '=' . $context['session_id']); + unset($listOptions['columns']['check'], $listOptions['form'], $listOptions['additional_rows'][0]); + + $context['sub_template'] = 'show_list'; + $context['default_list'] = 'restore_file_permissions'; + } + else + { + unset($listOptions['columns']['result']); + } + + // Create the list for display. + require_once($sourcedir . '/Subs-List.php'); + createList($listOptions); + + // If we just restored permissions then whereever we are, we are now done and dusted. + if (!empty($_POST['restore_perms'])) + obExit(); + } + // Otherwise, it's entirely irrelevant? + elseif ($restore_write_status) + return true; + + // This is where we report what we got up to. + $return_data = array( + 'files' => array( + 'writable' => array(), + 'notwritable' => array(), + ), + ); + + // If we have some FTP information already, then let's assume it was required and try to get ourselves connected. + if (!empty($_SESSION['pack_ftp']['connected'])) + { + // Load the file containing the ftp_connection class. + require_once($sourcedir . '/Class-Package.php'); + + $package_ftp = new ftp_connection($_SESSION['pack_ftp']['server'], $_SESSION['pack_ftp']['port'], $_SESSION['pack_ftp']['username'], package_crypt($_SESSION['pack_ftp']['password'])); + } + + // Just got a submission did we? + if (empty($package_ftp) && isset($_POST['ftp_username'])) + { + require_once($sourcedir . '/Class-Package.php'); + $ftp = new ftp_connection($_POST['ftp_server'], $_POST['ftp_port'], $_POST['ftp_username'], $_POST['ftp_password']); + + // We're connected, jolly good! + if ($ftp->error === false) + { + // Common mistake, so let's try to remedy it... + if (!$ftp->chdir($_POST['ftp_path'])) + { + $ftp_error = $ftp->last_message; + $ftp->chdir(preg_replace('~^/home[2]?/[^/]+?~', '', $_POST['ftp_path'])); + } + + if (!in_array($_POST['ftp_path'], array('', '/'))) + { + $ftp_root = strtr($boarddir, array($_POST['ftp_path'] => '')); + if (substr($ftp_root, -1) == '/' && ($_POST['ftp_path'] == '' || substr($_POST['ftp_path'], 0, 1) == '/')) + $ftp_root = substr($ftp_root, 0, -1); + } + else + $ftp_root = $boarddir; + + $_SESSION['pack_ftp'] = array( + 'server' => $_POST['ftp_server'], + 'port' => $_POST['ftp_port'], + 'username' => $_POST['ftp_username'], + 'password' => package_crypt($_POST['ftp_password']), + 'path' => $_POST['ftp_path'], + 'root' => $ftp_root, + 'connected' => true, + ); + + if (!isset($modSettings['package_path']) || $modSettings['package_path'] != $_POST['ftp_path']) + updateSettings(array('package_path' => $_POST['ftp_path'])); + + // This is now the primary connection. + $package_ftp = $ftp; + } + } + + // Now try to simply make the files writable, with whatever we might have. + if (!empty($chmodFiles)) + { + foreach ($chmodFiles as $k => $file) + { + // Sometimes this can somehow happen maybe? + if (empty($file)) + unset($chmodFiles[$k]); + // Already writable? + elseif (@is_writable($file)) + $return_data['files']['writable'][] = $file; + else + { + // Now try to change that. + $return_data['files'][package_chmod($file, 'writable', true) ? 'writable' : 'notwritable'][] = $file; + } + } + } + + // Have we still got nasty files which ain't writable? Dear me we need more FTP good sir. + if (empty($package_ftp) && (!empty($return_data['files']['notwritable']) || !empty($chmodOptions['force_find_error']))) + { + if (!isset($ftp) || $ftp->error !== false) + { + if (!isset($ftp)) + { + require_once($sourcedir . '/Class-Package.php'); + $ftp = new ftp_connection(null); + } + elseif ($ftp->error !== false && !isset($ftp_error)) + $ftp_error = $ftp->last_message === null ? '' : $ftp->last_message; + + list ($username, $detect_path, $found_path) = $ftp->detect_path($boarddir); + + if ($found_path) + $_POST['ftp_path'] = $detect_path; + elseif (!isset($_POST['ftp_path'])) + $_POST['ftp_path'] = isset($modSettings['package_path']) ? $modSettings['package_path'] : $detect_path; + + if (!isset($_POST['ftp_username'])) + $_POST['ftp_username'] = $username; + } + + $context['package_ftp'] = array( + 'server' => isset($_POST['ftp_server']) ? $_POST['ftp_server'] : (isset($modSettings['package_server']) ? $modSettings['package_server'] : 'localhost'), + 'port' => isset($_POST['ftp_port']) ? $_POST['ftp_port'] : (isset($modSettings['package_port']) ? $modSettings['package_port'] : '21'), + 'username' => isset($_POST['ftp_username']) ? $_POST['ftp_username'] : (isset($modSettings['package_username']) ? $modSettings['package_username'] : ''), + 'path' => $_POST['ftp_path'], + 'error' => empty($ftp_error) ? null : $ftp_error, + 'destination' => !empty($chmodOptions['destination_url']) ? $chmodOptions['destination_url'] : '', + ); + + // Which files failed? + if (!isset($context['notwritable_files'])) + $context['notwritable_files'] = array(); + $context['notwritable_files'] = array_merge($context['notwritable_files'], $return_data['files']['notwritable']); + + // Sent here to die? + if (!empty($chmodOptions['crash_on_error'])) + { + $context['page_title'] = $txt['package_ftp_necessary']; + $context['sub_template'] = 'ftp_required'; + obExit(); + } + } + + return $return_data; +} + +/** + * Use FTP functions to work with a package download/install + * + * @param string $destination_url The destination URL + * @param null|array $files The files to CHMOD + * @param bool $return Whether to return an array of file info if there's an error + * @return array An array of file info + */ +function packageRequireFTP($destination_url, $files = null, $return = false) +{ + global $context, $modSettings, $package_ftp, $boarddir, $txt, $sourcedir; + + // Try to make them writable the manual way. + if ($files !== null) + { + foreach ($files as $k => $file) + { + // If this file doesn't exist, then we actually want to look at the directory, no? + if (!file_exists($file)) + $file = dirname($file); + + // This looks odd, but it's an attempt to work around PHP suExec. + if (!@is_writable($file)) + smf_chmod($file, 0755); + if (!@is_writable($file)) + smf_chmod($file, 0777); + if (!@is_writable(dirname($file))) + smf_chmod($file, 0755); + if (!@is_writable(dirname($file))) + smf_chmod($file, 0777); + + $fp = is_dir($file) ? @opendir($file) : @fopen($file, 'rb'); + if (@is_writable($file) && $fp) + { + unset($files[$k]); + if (!is_dir($file)) + fclose($fp); + else + closedir($fp); + } + } + + // No FTP required! + if (empty($files)) + return array(); + } + + // They've opted to not use FTP, and try anyway. + if (isset($_SESSION['pack_ftp']) && $_SESSION['pack_ftp'] == false) + { + if ($files === null) + return array(); + + foreach ($files as $k => $file) + { + // This looks odd, but it's an attempt to work around PHP suExec. + if (!file_exists($file)) + { + mktree(dirname($file), 0755); + @touch($file); + smf_chmod($file, 0755); + } + + if (!@is_writable($file)) + smf_chmod($file, 0777); + if (!@is_writable(dirname($file))) + smf_chmod(dirname($file), 0777); + + if (@is_writable($file)) + unset($files[$k]); + } + + return $files; + } + elseif (isset($_SESSION['pack_ftp'])) + { + // Load the file containing the ftp_connection class. + require_once($sourcedir . '/Class-Package.php'); + + $package_ftp = new ftp_connection($_SESSION['pack_ftp']['server'], $_SESSION['pack_ftp']['port'], $_SESSION['pack_ftp']['username'], package_crypt($_SESSION['pack_ftp']['password'])); + + if ($files === null) + return array(); + + foreach ($files as $k => $file) + { + $ftp_file = strtr($file, array($_SESSION['pack_ftp']['root'] => '')); + + // This looks odd, but it's an attempt to work around PHP suExec. + if (!file_exists($file)) + { + mktree(dirname($file), 0755); + $package_ftp->create_file($ftp_file); + $package_ftp->chmod($ftp_file, 0755); + } + + if (!@is_writable($file)) + $package_ftp->chmod($ftp_file, 0777); + if (!@is_writable(dirname($file))) + $package_ftp->chmod(dirname($ftp_file), 0777); + + if (@is_writable($file)) + unset($files[$k]); + } + + return $files; + } + + if (isset($_POST['ftp_none'])) + { + $_SESSION['pack_ftp'] = false; + + $files = packageRequireFTP($destination_url, $files, $return); + return $files; + } + elseif (isset($_POST['ftp_username'])) + { + require_once($sourcedir . '/Class-Package.php'); + $ftp = new ftp_connection($_POST['ftp_server'], $_POST['ftp_port'], $_POST['ftp_username'], $_POST['ftp_password']); + + if ($ftp->error === false) + { + // Common mistake, so let's try to remedy it... + if (!$ftp->chdir($_POST['ftp_path'])) + { + $ftp_error = $ftp->last_message; + $ftp->chdir(preg_replace('~^/home[2]?/[^/]+?~', '', $_POST['ftp_path'])); + } + } + } + + if (!isset($ftp) || $ftp->error !== false) + { + if (!isset($ftp)) + { + require_once($sourcedir . '/Class-Package.php'); + $ftp = new ftp_connection(null); + } + elseif ($ftp->error !== false && !isset($ftp_error)) + $ftp_error = $ftp->last_message === null ? '' : $ftp->last_message; + + list ($username, $detect_path, $found_path) = $ftp->detect_path($boarddir); + + if ($found_path) + $_POST['ftp_path'] = $detect_path; + elseif (!isset($_POST['ftp_path'])) + $_POST['ftp_path'] = isset($modSettings['package_path']) ? $modSettings['package_path'] : $detect_path; + + if (!isset($_POST['ftp_username'])) + $_POST['ftp_username'] = $username; + + $context['package_ftp'] = array( + 'server' => isset($_POST['ftp_server']) ? $_POST['ftp_server'] : (isset($modSettings['package_server']) ? $modSettings['package_server'] : 'localhost'), + 'port' => isset($_POST['ftp_port']) ? $_POST['ftp_port'] : (isset($modSettings['package_port']) ? $modSettings['package_port'] : '21'), + 'username' => isset($_POST['ftp_username']) ? $_POST['ftp_username'] : (isset($modSettings['package_username']) ? $modSettings['package_username'] : ''), + 'path' => $_POST['ftp_path'], + 'error' => empty($ftp_error) ? null : $ftp_error, + 'destination' => $destination_url, + ); + + // If we're returning dump out here. + if ($return) + return $files; + + $context['page_title'] = $txt['package_ftp_necessary']; + $context['sub_template'] = 'ftp_required'; + obExit(); + } + else + { + if (!in_array($_POST['ftp_path'], array('', '/'))) + { + $ftp_root = strtr($boarddir, array($_POST['ftp_path'] => '')); + if (substr($ftp_root, -1) == '/' && ($_POST['ftp_path'] == '' || $_POST['ftp_path'][0] == '/')) + $ftp_root = substr($ftp_root, 0, -1); + } + else + $ftp_root = $boarddir; + + $_SESSION['pack_ftp'] = array( + 'server' => $_POST['ftp_server'], + 'port' => $_POST['ftp_port'], + 'username' => $_POST['ftp_username'], + 'password' => package_crypt($_POST['ftp_password']), + 'path' => $_POST['ftp_path'], + 'root' => $ftp_root, + ); + + if (!isset($modSettings['package_path']) || $modSettings['package_path'] != $_POST['ftp_path']) + updateSettings(array('package_path' => $_POST['ftp_path'])); + + $files = packageRequireFTP($destination_url, $files, $return); + } + + return $files; +} + +/** + * Parses the actions in package-info.xml file from packages. + * + * - package should be an xmlArray with package-info as its base. + * - testing_only should be true if the package should not actually be applied. + * - method can be upgrade, install, or uninstall. Its default is install. + * - previous_version should be set to the previous installed version of this package, if any. + * - does not handle failure terribly well; testing first is always better. + * + * @param xmlArray &$packageXML The info from the package-info file + * @param bool $testing_only Whether we're only testing + * @param string $method The method ('install', 'upgrade', or 'uninstall') + * @param string $previous_version The previous version of the mod, if method is 'upgrade' + * @return array An array of those changes made. + */ +function parsePackageInfo(&$packageXML, $testing_only = true, $method = 'install', $previous_version = '') +{ + global $packagesdir, $context, $temp_path, $language, $smcFunc; + + // Mayday! That action doesn't exist!! + if (empty($packageXML) || !$packageXML->exists($method)) + return array(); + + // We haven't found the package script yet... + $script = false; + $the_version = SMF_VERSION; + + // Emulation support... + if (!empty($_SESSION['version_emulate'])) + $the_version = $_SESSION['version_emulate']; + + // Single package emulation + if (!empty($_REQUEST['ve']) && !empty($_REQUEST['package'])) + { + $the_version = $_REQUEST['ve']; + $_SESSION['single_version_emulate'][$_REQUEST['package']] = $the_version; + } + if (!empty($_REQUEST['package']) && (!empty($_SESSION['single_version_emulate'][$_REQUEST['package']]))) + $the_version = $_SESSION['single_version_emulate'][$_REQUEST['package']]; + + // Get all the versions of this method and find the right one. + $these_methods = $packageXML->set($method); + foreach ($these_methods as $this_method) + { + // They specified certain versions this part is for. + if ($this_method->exists('@for')) + { + // Don't keep going if this won't work for this version of SMF. + if (!matchPackageVersion($the_version, $this_method->fetch('@for'))) + continue; + } + + // Upgrades may go from a certain old version of the mod. + if ($method == 'upgrade' && $this_method->exists('@from')) + { + // Well, this is for the wrong old version... + if (!matchPackageVersion($previous_version, $this_method->fetch('@from'))) + continue; + } + + // We've found it! + $script = $this_method; + break; + } + + // Bad news, a matching script wasn't found! + if (!($script instanceof xmlArray)) + return array(); + + // Find all the actions in this method - in theory, these should only be allowed actions. (* means all.) + $actions = $script->set('*'); + $return = array(); + + $temp_auto = 0; + $temp_path = $packagesdir . '/temp/' . (isset($context['base_path']) ? $context['base_path'] : ''); + + $context['readmes'] = array(); + $context['licences'] = array(); + + // This is the testing phase... nothing shall be done yet. + foreach ($actions as $action) + { + $actionType = $action->name(); + + if (in_array($actionType, array('readme', 'code', 'database', 'modification', 'redirect', 'license'))) + { + // Allow for translated readme and license files. + if ($actionType == 'readme' || $actionType == 'license') + { + $type = $actionType . 's'; + if ($action->exists('@lang')) + { + // Auto-select the language based on either request variable or current language. + if ((isset($_REQUEST['readme']) && $action->fetch('@lang') == $_REQUEST['readme']) || (isset($_REQUEST['license']) && $action->fetch('@lang') == $_REQUEST['license']) || (!isset($_REQUEST['readme']) && $action->fetch('@lang') == $language) || (!isset($_REQUEST['license']) && $action->fetch('@lang') == $language)) + { + // In case the user put the blocks in the wrong order. + if (isset($context[$type]['selected']) && $context[$type]['selected'] == 'default') + $context[$type][] = 'default'; + + $context[$type]['selected'] = $smcFunc['htmlspecialchars']($action->fetch('@lang')); + } + else + { + // We don't want this now, but we'll allow the user to select to read it. + $context[$type][] = $smcFunc['htmlspecialchars']($action->fetch('@lang')); + continue; + } + } + // Fallback when we have no lang parameter. + else + { + // Already selected one for use? + if (isset($context[$type]['selected'])) + { + $context[$type][] = 'default'; + continue; + } + else + $context[$type]['selected'] = 'default'; + } + } + + // @todo Make sure the file actually exists? Might not work when testing? + if ($action->exists('@type') && $action->fetch('@type') == 'inline') + { + $filename = $temp_path . '$auto_' . $temp_auto++ . (in_array($actionType, array('readme', 'redirect', 'license')) ? '.txt' : ($actionType == 'code' || $actionType == 'database' ? '.php' : '.mod')); + package_put_contents($filename, $action->fetch('.')); + $filename = strtr($filename, array($temp_path => '')); + } + else + $filename = $action->fetch('.'); + + $return[] = array( + 'type' => $actionType, + 'filename' => $filename, + 'description' => '', + 'reverse' => $action->exists('@reverse') && $action->fetch('@reverse') == 'true', + 'boardmod' => $action->exists('@format') && $action->fetch('@format') == 'boardmod', + 'redirect_url' => $action->exists('@url') ? $action->fetch('@url') : '', + 'redirect_timeout' => $action->exists('@timeout') ? (int) $action->fetch('@timeout') : '', + 'parse_bbc' => $action->exists('@parsebbc') && $action->fetch('@parsebbc') == 'true', + 'language' => (($actionType == 'readme' || $actionType == 'license') && $action->exists('@lang') && $action->fetch('@lang') == $language) ? $language : '', + ); + + continue; + } + elseif ($actionType == 'hook') + { + $return[] = array( + 'type' => $actionType, + 'function' => $action->exists('@function') ? $action->fetch('@function') : '', + 'hook' => $action->exists('@hook') ? $action->fetch('@hook') : $action->fetch('.'), + 'include_file' => $action->exists('@file') ? $action->fetch('@file') : '', + 'reverse' => $action->exists('@reverse') && $action->fetch('@reverse') == 'true' ? true : false, + 'object' => $action->exists('@object') && $action->fetch('@object') == 'true' ? true : false, + 'description' => '', + ); + continue; + } + elseif ($actionType == 'credits') + { + // quick check of any supplied url + $url = $action->exists('@url') ? $action->fetch('@url') : ''; + if (strlen(trim($url)) > 0 && substr($url, 0, 7) !== 'http://' && substr($url, 0, 8) !== 'https://') + { + $url = 'http://' . $url; + if (strlen($url) < 8 || (substr($url, 0, 7) !== 'http://' && substr($url, 0, 8) !== 'https://')) + $url = ''; + } + + $return[] = array( + 'type' => $actionType, + 'url' => $url, + 'license' => $action->exists('@license') ? $action->fetch('@license') : '', + 'licenseurl' => $action->exists('@licenseurl') ? $action->fetch('@licenseurl') : '', + 'copyright' => $action->exists('@copyright') ? $action->fetch('@copyright') : '', + 'title' => $action->fetch('.'), + ); + continue; + } + elseif ($actionType == 'requires') + { + $return[] = array( + 'type' => $actionType, + 'id' => $action->exists('@id') ? $action->fetch('@id') : '', + 'version' => $action->exists('@version') ? $action->fetch('@version') : $action->fetch('.'), + 'description' => '', + ); + continue; + } + elseif ($actionType == 'error') + { + $return[] = array( + 'type' => 'error', + ); + } + elseif (in_array($actionType, array('require-file', 'remove-file', 'require-dir', 'remove-dir', 'move-file', 'move-dir', 'create-file', 'create-dir'))) + { + $this_action = &$return[]; + $this_action = array( + 'type' => $actionType, + 'filename' => $action->fetch('@name'), + 'description' => $action->fetch('.') + ); + + // If there is a destination, make sure it makes sense. + if (substr($actionType, 0, 6) != 'remove') + { + $this_action['unparsed_destination'] = $action->fetch('@destination'); + $this_action['destination'] = parse_path($action->fetch('@destination')) . '/' . basename($this_action['filename']); + } + else + { + $this_action['unparsed_filename'] = $this_action['filename']; + $this_action['filename'] = parse_path($this_action['filename']); + } + + // If we're moving or requiring (copying) a file. + if (substr($actionType, 0, 4) == 'move' || substr($actionType, 0, 7) == 'require') + { + if ($action->exists('@from')) + $this_action['source'] = parse_path($action->fetch('@from')); + else + $this_action['source'] = $temp_path . $this_action['filename']; + } + + // Check if these things can be done. (chmod's etc.) + if ($actionType == 'create-dir') + { + if (!mktree($this_action['destination'], false)) + { + $temp = $this_action['destination']; + while (!file_exists($temp) && strlen($temp) > 1) + $temp = dirname($temp); + + $return[] = array( + 'type' => 'chmod', + 'filename' => $temp + ); + } + } + elseif ($actionType == 'create-file') + { + if (!mktree(dirname($this_action['destination']), false)) + { + $temp = dirname($this_action['destination']); + while (!file_exists($temp) && strlen($temp) > 1) + $temp = dirname($temp); + + $return[] = array( + 'type' => 'chmod', + 'filename' => $temp + ); + } + + if (!is_writable($this_action['destination']) && (file_exists($this_action['destination']) || !is_writable(dirname($this_action['destination'])))) + $return[] = array( + 'type' => 'chmod', + 'filename' => $this_action['destination'] + ); + } + elseif ($actionType == 'require-dir') + { + if (!mktree($this_action['destination'], false)) + { + $temp = $this_action['destination']; + while (!file_exists($temp) && strlen($temp) > 1) + $temp = dirname($temp); + + $return[] = array( + 'type' => 'chmod', + 'filename' => $temp + ); + } + } + elseif ($actionType == 'require-file') + { + if ($action->exists('@theme')) + $this_action['theme_action'] = $action->fetch('@theme'); + + if (!mktree(dirname($this_action['destination']), false)) + { + $temp = dirname($this_action['destination']); + while (!file_exists($temp) && strlen($temp) > 1) + $temp = dirname($temp); + + $return[] = array( + 'type' => 'chmod', + 'filename' => $temp + ); + } + + if (!is_writable($this_action['destination']) && (file_exists($this_action['destination']) || !is_writable(dirname($this_action['destination'])))) + $return[] = array( + 'type' => 'chmod', + 'filename' => $this_action['destination'] + ); + } + elseif ($actionType == 'move-dir' || $actionType == 'move-file') + { + if (!mktree(dirname($this_action['destination']), false)) + { + $temp = dirname($this_action['destination']); + while (!file_exists($temp) && strlen($temp) > 1) + $temp = dirname($temp); + + $return[] = array( + 'type' => 'chmod', + 'filename' => $temp + ); + } + + if (!is_writable($this_action['destination']) && (file_exists($this_action['destination']) || !is_writable(dirname($this_action['destination'])))) + $return[] = array( + 'type' => 'chmod', + 'filename' => $this_action['destination'] + ); + } + elseif ($actionType == 'remove-dir') + { + if (!is_writable($this_action['filename']) && file_exists($this_action['filename'])) + $return[] = array( + 'type' => 'chmod', + 'filename' => $this_action['filename'] + ); + } + elseif ($actionType == 'remove-file') + { + if (!is_writable($this_action['filename']) && file_exists($this_action['filename'])) + $return[] = array( + 'type' => 'chmod', + 'filename' => $this_action['filename'] + ); + } + } + else + { + $return[] = array( + 'type' => 'error', + 'error_msg' => 'unknown_action', + 'error_var' => $actionType + ); + } + } + + // Only testing - just return a list of things to be done. + if ($testing_only) + return $return; + + umask(0); + + $failure = false; + $not_done = array(array('type' => '!')); + foreach ($return as $action) + { + if (in_array($action['type'], array('modification', 'code', 'database', 'redirect', 'hook', 'credits'))) + $not_done[] = $action; + + if ($action['type'] == 'create-dir') + { + if (!mktree($action['destination'], 0755) || !is_writable($action['destination'])) + $failure |= !mktree($action['destination'], 0777); + } + elseif ($action['type'] == 'create-file') + { + if (!mktree(dirname($action['destination']), 0755) || !is_writable(dirname($action['destination']))) + $failure |= !mktree(dirname($action['destination']), 0777); + + // Create an empty file. + package_put_contents($action['destination'], package_get_contents($action['source']), $testing_only); + + if (!file_exists($action['destination'])) + $failure = true; + } + elseif ($action['type'] == 'require-dir') + { + copytree($action['source'], $action['destination']); + // Any other theme folders? + if (!empty($context['theme_copies']) && !empty($context['theme_copies'][$action['type']][$action['destination']])) + foreach ($context['theme_copies'][$action['type']][$action['destination']] as $theme_destination) + copytree($action['source'], $theme_destination); + } + elseif ($action['type'] == 'require-file') + { + if (!mktree(dirname($action['destination']), 0755) || !is_writable(dirname($action['destination']))) + $failure |= !mktree(dirname($action['destination']), 0777); + + package_put_contents($action['destination'], package_get_contents($action['source']), $testing_only); + + $failure |= !copy($action['source'], $action['destination']); + + // Any other theme files? + if (!empty($context['theme_copies']) && !empty($context['theme_copies'][$action['type']][$action['destination']])) + foreach ($context['theme_copies'][$action['type']][$action['destination']] as $theme_destination) + { + if (!mktree(dirname($theme_destination), 0755) || !is_writable(dirname($theme_destination))) + $failure |= !mktree(dirname($theme_destination), 0777); + + package_put_contents($theme_destination, package_get_contents($action['source']), $testing_only); + + $failure |= !copy($action['source'], $theme_destination); + } + } + elseif ($action['type'] == 'move-file') + { + if (!mktree(dirname($action['destination']), 0755) || !is_writable(dirname($action['destination']))) + $failure |= !mktree(dirname($action['destination']), 0777); + + $failure |= !rename($action['source'], $action['destination']); + } + elseif ($action['type'] == 'move-dir') + { + if (!mktree($action['destination'], 0755) || !is_writable($action['destination'])) + $failure |= !mktree($action['destination'], 0777); + + $failure |= !rename($action['source'], $action['destination']); + } + elseif ($action['type'] == 'remove-dir') + { + deltree($action['filename']); + + // Any other theme folders? + if (!empty($context['theme_copies']) && !empty($context['theme_copies'][$action['type']][$action['filename']])) + foreach ($context['theme_copies'][$action['type']][$action['filename']] as $theme_destination) + deltree($theme_destination); + } + elseif ($action['type'] == 'remove-file') + { + // Make sure the file exists before deleting it. + if (file_exists($action['filename'])) + { + package_chmod($action['filename']); + $failure |= !unlink($action['filename']); + } + // The file that was supposed to be deleted couldn't be found. + else + $failure = true; + + // Any other theme folders? + if (!empty($context['theme_copies']) && !empty($context['theme_copies'][$action['type']][$action['filename']])) + foreach ($context['theme_copies'][$action['type']][$action['filename']] as $theme_destination) + if (file_exists($theme_destination)) + $failure |= !unlink($theme_destination); + else + $failure = true; + } + } + + return $not_done; +} + +/** + * Checks if version matches any of the versions in `$versions`. + * + * - supports comma separated version numbers, with or without whitespace. + * - supports lower and upper bounds. (1.0-1.2) + * - returns true if the version matched. + * + * @param string $versions The versions that this package will install on + * @param boolean $reset Whether to reset $near_version + * @param string $the_version The forum version + * @return string|bool Highest install value string or false + */ +function matchHighestPackageVersion($versions, $reset, $the_version) +{ + static $near_version = 0; + + if ($reset) + $near_version = 0; + + // Normalize the $versions while we remove our previous Doh! + $versions = explode(',', str_replace(array(' ', '2.0rc1-1'), array('', '2.0rc1.1'), strtolower($versions))); + + // Loop through each version, save the highest we can find + foreach ($versions as $for) + { + // Adjust for those wild cards + if (strpos($for, '*') !== false) + $for = str_replace('*', '0dev0', $for) . '-' . str_replace('*', '999', $for); + + // If we have a range, grab the lower value, done this way so it looks normal-er to the user e.g. 2.0 vs 2.0.99 + if (strpos($for, '-') !== false) + list ($for, $higher) = explode('-', $for); + + // Do the compare, if the for is greater, than what we have but not greater than what we are running ..... + if (compareVersions($near_version, $for) === -1 && compareVersions($for, $the_version) !== 1) + $near_version = $for; + } + + return !empty($near_version) ? $near_version : false; +} + +/** + * Checks if the forum version matches any of the available versions from the package install xml. + * - supports comma separated version numbers, with or without whitespace. + * - supports lower and upper bounds. (1.0-1.2) + * - returns true if the version matched. + * + * @param string $version The forum version + * @param string $versions The versions that this package will install on + * @return bool Whether the version matched + */ +function matchPackageVersion($version, $versions) +{ + // Make sure everything is lowercase and clean of spaces and unpleasant history. + $version = str_replace(array(' ', '2.0rc1-1'), array('', '2.0rc1.1'), strtolower($version)); + $versions = explode(',', str_replace(array(' ', '2.0rc1-1'), array('', '2.0rc1.1'), strtolower($versions))); + + // Perhaps we do accept anything? + if (in_array('all', $versions)) + return true; + + // Loop through each version. + foreach ($versions as $for) + { + // Wild card spotted? + if (strpos($for, '*') !== false) + $for = str_replace('*', '0dev0', $for) . '-' . str_replace('*', '999', $for); + + // Do we have a range? + if (strpos($for, '-') !== false) + { + list ($lower, $upper) = explode('-', $for); + + // Compare the version against lower and upper bounds. + if (compareVersions($version, $lower) > -1 && compareVersions($version, $upper) < 1) + return true; + } + // Otherwise check if they are equal... + elseif (compareVersions($version, $for) === 0) + return true; + } + + return false; +} + +/** + * Compares two versions and determines if one is newer, older or the same, returns + * - (-1) if version1 is lower than version2 + * - (0) if version1 is equal to version2 + * - (1) if version1 is higher than version2 + * + * @param string $version1 The first version + * @param string $version2 The second version + * @return int -1 if version2 is greater than version1, 0 if they're equal, 1 if version1 is greater than version2 + */ +function compareVersions($version1, $version2) +{ + static $categories; + + $versions = array(); + foreach (array(1 => $version1, $version2) as $id => $version) + { + // Clean the version and extract the version parts. + $clean = str_replace(array(' ', '2.0rc1-1'), array('', '2.0rc1.1'), strtolower($version)); + preg_match('~(\d+)(?:\.(\d+|))?(?:\.)?(\d+|)(?:(alpha|beta|rc)(\d+|)(?:\.)?(\d+|))?(?:(dev))?(\d+|)~', $clean, $parts); + + // Build an array of parts. + $versions[$id] = array( + 'major' => !empty($parts[1]) ? (int) $parts[1] : 0, + 'minor' => !empty($parts[2]) ? (int) $parts[2] : 0, + 'patch' => !empty($parts[3]) ? (int) $parts[3] : 0, + 'type' => empty($parts[4]) ? 'stable' : $parts[4], + 'type_major' => !empty($parts[5]) ? (int) $parts[5] : 0, + 'type_minor' => !empty($parts[6]) ? (int) $parts[6] : 0, + 'dev' => !empty($parts[7]), + ); + } + + // Are they the same, perhaps? + if ($versions[1] === $versions[2]) + return 0; + + // Get version numbering categories... + if (!isset($categories)) + $categories = array_keys($versions[1]); + + // Loop through each category. + foreach ($categories as $category) + { + // Is there something for us to calculate? + if ($versions[1][$category] !== $versions[2][$category]) + { + // Dev builds are a problematic exception. + // (stable) dev < (stable) but (unstable) dev = (unstable) + if ($category == 'type') + return $versions[1][$category] > $versions[2][$category] ? ($versions[1]['dev'] ? -1 : 1) : ($versions[2]['dev'] ? 1 : -1); + elseif ($category == 'dev') + return $versions[1]['dev'] ? ($versions[2]['type'] == 'stable' ? -1 : 0) : ($versions[1]['type'] == 'stable' ? 1 : 0); + // Otherwise a simple comparison. + else + return $versions[1][$category] > $versions[2][$category] ? 1 : -1; + } + } + + // They are the same! + return 0; +} + +/** + * Parses special identifiers out of the specified path. + * + * @param string $path The path + * @return string The parsed path + */ +function parse_path($path) +{ + global $modSettings, $boarddir, $sourcedir, $settings, $temp_path, $txt; + + $dirs = array( + '\\' => '/', + '$boarddir' => $boarddir, + '$sourcedir' => $sourcedir, + '$avatardir' => $modSettings['avatar_directory'], + '$avatars_dir' => $modSettings['avatar_directory'], + '$themedir' => $settings['default_theme_dir'], + '$imagesdir' => $settings['default_theme_dir'] . '/' . basename($settings['default_images_url']), + '$themes_dir' => $boarddir . '/Themes', + '$languagedir' => $settings['default_theme_dir'] . '/languages', + '$languages_dir' => $settings['default_theme_dir'] . '/languages', + '$smileysdir' => $modSettings['smileys_dir'], + '$smileys_dir' => $modSettings['smileys_dir'], + ); + + // do we parse in a package directory? + if (!empty($temp_path)) + $dirs['$package'] = $temp_path; + + if (strlen($path) == 0) + { + loadLanguage('Errors'); + trigger_error($txt['parse_path_filename_required'], E_USER_ERROR); + } + + return strtr($path, $dirs); +} + +/** + * Deletes a directory, and all the files and direcories inside it. + * requires access to delete these files. + * + * @param string $dir A directory + * @param bool $delete_dir If false, only deletes everything inside the directory but not the directory itself + */ +function deltree($dir, $delete_dir = true) +{ + /** @var ftp_connection $package_ftp */ + global $package_ftp; + + if (!file_exists($dir)) + return; + + $current_dir = @opendir($dir); + if ($current_dir == false) + { + if ($delete_dir && isset($package_ftp)) + { + $ftp_file = strtr($dir, array($_SESSION['pack_ftp']['root'] => '')); + if (!is_dir($dir)) + $package_ftp->chmod($ftp_file, 0777); + $package_ftp->unlink($ftp_file); + } + + return; + } + + while ($entryname = readdir($current_dir)) + { + if (in_array($entryname, array('.', '..'))) + continue; + + if (is_dir($dir . '/' . $entryname)) + deltree($dir . '/' . $entryname); + else + { + // Here, 755 doesn't really matter since we're deleting it anyway. + if (isset($package_ftp)) + { + $ftp_file = strtr($dir . '/' . $entryname, array($_SESSION['pack_ftp']['root'] => '')); + + if (!is_writable($dir . '/' . $entryname)) + $package_ftp->chmod($ftp_file, 0777); + $package_ftp->unlink($ftp_file); + } + else + { + if (!is_writable($dir . '/' . $entryname)) + smf_chmod($dir . '/' . $entryname, 0777); + unlink($dir . '/' . $entryname); + } + } + } + + closedir($current_dir); + + if ($delete_dir) + { + if (isset($package_ftp)) + { + $ftp_file = strtr($dir, array($_SESSION['pack_ftp']['root'] => '')); + if (!is_writable($dir . '/' . $entryname)) + $package_ftp->chmod($ftp_file, 0777); + $package_ftp->unlink($ftp_file); + } + else + { + if (!is_writable($dir)) + smf_chmod($dir, 0777); + @rmdir($dir); + } + } +} + +/** + * Creates the specified tree structure with the mode specified. + * creates every directory in path until it finds one that already exists. + * + * @param string $strPath The path + * @param int $mode The permission mode for CHMOD (0666, etc.) + * @return bool True if successful, false otherwise + */ +function mktree($strPath, $mode) +{ + /** @var ftp_connection $package_ftp */ + global $package_ftp; + + if (is_dir($strPath)) + { + if (!is_writable($strPath) && $mode !== false) + { + if (isset($package_ftp)) + $package_ftp->chmod(strtr($strPath, array($_SESSION['pack_ftp']['root'] => '')), $mode); + else + smf_chmod($strPath, $mode); + } + + $test = @opendir($strPath); + if ($test) + { + closedir($test); + return is_writable($strPath); + } + else + return false; + } + // Is this an invalid path and/or we can't make the directory? + if ($strPath == dirname($strPath) || !mktree(dirname($strPath), $mode)) + return false; + + if (!is_writable(dirname($strPath)) && $mode !== false) + { + if (isset($package_ftp)) + $package_ftp->chmod(dirname(strtr($strPath, array($_SESSION['pack_ftp']['root'] => ''))), $mode); + else + smf_chmod(dirname($strPath), $mode); + } + + if ($mode !== false && isset($package_ftp)) + return $package_ftp->create_dir(strtr($strPath, array($_SESSION['pack_ftp']['root'] => ''))); + elseif ($mode === false) + { + $test = @opendir(dirname($strPath)); + if ($test) + { + closedir($test); + return true; + } + else + return false; + } + else + { + @mkdir($strPath, $mode); + $test = @opendir($strPath); + if ($test) + { + closedir($test); + return true; + } + else + return false; + } +} + +/** + * Copies one directory structure over to another. + * requires the destination to be writable. + * + * @param string $source The directory to copy + * @param string $destination The directory to copy $source to + */ +function copytree($source, $destination) +{ + /** @var ftp_connection $package_ftp */ + global $package_ftp; + + if (!file_exists($destination) || !is_writable($destination)) + mktree($destination, 0755); + if (!is_writable($destination)) + mktree($destination, 0777); + + $current_dir = opendir($source); + if ($current_dir == false) + return; + + while ($entryname = readdir($current_dir)) + { + if (in_array($entryname, array('.', '..'))) + continue; + + if (isset($package_ftp)) + $ftp_file = strtr($destination . '/' . $entryname, array($_SESSION['pack_ftp']['root'] => '')); + + if (is_file($source . '/' . $entryname)) + { + if (isset($package_ftp) && !file_exists($destination . '/' . $entryname)) + $package_ftp->create_file($ftp_file); + elseif (!file_exists($destination . '/' . $entryname)) + @touch($destination . '/' . $entryname); + } + + package_chmod($destination . '/' . $entryname); + + if (is_dir($source . '/' . $entryname)) + copytree($source . '/' . $entryname, $destination . '/' . $entryname); + elseif (file_exists($destination . '/' . $entryname)) + package_put_contents($destination . '/' . $entryname, package_get_contents($source . '/' . $entryname)); + else + copy($source . '/' . $entryname, $destination . '/' . $entryname); + } + + closedir($current_dir); +} + +/** + * Create a tree listing for a given directory path + * + * @param string $path The path + * @param string $sub_path The sub-path + * @return array An array of information about the files at the specified path/subpath + */ +function listtree($path, $sub_path = '') +{ + $data = array(); + + $dir = @dir($path . $sub_path); + if (!$dir) + return array(); + while ($entry = $dir->read()) + { + if ($entry == '.' || $entry == '..') + continue; + + if (is_dir($path . $sub_path . '/' . $entry)) + $data = array_merge($data, listtree($path, $sub_path . '/' . $entry)); + else + $data[] = array( + 'filename' => $sub_path == '' ? $entry : $sub_path . '/' . $entry, + 'size' => filesize($path . $sub_path . '/' . $entry), + 'skipped' => false, + ); + } + $dir->close(); + + return $data; +} + +/** + * Parses a xml-style modification file (file). + * + * @param string $file The modification file to parse + * @param bool $testing Whether we're just doing a test + * @param bool $undo If true, specifies that the modifications should be undone. Used when uninstalling. Doesn't work with regex. + * @param array $theme_paths An array of information about custom themes to apply the changes to + * @return array An array of those changes made. + */ +function parseModification($file, $testing = true, $undo = false, $theme_paths = array()) +{ + global $boarddir, $sourcedir, $txt, $modSettings; + + @set_time_limit(600); + require_once($sourcedir . '/Class-Package.php'); + $xml = new xmlArray(strtr($file, array("\r" => ''))); + $actions = array(); + $everything_found = true; + + if (!$xml->exists('modification') || !$xml->exists('modification/file')) + { + $actions[] = array( + 'type' => 'error', + 'filename' => '-', + 'debug' => $txt['package_modification_malformed'] + ); + return $actions; + } + + // Get the XML data. + $files = $xml->set('modification/file'); + + // Use this for holding all the template changes in this mod. + $template_changes = array(); + // This is needed to hold the long paths, as they can vary... + $long_changes = array(); + + // First, we need to build the list of all the files likely to get changed. + foreach ($files as $file) + { + // What is the filename we're currently on? + $filename = parse_path(trim($file->fetch('@name'))); + + // Now, we need to work out whether this is even a template file... + foreach ($theme_paths as $id => $theme) + { + // If this filename is relative, if so take a guess at what it should be. + $real_filename = $filename; + if (strpos($filename, 'Themes') === 0) + $real_filename = $boarddir . '/' . $filename; + + if (strpos($real_filename, $theme['theme_dir']) === 0) + { + $template_changes[$id][] = substr($real_filename, strlen($theme['theme_dir']) + 1); + $long_changes[$id][] = $filename; + } + } + } + + // Custom themes to add. + $custom_themes_add = array(); + + // If we have some template changes, we need to build a master link of what new ones are required for the custom themes. + if (!empty($template_changes[1])) + { + foreach ($theme_paths as $id => $theme) + { + // Default is getting done anyway, so no need for involvement here. + if ($id == 1) + continue; + + // For every template, do we want it? Yea, no, maybe? + foreach ($template_changes[1] as $index => $template_file) + { + // What, it exists and we haven't already got it?! Lordy, get it in! + if (file_exists($theme['theme_dir'] . '/' . $template_file) && (!isset($template_changes[$id]) || !in_array($template_file, $template_changes[$id]))) + { + // Now let's add it to the "todo" list. + $custom_themes_add[$long_changes[1][$index]][$id] = $theme['theme_dir'] . '/' . $template_file; + } + } + } + } + + foreach ($files as $file) + { + // This is the actual file referred to in the XML document... + $files_to_change = array( + 1 => parse_path(trim($file->fetch('@name'))), + ); + + // Sometimes though, we have some additional files for other themes, if we have add them to the mix. + if (isset($custom_themes_add[$files_to_change[1]])) + $files_to_change += $custom_themes_add[$files_to_change[1]]; + + // Now, loop through all the files we're changing, and, well, change them ;) + foreach ($files_to_change as $theme => $working_file) + { + if ($working_file[0] != '/' && $working_file[1] != ':') + { + loadLanguage('Errors'); + trigger_error(sprintf($txt['parse_modification_filename_not_full_path'], $working_file), E_USER_WARNING); + + $working_file = $boarddir . '/' . $working_file; + } + + // Doesn't exist - give an error or what? + if (!file_exists($working_file) && (!$file->exists('@error') || !in_array(trim($file->fetch('@error')), array('ignore', 'skip')))) + { + $actions[] = array( + 'type' => 'missing', + 'filename' => $working_file, + 'debug' => $txt['package_modification_missing'] + ); + + $everything_found = false; + continue; + } + // Skip the file if it doesn't exist. + elseif (!file_exists($working_file) && $file->exists('@error') && trim($file->fetch('@error')) == 'skip') + { + $actions[] = array( + 'type' => 'skipping', + 'filename' => $working_file, + ); + continue; + } + // Okay, we're creating this file then...? + elseif (!file_exists($working_file)) + $working_data = ''; + // Phew, it exists! Load 'er up! + else + $working_data = str_replace("\r", '', package_get_contents($working_file)); + + $actions[] = array( + 'type' => 'opened', + 'filename' => $working_file + ); + + $operations = $file->exists('operation') ? $file->set('operation') : array(); + foreach ($operations as $operation) + { + // Convert operation to an array. + $actual_operation = array( + 'searches' => array(), + 'error' => $operation->exists('@error') && in_array(trim($operation->fetch('@error')), array('ignore', 'fatal', 'required')) ? trim($operation->fetch('@error')) : 'fatal', + ); + + // The 'add' parameter is used for all searches in this operation. + $add = $operation->exists('add') ? $operation->fetch('add') : ''; + + // Grab all search items of this operation (in most cases just 1). + $searches = $operation->set('search'); + foreach ($searches as $i => $search) + $actual_operation['searches'][] = array( + 'position' => $search->exists('@position') && in_array(trim($search->fetch('@position')), array('before', 'after', 'replace', 'end')) ? trim($search->fetch('@position')) : 'replace', + 'is_reg_exp' => $search->exists('@regexp') && trim($search->fetch('@regexp')) === 'true', + 'loose_whitespace' => $search->exists('@whitespace') && trim($search->fetch('@whitespace')) === 'loose', + 'search' => $search->fetch('.'), + 'add' => $add, + 'preg_search' => '', + 'preg_replace' => '', + ); + + // At least one search should be defined. + if (empty($actual_operation['searches'])) + { + $actions[] = array( + 'type' => 'failure', + 'filename' => $working_file, + 'search' => $search['search'], + 'is_custom' => $theme > 1 ? $theme : 0, + ); + + // Skip to the next operation. + continue; + } + + // Reverse the operations in case of undoing stuff. + if ($undo) + { + foreach ($actual_operation['searches'] as $i => $search) + { + // Reverse modification of regular expressions are not allowed. + if ($search['is_reg_exp']) + { + if ($actual_operation['error'] === 'fatal') + $actions[] = array( + 'type' => 'failure', + 'filename' => $working_file, + 'search' => $search['search'], + 'is_custom' => $theme > 1 ? $theme : 0, + ); + + // Continue to the next operation. + continue 2; + } + + // The replacement is now the search subject... + if ($search['position'] === 'replace' || $search['position'] === 'end') + $actual_operation['searches'][$i]['search'] = $search['add']; + else + { + // Reversing a before/after modification becomes a replacement. + $actual_operation['searches'][$i]['position'] = 'replace'; + + if ($search['position'] === 'before') + $actual_operation['searches'][$i]['search'] .= $search['add']; + elseif ($search['position'] === 'after') + $actual_operation['searches'][$i]['search'] = $search['add'] . $search['search']; + } + + // ...and the search subject is now the replacement. + $actual_operation['searches'][$i]['add'] = $search['search']; + } + } + + // Sort the search list so the replaces come before the add before/after's. + if (count($actual_operation['searches']) !== 1) + { + $replacements = array(); + + foreach ($actual_operation['searches'] as $i => $search) + { + if ($search['position'] === 'replace') + { + $replacements[] = $search; + unset($actual_operation['searches'][$i]); + } + } + $actual_operation['searches'] = array_merge($replacements, $actual_operation['searches']); + } + + // Create regular expression replacements from each search. + foreach ($actual_operation['searches'] as $i => $search) + { + // Not much needed if the search subject is already a regexp. + if ($search['is_reg_exp']) + $actual_operation['searches'][$i]['preg_search'] = $search['search']; + else + { + // Make the search subject fit into a regular expression. + $actual_operation['searches'][$i]['preg_search'] = preg_quote($search['search'], '~'); + + // Using 'loose', a random amount of tabs and spaces may be used. + if ($search['loose_whitespace']) + $actual_operation['searches'][$i]['preg_search'] = preg_replace('~[ \t]+~', '[ \t]+', $actual_operation['searches'][$i]['preg_search']); + } + + // Shuzzup. This is done so we can safely use a regular expression. ($0 is bad!!) + $actual_operation['searches'][$i]['preg_replace'] = strtr($search['add'], array('$' => '[$PACK' . 'AGE1$]', '\\' => '[$PACK' . 'AGE2$]')); + + // Before, so the replacement comes after the search subject :P + if ($search['position'] === 'before') + { + $actual_operation['searches'][$i]['preg_search'] = '(' . $actual_operation['searches'][$i]['preg_search'] . ')'; + $actual_operation['searches'][$i]['preg_replace'] = '$1' . $actual_operation['searches'][$i]['preg_replace']; + } + + // After, after what? + elseif ($search['position'] === 'after') + { + $actual_operation['searches'][$i]['preg_search'] = '(' . $actual_operation['searches'][$i]['preg_search'] . ')'; + $actual_operation['searches'][$i]['preg_replace'] .= '$1'; + } + + // Position the replacement at the end of the file (or just before the closing PHP tags). + elseif ($search['position'] === 'end') + { + if ($undo) + { + $actual_operation['searches'][$i]['preg_replace'] = ''; + } + else + { + $actual_operation['searches'][$i]['preg_search'] = '(\\n\\?\\>)?$'; + $actual_operation['searches'][$i]['preg_replace'] .= '$1'; + } + } + + // Testing 1, 2, 3... + $failed = preg_match('~' . $actual_operation['searches'][$i]['preg_search'] . '~s', $working_data) === 0; + + // Nope, search pattern not found. + if ($failed && $actual_operation['error'] === 'fatal') + { + $actions[] = array( + 'type' => 'failure', + 'filename' => $working_file, + 'search' => $actual_operation['searches'][$i]['preg_search'], + 'search_original' => $actual_operation['searches'][$i]['search'], + 'replace_original' => $actual_operation['searches'][$i]['add'], + 'position' => $search['position'], + 'is_custom' => $theme > 1 ? $theme : 0, + 'failed' => $failed, + ); + + $everything_found = false; + continue; + } + + // Found, but in this case, that means failure! + elseif (!$failed && $actual_operation['error'] === 'required') + { + $actions[] = array( + 'type' => 'failure', + 'filename' => $working_file, + 'search' => $actual_operation['searches'][$i]['preg_search'], + 'search_original' => $actual_operation['searches'][$i]['search'], + 'replace_original' => $actual_operation['searches'][$i]['add'], + 'position' => $search['position'], + 'is_custom' => $theme > 1 ? $theme : 0, + 'failed' => $failed, + ); + + $everything_found = false; + continue; + } + + // Replace it into nothing? That's not an option...unless it's an undoing end. + if ($search['add'] === '' && ($search['position'] !== 'end' || !$undo)) + continue; + + // Finally, we're doing some replacements. + $working_data = preg_replace('~' . $actual_operation['searches'][$i]['preg_search'] . '~s', $actual_operation['searches'][$i]['preg_replace'], $working_data, 1); + + $actions[] = array( + 'type' => 'replace', + 'filename' => $working_file, + 'search' => $actual_operation['searches'][$i]['preg_search'], + 'replace' => $actual_operation['searches'][$i]['preg_replace'], + 'search_original' => $actual_operation['searches'][$i]['search'], + 'replace_original' => $actual_operation['searches'][$i]['add'], + 'position' => $search['position'], + 'failed' => $failed, + 'ignore_failure' => $failed && $actual_operation['error'] === 'ignore', + 'is_custom' => $theme > 1 ? $theme : 0, + ); + } + } + + // Fix any little helper symbols ;). + $working_data = strtr($working_data, array('[$PACK' . 'AGE1$]' => '$', '[$PACK' . 'AGE2$]' => '\\')); + + package_chmod($working_file); + + if ((file_exists($working_file) && !is_writable($working_file)) || (!file_exists($working_file) && !is_writable(dirname($working_file)))) + $actions[] = array( + 'type' => 'chmod', + 'filename' => $working_file + ); + + if (basename($working_file) == 'Settings_bak.php') + continue; + + if (!$testing && !empty($modSettings['package_make_backups']) && file_exists($working_file)) + { + // No, no, not Settings.php! + if (basename($working_file) == 'Settings.php') + @copy($working_file, dirname($working_file) . '/Settings_bak.php'); + else + @copy($working_file, $working_file . '~'); + } + + // Always call this, even if in testing, because it won't really be written in testing mode. + package_put_contents($working_file, $working_data, $testing); + + $actions[] = array( + 'type' => 'saved', + 'filename' => $working_file, + 'is_custom' => $theme > 1 ? $theme : 0, + ); + } + } + + $actions[] = array( + 'type' => 'result', + 'status' => $everything_found + ); + + return $actions; +} + +/** + * Parses a boardmod-style (.mod) modification file + * + * @param string $file The modification file to parse + * @param bool $testing Whether we're just doing a test + * @param bool $undo If true, specifies that the modifications should be undone. Used when uninstalling. + * @param array $theme_paths An array of information about custom themes to apply the changes to + * @return array An array of those changes made. + */ +function parseBoardMod($file, $testing = true, $undo = false, $theme_paths = array()) +{ + global $boarddir, $sourcedir, $settings, $modSettings, $txt; + + @set_time_limit(600); + $file = strtr($file, array("\r" => '')); + + $working_file = null; + $working_search = null; + $working_data = ''; + $replace_with = null; + + $actions = array(); + $everything_found = true; + + // This holds all the template changes in the standard mod file. + $template_changes = array(); + // This is just the temporary file. + $temp_file = $file; + // This holds the actual changes on a step counter basis. + $temp_changes = array(); + $counter = 0; + $step_counter = 0; + + // Before we do *anything*, let's build a list of what we're editing, as it's going to be used for other theme edits. + while (preg_match('~<(edit file|file|search|search for|add|add after|replace|add before|add above|above|before)>\n(.*?)\n~is', $temp_file, $code_match) != 0) + { + $counter++; + + // Get rid of the old stuff. + $temp_file = substr_replace($temp_file, '', strpos($temp_file, $code_match[0]), strlen($code_match[0])); + + // No interest to us? + if ($code_match[1] != 'edit file' && $code_match[1] != 'file') + { + // It's a step, let's add that to the current steps. + if (isset($temp_changes[$step_counter])) + $temp_changes[$step_counter]['changes'][] = $code_match[0]; + continue; + } + + // We've found a new edit - let's make ourself heard, kind of. + $step_counter = $counter; + $temp_changes[$step_counter] = array( + 'title' => $code_match[0], + 'changes' => array(), + ); + + $filename = parse_path($code_match[2]); + + // Now, is this a template file, and if so, which? + foreach ($theme_paths as $id => $theme) + { + // If this filename is relative, if so take a guess at what it should be. + if (strpos($filename, 'Themes') === 0) + $filename = $boarddir . '/' . $filename; + + if (strpos($filename, $theme['theme_dir']) === 0) + $template_changes[$id][$counter] = substr($filename, strlen($theme['theme_dir']) + 1); + } + } + + // Reference for what theme ID this action belongs to. + $theme_id_ref = array(); + + // Now we know what templates we need to touch, cycle through each theme and work out what we need to edit. + if (!empty($template_changes[1])) + { + foreach ($theme_paths as $id => $theme) + { + // Don't do default, it means nothing to me. + if ($id == 1) + continue; + + // Now, for each file do we need to edit it? + foreach ($template_changes[1] as $pos => $template_file) + { + // It does? Add it to the list darlin'. + if (file_exists($theme['theme_dir'] . '/' . $template_file) && (!isset($template_changes[$id][$pos]) || !in_array($template_file, $template_changes[$id][$pos]))) + { + // Actually add it to the mod file too, so we can see that it will work ;) + if (!empty($temp_changes[$pos]['changes'])) + { + $file .= "\n\n" . '' . "\n" . $theme['theme_dir'] . '/' . $template_file . "\n" . '' . "\n\n" . implode("\n\n", $temp_changes[$pos]['changes']); + $theme_id_ref[$counter] = $id; + $counter += 1 + count($temp_changes[$pos]['changes']); + } + } + } + } + } + + $counter = 0; + $is_custom = 0; + while (preg_match('~<(edit file|file|search|search for|add|add after|replace|add before|add above|above|before)>\n(.*?)\n~is', $file, $code_match) != 0) + { + // This is for working out what we should be editing. + $counter++; + + // Edit a specific file. + if ($code_match[1] == 'file' || $code_match[1] == 'edit file') + { + // Backup the old file. + if ($working_file !== null) + { + package_chmod($working_file); + + // Don't even dare. + if (basename($working_file) == 'Settings_bak.php') + continue; + + if (!is_writable($working_file)) + $actions[] = array( + 'type' => 'chmod', + 'filename' => $working_file + ); + + if (!$testing && !empty($modSettings['package_make_backups']) && file_exists($working_file)) + { + if (basename($working_file) == 'Settings.php') + @copy($working_file, dirname($working_file) . '/Settings_bak.php'); + else + @copy($working_file, $working_file . '~'); + } + + package_put_contents($working_file, $working_data, $testing); + } + + if ($working_file !== null) + $actions[] = array( + 'type' => 'saved', + 'filename' => $working_file, + 'is_custom' => $is_custom, + ); + + // Is this "now working on" file a theme specific one? + $is_custom = isset($theme_id_ref[$counter - 1]) ? $theme_id_ref[$counter - 1] : 0; + + // Make sure the file exists! + $working_file = parse_path($code_match[2]); + + if ($working_file[0] != '/' && $working_file[1] != ':') + { + loadLanguage('Errors'); + trigger_error(sprintf($txt['parse_boardmod_filename_not_full_path'], $working_file), E_USER_WARNING); + + $working_file = $boarddir . '/' . $working_file; + } + + if (!file_exists($working_file)) + { + $places_to_check = array($boarddir, $sourcedir, $settings['default_theme_dir'], $settings['default_theme_dir'] . '/languages'); + + foreach ($places_to_check as $place) + if (file_exists($place . '/' . $working_file)) + { + $working_file = $place . '/' . $working_file; + break; + } + } + + if (file_exists($working_file)) + { + // Load the new file. + $working_data = str_replace("\r", '', package_get_contents($working_file)); + + $actions[] = array( + 'type' => 'opened', + 'filename' => $working_file + ); + } + else + { + $actions[] = array( + 'type' => 'missing', + 'filename' => $working_file + ); + + $working_file = null; + $everything_found = false; + } + + // Can't be searching for something... + $working_search = null; + } + // Search for a specific string. + elseif (($code_match[1] == 'search' || $code_match[1] == 'search for') && $working_file !== null) + { + if ($working_search !== null) + { + $actions[] = array( + 'type' => 'error', + 'filename' => $working_file + ); + + $everything_found = false; + } + + $working_search = $code_match[2]; + } + // Must've already loaded a search string. + elseif ($working_search !== null) + { + // This is the base string.... + $replace_with = $code_match[2]; + + // Add this afterward... + if ($code_match[1] == 'add' || $code_match[1] == 'add after') + $replace_with = $working_search . "\n" . $replace_with; + // Add this beforehand. + elseif ($code_match[1] == 'before' || $code_match[1] == 'add before' || $code_match[1] == 'above' || $code_match[1] == 'add above') + $replace_with .= "\n" . $working_search; + // Otherwise.. replace with $replace_with ;). + } + + // If we have a search string, replace string, and open file.. + if ($working_search !== null && $replace_with !== null && $working_file !== null) + { + // Make sure it's somewhere in the string. + if ($undo) + { + $temp = $replace_with; + $replace_with = $working_search; + $working_search = $temp; + } + + if (strpos($working_data, $working_search) !== false) + { + $working_data = str_replace($working_search, $replace_with, $working_data); + + $actions[] = array( + 'type' => 'replace', + 'filename' => $working_file, + 'search' => $working_search, + 'replace' => $replace_with, + 'search_original' => $working_search, + 'replace_original' => $replace_with, + 'position' => $code_match[1] == 'replace' ? 'replace' : ($code_match[1] == 'add' || $code_match[1] == 'add after' ? 'before' : 'after'), + 'is_custom' => $is_custom, + 'failed' => false, + ); + } + // It wasn't found! + else + { + $actions[] = array( + 'type' => 'failure', + 'filename' => $working_file, + 'search' => $working_search, + 'is_custom' => $is_custom, + 'search_original' => $working_search, + 'replace_original' => $replace_with, + 'position' => $code_match[1] == 'replace' ? 'replace' : ($code_match[1] == 'add' || $code_match[1] == 'add after' ? 'before' : 'after'), + 'is_custom' => $is_custom, + 'failed' => true, + ); + + $everything_found = false; + } + + // These don't hold any meaning now. + $working_search = null; + $replace_with = null; + } + + // Get rid of the old tag. + $file = substr_replace($file, '', strpos($file, $code_match[0]), strlen($code_match[0])); + } + + // Backup the old file. + if ($working_file !== null) + { + package_chmod($working_file); + + if (!is_writable($working_file)) + $actions[] = array( + 'type' => 'chmod', + 'filename' => $working_file + ); + + if (!$testing && !empty($modSettings['package_make_backups']) && file_exists($working_file)) + { + if (basename($working_file) == 'Settings.php') + @copy($working_file, dirname($working_file) . '/Settings_bak.php'); + else + @copy($working_file, $working_file . '~'); + } + + package_put_contents($working_file, $working_data, $testing); + } + + if ($working_file !== null) + $actions[] = array( + 'type' => 'saved', + 'filename' => $working_file, + 'is_custom' => $is_custom, + ); + + $actions[] = array( + 'type' => 'result', + 'status' => $everything_found + ); + + return $actions; +} + +/** + * Get the physical contents of a packages file + * + * @param string $filename The package file + * @return string The contents of the specified file + */ +function package_get_contents($filename) +{ + global $package_cache, $modSettings; + + if (!isset($package_cache)) + { + $mem_check = setMemoryLimit('128M'); + + // Windows doesn't seem to care about the memory_limit. + if (!empty($modSettings['package_disable_cache']) || $mem_check || stripos(PHP_OS, 'win') !== false) + $package_cache = array(); + else + $package_cache = false; + } + + if (strpos($filename, 'Packages/') !== false || $package_cache === false || !isset($package_cache[$filename])) + return file_get_contents($filename); + else + return $package_cache[$filename]; +} + +/** + * Writes data to a file, almost exactly like the file_put_contents() function. + * uses FTP to create/chmod the file when necessary and available. + * uses text mode for text mode file extensions. + * returns the number of bytes written. + * + * @param string $filename The name of the file + * @param string $data The data to write to the file + * @param bool $testing Whether we're just testing things + * @return int The length of the data written (in bytes) + */ +function package_put_contents($filename, $data, $testing = false) +{ + /** @var ftp_connection $package_ftp */ + global $package_ftp, $package_cache, $modSettings; + static $text_filetypes = array('php', 'txt', '.js', 'css', 'vbs', 'tml', 'htm'); + + if (!isset($package_cache)) + { + // Try to increase the memory limit - we don't want to run out of ram! + $mem_check = setMemoryLimit('128M'); + + if (!empty($modSettings['package_disable_cache']) || $mem_check || stripos(PHP_OS, 'win') !== false) + $package_cache = array(); + else + $package_cache = false; + } + + if (isset($package_ftp)) + $ftp_file = strtr($filename, array($_SESSION['pack_ftp']['root'] => '')); + + if (!file_exists($filename) && isset($package_ftp)) + $package_ftp->create_file($ftp_file); + elseif (!file_exists($filename)) + @touch($filename); + + package_chmod($filename); + + if (!$testing && (strpos($filename, 'Packages/') !== false || $package_cache === false)) + { + $fp = @fopen($filename, in_array(substr($filename, -3), $text_filetypes) ? 'w' : 'wb'); + + // We should show an error message or attempt a rollback, no? + if (!$fp) + return false; + + fwrite($fp, $data); + fclose($fp); + } + elseif (strpos($filename, 'Packages/') !== false || $package_cache === false) + return strlen($data); + else + { + $package_cache[$filename] = $data; + + // Permission denied, eh? + $fp = @fopen($filename, 'r+'); + if (!$fp) + return false; + fclose($fp); + } + + return strlen($data); +} + +/** + * Flushes the cache from memory to the filesystem + * + * @param bool $trash + */ +function package_flush_cache($trash = false) +{ + /** @var ftp_connection $package_ftp */ + global $package_ftp, $package_cache, $txt; + static $text_filetypes = array('php', 'txt', '.js', 'css', 'vbs', 'tml', 'htm'); + + if (empty($package_cache)) + return; + + // First, let's check permissions! + foreach ($package_cache as $filename => $data) + { + if (isset($package_ftp)) + $ftp_file = strtr($filename, array($_SESSION['pack_ftp']['root'] => '')); + + if (!file_exists($filename) && isset($package_ftp)) + $package_ftp->create_file($ftp_file); + elseif (!file_exists($filename)) + @touch($filename); + + $result = package_chmod($filename); + + // if we are not doing our test pass, then lets do a full write check + // bypass directories when doing this test + if ((!$trash) && !is_dir($filename)) + { + // acid test, can we really open this file for writing? + $fp = ($result) ? fopen($filename, 'r+') : $result; + if (!$fp) + { + // We should have package_chmod()'d them before, no?! + loadLanguage('Errors'); + trigger_error($txt['package_flush_cache_not_writable'], E_USER_WARNING); + return; + } + fclose($fp); + } + } + + if ($trash) + { + $package_cache = array(); + return; + } + + // Write the cache to disk here. + // Bypass directories when doing so - no data to write & the fopen will crash. + foreach ($package_cache as $filename => $data) + { + if (!is_dir($filename)) + { + $fp = fopen($filename, in_array(substr($filename, -3), $text_filetypes) ? 'w' : 'wb'); + fwrite($fp, $data); + fclose($fp); + } + } + + $package_cache = array(); +} + +/** + * Try to make a file writable. + * + * @param string $filename The name of the file + * @param string $perm_state The permission state - can be either 'writable' or 'execute' + * @param bool $track_change Whether to track this change + * @return boolean True if it worked, false if it didn't + */ +function package_chmod($filename, $perm_state = 'writable', $track_change = false) +{ + /** @var ftp_connection $package_ftp */ + global $package_ftp; + + if (file_exists($filename) && is_writable($filename) && $perm_state == 'writable') + return true; + + // Start off checking without FTP. + if (!isset($package_ftp) || $package_ftp === false) + { + for ($i = 0; $i < 2; $i++) + { + $chmod_file = $filename; + + // Start off with a less aggressive test. + if ($i == 0) + { + // If this file doesn't exist, then we actually want to look at whatever parent directory does. + $subTraverseLimit = 2; + while (!file_exists($chmod_file) && $subTraverseLimit) + { + $chmod_file = dirname($chmod_file); + $subTraverseLimit--; + } + + // Keep track of the writable status here. + $file_permissions = @fileperms($chmod_file); + } + else + { + // This looks odd, but it's an attempt to work around PHP suExec. + if (!file_exists($chmod_file) && $perm_state == 'writable') + { + $file_permissions = @fileperms(dirname($chmod_file)); + + mktree(dirname($chmod_file), 0755); + @touch($chmod_file); + smf_chmod($chmod_file, 0755); + } + else + $file_permissions = @fileperms($chmod_file); + } + + // This looks odd, but it's another attempt to work around PHP suExec. + if ($perm_state != 'writable') + smf_chmod($chmod_file, $perm_state == 'execute' ? 0755 : 0644); + else + { + if (!@is_writable($chmod_file)) + smf_chmod($chmod_file, 0755); + if (!@is_writable($chmod_file)) + smf_chmod($chmod_file, 0777); + if (!@is_writable(dirname($chmod_file))) + smf_chmod($chmod_file, 0755); + if (!@is_writable(dirname($chmod_file))) + smf_chmod($chmod_file, 0777); + } + + // The ultimate writable test. + if ($perm_state == 'writable') + { + $fp = is_dir($chmod_file) ? @opendir($chmod_file) : @fopen($chmod_file, 'rb'); + if (@is_writable($chmod_file) && $fp) + { + if (!is_dir($chmod_file)) + fclose($fp); + else + closedir($fp); + + // It worked! + if ($track_change) + $_SESSION['pack_ftp']['original_perms'][$chmod_file] = $file_permissions; + + return true; + } + } + elseif ($perm_state != 'writable' && isset($_SESSION['pack_ftp']['original_perms'][$chmod_file])) + unset($_SESSION['pack_ftp']['original_perms'][$chmod_file]); + } + + // If we're here we're a failure. + return false; + } + // Otherwise we do have FTP? + elseif ($package_ftp !== false && !empty($_SESSION['pack_ftp'])) + { + $ftp_file = strtr($filename, array($_SESSION['pack_ftp']['root'] => '')); + + // This looks odd, but it's an attempt to work around PHP suExec. + if (!file_exists($filename) && $perm_state == 'writable') + { + $file_permissions = @fileperms(dirname($filename)); + + mktree(dirname($filename), 0755); + $package_ftp->create_file($ftp_file); + $package_ftp->chmod($ftp_file, 0755); + } + else + $file_permissions = @fileperms($filename); + + if ($perm_state != 'writable') + { + $package_ftp->chmod($ftp_file, $perm_state == 'execute' ? 0755 : 0644); + } + else + { + if (!@is_writable($filename)) + $package_ftp->chmod($ftp_file, 0777); + if (!@is_writable(dirname($filename))) + $package_ftp->chmod(dirname($ftp_file), 0777); + } + + if (@is_writable($filename)) + { + if ($track_change) + $_SESSION['pack_ftp']['original_perms'][$filename] = $file_permissions; + + return true; + } + elseif ($perm_state != 'writable' && isset($_SESSION['pack_ftp']['original_perms'][$filename])) + unset($_SESSION['pack_ftp']['original_perms'][$filename]); + } + + // Oh dear, we failed if we get here. + return false; +} + +/** + * Used to crypt the supplied ftp password in this session + * + * @param string $pass The password + * @return string The encrypted password + */ +function package_crypt($pass) +{ + $n = strlen($pass); + + $salt = session_id(); + while (strlen($salt) < $n) + $salt .= session_id(); + + for ($i = 0; $i < $n; $i++) + $pass[$i] = chr(ord($pass[$i]) ^ (ord($salt[$i]) - 32)); + + return $pass; +} + +/** + * @param string $dir + * @param string $filename The filename without an extension + * @param string $ext + * @return string The filename with a number appended but no extension + * @since 2.1 + */ +function package_unique_filename($dir, $filename, $ext) +{ + if (file_exists($dir . '/' . $filename . '.' . $ext)) + { + $i = 1; + while (file_exists($dir . '/' . $filename . '_' . $i . '.' . $ext)) + $i++; + $filename .= '_' . $i; + } + + return $filename; +} + +/** + * Creates a backup of forum files prior to modifying them + * + * @param string $id The name of the backup + * @return bool True if it worked, false if it didn't + */ +function package_create_backup($id = 'backup') +{ + global $sourcedir, $boarddir, $packagesdir, $smcFunc; + + $files = array(); + + $base_files = array('index.php', 'SSI.php', 'agreement.txt', 'cron.php', 'proxy.php', 'ssi_examples.php', 'ssi_examples.shtml', 'subscriptions.php'); + foreach ($base_files as $file) + { + if (file_exists($boarddir . '/' . $file)) + $files[empty($_REQUEST['use_full_paths']) ? $file : $boarddir . '/' . $file] = $boarddir . '/' . $file; + } + + $dirs = array( + $sourcedir => empty($_REQUEST['use_full_paths']) ? 'Sources/' : strtr($sourcedir . '/', '\\', '/') + ); + + $request = $smcFunc['db_query']('', ' + SELECT value + FROM {db_prefix}themes + WHERE id_member = {int:no_member} + AND variable = {string:theme_dir}', + array( + 'no_member' => 0, + 'theme_dir' => 'theme_dir', + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $dirs[$row['value']] = empty($_REQUEST['use_full_paths']) ? 'Themes/' . basename($row['value']) . '/' : strtr($row['value'] . '/', '\\', '/'); + $smcFunc['db_free_result']($request); + + try + { + foreach ($dirs as $dir => $dest) + { + $iter = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST, + RecursiveIteratorIterator::CATCH_GET_CHILD // Ignore "Permission denied" + ); + + foreach ($iter as $entry => $dir) + { + if ($dir->isDir()) + continue; + + if (preg_match('~^(\.{1,2}|CVS|backup.*|help|images|.*\~|.*minified_[a-z0-9]{32}\.(js|css))$~', $entry) != 0) + continue; + + $files[empty($_REQUEST['use_full_paths']) ? str_replace(realpath($boarddir), '', $entry) : $entry] = $entry; + } + } + $obj = new ArrayObject($files); + $iterator = $obj->getIterator(); + + if (!file_exists($packagesdir . '/backups')) + mktree($packagesdir . '/backups', 0777); + if (!is_writable($packagesdir . '/backups')) + package_chmod($packagesdir . '/backups'); + $output_file = $packagesdir . '/backups/' . smf_strftime('%Y-%m-%d_') . preg_replace('~[$\\\\/:<>|?*"\']~', '', $id); + $output_ext = '.tar'; + $output_ext_target = '.tar.gz'; + + if (file_exists($output_file . $output_ext_target)) + { + $i = 2; + while (file_exists($output_file . '_' . $i . $output_ext_target)) + $i++; + $output_file = $output_file . '_' . $i . $output_ext; + } + else + $output_file .= $output_ext; + + @set_time_limit(300); + if (function_exists('apache_reset_timeout')) + @apache_reset_timeout(); + + // Phar doesn't handle open_basedir restrictions very well and throws a PHP Warning. Ignore that. + set_error_handler( + function($errno, $errstr, $errfile, $errline) + { + // error was suppressed with the @-operator + if (0 === error_reporting()) + return false; + + if (strpos($errstr, 'PharData::__construct(): open_basedir') === false && strpos($errstr, 'PharData::compress(): open_basedir') === false) + log_error($errstr, 'general', $errfile, $errline); + + return true; + } + ); + $a = new PharData($output_file); + $a->buildFromIterator($iterator); + $a->compress(Phar::GZ); + restore_error_handler(); + + /* + * Destroying the local var tells PharData to close its internal + * file pointer, enabling us to delete the uncompressed tarball. + */ + unset($a); + unlink($output_file); + } + catch (Exception $e) + { + log_error($e->getMessage(), 'backup'); + + return false; + } + + return true; +} + +if (!function_exists('smf_crc32')) +{ + /** + * crc32 doesn't work as expected on 64-bit functions - make our own. + * https://php.net/crc32#79567 + * + * @param string $number + * @return string The crc32 + */ + function smf_crc32($number) + { + $crc = crc32($number); + + if ($crc & 0x80000000) + { + $crc ^= 0xffffffff; + $crc += 1; + $crc = -$crc; + } + + return $crc; + } +} + +/** + * Validate a package during install + * + * @param array $package Package data + * @return array Results from the package validation. + */ +function package_validate_installtest($package) +{ + global $context; + + // Don't validate directories. + $context['package_sha256_hash'] = is_dir($package['file_name']) ? null : hash_file('sha256', $package['file_name']); + + $sendData = array(array( + 'sha256_hash' => $context['package_sha256_hash'], + 'file_name' => basename($package['file_name']), + 'custom_id' => $package['custom_id'], + 'custom_type' => $package['custom_type'], + )); + + return package_validate_send($sendData); +} + +/** + * Validate multiple packages. + * + * @param array $packages Package data + * @return array Results from the package validation. + */ +function package_validate($packages) +{ + global $context, $smcFunc; + + // Setup our send data. + $sendData = array(); + + // Go through all packages and get them ready to send up. + foreach ($packages as $id_package => $package) + { + $sha256_hash = hash_file('sha256', $package); + $packageInfo = getPackageInfo($package); + + $packageID = ''; + if (isset($packageInfo['id'])) + $packageID = $packageInfo['id']; + + $packageType = 'modification'; + if (isset($package['type'])) + $packageType = $package['type']; + + $sendData[] = array( + 'sha256_hash' => $sha256_hash, + 'file_name' => basename($package), + 'custom_id' => $packageID, + 'custom_type' => $packageType, + ); + } + + return package_validate_send($sendData); +} + +/** + * Sending data off to validate packages. + * + * @param array $sendData Json encoded data to be sent to the validation servers. + * @return array Results from the package validation. + */ +function package_validate_send($sendData) +{ + global $context, $smcFunc; + + // First lets get all package servers into here. + if (empty($context['package_servers'])) + { + $request = $smcFunc['db_query']('', ' + SELECT id_server, name, validation_url, extra + FROM {db_prefix}package_servers + WHERE validation_url != {string:empty}', + array( + 'empty' => '', + )); + $context['package_servers'] = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $context['package_servers'][$row['id_server']] = $row; + $smcFunc['db_free_result']($request); + } + + $the_version = SMF_VERSION; + if (!empty($_SESSION['version_emulate'])) + $the_version = $_SESSION['version_emulate']; + + // Test each server. + $return_data = array(); + foreach ($context['package_servers'] as $id_server => $server) + { + $return_data[$id_server] = array(); + + // Sub out any variables we support in the validation url. + $validate_url = strtr($server['validation_url'], array( + '{SMF_VERSION}' => urlencode($the_version) + )); + + $results = fetch_web_data($validate_url, 'data=' . json_encode($sendData)); + + $parsed_data = $smcFunc['json_decode']($results, true); + if (is_array($parsed_data) && isset($parsed_data['data']) && is_array($parsed_data['data'])) + { + foreach ($parsed_data['data'] as $sha256_hash => $status) + { + if ((string) $status === 'blacklist') + $context['package_blacklist_found'] = true; + + $return_data[$id_server][(string) $sha256_hash] = 'package_validation_status_' . ((string) $status); + } + } + } + + return $return_data; +} + +?> \ No newline at end of file diff --git a/Sources/Subs-Post.php b/Sources/Subs-Post.php new file mode 100644 index 0000000..187c6d5 --- /dev/null +++ b/Sources/Subs-Post.php @@ -0,0 +1,3182 @@ + '␀', "\x01" => '␁', "\x02" => '␂', "\x03" => '␃', + "\x04" => '␄', "\x05" => '␅', "\x06" => '␆', "\x07" => '␇', + "\x08" => '␈', "\x0b" => '␋', "\x0c" => '␌', "\x0e" => '␎', + "\x0f" => '␏', "\x10" => '␐', "\x11" => '␑', "\x12" => '␒', + "\x13" => '␓', "\x14" => '␔', "\x15" => '␕', "\x16" => '␖', + "\x17" => '␗', "\x18" => '␘', "\x19" => '␙', "\x1a" => '␚', + "\x1b" => '␛', "\x1c" => '␜', "\x1d" => '␝', "\x1e" => '␞', + "\x1f" => '␟', + ); + $message = strtr($message, $control_replacements); + + // This line makes all languages *theoretically* work even with the wrong charset ;). + if (empty($context['utf8'])) + $message = preg_replace('~&#(\d{4,5}|[2-9]\d{2,4}|1[2-9]\d);~', '&#$1;', $message); + + // Normalize Unicode characters for storage efficiency, better searching, etc. + else + $message = $smcFunc['normalize']($message); + + // Clean out any other funky stuff. + $message = sanitize_chars($message, 0); + + // Clean up after nobbc ;). + $message = preg_replace_callback( + '~\[nobbc\](.+?)\[/nobbc\]~is', + function($a) + { + return '[nobbc]' . strtr($a[1], array('[' => '[', ']' => ']', ':' => ':', '@' => '@')) . '[/nobbc]'; + }, + $message + ); + + // Remove \r's... they're evil! + $message = strtr($message, array("\r" => '')); + + // You won't believe this - but too many periods upsets apache it seems! + $message = preg_replace('~\.{100,}~', '...', $message); + + // Trim off trailing quotes - these often happen by accident. + while (substr($message, -7) == '[quote]') + $message = substr($message, 0, -7); + while (substr($message, 0, 8) == '[/quote]') + $message = substr($message, 8); + + if (strpos($message, '[cowsay') !== false && !allowedTo('bbc_cowsay')) + $message = preg_replace('~\[(/?)cowsay[^\]]*\]~iu', '[$1pre]', $message); + + // Find all code blocks, work out whether we'd be parsing them, then ensure they are all closed. + $in_tag = false; + $had_tag = false; + $codeopen = 0; + if (preg_match_all('~(\[(/)*code(?:=[^\]]+)?\])~is', $message, $matches)) + foreach ($matches[0] as $index => $dummy) + { + // Closing? + if (!empty($matches[2][$index])) + { + // If it's closing and we're not in a tag we need to open it... + if (!$in_tag) + $codeopen = true; + // Either way we ain't in one any more. + $in_tag = false; + } + // Opening tag... + else + { + $had_tag = true; + // If we're in a tag don't do nought! + if (!$in_tag) + $in_tag = true; + } + } + + // If we have an open tag, close it. + if ($in_tag) + $message .= '[/code]'; + // Open any ones that need to be open, only if we've never had a tag. + if ($codeopen && !$had_tag) + $message = '[code]' . $message; + + // Replace code BBC with placeholders. We'll restore them at the end. + $parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE); + for ($i = 0, $n = count($parts); $i < $n; $i++) + { + // It goes 0 = outside, 1 = begin tag, 2 = inside, 3 = close tag, repeat. + if ($i % 4 == 2) + { + $code_tag = $parts[$i - 1] . $parts[$i] . $parts[$i + 1]; + $substitute = $parts[$i - 1] . $i . $parts[$i + 1]; + $code_tags[$substitute] = $code_tag; + $parts[$i] = $i; + } + } + + $message = implode('', $parts); + + // The regular expression non breaking space has many versions. + $non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0'; + + // Now that we've fixed all the code tags, let's fix the img and url tags... + fixTags($message); + + // Replace /me.+?\n with [me=name]dsf[/me]\n. + if (strpos($user_info['name'], '[') !== false || strpos($user_info['name'], ']') !== false || strpos($user_info['name'], '\'') !== false || strpos($user_info['name'], '"') !== false) + $message = preg_replace('~(\A|\n)/me(?: | )([^\n]*)(?:\z)?~i', '$1[me="' . $user_info['name'] . '"]$2[/me]', $message); + else + $message = preg_replace('~(\A|\n)/me(?: | )([^\n]*)(?:\z)?~i', '$1[me=' . $user_info['name'] . ']$2[/me]', $message); + + if (!$previewing && strpos($message, '[html]') !== false) + { + if (allowedTo('bbc_html')) + $message = preg_replace_callback( + '~\[html\](.+?)\[/html\]~is', + function($m) + { + return '[html]' . strtr(un_htmlspecialchars($m[1]), array("\n" => ' ', ' ' => ' ', '[' => '[', ']' => ']')) . '[/html]'; + }, + $message + ); + + // We should edit them out, or else if an admin edits the message they will get shown... + else + { + while (strpos($message, '[html]') !== false) + $message = preg_replace('~\[[/]?html\]~i', '', $message); + } + } + + // Let's look at the time tags... + $message = preg_replace_callback( + '~\[time(?:=(absolute))*\](.+?)\[/time\]~i', + function($m) use ($modSettings, $user_info) + { + return "[time]" . (is_numeric("$m[2]") || @strtotime("$m[2]") == 0 ? "$m[2]" : strtotime("$m[2]") - ("$m[1]" == "absolute" ? 0 : (($modSettings["time_offset"] + $user_info["time_offset"]) * 3600))) . "[/time]"; + }, + $message + ); + + // Change the color specific tags to [color=the color]. + $message = preg_replace('~\[(black|blue|green|red|white)\]~', '[color=$1]', $message); // First do the opening tags. + $message = preg_replace('~\[/(black|blue|green|red|white)\]~', '[/color]', $message); // And now do the closing tags + + // Neutralize any BBC tags this member isn't permitted to use. + if (empty($disallowed_tags_regex)) + { + // Legacy BBC are only retained for historical reasons. They're not for use in new posts. + $disallowed_bbc = $context['legacy_bbc']; + + // Some BBC require permissions. + foreach ($context['restricted_bbc'] as $bbc) + { + // Skip html, since we handled it separately above. + if ($bbc === 'html') + continue; + if (!allowedTo('bbc_' . $bbc)) + $disallowed_bbc[] = $bbc; + } + + $disallowed_tags_regex = build_regex(array_unique($disallowed_bbc), '~'); + } + if (!empty($disallowed_tags_regex)) + $message = preg_replace('~\[(?=/?' . $disallowed_tags_regex . '\b)~i', '[', $message); + + // Make sure all tags are lowercase. + $message = preg_replace_callback( + '~\[(/?)(list|li|table|tr|td)\b([^\]]*)\]~i', + function($m) + { + return "[$m[1]" . strtolower("$m[2]") . "$m[3]]"; + }, + $message + ); + + $list_open = substr_count($message, '[list]') + substr_count($message, '[list '); + $list_close = substr_count($message, '[/list]'); + if ($list_close - $list_open > 0) + $message = str_repeat('[list]', $list_close - $list_open) . $message; + if ($list_open - $list_close > 0) + $message = $message . str_repeat('[/list]', $list_open - $list_close); + + $mistake_fixes = array( + // Find [table]s not followed by [tr]. + '~\[table\](?![\s' . $non_breaking_space . ']*\[tr\])~s' . ($context['utf8'] ? 'u' : '') => '[table][tr]', + // Find [tr]s not followed by [td]. + '~\[tr\](?![\s' . $non_breaking_space . ']*\[td\])~s' . ($context['utf8'] ? 'u' : '') => '[tr][td]', + // Find [/td]s not followed by something valid. + '~\[/td\](?![\s' . $non_breaking_space . ']*(?:\[td\]|\[/tr\]|\[/table\]))~s' . ($context['utf8'] ? 'u' : '') => '[/td][/tr]', + // Find [/tr]s not followed by something valid. + '~\[/tr\](?![\s' . $non_breaking_space . ']*(?:\[tr\]|\[/table\]))~s' . ($context['utf8'] ? 'u' : '') => '[/tr][/table]', + // Find [/td]s incorrectly followed by [/table]. + '~\[/td\][\s' . $non_breaking_space . ']*\[/table\]~s' . ($context['utf8'] ? 'u' : '') => '[/td][/tr][/table]', + // Find [table]s, [tr]s, and [/td]s (possibly correctly) followed by [td]. + '~\[(table|tr|/td)\]([\s' . $non_breaking_space . ']*)\[td\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_td_]', + // Now, any [td]s left should have a [tr] before them. + '~\[td\]~s' => '[tr][td]', + // Look for [tr]s which are correctly placed. + '~\[(table|/tr)\]([\s' . $non_breaking_space . ']*)\[tr\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_tr_]', + // Any remaining [tr]s should have a [table] before them. + '~\[tr\]~s' => '[table][tr]', + // Look for [/td]s followed by [/tr]. + '~\[/td\]([\s' . $non_breaking_space . ']*)\[/tr\]~s' . ($context['utf8'] ? 'u' : '') => '[/td]$1[_/tr_]', + // Any remaining [/tr]s should have a [/td]. + '~\[/tr\]~s' => '[/td][/tr]', + // Look for properly opened [li]s which aren't closed. + '~\[li\]([^\[\]]+?)\[li\]~s' => '[li]$1[_/li_][_li_]', + '~\[li\]([^\[\]]+?)\[/list\]~s' => '[_li_]$1[_/li_][/list]', + '~\[li\]([^\[\]]+?)$~s' => '[li]$1[/li]', + // Lists - find correctly closed items/lists. + '~\[/li\]([\s' . $non_breaking_space . ']*)\[/list\]~s' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[/list]', + // Find list items closed and then opened. + '~\[/li\]([\s' . $non_breaking_space . ']*)\[li\]~s' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[_li_]', + // Now, find any [list]s or [/li]s followed by [li]. + '~\[(list(?: [^\]]*?)?|/li)\]([\s' . $non_breaking_space . ']*)\[li\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_li_]', + // Allow for sub lists. + '~\[/li\]([\s' . $non_breaking_space . ']*)\[list\]~' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[list]', + '~\[/list\]([\s' . $non_breaking_space . ']*)\[li\]~' . ($context['utf8'] ? 'u' : '') => '[/list]$1[_li_]', + // Any remaining [li]s weren't inside a [list]. + '~\[li\]~' => '[list][li]', + // Any remaining [/li]s weren't before a [/list]. + '~\[/li\]~' => '[/li][/list]', + // Put the correct ones back how we found them. + '~\[_(li|/li|td|tr|/tr)_\]~' => '[$1]', + // Images with no real url. + '~\[img\]https?://.{0,7}\[/img\]~' => '', + ); + + // Fix up some use of tables without [tr]s, etc. (it has to be done more than once to catch it all.) + for ($j = 0; $j < 3; $j++) + $message = preg_replace(array_keys($mistake_fixes), $mistake_fixes, $message); + + // Remove empty bbc from the sections outside the code tags + if (empty($tags_regex)) + { + require_once($sourcedir . '/Subs.php'); + + $allowed_empty = array('anchor', 'td',); + + $tags = array(); + foreach (($codes = parse_bbc(false)) as $code) + if (!in_array($code['tag'], $allowed_empty)) + $tags[] = $code['tag']; + + $tags_regex = build_regex($tags, '~'); + } + while (preg_match('~\[(' . $tags_regex . ')\b[^\]]*\]\s*\[/\1\]\s?~i', $message)) + $message = preg_replace('~\[(' . $tags_regex . ')[^\]]*\]\s*\[/\1\]\s?~i', '', $message); + + // Restore code blocks + if (!empty($code_tags)) + $message = str_replace(array_keys($code_tags), array_values($code_tags), $message); + + // Restore white space entities + if (!$previewing) + $message = strtr($message, array(' ' => '  ', "\n" => '
          ', $context['utf8'] ? "\xC2\xA0" : "\xA0" => ' ')); + else + $message = strtr($message, array(' ' => '  ', $context['utf8'] ? "\xC2\xA0" : "\xA0" => ' ')); + + // Now let's quickly clean up things that will slow our parser (which are common in posted code.) + $message = strtr($message, array('[]' => '[]', '['' => '['')); + + // Any hooks want to work here? + call_integration_hook('integrate_preparsecode', array(&$message, $previewing)); +} + +/** + * This is very simple, and just removes things done by preparsecode. + * + * @param string $message The message + */ +function un_preparsecode($message) +{ + global $smcFunc; + + // Any hooks want to work here? + call_integration_hook('integrate_unpreparsecode', array(&$message)); + + $parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE); + + // We're going to unparse only the stuff outside [code]... + for ($i = 0, $n = count($parts); $i < $n; $i++) + { + // If $i is a multiple of four (0, 4, 8, ...) then it's not a code section... + if ($i % 4 == 2) + { + $code_tag = $parts[$i - 1] . $parts[$i] . $parts[$i + 1]; + $substitute = $parts[$i - 1] . $i . $parts[$i + 1]; + $code_tags[$substitute] = $code_tag; + $parts[$i] = $i; + } + } + + $message = implode('', $parts); + + $message = preg_replace_callback( + '~\[html\](.+?)\[/html\]~i', + function($m) use ($smcFunc) + { + return "[html]" . strtr($smcFunc['htmlspecialchars']("$m[1]", ENT_QUOTES), array("\\"" => """, "&#13;" => "
          ", "&#32;" => " ", "&#91;" => "[", "&#93;" => "]")) . "[/html]"; + }, + $message + ); + + if (strpos($message, '[cowsay') !== false && !allowedTo('bbc_cowsay')) + $message = preg_replace('~\[(/?)cowsay[^\]]*\]~iu', '[$1pre]', $message); + + // Attempt to un-parse the time to something less awful. + $message = preg_replace_callback( + '~\[time\](\d{0,10})\[/time\]~i', + function($m) + { + return "[time]" . timeformat("$m[1]", false) . "[/time]"; + }, + $message + ); + + if (!empty($code_tags)) + $message = strtr($message, $code_tags); + + // Change breaks back to \n's and &nsbp; back to spaces. + return preg_replace('~~', "\n", str_replace(' ', ' ', $message)); +} + +/** + * Fix any URLs posted - ie. remove 'javascript:'. + * Used by preparsecode, fixes links in message and returns nothing. + * + * @param string $message The message + */ +function fixTags(&$message) +{ + global $modSettings; + + // WARNING: Editing the below can cause large security holes in your forum. + // Edit only if you are sure you know what you are doing. + + $fixArray = array( + // [img]http://...[/img] or [img width=1]http://...[/img] + array( + 'tag' => 'img', + 'protocols' => array('http', 'https'), + 'embeddedUrl' => false, + 'hasEqualSign' => false, + 'hasExtra' => true, + ), + // [url]http://...[/url] + array( + 'tag' => 'url', + 'protocols' => array('http', 'https'), + 'embeddedUrl' => false, + 'hasEqualSign' => false, + ), + // [url=http://...]name[/url] + array( + 'tag' => 'url', + 'protocols' => array('http', 'https'), + 'embeddedUrl' => true, + 'hasEqualSign' => true, + ), + // [iurl]http://...[/iurl] + array( + 'tag' => 'iurl', + 'protocols' => array('http', 'https'), + 'embeddedUrl' => false, + 'hasEqualSign' => false, + ), + // [iurl=http://...]name[/iurl] + array( + 'tag' => 'iurl', + 'protocols' => array('http', 'https'), + 'embeddedUrl' => true, + 'hasEqualSign' => true, + ), + // The rest of these are deprecated. + // [ftp]ftp://...[/ftp] + array( + 'tag' => 'ftp', + 'protocols' => array('ftp', 'ftps', 'sftp'), + 'embeddedUrl' => false, + 'hasEqualSign' => false, + ), + // [ftp=ftp://...]name[/ftp] + array( + 'tag' => 'ftp', + 'protocols' => array('ftp', 'ftps', 'sftp'), + 'embeddedUrl' => true, + 'hasEqualSign' => true, + ), + // [flash]http://...[/flash] + array( + 'tag' => 'flash', + 'protocols' => array('http', 'https'), + 'embeddedUrl' => false, + 'hasEqualSign' => false, + 'hasExtra' => true, + ), + ); + + // Fix each type of tag. + foreach ($fixArray as $param) + fixTag($message, $param['tag'], $param['protocols'], $param['embeddedUrl'], $param['hasEqualSign'], !empty($param['hasExtra'])); + + // Now fix possible security problems with images loading links automatically... + $message = preg_replace_callback( + '~(\[img.*?\])(.+?)\[/img\]~is', + function($m) + { + return "$m[1]" . preg_replace("~action(=|%3d)(?!dlattach)~i", "action-", "$m[2]") . "[/img]"; + }, + $message + ); + +} + +/** + * Fix a specific class of tag - ie. url with =. + * Used by fixTags, fixes a specific tag's links. + * + * @param string $message The message + * @param string $myTag The tag + * @param string $protocols The protocols + * @param bool $embeddedUrl Whether it *can* be set to something + * @param bool $hasEqualSign Whether it *is* set to something + * @param bool $hasExtra Whether it can have extra cruft after the begin tag. + */ +function fixTag(&$message, $myTag, $protocols, $embeddedUrl = false, $hasEqualSign = false, $hasExtra = false) +{ + global $boardurl, $scripturl; + + $forbidden_protocols = array( + // Poses security risks. + 'javascript', + // Allows file data to be embedded, bypassing our attachment system. + 'data', + ); + + if (preg_match('~^([^:]+://[^/]+)~', $boardurl, $match) != 0) + $domain_url = $match[1]; + else + $domain_url = $boardurl . '/'; + + $replaces = array(); + + if ($hasEqualSign && $embeddedUrl) + { + $quoted = preg_match('~\[(' . $myTag . ')="~', $message); + preg_match_all('~\[(' . $myTag . ')=' . ($quoted ? '"(.*?)"' : '([^\]]*?)') . '\](?:(.+?)\[/(' . $myTag . ')\])?~is', $message, $matches); + } + elseif ($hasEqualSign) + preg_match_all('~\[(' . $myTag . ')=([^\]]*?)\](?:(.+?)\[/(' . $myTag . ')\])?~is', $message, $matches); + else + preg_match_all('~\[(' . $myTag . ($hasExtra ? '(?:[^\]]*?)' : '') . ')\](.+?)\[/(' . $myTag . ')\]~is', $message, $matches); + + foreach ($matches[0] as $k => $dummy) + { + // Remove all leading and trailing whitespace. + $replace = trim($matches[2][$k]); + $this_tag = $matches[1][$k]; + $this_close = $hasEqualSign ? (empty($matches[4][$k]) ? '' : $matches[4][$k]) : $matches[3][$k]; + + $found = false; + foreach ($protocols as $protocol) + { + $found = strncasecmp($replace, $protocol . '://', strlen($protocol) + 3) === 0; + if ($found) + break; + } + + $current_protocol = strtolower(parse_iri($replace, PHP_URL_SCHEME) ?? ""); + + if (in_array($current_protocol, $forbidden_protocols)) + { + $replace = 'about:invalid'; + } + elseif (!$found && $protocols[0] == 'http') + { + // A path + if (substr($replace, 0, 1) == '/' && substr($replace, 0, 2) != '//') + $replace = $domain_url . $replace; + // A query + elseif (substr($replace, 0, 1) == '?') + $replace = $scripturl . $replace; + // A fragment + elseif (substr($replace, 0, 1) == '#' && $embeddedUrl) + { + $replace = '#' . preg_replace('~[^A-Za-z0-9_\-#]~', '', substr($replace, 1)); + $this_tag = 'iurl'; + $this_close = 'iurl'; + } + elseif (substr($replace, 0, 2) != '//' && empty($current_protocol)) + $replace = $protocols[0] . '://' . $replace; + } + elseif (!$found && $protocols[0] == 'ftp') + $replace = $protocols[0] . '://' . preg_replace('~^(?!ftps?)[^:]+://~', '', $replace); + elseif (!$found && empty($current_protocol)) + $replace = $protocols[0] . '://' . $replace; + + if ($hasEqualSign && $embeddedUrl) + $replaces[$matches[0][$k]] = '[' . $this_tag . '="' . $replace . '"]' . (empty($matches[4][$k]) ? '' : $matches[3][$k] . '[/' . $this_close . ']'); + elseif ($hasEqualSign) + $replaces['[' . $matches[1][$k] . '=' . $matches[2][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']'; + elseif ($embeddedUrl) + $replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']' . $matches[2][$k] . '[/' . $this_close . ']'; + else + $replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . ']' . $replace . '[/' . $this_close . ']'; + } + + foreach ($replaces as $k => $v) + { + if ($k == $v) + unset($replaces[$k]); + } + + if (!empty($replaces)) + $message = strtr($message, $replaces); +} + +/** + * This function sends an email to the specified recipient(s). + * It uses the mail_type settings and webmaster_email variable. + * + * @param array $to The email(s) to send to + * @param string $subject Email subject, expected to have entities, and slashes, but not be parsed + * @param string $message Email body, expected to have slashes, no htmlentities + * @param string $from The address to use for replies + * @param string $message_id If specified, it will be used as local part of the Message-ID header. + * @param bool $send_html Whether or not the message is HTML vs. plain text + * @param int $priority The priority of the message + * @param bool $hotmail_fix Whether to apply the "hotmail fix" + * @param bool $is_private Whether this is private + * @return boolean Whether ot not the email was sent properly. + */ +function sendmail($to, $subject, $message, $from = null, $message_id = null, $send_html = false, $priority = 3, $hotmail_fix = null, $is_private = false) +{ + global $webmaster_email, $context, $modSettings, $txt, $scripturl; + + // Use sendmail if it's set or if no SMTP server is set. + $use_sendmail = empty($modSettings['mail_type']) || $modSettings['smtp_host'] == ''; + + // Line breaks need to be \r\n only in windows or for SMTP. + // Starting with php 8x, line breaks need to be \r\n even for linux. + $line_break = ($context['server']['is_windows'] || !$use_sendmail || version_compare(PHP_VERSION, '8.0.0', '>=')) ? "\r\n" : "\n"; + + // So far so good. + $mail_result = true; + + // If the recipient list isn't an array, make it one. + $to_array = is_array($to) ? $to : array($to); + + // Make sure we actually have email addresses to send this to + foreach ($to_array as $k => $v) + { + // This should never happen, but better safe than sorry + if (trim($v) == '') + { + unset($to_array[$k]); + } + } + + // Nothing left? Nothing else to do + if (empty($to_array)) + return true; + + // Once upon a time, Hotmail could not interpret non-ASCII mails. + // In honour of those days, it's still called the 'hotmail fix'. + if ($hotmail_fix === null) + { + $hotmail_to = array(); + foreach ($to_array as $i => $to_address) + { + if (preg_match('~@(att|comcast|bellsouth)\.[a-zA-Z\.]{2,6}$~i', $to_address) === 1) + { + $hotmail_to[] = $to_address; + $to_array = array_diff($to_array, array($to_address)); + } + } + + // Call this function recursively for the hotmail addresses. + if (!empty($hotmail_to)) + $mail_result = sendmail($hotmail_to, $subject, $message, $from, $message_id, $send_html, $priority, true, $is_private); + + // The remaining addresses no longer need the fix. + $hotmail_fix = false; + + // No other addresses left? Return instantly. + if (empty($to_array)) + return $mail_result; + } + + // Get rid of entities. + $subject = un_htmlspecialchars($subject); + // Make the message use the proper line breaks. + $message = str_replace(array("\r", "\n"), array('', $line_break), $message); + + // Make sure hotmail mails are sent as HTML so that HTML entities work. + if ($hotmail_fix && !$send_html) + { + $send_html = true; + $message = strtr($message, array($line_break => '
          ' . $line_break)); + $message = preg_replace('~(' . preg_quote($scripturl, '~') . '(?:[?/][\w\-_%\.,\?&;=#]+)?)~', '$1', $message); + } + + list (, $from_name) = mimespecialchars(addcslashes($from !== null ? $from : $context['forum_name'], '<>()\'\\"'), true, $hotmail_fix, $line_break); + list (, $subject) = mimespecialchars($subject, true, $hotmail_fix, $line_break); + + // Construct the mail headers... + $headers = 'From: "' . $from_name . '" <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>' . $line_break; + $headers .= $from !== null ? 'Reply-To: <' . $from . '>' . $line_break : ''; + $headers .= 'Return-Path: ' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . $line_break; + $headers .= 'Date: ' . gmdate('D, d M Y H:i:s') . ' -0000' . $line_break; + + if ($message_id !== null && empty($modSettings['mail_no_message_id'])) + $headers .= 'Message-ID: <' . md5($scripturl . microtime()) . '-' . $message_id . strstr(empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from'], '@') . '>' . $line_break; + $headers .= 'X-Mailer: SMF' . $line_break; + + // Pass this to the integration before we start modifying the output -- it'll make it easier later. + if (in_array(false, call_integration_hook('integrate_outgoing_email', array(&$subject, &$message, &$headers, &$to_array)), true)) + return false; + + // Save the original message... + $orig_message = $message; + + // The mime boundary separates the different alternative versions. + $mime_boundary = 'SMF-' . md5($message . time()); + + // Using mime, as it allows to send a plain unencoded alternative. + $headers .= 'Mime-Version: 1.0' . $line_break; + $headers .= 'content-type: multipart/alternative; boundary="' . $mime_boundary . '"' . $line_break; + $headers .= 'content-transfer-encoding: 7bit' . $line_break; + + // Sending HTML? Let's plop in some basic stuff, then. + if ($send_html) + { + $no_html_message = un_htmlspecialchars(strip_tags(strtr($orig_message, array('' => $line_break)))); + + // But, then, dump it and use a plain one for dinosaur clients. + list(, $plain_message) = mimespecialchars($no_html_message, false, true, $line_break); + $message = $plain_message . $line_break . '--' . $mime_boundary . $line_break; + + // This is the plain text version. Even if no one sees it, we need it for spam checkers. + list($charset, $plain_charset_message, $encoding) = mimespecialchars($no_html_message, false, false, $line_break); + $message .= 'content-type: text/plain; charset=' . $charset . $line_break; + $message .= 'content-transfer-encoding: ' . $encoding . $line_break . $line_break; + $message .= $plain_charset_message . $line_break . '--' . $mime_boundary . $line_break; + + // This is the actual HTML message, prim and proper. If we wanted images, they could be inlined here (with multipart/related, etc.) + list($charset, $html_message, $encoding) = mimespecialchars($orig_message, false, $hotmail_fix, $line_break); + $message .= 'content-type: text/html; charset=' . $charset . $line_break; + $message .= 'content-transfer-encoding: ' . ($encoding == '' ? '7bit' : $encoding) . $line_break . $line_break; + $message .= $html_message . $line_break . '--' . $mime_boundary . '--'; + } + // Text is good too. + else + { + // Send a plain message first, for the older web clients. + list(, $plain_message) = mimespecialchars($orig_message, false, true, $line_break); + $message = $plain_message . $line_break . '--' . $mime_boundary . $line_break; + + // Now add an encoded message using the forum's character set. + list ($charset, $encoded_message, $encoding) = mimespecialchars($orig_message, false, false, $line_break); + $message .= 'content-type: text/plain; charset=' . $charset . $line_break; + $message .= 'content-transfer-encoding: ' . $encoding . $line_break . $line_break; + $message .= $encoded_message . $line_break . '--' . $mime_boundary . '--'; + } + + // Are we using the mail queue, if so this is where we butt in... + if ($priority != 0) + return AddMailQueue(false, $to_array, $subject, $message, $headers, $send_html, $priority, $is_private); + + // If it's a priority mail, send it now - note though that this should NOT be used for sending many at once. + elseif (!empty($modSettings['mail_limit'])) + { + list ($last_mail_time, $mails_this_minute) = @explode('|', $modSettings['mail_recent']); + if (empty($mails_this_minute) || time() > $last_mail_time + 60) + $new_queue_stat = time() . '|' . 1; + else + $new_queue_stat = $last_mail_time . '|' . ((int) $mails_this_minute + 1); + + updateSettings(array('mail_recent' => $new_queue_stat)); + } + + // SMTP or sendmail? + if ($use_sendmail) + { + $subject = strtr($subject, array("\r" => '', "\n" => '')); + if (!empty($modSettings['mail_strip_carriage'])) + { + $message = strtr($message, array("\r" => '')); + $headers = strtr($headers, array("\r" => '')); + } + + foreach ($to_array as $to) + { + set_error_handler( + function($errno, $errstr, $errfile, $errline) + { + // error was suppressed with the @-operator + if (0 === error_reporting()) + return false; + + throw new ErrorException($errstr, 0, $errno, $errfile, $errline); + } + ); + try + { + if (!mail(strtr($to, array("\r" => '', "\n" => '')), $subject, $message, $headers)) + { + log_error(sprintf($txt['mail_send_unable'], $to)); + $mail_result = false; + } + } + catch (ErrorException $e) + { + log_error($e->getMessage(), 'general', $e->getFile(), $e->getLine()); + log_error(sprintf($txt['mail_send_unable'], $to)); + $mail_result = false; + } + restore_error_handler(); + + // Wait, wait, I'm still sending here! + @set_time_limit(300); + if (function_exists('apache_reset_timeout')) + @apache_reset_timeout(); + } + } + else + $mail_result = $mail_result && smtp_mail($to_array, $subject, $message, $headers); + + // Everything go smoothly? + return $mail_result; +} + +/** + * Add an email to the mail queue. + * + * @param bool $flush Whether to flush the queue + * @param array $to_array An array of recipients + * @param string $subject The subject of the message + * @param string $message The message + * @param string $headers The headers + * @param bool $send_html Whether to send in HTML format + * @param int $priority The priority + * @param bool $is_private Whether this is private + * @return boolean Whether the message was added + */ +function AddMailQueue($flush = false, $to_array = array(), $subject = '', $message = '', $headers = '', $send_html = false, $priority = 3, $is_private = false) +{ + global $context, $smcFunc; + + static $cur_insert = array(); + static $cur_insert_len = 0; + + if ($cur_insert_len == 0) + $cur_insert = array(); + + // If we're flushing, make the final inserts - also if we're near the MySQL length limit! + if (($flush || $cur_insert_len > 800000) && !empty($cur_insert)) + { + // Only do these once. + $cur_insert_len = 0; + + // Dump the data... + $smcFunc['db_insert']('', + '{db_prefix}mail_queue', + array( + 'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255', + 'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int', + ), + $cur_insert, + array('id_mail') + ); + + $cur_insert = array(); + $context['flush_mail'] = false; + } + + // If we're flushing we're done. + if ($flush) + { + $nextSendTime = time() + 10; + + $smcFunc['db_query']('', ' + UPDATE {db_prefix}settings + SET value = {string:nextSendTime} + WHERE variable = {literal:mail_next_send} + AND value = {string:no_outstanding}', + array( + 'nextSendTime' => $nextSendTime, + 'no_outstanding' => '0', + ) + ); + + return true; + } + + // Ensure we tell obExit to flush. + $context['flush_mail'] = true; + + foreach ($to_array as $to) + { + // Will this insert go over MySQL's limit? + $this_insert_len = strlen($to) + strlen($message) + strlen($headers) + 700; + + // Insert limit of 1M (just under the safety) is reached? + if ($this_insert_len + $cur_insert_len > 1000000) + { + // Flush out what we have so far. + $smcFunc['db_insert']('', + '{db_prefix}mail_queue', + array( + 'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255', + 'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int', + ), + $cur_insert, + array('id_mail') + ); + + // Clear this out. + $cur_insert = array(); + $cur_insert_len = 0; + } + + // Now add the current insert to the array... + $cur_insert[] = array(time(), (string) $to, (string) $message, (string) $subject, (string) $headers, ($send_html ? 1 : 0), $priority, (int) $is_private); + $cur_insert_len += $this_insert_len; + } + + // If they are using SSI there is a good chance obExit will never be called. So lets be nice and flush it for them. + if (SMF === 'SSI' || SMF === 'BACKGROUND') + return AddMailQueue(true); + + return true; +} + +/** + * Sends an personal message from the specified person to the specified people + * ($from defaults to the user) + * + * @param array $recipients An array containing the arrays 'to' and 'bcc', both containing id_member's. + * @param string $subject Should have no slashes and no html entities + * @param string $message Should have no slashes and no html entities + * @param bool $store_outbox Whether to store it in the sender's outbox + * @param array $from An array with the id, name, and username of the member. + * @param int $pm_head The ID of the chain being replied to - if any. + * @return array An array with log entries telling how many recipients were successful and which recipients it failed to send to. + */ +function sendpm($recipients, $subject, $message, $store_outbox = false, $from = null, $pm_head = 0) +{ + global $scripturl, $txt, $user_info, $language, $sourcedir; + global $modSettings, $smcFunc; + + // Make sure the PM language file is loaded, we might need something out of it. + loadLanguage('PersonalMessage'); + + // Initialize log array. + $log = array( + 'failed' => array(), + 'sent' => array() + ); + + if ($from === null) + $from = array( + 'id' => $user_info['id'], + 'name' => $user_info['name'], + 'username' => $user_info['username'] + ); + + // This is the one that will go in their inbox. + $htmlmessage = $smcFunc['htmlspecialchars']($message, ENT_QUOTES); + preparsecode($htmlmessage); + $htmlsubject = strtr($smcFunc['htmlspecialchars']($subject), array("\r" => '', "\n" => '', "\t" => '')); + if ($smcFunc['strlen']($htmlsubject) > 100) + $htmlsubject = $smcFunc['substr']($htmlsubject, 0, 100); + + // Make sure is an array + if (!is_array($recipients)) + $recipients = array($recipients); + + // Integrated PMs + call_integration_hook('integrate_personal_message', array(&$recipients, &$from, &$subject, &$message)); + + // Get a list of usernames and convert them to IDs. + $usernames = array(); + foreach ($recipients as $rec_type => $rec) + { + foreach ($rec as $id => $member) + { + if (!is_numeric($recipients[$rec_type][$id])) + { + $recipients[$rec_type][$id] = $smcFunc['strtolower'](trim(preg_replace('~[<>&"\'=\\\]~', '', $recipients[$rec_type][$id]))); + $usernames[$recipients[$rec_type][$id]] = 0; + } + } + } + if (!empty($usernames)) + { + $request = $smcFunc['db_query']('pm_find_username', ' + SELECT id_member, member_name + FROM {db_prefix}members + WHERE ' . ($smcFunc['db_case_sensitive'] ? 'LOWER(member_name)' : 'member_name') . ' IN ({array_string:usernames})', + array( + 'usernames' => array_keys($usernames), + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + if (isset($usernames[$smcFunc['strtolower']($row['member_name'])])) + $usernames[$smcFunc['strtolower']($row['member_name'])] = $row['id_member']; + $smcFunc['db_free_result']($request); + + // Replace the usernames with IDs. Drop usernames that couldn't be found. + foreach ($recipients as $rec_type => $rec) + foreach ($rec as $id => $member) + { + if (is_numeric($recipients[$rec_type][$id])) + continue; + + if (!empty($usernames[$member])) + $recipients[$rec_type][$id] = $usernames[$member]; + else + { + $log['failed'][$id] = sprintf($txt['pm_error_user_not_found'], $recipients[$rec_type][$id]); + unset($recipients[$rec_type][$id]); + } + } + } + + // Make sure there are no duplicate 'to' members. + $recipients['to'] = array_unique($recipients['to']); + + // Only 'bcc' members that aren't already in 'to'. + $recipients['bcc'] = array_diff(array_unique($recipients['bcc']), $recipients['to']); + + // Combine 'to' and 'bcc' recipients. + $all_to = array_merge($recipients['to'], $recipients['bcc']); + + // Check no-one will want it deleted right away! + $request = $smcFunc['db_query']('', ' + SELECT + id_member, criteria, is_or + FROM {db_prefix}pm_rules + WHERE id_member IN ({array_int:to_members}) + AND delete_pm = {int:delete_pm}', + array( + 'to_members' => $all_to, + 'delete_pm' => 1, + ) + ); + $deletes = array(); + // Check whether we have to apply anything... + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $criteria = $smcFunc['json_decode']($row['criteria'], true); + // Note we don't check the buddy status, cause deletion from buddy = madness! + $delete = false; + foreach ($criteria as $criterium) + { + if (($criterium['t'] == 'mid' && $criterium['v'] == $from['id']) || ($criterium['t'] == 'gid' && in_array($criterium['v'], $user_info['groups'])) || ($criterium['t'] == 'sub' && strpos($subject, $criterium['v']) !== false) || ($criterium['t'] == 'msg' && strpos($message, $criterium['v']) !== false)) + $delete = true; + // If we're adding and one criteria don't match then we stop! + elseif (!$row['is_or']) + { + $delete = false; + break; + } + } + if ($delete) + $deletes[$row['id_member']] = 1; + } + $smcFunc['db_free_result']($request); + + // Load the membergrounp message limits. + // @todo Consider caching this? + static $message_limit_cache = array(); + if (!allowedTo('moderate_forum') && empty($message_limit_cache)) + { + $request = $smcFunc['db_query']('', ' + SELECT id_group, max_messages + FROM {db_prefix}membergroups', + array( + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $message_limit_cache[$row['id_group']] = $row['max_messages']; + $smcFunc['db_free_result']($request); + } + + // Load the groups that are allowed to read PMs. + require_once($sourcedir . '/Subs-Members.php'); + $pmReadGroups = groupsAllowedTo('pm_read'); + + if (empty($modSettings['permission_enable_deny'])) + $pmReadGroups['denied'] = array(); + + // Load their alert preferences + require_once($sourcedir . '/Subs-Notify.php'); + $notifyPrefs = getNotifyPrefs($all_to, array('pm_new', 'pm_reply', 'pm_notify'), true); + + $request = $smcFunc['db_query']('', ' + SELECT + member_name, real_name, id_member, email_address, lngfile, + instant_messages,' . (allowedTo('moderate_forum') ? ' 0' : ' + (pm_receive_from = {int:admins_only}' . (empty($modSettings['enable_buddylist']) ? '' : ' OR + (pm_receive_from = {int:buddies_only} AND FIND_IN_SET({string:from_id}, buddy_list) = 0) OR + (pm_receive_from = {int:not_on_ignore_list} AND FIND_IN_SET({string:from_id}, pm_ignore_list) != 0)') . ')') . ' AS ignored, + FIND_IN_SET({string:from_id}, buddy_list) != 0 AS is_buddy, is_activated, + additional_groups, id_group, id_post_group + FROM {db_prefix}members + WHERE id_member IN ({array_int:recipients}) + ORDER BY lngfile + LIMIT {int:count_recipients}', + array( + 'not_on_ignore_list' => 1, + 'buddies_only' => 2, + 'admins_only' => 3, + 'recipients' => $all_to, + 'count_recipients' => count($all_to), + 'from_id' => $from['id'], + ) + ); + $notifications = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // Don't do anything for members to be deleted! + if (isset($deletes[$row['id_member']])) + continue; + + // Load the preferences for this member (if any) + $prefs = !empty($notifyPrefs[$row['id_member']]) ? $notifyPrefs[$row['id_member']] : array(); + $prefs = array_merge(array( + 'pm_new' => 0, + 'pm_reply' => 0, + 'pm_notify' => 0, + ), $prefs); + + // We need to know this members groups. + $groups = explode(',', $row['additional_groups']); + $groups[] = $row['id_group']; + $groups[] = $row['id_post_group']; + + $message_limit = -1; + // For each group see whether they've gone over their limit - assuming they're not an admin. + if (!in_array(1, $groups)) + { + foreach ($groups as $id) + { + if (isset($message_limit_cache[$id]) && $message_limit != 0 && $message_limit < $message_limit_cache[$id]) + $message_limit = $message_limit_cache[$id]; + } + + if ($message_limit > 0 && $message_limit <= $row['instant_messages']) + { + $log['failed'][$row['id_member']] = sprintf($txt['pm_error_data_limit_reached'], $row['real_name']); + unset($all_to[array_search($row['id_member'], $all_to)]); + continue; + } + + // Do they have any of the allowed groups? + if (count(array_intersect($pmReadGroups['allowed'], $groups)) == 0 || count(array_intersect($pmReadGroups['denied'], $groups)) != 0) + { + $log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']); + unset($all_to[array_search($row['id_member'], $all_to)]); + continue; + } + } + + // Note that PostgreSQL can return a lowercase t/f for FIND_IN_SET + if (!empty($row['ignored']) && $row['ignored'] != 'f' && $row['id_member'] != $from['id']) + { + $log['failed'][$row['id_member']] = sprintf($txt['pm_error_ignored_by_user'], $row['real_name']); + unset($all_to[array_search($row['id_member'], $all_to)]); + continue; + } + + // If the receiving account is banned (>=10) or pending deletion (4), refuse to send the PM. + if ($row['is_activated'] >= 10 || ($row['is_activated'] == 4 && !$user_info['is_admin'])) + { + $log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']); + unset($all_to[array_search($row['id_member'], $all_to)]); + continue; + } + + // Send a notification, if enabled - taking the buddy list into account. + if (!empty($row['email_address']) + && ((empty($pm_head) && $prefs['pm_new'] & 0x02) || (!empty($pm_head) && $prefs['pm_reply'] & 0x02)) + && ($prefs['pm_notify'] <= 1 || ($prefs['pm_notify'] > 1 && (!empty($modSettings['enable_buddylist']) && $row['is_buddy']))) && $row['is_activated'] == 1) + { + $notifications[empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile']][] = $row['email_address']; + } + + $log['sent'][$row['id_member']] = sprintf(isset($txt['pm_successfully_sent']) ? $txt['pm_successfully_sent'] : '', $row['real_name']); + } + $smcFunc['db_free_result']($request); + + // Only 'send' the message if there are any recipients left. + if (empty($all_to)) + return $log; + + // Insert the message itself and then grab the last insert id. + $id_pm = $smcFunc['db_insert']('', + '{db_prefix}personal_messages', + array( + 'id_pm_head' => 'int', 'id_member_from' => 'int', 'deleted_by_sender' => 'int', + 'from_name' => 'string-255', 'msgtime' => 'int', 'subject' => 'string-255', 'body' => 'string-65534', + ), + array( + $pm_head, $from['id'], ($store_outbox ? 0 : 1), + $from['username'], time(), $htmlsubject, $htmlmessage, + ), + array('id_pm'), + 1 + ); + + // Add the recipients. + if (!empty($id_pm)) + { + // If this is new we need to set it part of it's own conversation. + if (empty($pm_head)) + $smcFunc['db_query']('', ' + UPDATE {db_prefix}personal_messages + SET id_pm_head = {int:id_pm_head} + WHERE id_pm = {int:id_pm_head}', + array( + 'id_pm_head' => $id_pm, + ) + ); + + // Some people think manually deleting personal_messages is fun... it's not. We protect against it though :) + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}pm_recipients + WHERE id_pm = {int:id_pm}', + array( + 'id_pm' => $id_pm, + ) + ); + + $insertRows = array(); + $to_list = array(); + foreach ($all_to as $to) + { + $insertRows[] = array($id_pm, $to, in_array($to, $recipients['bcc']) ? 1 : 0, isset($deletes[$to]) ? 1 : 0, 1); + if (!in_array($to, $recipients['bcc'])) + $to_list[] = $to; + } + + $smcFunc['db_insert']('insert', + '{db_prefix}pm_recipients', + array( + 'id_pm' => 'int', 'id_member' => 'int', 'bcc' => 'int', 'deleted' => 'int', 'is_new' => 'int' + ), + $insertRows, + array('id_pm', 'id_member') + ); + } + + $to_names = array(); + if (count($to_list) > 1) + { + $request = $smcFunc['db_query']('', ' + SELECT real_name + FROM {db_prefix}members + WHERE id_member IN ({array_int:to_members}) + AND id_member != {int:from}', + array( + 'to_members' => $to_list, + 'from' => $from['id'], + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $to_names[] = un_htmlspecialchars($row['real_name']); + $smcFunc['db_free_result']($request); + } + $replacements = array( + 'SUBJECT' => $subject, + 'MESSAGE' => $message, + 'SENDER' => un_htmlspecialchars($from['name']), + 'READLINK' => $scripturl . '?action=pm;pmsg=' . $id_pm . '#msg' . $id_pm, + 'REPLYLINK' => $scripturl . '?action=pm;sa=send;f=inbox;pmsg=' . $id_pm . ';quote;u=' . $from['id'], + 'TOLIST' => implode(', ', $to_names), + ); + $email_template = 'new_pm' . (empty($modSettings['disallow_sendBody']) ? '_body' : '') . (!empty($to_names) ? '_tolist' : ''); + + $notification_texts = array(); + + foreach ($notifications as $lang => $notification_list) + { + // Censor and parse BBC in the receiver's language. Only do each language once. + if (empty($notification_texts[$lang])) + { + if ($lang != $user_info['language']) + loadLanguage('index+Modifications', $lang, false); + + $notification_texts[$lang]['subject'] = $subject; + censorText($notification_texts[$lang]['subject']); + + if (empty($modSettings['disallow_sendBody'])) + { + $notification_texts[$lang]['body'] = $message; + + censorText($notification_texts[$lang]['body']); + + $notification_texts[$lang]['body'] = trim(un_htmlspecialchars(strip_tags(strtr(parse_bbc($smcFunc['htmlspecialchars']($notification_texts[$lang]['body']), false), array('
          ' => "\n", '
' => "\n", '' => "\n", '[' => '[', ']' => ']'))))); + } + else + $notification_texts[$lang]['body'] = ''; + + + if ($lang != $user_info['language']) + loadLanguage('index+Modifications', $user_info['language'], false); + } + + $replacements['SUBJECT'] = $notification_texts[$lang]['subject']; + $replacements['MESSAGE'] = $notification_texts[$lang]['body']; + + $emaildata = loadEmailTemplate($email_template, $replacements, $lang); + + // Off the notification email goes! + sendmail($notification_list, $emaildata['subject'], $emaildata['body'], null, 'p' . $id_pm, $emaildata['is_html'], 2, null, true); + } + + // Integrated After PMs + call_integration_hook('integrate_personal_message_after', array(&$id_pm, &$log, &$recipients, &$from, &$subject, &$message)); + + // Back to what we were on before! + loadLanguage('index+PersonalMessage'); + + // Add one to their unread and read message counts. + foreach ($all_to as $k => $id) + if (isset($deletes[$id])) + unset($all_to[$k]); + if (!empty($all_to)) + updateMemberData($all_to, array('instant_messages' => '+', 'unread_messages' => '+', 'new_pm' => 1)); + + return $log; +} + +/** + * Prepare text strings for sending as email body or header. + * In case there are higher ASCII characters in the given string, this + * function will attempt the transport method 'quoted-printable'. + * Otherwise the transport method '7bit' is used. + * + * @param string $string The string + * @param bool $with_charset Whether we're specifying a charset ($custom_charset must be set here) + * @param bool $hotmail_fix Whether to apply the hotmail fix (all higher ASCII characters are converted to HTML entities to assure proper display of the mail) + * @param string $line_break The linebreak + * @param string $custom_charset If set, it uses this character set + * @return array An array containing the character set, the converted string and the transport method. + */ +function mimespecialchars($string, $with_charset = true, $hotmail_fix = false, $line_break = "\r\n", $custom_charset = null) +{ + global $context; + + $charset = $custom_charset !== null ? $custom_charset : $context['character_set']; + + // This is the fun part.... + if (preg_match_all('~&#(\d{3,8});~', $string, $matches) !== 0 && !$hotmail_fix) + { + // Let's, for now, assume there are only 'ish characters. + $simple = true; + + foreach ($matches[1] as $entity) + if ($entity > 128) + $simple = false; + unset($matches); + + if ($simple) + $string = preg_replace_callback( + '~&#(\d{3,8});~', + function($m) + { + return chr("$m[1]"); + }, + $string + ); + else + { + // Try to convert the string to UTF-8. + if (!$context['utf8'] && function_exists('iconv')) + { + $newstring = @iconv($context['character_set'], 'UTF-8', $string); + if ($newstring) + $string = $newstring; + } + + $string = preg_replace_callback('~&#(\d{3,8});~', 'fixchar__callback', $string); + + // Unicode, baby. + $charset = 'UTF-8'; + } + } + + // Convert all special characters to HTML entities...just for Hotmail :-\ + if ($hotmail_fix && ($context['utf8'] || function_exists('iconv') || $context['character_set'] === 'ISO-8859-1')) + { + if (!$context['utf8'] && function_exists('iconv')) + { + $newstring = @iconv($context['character_set'], 'UTF-8', $string); + if ($newstring) + $string = $newstring; + } + + $entityConvert = function($m) + { + $c = $m[1]; + if (strlen($c) === 1 && ord($c[0]) <= 0x7F) + return $c; + elseif (strlen($c) === 2 && ord($c[0]) >= 0xC0 && ord($c[0]) <= 0xDF) + return "&#" . (((ord($c[0]) ^ 0xC0) << 6) + (ord($c[1]) ^ 0x80)) . ";"; + elseif (strlen($c) === 3 && ord($c[0]) >= 0xE0 && ord($c[0]) <= 0xEF) + return "&#" . (((ord($c[0]) ^ 0xE0) << 12) + ((ord($c[1]) ^ 0x80) << 6) + (ord($c[2]) ^ 0x80)) . ";"; + elseif (strlen($c) === 4 && ord($c[0]) >= 0xF0 && ord($c[0]) <= 0xF7) + return "&#" . (((ord($c[0]) ^ 0xF0) << 18) + ((ord($c[1]) ^ 0x80) << 12) + ((ord($c[2]) ^ 0x80) << 6) + (ord($c[3]) ^ 0x80)) . ";"; + else + return ""; + }; + + // Convert all 'special' characters to HTML entities. + return array($charset, preg_replace_callback('~([\x80-\x{10FFFF}])~u', $entityConvert, $string), '7bit'); + } + + // We don't need to mess with the subject line if no special characters were in it.. + elseif (!$hotmail_fix && preg_match('~([^\x09\x0A\x0D\x20-\x7F])~', $string) === 1) + { + // Base64 encode. + $string = base64_encode($string); + + // Show the characterset and the transfer-encoding for header strings. + if ($with_charset) + $string = '=?' . $charset . '?B?' . $string . '?='; + + // Break it up in lines (mail body). + else + $string = chunk_split($string, 76, $line_break); + + return array($charset, $string, 'base64'); + } + + else + return array($charset, $string, '7bit'); +} + +/** + * Sends mail, like mail() but over SMTP. + * It expects no slashes or entities. + * + * @internal + * + * @param array $mail_to_array Array of strings (email addresses) + * @param string $subject Email subject + * @param string $message Email message + * @param string $headers Email headers + * @return boolean Whether it sent or not. + */ +function smtp_mail($mail_to_array, $subject, $message, $headers) +{ + global $modSettings, $webmaster_email, $txt, $boardurl, $sourcedir; + + static $helo; + + $modSettings['smtp_host'] = trim($modSettings['smtp_host']); + + // Try POP3 before SMTP? + // @todo There's no interface for this yet. + if ($modSettings['mail_type'] == 3 && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '') + { + $socket = fsockopen($modSettings['smtp_host'], 110, $errno, $errstr, 2); + if (!$socket && (substr($modSettings['smtp_host'], 0, 5) == 'smtp.' || substr($modSettings['smtp_host'], 0, 11) == 'ssl://smtp.')) + $socket = fsockopen(strtr($modSettings['smtp_host'], array('smtp.' => 'pop.')), 110, $errno, $errstr, 2); + + if ($socket) + { + fgets($socket, 256); + fputs($socket, 'USER ' . $modSettings['smtp_username'] . "\r\n"); + fgets($socket, 256); + fputs($socket, 'PASS ' . base64_decode($modSettings['smtp_password']) . "\r\n"); + fgets($socket, 256); + fputs($socket, 'QUIT' . "\r\n"); + + fclose($socket); + } + } + + // Try to connect to the SMTP server... if it doesn't exist, only wait three seconds. + if (!$socket = fsockopen($modSettings['smtp_host'], empty($modSettings['smtp_port']) ? 25 : $modSettings['smtp_port'], $errno, $errstr, 3)) + { + // Maybe we can still save this? The port might be wrong. + if (substr($modSettings['smtp_host'], 0, 4) == 'ssl:' && (empty($modSettings['smtp_port']) || $modSettings['smtp_port'] == 25)) + { + // ssl:hostname can cause fsocketopen to fail with a lookup failure, ensure it exists for this test. + if (substr($modSettings['smtp_host'], 0, 6) != 'ssl://') + $modSettings['smtp_host'] = str_replace('ssl:', 'ss://', $modSettings['smtp_host']); + + if ($socket = fsockopen($modSettings['smtp_host'], 465, $errno, $errstr, 3)) + log_error($txt['smtp_port_ssl']); + } + + // Unable to connect! Don't show any error message, but just log one and try to continue anyway. + if (!$socket) + { + log_error($txt['smtp_no_connect'] . ': ' . $errno . ' : ' . $errstr); + return false; + } + } + + // Wait for a response of 220, without "-" continuer. + if (!server_parse(null, $socket, '220')) + return false; + + // Try to determine the server's fully qualified domain name + // Can't rely on $_SERVER['SERVER_NAME'] because it can be spoofed on Apache + if (empty($helo)) + { + // See if we can get the domain name from the host itself + if (function_exists('gethostname')) + $helo = gethostname(); + elseif (function_exists('php_uname')) + $helo = php_uname('n'); + + // If the hostname isn't a fully qualified domain name, we can use the host name from $boardurl instead + if (empty($helo) || strpos($helo, '.') === false || substr_compare($helo, '.local', -6) === 0 || (!empty($modSettings['tld_regex']) && !preg_match('/\.' . $modSettings['tld_regex'] . '$/u', $helo))) + $helo = parse_iri($boardurl, PHP_URL_HOST); + + // This is one of those situations where 'www.' is undesirable + if (strpos($helo, 'www.') === 0) + $helo = substr($helo, 4); + + if (!function_exists('idn_to_ascii')) + require_once($sourcedir . '/Subs-Compat.php'); + + $helo = idn_to_ascii($helo, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46); + } + + // SMTP = 1, SMTP - STARTTLS = 2 + if (in_array($modSettings['mail_type'], array(1, 2)) && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '') + { + // EHLO could be understood to mean encrypted hello... + if (server_parse('EHLO ' . $helo, $socket, null, $response) == '250') + { + // Are we using STARTTLS and does the server support STARTTLS? + if ($modSettings['mail_type'] == 2 && preg_match("~250( |-)STARTTLS~mi", $response)) + { + // Send STARTTLS to enable encryption + if (!server_parse('STARTTLS', $socket, '220')) + return false; + // Enable the encryption + // php 5.6+ fix + $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT; + + if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) + { + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + } + + if (!@stream_socket_enable_crypto($socket, true, $crypto_method)) + return false; + // Send the EHLO command again + if (!server_parse('EHLO ' . $helo, $socket, null) == '250') + return false; + } + + if (!server_parse('AUTH LOGIN', $socket, '334')) + return false; + // Send the username and password, encoded. + if (!server_parse(base64_encode($modSettings['smtp_username']), $socket, '334')) + return false; + // The password is already encoded ;) + if (!server_parse($modSettings['smtp_password'], $socket, '235')) + return false; + } + elseif (!server_parse('HELO ' . $helo, $socket, '250')) + return false; + } + else + { + // Just say "helo". + if (!server_parse('HELO ' . $helo, $socket, '250')) + return false; + } + + // Fix the message for any lines beginning with a period! (the first is ignored, you see.) + $message = strtr($message, array("\r\n" . '.' => "\r\n" . '..')); + + // !! Theoretically, we should be able to just loop the RCPT TO. + $mail_to_array = array_values($mail_to_array); + foreach ($mail_to_array as $i => $mail_to) + { + // Reset the connection to send another email. + if ($i != 0) + { + if (!server_parse('RSET', $socket, '250')) + return false; + } + + // From, to, and then start the data... + if (!server_parse('MAIL FROM: <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>', $socket, '250')) + return false; + if (!server_parse('RCPT TO: <' . $mail_to . '>', $socket, '250')) + return false; + if (!server_parse('DATA', $socket, '354')) + return false; + fputs($socket, 'Subject: ' . $subject . "\r\n"); + if (strlen($mail_to) > 0) + fputs($socket, 'To: <' . $mail_to . '>' . "\r\n"); + fputs($socket, $headers . "\r\n\r\n"); + fputs($socket, $message . "\r\n"); + + // Send a ., or in other words "end of data". + if (!server_parse('.', $socket, '250')) + return false; + + // Almost done, almost done... don't stop me just yet! + @set_time_limit(300); + if (function_exists('apache_reset_timeout')) + @apache_reset_timeout(); + } + fputs($socket, 'QUIT' . "\r\n"); + fclose($socket); + + return true; +} + +/** + * Parse a message to the SMTP server. + * Sends the specified message to the server, and checks for the + * expected response. + * + * @internal + * + * @param string $message The message to send + * @param resource $socket Socket to send on + * @param string $code The expected response code + * @param string $response The response from the SMTP server + * @return bool Whether it responded as such. + */ +function server_parse($message, $socket, $code, &$response = null) +{ + global $txt; + + if ($message !== null) + fputs($socket, $message . "\r\n"); + + // No response yet. + $server_response = ''; + + while (substr($server_response, 3, 1) != ' ') + { + if (!($server_response = fgets($socket, 256))) + { + // @todo Change this message to reflect that it may mean bad user/password/server issues/etc. + log_error($txt['smtp_bad_response']); + return false; + } + $response .= $server_response; + } + + if ($code === null) + return substr($server_response, 0, 3); + + if (substr($server_response, 0, 3) != $code) + { + log_error($txt['smtp_error'] . $server_response); + return false; + } + + return true; +} + +/** + * Spell checks the post for typos ;). + * It uses the pspell or enchant library, one of which MUST be installed. + * It has problems with internationalization. + * It is accessed via ?action=spellcheck. + */ +function SpellCheck() +{ + global $txt, $context, $smcFunc; + + // A list of "words" we know about but pspell doesn't. + $known_words = array('smf', 'php', 'mysql', 'www', 'gif', 'jpeg', 'png', 'http', 'smfisawesome', 'grandia', 'terranigma', 'rpgs'); + + loadLanguage('Post'); + loadTemplate('Post'); + + // Create a pspell or enchant dictionary resource + $dict = spell_init(); + + if (!isset($_POST['spellstring']) || !$dict) + die; + + // Construct a bit of Javascript code. + $context['spell_js'] = ' + var txt = {"done": "' . $txt['spellcheck_done'] . '"}; + var mispstr = window.opener.spellCheckGetText(spell_fieldname); + var misps = Array('; + + // Get all the words (Javascript already separated them). + $alphas = explode("\n", strtr($_POST['spellstring'], array("\r" => ''))); + + $found_words = false; + for ($i = 0, $n = count($alphas); $i < $n; $i++) + { + // Words are sent like 'word|offset_begin|offset_end'. + $check_word = explode('|', $alphas[$i]); + + // If the word is a known word, or spelled right... + if (in_array($smcFunc['strtolower']($check_word[0]), $known_words) || spell_check($dict, $check_word[0]) || !isset($check_word[2])) + continue; + + // Find the word, and move up the "last occurrence" to here. + $found_words = true; + + // Add on the javascript for this misspelling. + $context['spell_js'] .= ' + new misp("' . strtr($check_word[0], array('\\' => '\\\\', '"' => '\\"', '<' => '', '>' => '')) . '", ' . (int) $check_word[1] . ', ' . (int) $check_word[2] . ', ['; + + // If there are suggestions, add them in... + $suggestions = spell_suggest($dict, $check_word[0]); + if (!empty($suggestions)) + { + // But first check they aren't going to be censored - no naughty words! + foreach ($suggestions as $k => $word) + if ($suggestions[$k] != censorText($word)) + unset($suggestions[$k]); + + if (!empty($suggestions)) + $context['spell_js'] .= '"' . implode('", "', $suggestions) . '"'; + } + + $context['spell_js'] .= ']),'; + } + + // If words were found, take off the last comma. + if ($found_words) + $context['spell_js'] = substr($context['spell_js'], 0, -1); + + $context['spell_js'] .= ' + );'; + + // And instruct the template system to just show the spellcheck sub template. + $context['template_layers'] = array(); + $context['sub_template'] = 'spellcheck'; + + // Free resources for enchant... + if (isset($context['enchant_broker'])) + { + enchant_broker_free_dict($dict); + enchant_broker_free($context['enchant_broker']); + } +} + +/** + * Sends a notification to members who have elected to receive emails + * when things happen to a topic, such as replies are posted. + * The function automatically finds the subject and its board, and + * checks permissions for each member who is "signed up" for notifications. + * It will not send 'reply' notifications more than once in a row. + * Uses Post language file + * + * @param array $topics Represents the topics the action is happening to. + * @param string $type Can be any of reply, sticky, lock, unlock, remove, move, merge, and split. An appropriate message will be sent for each. + * @param array $exclude Members in the exclude array will not be processed for the topic with the same key. + * @param array $members_only Are the only ones that will be sent the notification if they have it on. + */ +function sendNotifications($topics, $type, $exclude = array(), $members_only = array()) +{ + global $user_info, $smcFunc; + + // Can't do it if there's no topics. + if (empty($topics)) + return; + // It must be an array - it must! + if (!is_array($topics)) + $topics = array($topics); + + // Get the subject and body... + $result = $smcFunc['db_query']('', ' + SELECT mf.subject, ml.body, ml.id_member, t.id_last_msg, t.id_topic, t.id_board, + COALESCE(mem.real_name, ml.poster_name) AS poster_name, mf.id_msg + FROM {db_prefix}topics AS t + INNER JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg) + INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg) + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ml.id_member) + WHERE t.id_topic IN ({array_int:topic_list}) + LIMIT 1', + array( + 'topic_list' => $topics, + ) + ); + $task_rows = array(); + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + $task_rows[] = array( + '$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array( + 'msgOptions' => array( + 'id' => $row['id_msg'], + 'subject' => $row['subject'], + 'body' => $row['body'], + ), + 'topicOptions' => array( + 'id' => $row['id_topic'], + 'board' => $row['id_board'], + ), + // Kinda cheeky, but for any action the originator is usually the current user + 'posterOptions' => array( + 'id' => $user_info['id'], + 'name' => $user_info['name'], + ), + 'type' => $type, + 'members_only' => $members_only, + )), 0 + ); + } + $smcFunc['db_free_result']($result); + + if (!empty($task_rows)) + $smcFunc['db_insert']('', + '{db_prefix}background_tasks', + array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'), + $task_rows, + array('id_task') + ); +} + +/** + * Create a post, either as new topic (id_topic = 0) or in an existing one. + * The input parameters of this function assume: + * - Strings have been escaped. + * - Integers have been cast to integer. + * - Mandatory parameters are set. + * + * @param array $msgOptions An array of information/options for the post + * @param array $topicOptions An array of information/options for the topic + * @param array $posterOptions An array of information/options for the poster + * @return bool Whether the operation was a success + */ +function createPost(&$msgOptions, &$topicOptions, &$posterOptions) +{ + global $user_info, $txt, $modSettings, $smcFunc, $sourcedir; + + require_once($sourcedir . '/Mentions.php'); + + // Set optional parameters to the default value. + $msgOptions['icon'] = empty($msgOptions['icon']) ? 'xx' : $msgOptions['icon']; + $msgOptions['smileys_enabled'] = !empty($msgOptions['smileys_enabled']); + $msgOptions['attachments'] = empty($msgOptions['attachments']) ? array() : $msgOptions['attachments']; + $msgOptions['approved'] = isset($msgOptions['approved']) ? (int) $msgOptions['approved'] : 1; + $msgOptions['poster_time'] = isset($msgOptions['poster_time']) ? (int) $msgOptions['poster_time'] : time(); + $topicOptions['id'] = empty($topicOptions['id']) ? 0 : (int) $topicOptions['id']; + $topicOptions['poll'] = isset($topicOptions['poll']) ? (int) $topicOptions['poll'] : null; + $topicOptions['lock_mode'] = isset($topicOptions['lock_mode']) ? $topicOptions['lock_mode'] : null; + $topicOptions['sticky_mode'] = isset($topicOptions['sticky_mode']) ? $topicOptions['sticky_mode'] : null; + $topicOptions['redirect_expires'] = isset($topicOptions['redirect_expires']) ? $topicOptions['redirect_expires'] : null; + $topicOptions['redirect_topic'] = isset($topicOptions['redirect_topic']) ? $topicOptions['redirect_topic'] : null; + $posterOptions['id'] = empty($posterOptions['id']) ? 0 : (int) $posterOptions['id']; + $posterOptions['ip'] = empty($posterOptions['ip']) ? $user_info['ip'] : $posterOptions['ip']; + + // Not exactly a post option but it allows hooks and/or other sources to skip sending notifications if they don't want to + $msgOptions['send_notifications'] = isset($msgOptions['send_notifications']) ? (bool) $msgOptions['send_notifications'] : true; + + // We need to know if the topic is approved. If we're told that's great - if not find out. + if (!$modSettings['postmod_active']) + $topicOptions['is_approved'] = true; + elseif (!empty($topicOptions['id']) && !isset($topicOptions['is_approved'])) + { + $request = $smcFunc['db_query']('', ' + SELECT approved + FROM {db_prefix}topics + WHERE id_topic = {int:id_topic} + LIMIT 1', + array( + 'id_topic' => $topicOptions['id'], + ) + ); + list ($topicOptions['is_approved']) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + } + + // If nothing was filled in as name/e-mail address, try the member table. + if (!isset($posterOptions['name']) || $posterOptions['name'] == '' || (empty($posterOptions['email']) && !empty($posterOptions['id']))) + { + if (empty($posterOptions['id'])) + { + $posterOptions['id'] = 0; + $posterOptions['name'] = $txt['guest_title']; + $posterOptions['email'] = ''; + } + elseif ($posterOptions['id'] != $user_info['id']) + { + $request = $smcFunc['db_query']('', ' + SELECT member_name, email_address + FROM {db_prefix}members + WHERE id_member = {int:id_member} + LIMIT 1', + array( + 'id_member' => $posterOptions['id'], + ) + ); + // Couldn't find the current poster? + if ($smcFunc['db_num_rows']($request) == 0) + { + loadLanguage('Errors'); + trigger_error(sprintf($txt['create_post_invalid_member_id'], $posterOptions['id']), E_USER_NOTICE); + $posterOptions['id'] = 0; + $posterOptions['name'] = $txt['guest_title']; + $posterOptions['email'] = ''; + } + else + list ($posterOptions['name'], $posterOptions['email']) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + } + else + { + $posterOptions['name'] = $user_info['name']; + $posterOptions['email'] = $user_info['email']; + } + } + + // Get any members who were quoted in this post. + $msgOptions['quoted_members'] = Mentions::getQuotedMembers($msgOptions['body'], $posterOptions['id']); + + if (!empty($modSettings['enable_mentions'])) + { + // Get any members who were possibly mentioned + $msgOptions['mentioned_members'] = Mentions::getMentionedMembers($msgOptions['body']); + if (!empty($msgOptions['mentioned_members'])) + { + // Replace @name with [member=id]name[/member] + $msgOptions['body'] = Mentions::getBody($msgOptions['body'], $msgOptions['mentioned_members']); + + // Remove any members who weren't actually mentioned, to prevent bogus notifications + $msgOptions['mentioned_members'] = Mentions::verifyMentionedMembers($msgOptions['body'], $msgOptions['mentioned_members']); + } + } + + // It's do or die time: forget any user aborts! + $previous_ignore_user_abort = ignore_user_abort(true); + + $new_topic = empty($topicOptions['id']); + + $message_columns = array( + 'id_board' => 'int', 'id_topic' => 'int', 'id_member' => 'int', 'subject' => 'string-255', 'body' => (!empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] > 65534 ? 'string-' . $modSettings['max_messageLength'] : (empty($modSettings['max_messageLength']) ? 'string' : 'string-65534')), + 'poster_name' => 'string-255', 'poster_email' => 'string-255', 'poster_time' => 'int', 'poster_ip' => 'inet', + 'smileys_enabled' => 'int', 'modified_name' => 'string', 'icon' => 'string-16', 'approved' => 'int', + ); + + $message_parameters = array( + $topicOptions['board'], $topicOptions['id'], $posterOptions['id'], $msgOptions['subject'], $msgOptions['body'], + $posterOptions['name'], $posterOptions['email'], $msgOptions['poster_time'], $posterOptions['ip'], + $msgOptions['smileys_enabled'] ? 1 : 0, '', $msgOptions['icon'], $msgOptions['approved'], + ); + + // What if we want to do anything with posts? + call_integration_hook('integrate_create_post', array(&$msgOptions, &$topicOptions, &$posterOptions, &$message_columns, &$message_parameters)); + + // Insert the post. + $msgOptions['id'] = $smcFunc['db_insert']('', + '{db_prefix}messages', + $message_columns, + $message_parameters, + array('id_msg'), + 1 + ); + + // Something went wrong creating the message... + if (empty($msgOptions['id'])) + return false; + + // Fix the attachments. + if (!empty($msgOptions['attachments'])) + $smcFunc['db_query']('', ' + UPDATE {db_prefix}attachments + SET id_msg = {int:id_msg} + WHERE id_attach IN ({array_int:attachment_list})', + array( + 'attachment_list' => $msgOptions['attachments'], + 'id_msg' => $msgOptions['id'], + ) + ); + + // What if we want to export new posts out to a CMS? + call_integration_hook('integrate_after_create_post', array($msgOptions, $topicOptions, $posterOptions, $message_columns, $message_parameters)); + + // Insert a new topic (if the topicID was left empty.) + if ($new_topic) + { + $topic_columns = array( + 'id_board' => 'int', 'id_member_started' => 'int', 'id_member_updated' => 'int', 'id_first_msg' => 'int', + 'id_last_msg' => 'int', 'locked' => 'int', 'is_sticky' => 'int', 'num_views' => 'int', + 'id_poll' => 'int', 'unapproved_posts' => 'int', 'approved' => 'int', + 'redirect_expires' => 'int', 'id_redirect_topic' => 'int', + ); + $topic_parameters = array( + $topicOptions['board'], $posterOptions['id'], $posterOptions['id'], $msgOptions['id'], + $msgOptions['id'], $topicOptions['lock_mode'] === null ? 0 : $topicOptions['lock_mode'], $topicOptions['sticky_mode'] === null ? 0 : $topicOptions['sticky_mode'], 0, + $topicOptions['poll'] === null ? 0 : $topicOptions['poll'], $msgOptions['approved'] ? 0 : 1, $msgOptions['approved'], + $topicOptions['redirect_expires'] === null ? 0 : $topicOptions['redirect_expires'], $topicOptions['redirect_topic'] === null ? 0 : $topicOptions['redirect_topic'], + ); + + call_integration_hook('integrate_before_create_topic', array(&$msgOptions, &$topicOptions, &$posterOptions, &$topic_columns, &$topic_parameters)); + + $topicOptions['id'] = $smcFunc['db_insert']('', + '{db_prefix}topics', + $topic_columns, + $topic_parameters, + array('id_topic'), + 1 + ); + + // The topic couldn't be created for some reason. + if (empty($topicOptions['id'])) + { + // We should delete the post that did work, though... + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}messages + WHERE id_msg = {int:id_msg}', + array( + 'id_msg' => $msgOptions['id'], + ) + ); + + return false; + } + + // Fix the message with the topic. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}messages + SET id_topic = {int:id_topic} + WHERE id_msg = {int:id_msg}', + array( + 'id_topic' => $topicOptions['id'], + 'id_msg' => $msgOptions['id'], + ) + ); + + // There's been a new topic AND a new post today. + trackStats(array('topics' => '+', 'posts' => '+')); + + updateStats('topic', true); + updateStats('subject', $topicOptions['id'], $msgOptions['subject']); + + // What if we want to export new topics out to a CMS? + call_integration_hook('integrate_create_topic', array(&$msgOptions, &$topicOptions, &$posterOptions)); + } + // The topic already exists, it only needs a little updating. + else + { + $update_parameters = array( + 'poster_id' => $posterOptions['id'], + 'id_msg' => $msgOptions['id'], + 'locked' => $topicOptions['lock_mode'], + 'is_sticky' => $topicOptions['sticky_mode'], + 'id_topic' => $topicOptions['id'], + 'counter_increment' => 1, + ); + if ($msgOptions['approved']) + $topics_columns = array( + 'id_member_updated = {int:poster_id}', + 'id_last_msg = {int:id_msg}', + 'num_replies = num_replies + {int:counter_increment}', + ); + else + $topics_columns = array( + 'unapproved_posts = unapproved_posts + {int:counter_increment}', + ); + if ($topicOptions['lock_mode'] !== null) + $topics_columns[] = 'locked = {int:locked}'; + if ($topicOptions['sticky_mode'] !== null) + $topics_columns[] = 'is_sticky = {int:is_sticky}'; + + call_integration_hook('integrate_modify_topic', array(&$topics_columns, &$update_parameters, &$msgOptions, &$topicOptions, &$posterOptions)); + + // Update the number of replies and the lock/sticky status. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}topics + SET + ' . implode(', ', $topics_columns) . ' + WHERE id_topic = {int:id_topic}', + $update_parameters + ); + + // One new post has been added today. + trackStats(array('posts' => '+')); + } + + // Creating is modifying...in a way. + // @todo Why not set id_msg_modified on the insert? + $smcFunc['db_query']('', ' + UPDATE {db_prefix}messages + SET id_msg_modified = {int:id_msg} + WHERE id_msg = {int:id_msg}', + array( + 'id_msg' => $msgOptions['id'], + ) + ); + + // Increase the number of posts and topics on the board. + if ($msgOptions['approved']) + $smcFunc['db_query']('', ' + UPDATE {db_prefix}boards + SET num_posts = num_posts + 1' . ($new_topic ? ', num_topics = num_topics + 1' : '') . ' + WHERE id_board = {int:id_board}', + array( + 'id_board' => $topicOptions['board'], + ) + ); + else + { + $smcFunc['db_query']('', ' + UPDATE {db_prefix}boards + SET unapproved_posts = unapproved_posts + 1' . ($new_topic ? ', unapproved_topics = unapproved_topics + 1' : '') . ' + WHERE id_board = {int:id_board}', + array( + 'id_board' => $topicOptions['board'], + ) + ); + + // Add to the approval queue too. + $smcFunc['db_insert']('', + '{db_prefix}approval_queue', + array( + 'id_msg' => 'int', + ), + array( + $msgOptions['id'], + ), + array() + ); + + $smcFunc['db_insert']('', + '{db_prefix}background_tasks', + array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'), + array( + '$sourcedir/tasks/ApprovePost-Notify.php', 'ApprovePost_Notify_Background', $smcFunc['json_encode'](array( + 'msgOptions' => $msgOptions, + 'topicOptions' => $topicOptions, + 'posterOptions' => $posterOptions, + 'type' => $new_topic ? 'topic' : 'post', + )), 0 + ), + array('id_task') + ); + } + + // Mark inserted topic as read (only for the user calling this function). + if (!empty($topicOptions['mark_as_read']) && !$user_info['is_guest']) + { + // Since it's likely they *read* it before replying, let's try an UPDATE first. + if (!$new_topic) + { + $smcFunc['db_query']('', ' + UPDATE {db_prefix}log_topics + SET id_msg = {int:id_msg} + WHERE id_member = {int:current_member} + AND id_topic = {int:id_topic}', + array( + 'current_member' => $posterOptions['id'], + 'id_msg' => $msgOptions['id'], + 'id_topic' => $topicOptions['id'], + ) + ); + + $flag = $smcFunc['db_affected_rows']() != 0; + } + + if (empty($flag)) + { + $smcFunc['db_insert']('ignore', + '{db_prefix}log_topics', + array('id_topic' => 'int', 'id_member' => 'int', 'id_msg' => 'int'), + array($topicOptions['id'], $posterOptions['id'], $msgOptions['id']), + array('id_topic', 'id_member') + ); + } + } + + if ($msgOptions['approved'] && empty($topicOptions['is_approved']) && $posterOptions['id'] != $user_info['id']) + $smcFunc['db_insert']('', + '{db_prefix}background_tasks', + array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'), + array( + '$sourcedir/tasks/ApproveReply-Notify.php', 'ApproveReply_Notify_Background', $smcFunc['json_encode'](array( + 'msgOptions' => $msgOptions, + 'topicOptions' => $topicOptions, + 'posterOptions' => $posterOptions, + )), 0 + ), + array('id_task') + ); + + // If there's a custom search index, it may need updating... + require_once($sourcedir . '/Search.php'); + $searchAPI = findSearchAPI(); + if (is_callable(array($searchAPI, 'postCreated'))) + $searchAPI->postCreated($msgOptions, $topicOptions, $posterOptions); + + // Increase the post counter for the user that created the post. + if (!empty($posterOptions['update_post_count']) && !empty($posterOptions['id']) && $msgOptions['approved']) + { + // Are you the one that happened to create this post? + if ($user_info['id'] == $posterOptions['id']) + $user_info['posts']++; + updateMemberData($posterOptions['id'], array('posts' => '+')); + } + + // They've posted, so they can make the view count go up one if they really want. (this is to keep views >= replies...) + $_SESSION['last_read_topic'] = 0; + + // Better safe than sorry. + if (isset($_SESSION['topicseen_cache'][$topicOptions['board']])) + $_SESSION['topicseen_cache'][$topicOptions['board']]--; + + // Keep track of quotes and mentions. + if (!empty($msgOptions['quoted_members'])) + Mentions::insertMentions('quote', $msgOptions['id'], $msgOptions['quoted_members'], $posterOptions['id']); + if (!empty($msgOptions['mentioned_members'])) + Mentions::insertMentions('msg', $msgOptions['id'], $msgOptions['mentioned_members'], $posterOptions['id']); + + // Update all the stats so everyone knows about this new topic and message. + updateStats('message', true, $msgOptions['id']); + + // Update the last message on the board assuming it's approved AND the topic is. + if ($msgOptions['approved']) + updateLastMessages($topicOptions['board'], $new_topic || !empty($topicOptions['is_approved']) ? $msgOptions['id'] : 0); + + // Queue createPost background notification + if ($msgOptions['send_notifications'] && $msgOptions['approved']) + $smcFunc['db_insert']('', + '{db_prefix}background_tasks', + array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'), + array('$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array( + 'msgOptions' => $msgOptions, + 'topicOptions' => $topicOptions, + 'posterOptions' => $posterOptions, + 'type' => $new_topic ? 'topic' : 'reply', + )), 0), + array('id_task') + ); + + // Alright, done now... we can abort now, I guess... at least this much is done. + ignore_user_abort($previous_ignore_user_abort); + + // Success. + return true; +} + +/** + * Modifying a post... + * + * @param array &$msgOptions An array of information/options for the post + * @param array &$topicOptions An array of information/options for the topic + * @param array &$posterOptions An array of information/options for the poster + * @return bool Whether the post was modified successfully + */ +function modifyPost(&$msgOptions, &$topicOptions, &$posterOptions) +{ + global $user_info, $modSettings, $smcFunc, $sourcedir; + + $topicOptions['poll'] = isset($topicOptions['poll']) ? (int) $topicOptions['poll'] : null; + $topicOptions['lock_mode'] = isset($topicOptions['lock_mode']) ? $topicOptions['lock_mode'] : null; + $topicOptions['sticky_mode'] = isset($topicOptions['sticky_mode']) ? $topicOptions['sticky_mode'] : null; + + // This is longer than it has to be, but makes it so we only set/change what we have to. + $messages_columns = array(); + if (isset($posterOptions['name'])) + $messages_columns['poster_name'] = $posterOptions['name']; + if (isset($posterOptions['email'])) + $messages_columns['poster_email'] = $posterOptions['email']; + if (isset($msgOptions['icon'])) + $messages_columns['icon'] = $msgOptions['icon']; + if (isset($msgOptions['subject'])) + $messages_columns['subject'] = $msgOptions['subject']; + if (isset($msgOptions['body'])) + { + $messages_columns['body'] = $msgOptions['body']; + + // using a custom search index, then lets get the old message so we can update our index as needed + if (!empty($modSettings['search_custom_index_config'])) + { + $request = $smcFunc['db_query']('', ' + SELECT body + FROM {db_prefix}messages + WHERE id_msg = {int:id_msg}', + array( + 'id_msg' => $msgOptions['id'], + ) + ); + list ($msgOptions['old_body']) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + } + } + if (!empty($msgOptions['modify_time'])) + { + $messages_columns['modified_time'] = $msgOptions['modify_time']; + $messages_columns['modified_name'] = $msgOptions['modify_name']; + $messages_columns['modified_reason'] = $msgOptions['modify_reason']; + $messages_columns['id_msg_modified'] = $modSettings['maxMsgID']; + } + if (isset($msgOptions['smileys_enabled'])) + $messages_columns['smileys_enabled'] = empty($msgOptions['smileys_enabled']) ? 0 : 1; + + // Which columns need to be ints? + $messageInts = array('modified_time', 'id_msg_modified', 'smileys_enabled'); + $update_parameters = array( + 'id_msg' => $msgOptions['id'], + ); + + // Update search api + require_once($sourcedir . '/Search.php'); + $searchAPI = findSearchAPI(); + if ($searchAPI->supportsMethod('postRemoved')) + $searchAPI->postRemoved($msgOptions['id']); + + // Anyone quoted or mentioned? + require_once($sourcedir . '/Mentions.php'); + + $quoted_members = Mentions::getQuotedMembers($msgOptions['body'], $posterOptions['id']); + $quoted_modifications = Mentions::modifyMentions('quote', $msgOptions['id'], $quoted_members, $posterOptions['id']); + + if (!empty($quoted_modifications['added'])) + { + $msgOptions['quoted_members'] = array_intersect_key($quoted_members, array_flip(array_keys($quoted_modifications['added']))); + + // You don't need a notification about quoting yourself. + unset($msgOptions['quoted_members'][$user_info['id']]); + } + + if (!empty($modSettings['enable_mentions']) && isset($msgOptions['body'])) + { + $mentions = Mentions::getMentionedMembers($msgOptions['body']); + $messages_columns['body'] = $msgOptions['body'] = Mentions::getBody($msgOptions['body'], $mentions); + $mentions = Mentions::verifyMentionedMembers($msgOptions['body'], $mentions); + + // Update our records in the database. + $mention_modifications = Mentions::modifyMentions('msg', $msgOptions['id'], $mentions, $posterOptions['id']); + + if (!empty($mention_modifications['added'])) + { + // Queue this for notification. + $msgOptions['mentioned_members'] = array_intersect_key($mentions, array_flip(array_keys($mention_modifications['added']))); + + // Mentioning yourself is silly, and we aren't going to notify you about it. + unset($msgOptions['mentioned_members'][$user_info['id']]); + } + } + + // This allows mods to skip sending notifications if they don't want to. + $msgOptions['send_notifications'] = isset($msgOptions['send_notifications']) ? (bool) $msgOptions['send_notifications'] : true; + + // Maybe a mod wants to make some changes? + call_integration_hook('integrate_modify_post', array(&$messages_columns, &$update_parameters, &$msgOptions, &$topicOptions, &$posterOptions, &$messageInts)); + + foreach ($messages_columns as $var => $val) + { + $messages_columns[$var] = $var . ' = {' . (in_array($var, $messageInts) ? 'int' : 'string') . ':var_' . $var . '}'; + $update_parameters['var_' . $var] = $val; + } + + // Nothing to do? + if (empty($messages_columns)) + return true; + + // Change the post. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}messages + SET ' . implode(', ', $messages_columns) . ' + WHERE id_msg = {int:id_msg}', + $update_parameters + ); + + // Lock and or sticky the post. + if ($topicOptions['sticky_mode'] !== null || $topicOptions['lock_mode'] !== null || $topicOptions['poll'] !== null) + { + $smcFunc['db_query']('', ' + UPDATE {db_prefix}topics + SET + is_sticky = {raw:is_sticky}, + locked = {raw:locked}, + id_poll = {raw:id_poll} + WHERE id_topic = {int:id_topic}', + array( + 'is_sticky' => $topicOptions['sticky_mode'] === null ? 'is_sticky' : (int) $topicOptions['sticky_mode'], + 'locked' => $topicOptions['lock_mode'] === null ? 'locked' : (int) $topicOptions['lock_mode'], + 'id_poll' => $topicOptions['poll'] === null ? 'id_poll' : (int) $topicOptions['poll'], + 'id_topic' => $topicOptions['id'], + ) + ); + } + + // Mark the edited post as read. + if (!empty($topicOptions['mark_as_read']) && !$user_info['is_guest']) + { + // Since it's likely they *read* it before editing, let's try an UPDATE first. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}log_topics + SET id_msg = {int:id_msg} + WHERE id_member = {int:current_member} + AND id_topic = {int:id_topic}', + array( + 'current_member' => $user_info['id'], + 'id_msg' => $modSettings['maxMsgID'], + 'id_topic' => $topicOptions['id'], + ) + ); + + $flag = $smcFunc['db_affected_rows']() != 0; + + if (empty($flag)) + { + $smcFunc['db_insert']('ignore', + '{db_prefix}log_topics', + array('id_topic' => 'int', 'id_member' => 'int', 'id_msg' => 'int'), + array($topicOptions['id'], $user_info['id'], $modSettings['maxMsgID']), + array('id_topic', 'id_member') + ); + } + } + + // If there's a custom search index, it needs to be modified... + require_once($sourcedir . '/Search.php'); + $searchAPI = findSearchAPI(); + if (is_callable(array($searchAPI, 'postModified'))) + $searchAPI->postModified($msgOptions, $topicOptions, $posterOptions); + + // Send notifications about any new quotes or mentions. + if ($msgOptions['send_notifications'] && !empty($msgOptions['approved']) && (!empty($msgOptions['quoted_members']) || !empty($msgOptions['mentioned_members']) || !empty($mention_modifications['removed']) || !empty($quoted_modifications['removed']))) + { + $smcFunc['db_insert']('', + '{db_prefix}background_tasks', + array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'), + array('$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array( + 'msgOptions' => $msgOptions, + 'topicOptions' => $topicOptions, + 'posterOptions' => $posterOptions, + 'type' => 'edit', + )), 0), + array('id_task') + ); + } + + if (isset($msgOptions['subject'])) + { + // Only update the subject if this was the first message in the topic. + $request = $smcFunc['db_query']('', ' + SELECT id_topic + FROM {db_prefix}topics + WHERE id_first_msg = {int:id_first_msg} + LIMIT 1', + array( + 'id_first_msg' => $msgOptions['id'], + ) + ); + if ($smcFunc['db_num_rows']($request) == 1) + updateStats('subject', $topicOptions['id'], $msgOptions['subject']); + $smcFunc['db_free_result']($request); + } + + // Finally, if we are setting the approved state we need to do much more work :( + if ($modSettings['postmod_active'] && isset($msgOptions['approved'])) + approvePosts($msgOptions['id'], $msgOptions['approved']); + + return true; +} + +/** + * Approve (or not) some posts... without permission checks... + * + * @param array $msgs Array of message ids + * @param bool $approve Whether to approve the posts (if false, posts are unapproved) + * @param bool $notify Whether to notify users + * @return bool Whether the operation was successful + */ +function approvePosts($msgs, $approve = true, $notify = true) +{ + global $smcFunc; + + if (!is_array($msgs)) + $msgs = array($msgs); + + if (empty($msgs)) + return false; + + // May as well start at the beginning, working out *what* we need to change. + $request = $smcFunc['db_query']('', ' + SELECT m.id_msg, m.approved, m.id_topic, m.id_board, t.id_first_msg, t.id_last_msg, + m.body, m.subject, COALESCE(mem.real_name, m.poster_name) AS poster_name, m.id_member, + t.approved AS topic_approved, b.count_posts + FROM {db_prefix}messages AS m + INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic) + INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board) + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member) + WHERE m.id_msg IN ({array_int:message_list}) + AND m.approved = {int:approved_state}', + array( + 'message_list' => $msgs, + 'approved_state' => $approve ? 0 : 1, + ) + ); + $msgs = array(); + $topics = array(); + $topic_changes = array(); + $board_changes = array(); + $notification_topics = array(); + $notification_posts = array(); + $member_post_changes = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // Easy... + $msgs[] = $row['id_msg']; + $topics[] = $row['id_topic']; + + // Ensure our change array exists already. + if (!isset($topic_changes[$row['id_topic']])) + $topic_changes[$row['id_topic']] = array( + 'id_last_msg' => $row['id_last_msg'], + 'approved' => $row['topic_approved'], + 'replies' => 0, + 'unapproved_posts' => 0, + ); + if (!isset($board_changes[$row['id_board']])) + $board_changes[$row['id_board']] = array( + 'posts' => 0, + 'topics' => 0, + 'unapproved_posts' => 0, + 'unapproved_topics' => 0, + ); + + // If it's the first message then the topic state changes! + if ($row['id_msg'] == $row['id_first_msg']) + { + $topic_changes[$row['id_topic']]['approved'] = $approve ? 1 : 0; + + $board_changes[$row['id_board']]['unapproved_topics'] += $approve ? -1 : 1; + $board_changes[$row['id_board']]['topics'] += $approve ? 1 : -1; + + // Note we need to ensure we announce this topic! + $notification_topics[] = array( + 'body' => $row['body'], + 'subject' => $row['subject'], + 'name' => $row['poster_name'], + 'board' => $row['id_board'], + 'topic' => $row['id_topic'], + 'msg' => $row['id_first_msg'], + 'poster' => $row['id_member'], + 'new_topic' => true, + ); + } + else + { + $topic_changes[$row['id_topic']]['replies'] += $approve ? 1 : -1; + + // This will be a post... but don't notify unless it's not followed by approved ones. + if ($row['id_msg'] > $row['id_last_msg']) + $notification_posts[$row['id_topic']] = array( + 'id' => $row['id_msg'], + 'body' => $row['body'], + 'subject' => $row['subject'], + 'name' => $row['poster_name'], + 'topic' => $row['id_topic'], + 'board' => $row['id_board'], + 'poster' => $row['id_member'], + 'new_topic' => false, + 'msg' => $row['id_msg'], + ); + } + + // If this is being approved and id_msg is higher than the current id_last_msg then it changes. + if ($approve && $row['id_msg'] > $topic_changes[$row['id_topic']]['id_last_msg']) + $topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_msg']; + // If this is being unapproved, and it's equal to the id_last_msg we need to find a new one! + elseif (!$approve) + // Default to the first message and then we'll override in a bit ;) + $topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_first_msg']; + + $topic_changes[$row['id_topic']]['unapproved_posts'] += $approve ? -1 : 1; + $board_changes[$row['id_board']]['unapproved_posts'] += $approve ? -1 : 1; + $board_changes[$row['id_board']]['posts'] += $approve ? 1 : -1; + + // Post count for the user? + if ($row['id_member'] && empty($row['count_posts'])) + $member_post_changes[$row['id_member']] = isset($member_post_changes[$row['id_member']]) ? $member_post_changes[$row['id_member']] + 1 : 1; + } + $smcFunc['db_free_result']($request); + + if (empty($msgs)) + return; + + // Now we have the differences make the changes, first the easy one. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}messages + SET approved = {int:approved_state} + WHERE id_msg IN ({array_int:message_list})', + array( + 'message_list' => $msgs, + 'approved_state' => $approve ? 1 : 0, + ) + ); + + // If we were unapproving find the last msg in the topics... + if (!$approve) + { + $request = $smcFunc['db_query']('', ' + SELECT id_topic, MAX(id_msg) AS id_last_msg + FROM {db_prefix}messages + WHERE id_topic IN ({array_int:topic_list}) + AND approved = {int:approved} + GROUP BY id_topic', + array( + 'topic_list' => $topics, + 'approved' => 1, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_last_msg']; + $smcFunc['db_free_result']($request); + } + + // ... next the topics... + foreach ($topic_changes as $id => $changes) + $smcFunc['db_query']('', ' + UPDATE {db_prefix}topics + SET approved = {int:approved}, unapproved_posts = unapproved_posts + {int:unapproved_posts}, + num_replies = num_replies + {int:num_replies}, id_last_msg = {int:id_last_msg} + WHERE id_topic = {int:id_topic}', + array( + 'approved' => $changes['approved'], + 'unapproved_posts' => $changes['unapproved_posts'], + 'num_replies' => $changes['replies'], + 'id_last_msg' => $changes['id_last_msg'], + 'id_topic' => $id, + ) + ); + + // ... finally the boards... + foreach ($board_changes as $id => $changes) + $smcFunc['db_query']('', ' + UPDATE {db_prefix}boards + SET num_posts = num_posts + {int:num_posts}, unapproved_posts = unapproved_posts + {int:unapproved_posts}, + num_topics = num_topics + {int:num_topics}, unapproved_topics = unapproved_topics + {int:unapproved_topics} + WHERE id_board = {int:id_board}', + array( + 'num_posts' => $changes['posts'], + 'unapproved_posts' => $changes['unapproved_posts'], + 'num_topics' => $changes['topics'], + 'unapproved_topics' => $changes['unapproved_topics'], + 'id_board' => $id, + ) + ); + + // Finally, least importantly, notifications! + if ($approve) + { + $task_rows = array(); + foreach (array_merge($notification_topics, $notification_posts) as $topic) + $task_rows[] = array( + '$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array( + 'msgOptions' => array( + 'id' => $topic['msg'], + 'body' => $topic['body'], + 'subject' => $topic['subject'], + ), + 'topicOptions' => array( + 'id' => $topic['topic'], + 'board' => $topic['board'], + ), + 'posterOptions' => array( + 'id' => $topic['poster'], + 'name' => $topic['name'], + ), + 'type' => $topic['new_topic'] ? 'topic' : 'reply', + )), 0 + ); + + if ($notify) + $smcFunc['db_insert']('', + '{db_prefix}background_tasks', + array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'), + $task_rows, + array('id_task') + ); + + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}approval_queue + WHERE id_msg IN ({array_int:message_list}) + AND id_attach = {int:id_attach}', + array( + 'message_list' => $msgs, + 'id_attach' => 0, + ) + ); + + // Clean up moderator alerts + if (!empty($notification_topics)) + clearApprovalAlerts(array_column($notification_topics, 'topic'), 'unapproved_topic'); + if (!empty($notification_posts)) + clearApprovalAlerts(array_column($notification_posts, 'id'), 'unapproved_post'); + } + // If unapproving add to the approval queue! + else + { + $msgInserts = array(); + foreach ($msgs as $msg) + $msgInserts[] = array($msg); + + $smcFunc['db_insert']('ignore', + '{db_prefix}approval_queue', + array('id_msg' => 'int'), + $msgInserts, + array('id_msg') + ); + } + + // Update the last messages on the boards... + updateLastMessages(array_keys($board_changes)); + + // Post count for the members? + if (!empty($member_post_changes)) + foreach ($member_post_changes as $id_member => $count_change) + updateMemberData($id_member, array('posts' => 'posts ' . ($approve ? '+' : '-') . ' ' . $count_change)); + + // In case an external CMS needs to know about this approval/unapproval. + call_integration_hook('integrate_after_approve_posts', array($approve, $msgs, $topic_changes, $member_post_changes)); + + return true; +} + +/** + * Approve topics? + * + * @todo shouldn't this be in topic + * + * @param array $topics Array of topic ids + * @param bool $approve Whether to approve the topics. If false, unapproves them instead + * @return bool Whether the operation was successful + */ +function approveTopics($topics, $approve = true) +{ + global $smcFunc; + + if (!is_array($topics)) + $topics = array($topics); + + if (empty($topics)) + return false; + + $approve_type = $approve ? 0 : 1; + + // Just get the messages to be approved and pass through... + $request = $smcFunc['db_query']('', ' + SELECT id_first_msg + FROM {db_prefix}topics + WHERE id_topic IN ({array_int:topic_list}) + AND approved = {int:approve_type}', + array( + 'topic_list' => $topics, + 'approve_type' => $approve_type, + ) + ); + $msgs = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $msgs[] = $row['id_first_msg']; + $smcFunc['db_free_result']($request); + + return approvePosts($msgs, $approve); +} + +/** + * Upon approval, clear unread alerts. + * + * @param int[] $content_ids either id_msgs or id_topics + * @param string $content_action will be either 'unapproved_post' or 'unapproved_topic' + * @return void + */ +function clearApprovalAlerts($content_ids, $content_action) +{ + global $smcFunc; + + // Some data hygiene... + if (!is_array($content_ids)) + return; + $content_ids = array_filter(array_map('intval', $content_ids)); + if (empty($content_ids)) + return; + + if (!in_array($content_action, array('unapproved_post', 'unapproved_topic'))) + return; + + // Check to see if there are unread alerts to delete... + // Might be multiple alerts, for multiple moderators... + $alerts = array(); + $moderators = array(); + $result = $smcFunc['db_query']('', ' + SELECT id_alert, id_member FROM {db_prefix}user_alerts + WHERE content_id IN ({array_int:content_ids}) + AND content_type = {string:content_type} + AND content_action = {string:content_action} + AND is_read = {int:unread}', + array( + 'content_ids' => $content_ids, + 'content_type' => $content_action === 'unapproved_topic' ? 'topic' : 'msg', + 'content_action' => $content_action, + 'unread' => 0, + ) + ); + // Found any? + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + $alerts[] = $row['id_alert']; + $moderators[] = $row['id_member']; + } + if (!empty($alerts)) + { + // Delete 'em + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}user_alerts + WHERE id_alert IN ({array_int:alerts})', + array( + 'alerts' => $alerts, + ) + ); + // Decrement counter for each moderator who received an alert + updateMemberData($moderators, array('alerts' => '-')); + } +} + +/** + * Takes an array of board IDs and updates their last messages. + * If the board has a parent, that parent board is also automatically + * updated. + * The columns updated are id_last_msg and last_updated. + * Note that id_last_msg should always be updated using this function, + * and is not automatically updated upon other changes. + * + * @param array $setboards An array of board IDs + * @param int $id_msg The ID of the message + * @return void|false Returns false if $setboards is empty for some reason + */ +function updateLastMessages($setboards, $id_msg = 0) +{ + global $board_info, $board, $smcFunc; + + // Please - let's be sane. + if (empty($setboards)) + return false; + + if (!is_array($setboards)) + $setboards = array($setboards); + + // If we don't know the id_msg we need to find it. + if (!$id_msg) + { + // Find the latest message on this board (highest id_msg.) + $request = $smcFunc['db_query']('', ' + SELECT id_board, MAX(id_last_msg) AS id_msg + FROM {db_prefix}topics + WHERE id_board IN ({array_int:board_list}) + AND approved = {int:approved} + GROUP BY id_board', + array( + 'board_list' => $setboards, + 'approved' => 1, + ) + ); + $lastMsg = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $lastMsg[$row['id_board']] = $row['id_msg']; + $smcFunc['db_free_result']($request); + } + else + { + // Just to note - there should only be one board passed if we are doing this. + foreach ($setboards as $id_board) + $lastMsg[$id_board] = $id_msg; + } + + $parent_boards = array(); + // Keep track of last modified dates. + $lastModified = $lastMsg; + // Get all the child boards for the parents, if they have some... + foreach ($setboards as $id_board) + { + if (!isset($lastMsg[$id_board])) + { + $lastMsg[$id_board] = 0; + $lastModified[$id_board] = 0; + } + + if (!empty($board) && $id_board == $board) + $parents = $board_info['parent_boards']; + else + $parents = getBoardParents($id_board); + + // Ignore any parents on the top child level. + // @todo Why? + foreach ($parents as $id => $parent) + { + if ($parent['level'] != 0) + { + // If we're already doing this one as a board, is this a higher last modified? + if (isset($lastModified[$id]) && $lastModified[$id_board] > $lastModified[$id]) + $lastModified[$id] = $lastModified[$id_board]; + elseif (!isset($lastModified[$id]) && (!isset($parent_boards[$id]) || $parent_boards[$id] < $lastModified[$id_board])) + $parent_boards[$id] = $lastModified[$id_board]; + } + } + } + + // Note to help understand what is happening here. For parents we update the timestamp of the last message for determining + // whether there are child boards which have not been read. For the boards themselves we update both this and id_last_msg. + + $board_updates = array(); + $parent_updates = array(); + // Finally, to save on queries make the changes... + foreach ($parent_boards as $id => $msg) + { + if (!isset($parent_updates[$msg])) + $parent_updates[$msg] = array($id); + else + $parent_updates[$msg][] = $id; + } + + foreach ($lastMsg as $id => $msg) + { + if (!isset($board_updates[$msg . '-' . $lastModified[$id]])) + $board_updates[$msg . '-' . $lastModified[$id]] = array( + 'id' => $msg, + 'updated' => $lastModified[$id], + 'boards' => array($id) + ); + + else + $board_updates[$msg . '-' . $lastModified[$id]]['boards'][] = $id; + } + + // Now commit the changes! + foreach ($parent_updates as $id_msg => $boards) + { + $smcFunc['db_query']('', ' + UPDATE {db_prefix}boards + SET id_msg_updated = {int:id_msg_updated} + WHERE id_board IN ({array_int:board_list}) + AND id_msg_updated < {int:id_msg_updated}', + array( + 'board_list' => $boards, + 'id_msg_updated' => $id_msg, + ) + ); + } + foreach ($board_updates as $board_data) + { + $smcFunc['db_query']('', ' + UPDATE {db_prefix}boards + SET id_last_msg = {int:id_last_msg}, id_msg_updated = {int:id_msg_updated} + WHERE id_board IN ({array_int:board_list})', + array( + 'board_list' => $board_data['boards'], + 'id_last_msg' => $board_data['id'], + 'id_msg_updated' => $board_data['updated'], + ) + ); + } +} + +/** + * This simple function gets a list of all administrators and sends them an email + * to let them know a new member has joined. + * Called by registerMember() function in Subs-Members.php. + * Email is sent to all groups that have the moderate_forum permission. + * The language set by each member is being used (if available). + * Uses the Login language file + * + * @param string $type The type. Types supported are 'approval', 'activation', and 'standard'. + * @param int $memberID The ID of the member + * @param string $member_name The name of the member (if null, it is pulled from the database) + */ +function adminNotify($type, $memberID, $member_name = null) +{ + global $smcFunc; + + if ($member_name == null) + { + // Get the new user's name.... + $request = $smcFunc['db_query']('', ' + SELECT real_name + FROM {db_prefix}members + WHERE id_member = {int:id_member} + LIMIT 1', + array( + 'id_member' => $memberID, + ) + ); + list ($member_name) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + } + + // This is really just a wrapper for making a new background task to deal with all the fun. + $smcFunc['db_insert']('insert', + '{db_prefix}background_tasks', + array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'), + array('$sourcedir/tasks/Register-Notify.php', 'Register_Notify_Background', $smcFunc['json_encode'](array( + 'new_member_id' => $memberID, + 'new_member_name' => $member_name, + 'notify_type' => $type, + 'time' => time(), + )), 0), + array('id_task') + ); +} + +/** + * Load a template from EmailTemplates language file. + * + * @param string $template The name of the template to load + * @param array $replacements An array of replacements for the variables in the template + * @param string $lang The language to use, if different than the user's current language + * @param bool $loadLang Whether to load the language file first + * @return array An array containing the subject and body of the email template, with replacements made + */ +function loadEmailTemplate($template, $replacements = array(), $lang = '', $loadLang = true) +{ + global $txt, $mbname, $scripturl, $settings, $context; + + // First things first, load up the email templates language file, if we need to. + if ($loadLang) + loadLanguage('EmailTemplates', $lang); + + if (!isset($txt[$template . '_subject']) || !isset($txt[$template . '_body'])) + fatal_lang_error('email_no_template', 'template', array($template)); + + $ret = array( + 'subject' => $txt[$template . '_subject'], + 'body' => $txt[$template . '_body'], + 'is_html' => !empty($txt[$template . '_html']), + ); + + // Add in the default replacements. + $replacements += array( + 'FORUMNAME' => $mbname, + 'SCRIPTURL' => $scripturl, + 'THEMEURL' => $settings['theme_url'], + 'IMAGESURL' => $settings['images_url'], + 'DEFAULT_THEMEURL' => $settings['default_theme_url'], + 'REGARDS' => sprintf($txt['regards_team'], $context['forum_name']), + ); + + // Split the replacements up into two arrays, for use with str_replace + $find = array(); + $replace = array(); + + foreach ($replacements as $f => $r) + { + $find[] = '{' . $f . '}'; + $replace[] = $r; + } + + // Do the variable replacements. + $ret['subject'] = str_replace($find, $replace, $ret['subject']); + $ret['body'] = str_replace($find, $replace, $ret['body']); + + // Now deal with the {USER.variable} items. + $ret['subject'] = preg_replace_callback('~{USER.([^}]+)}~', 'user_info_callback', $ret['subject']); + $ret['body'] = preg_replace_callback('~{USER.([^}]+)}~', 'user_info_callback', $ret['body']); + + // Finally return the email to the caller so they can send it out. + return $ret; +} + +/** + * Callback function for loademaitemplate on subject and body + * Uses capture group 1 in array + * + * @param array $matches An array of matches + * @return string The match + */ +function user_info_callback($matches) +{ + global $user_info; + if (empty($matches[1])) + return ''; + + $use_ref = true; + $ref = &$user_info; + + foreach (explode('.', $matches[1]) as $index) + { + if ($use_ref && isset($ref[$index])) + $ref = &$ref[$index]; + else + { + $use_ref = false; + break; + } + } + + return $use_ref ? $ref : $matches[0]; +} + +/** + * spell_init() + * + * Sets up a dictionary resource handle. Tries enchant first then falls through to pspell. + * + * @return resource|bool An enchant or pspell dictionary resource handle or false if the dictionary couldn't be loaded + */ +function spell_init() +{ + global $context, $txt; + + // Check for UTF-8 and strip ".utf8" off the lang_locale string for enchant + $context['spell_utf8'] = ($txt['lang_character_set'] == 'UTF-8'); + $lang_locale = str_replace('.utf8', '', $txt['lang_locale']); + + // Try enchant first since PSpell is (supposedly) deprecated as of PHP 5.3 + // enchant only does UTF-8, so we need iconv if you aren't using UTF-8 + if (function_exists('enchant_broker_init') && ($context['spell_utf8'] || function_exists('iconv'))) + { + // We'll need this to free resources later... + $context['enchant_broker'] = enchant_broker_init(); + + // Try locale first, then general... + if (!empty($lang_locale) && enchant_broker_dict_exists($context['enchant_broker'], $lang_locale)) + { + $enchant_link = enchant_broker_request_dict($context['enchant_broker'], $lang_locale); + } + elseif (enchant_broker_dict_exists($context['enchant_broker'], $txt['lang_dictionary'])) + { + $enchant_link = enchant_broker_request_dict($context['enchant_broker'], $txt['lang_dictionary']); + } + + // Success + if (!empty($enchant_link)) + { + $context['provider'] = 'enchant'; + return $enchant_link; + } + else + { + // Free up any resources used... + @enchant_broker_free($context['enchant_broker']); + } + } + + // Fall through to pspell if enchant didn't work + if (function_exists('pspell_new')) + { + // Okay, this looks funny, but it actually fixes a weird bug. + ob_start(); + $old = error_reporting(0); + + // See, first, some windows machines don't load pspell properly on the first try. Dumb, but this is a workaround. + pspell_new('en'); + + // Next, the dictionary in question may not exist. So, we try it... but... + $pspell_link = pspell_new($txt['lang_dictionary'], '', '', strtr($context['character_set'], array('iso-' => 'iso', 'ISO-' => 'iso')), PSPELL_FAST | PSPELL_RUN_TOGETHER); + + // Most people don't have anything but English installed... So we use English as a last resort. + if (!$pspell_link) + $pspell_link = pspell_new('en', '', '', '', PSPELL_FAST | PSPELL_RUN_TOGETHER); + + error_reporting($old); + ob_end_clean(); + + // If we have pspell, exit now... + if ($pspell_link) + { + $context['provider'] = 'pspell'; + return $pspell_link; + } + } + + // If we get this far, we're doomed + return false; +} + +/** + * spell_check() + * + * Determines whether or not the specified word is spelled correctly + * + * @param resource $dict An enchant or pspell dictionary resource set up by {@link spell_init()} + * @param string $word A word to check the spelling of + * @return bool Whether or not the specified word is spelled properly + */ +function spell_check($dict, $word) +{ + global $context, $txt; + + // Enchant or pspell? + if ($context['provider'] == 'enchant') + { + // This is a bit tricky here... + if (!$context['spell_utf8']) + { + // Convert the word to UTF-8 with iconv + $word = iconv($txt['lang_character_set'], 'UTF-8', $word); + } + return enchant_dict_check($dict, $word); + } + elseif ($context['provider'] == 'pspell') + { + return pspell_check($dict, $word); + } +} + +/** + * spell_suggest() + * + * Returns an array of suggested replacements for the specified word + * + * @param resource $dict An enchant or pspell dictionary resource + * @param string $word A misspelled word + * @return array An array of suggested replacements for the misspelled word + */ +function spell_suggest($dict, $word) +{ + global $context, $txt; + + if ($context['provider'] == 'enchant') + { + // If we're not using UTF-8, we need iconv to handle some stuff... + if (!$context['spell_utf8']) + { + // Convert the word to UTF-8 before getting suggestions + $word = iconv($txt['lang_character_set'], 'UTF-8', $word); + $suggestions = enchant_dict_suggest($dict, $word); + + // Go through the suggestions and convert them back to the proper character set + foreach ($suggestions as $index => $suggestion) + { + // //TRANSLIT makes it use similar-looking characters for incompatible ones... + $suggestions[$index] = iconv('UTF-8', $txt['lang_character_set'] . '//TRANSLIT', $suggestion); + } + + return $suggestions; + } + else + { + return enchant_dict_suggest($dict, $word); + } + } + elseif ($context['provider'] == 'pspell') + { + return pspell_suggest($dict, $word); + } +} + +?> \ No newline at end of file diff --git a/Sources/Subs-Recent.php b/Sources/Subs-Recent.php new file mode 100644 index 0000000..7abeae9 --- /dev/null +++ b/Sources/Subs-Recent.php @@ -0,0 +1,127 @@ += {int:likely_max_msg}' . + (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? ' + AND b.id_board != {int:recycle_board}' : '') . ' + AND {query_wanna_see_board}' . ($modSettings['postmod_active'] ? ' + AND t.approved = {int:is_approved} + AND m.approved = {int:is_approved}' : '') . ' + ORDER BY m.id_msg DESC + LIMIT ' . $latestPostOptions['number_posts'], + array( + 'likely_max_msg' => max(0, $modSettings['maxMsgID'] - 50 * $latestPostOptions['number_posts']), + 'recycle_board' => $modSettings['recycle_board'], + 'is_approved' => 1, + ) + ); + $rows = $smcFunc['db_fetch_all']($request); + + // If the ability to embed attachments in posts is enabled, load the attachments now for efficiency + if (!empty($modSettings['attachmentEnable']) && (empty($modSettings['disabledBBC']) || !in_array('attach', explode(',', strtolower($modSettings['disabledBBC']))))) + { + $msgIDs = array(); + foreach ($rows as $row) + $msgIDs[] = $row['id_msg']; + + require_once($sourcedir . '/Subs-Attachments.php'); + prepareAttachsByMsg($msgIDs); + } + + $posts = array(); + foreach ($rows as $row) + { + // Censor the subject and post for the preview ;). + censorText($row['subject']); + censorText($row['body']); + + $row['body'] = strip_tags(strtr(parse_bbc($row['body'], $row['smileys_enabled'], $row['id_msg']), array('
' => ' '))); + if ($smcFunc['strlen']($row['body']) > 128) + $row['body'] = $smcFunc['substr']($row['body'], 0, 128) . '...'; + + // Build the array. + $posts[] = array( + 'board' => array( + 'id' => $row['id_board'], + 'name' => $row['board_name'], + 'href' => $scripturl . '?board=' . $row['id_board'] . '.0', + 'link' => '' . $row['board_name'] . '' + ), + 'topic' => $row['id_topic'], + 'poster' => array( + 'id' => $row['id_member'], + 'name' => $row['poster_name'], + 'href' => empty($row['id_member']) ? '' : $scripturl . '?action=profile;u=' . $row['id_member'], + 'link' => empty($row['id_member']) ? $row['poster_name'] : '' . $row['poster_name'] . '' + ), + 'subject' => $row['subject'], + 'short_subject' => shorten_subject($row['subject'], 24), + 'preview' => $row['body'], + 'time' => timeformat($row['poster_time']), + 'timestamp' => $row['poster_time'], + 'raw_timestamp' => $row['poster_time'], + 'href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . ';topicseen#msg' . $row['id_msg'], + 'link' => '' . $row['subject'] . '' + ); + } + $smcFunc['db_free_result']($request); + + return $posts; +} + +/** + * Callback-function for the cache for getLastPosts(). + * + * @param array $latestPostOptions + */ +function cache_getLastPosts($latestPostOptions) +{ + return array( + 'data' => getLastPosts($latestPostOptions), + 'expires' => time() + 60, + 'post_retri_eval' => ' + foreach ($cache_block[\'data\'] as $k => $post) + { + $cache_block[\'data\'][$k][\'time\'] = timeformat($post[\'raw_timestamp\']); + $cache_block[\'data\'][$k][\'timestamp\'] = $post[\'raw_timestamp\']; + }', + ); +} + +?> \ No newline at end of file diff --git a/Sources/Subs-ReportedContent.php b/Sources/Subs-ReportedContent.php new file mode 100644 index 0000000..d514a55 --- /dev/null +++ b/Sources/Subs-ReportedContent.php @@ -0,0 +1,745 @@ + $action, + 'value' => $value, + 'id_report' => $report_id, + ) + ); + + // From now on, lets work with arrays, makes life easier. + $report_id = (array) $report_id; + + // Set up the data for the log... + $extra = array(); + + if ($context['report_type'] == 'posts') + { + // Get the board, topic and message for this report + $request = $smcFunc['db_query']('', ' + SELECT id_board, id_topic, id_msg, id_report + FROM {db_prefix}log_reported + WHERE id_report IN ({array_int:id_report})', + array( + 'id_report' => $report_id, + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + $extra[$row['id_report']] = array( + 'report' => $row['id_report'], + 'board' => $row['id_board'], + 'message' => $row['id_msg'], + 'topic' => $row['id_topic'], + ); + + $smcFunc['db_free_result']($request); + } + else + { + $request = $smcFunc['db_query']('', ' + SELECT id_report, id_member, membername + FROM {db_prefix}log_reported + WHERE id_report IN ({array_int:id_report})', + array( + 'id_report' => $report_id, + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + $extra[$row['id_report']] = array( + 'report' => $row['id_report'], + 'member' => $row['id_member'], + ); + + $smcFunc['db_free_result']($request); + } + + // Back to "ignore". + if ($action == 'ignore_all') + $action = 'ignore'; + + $log_report = $action == 'ignore' ? (!empty($value) ? 'ignore' : 'unignore') : (!empty($value) ? 'close' : 'open'); + + if ($context['report_type'] == 'members') + $log_report .= '_user'; + + // See if any report alerts need to be cleaned up upon close/ignore + if (in_array($log_report, array('close', 'ignore', 'close_user', 'ignore_user'))) + clearReportAlerts($log_report, $extra); + + // Log this action. + if (!empty($extra)) + foreach ($extra as $report) + logAction($log_report . '_report', $report); + + // Time to update. + updateSettings(array('last_mod_report_action' => time())); + recountOpenReports($context['report_type']); +} + +/** + * Upon close/ignore, mark unread alerts as read. + * + * @param string $log_report - what action is being taken + * @param mixed[] $extra - detailed info about the report + * @return void + */ +function clearReportAlerts($log_report, $extra) +{ + global $smcFunc; + + // Setup the query, depending on if it's a member report or a msg report. + // In theory, these should be unique (reports for the same things get combined), but since $extra is an array, treat as an array. + if (strpos($log_report, '_user') !== false) + { + $content_ids = array_unique(array_column($extra, 'member')); + $content_type = 'member'; + } + else + { + $content_ids = array_unique(array_column($extra, 'message')); + $content_type = 'msg'; + } + + // Check to see if there are unread alerts to flag as read... + // Might be multiple alerts, for multiple moderators... + $alerts = array(); + $moderators = array(); + $result = $smcFunc['db_query']('', ' + SELECT id_alert, id_member FROM {db_prefix}user_alerts + WHERE content_id IN ({array_int:content_ids}) + AND content_type = {string:content_type} + AND content_action = {string:content_action} + AND is_read = {int:unread}', + array( + 'content_ids' => $content_ids, + 'content_type' => $content_type, + 'content_action' => 'report', + 'unread' => 0, + ) + ); + // Found any? + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + $alerts[] = $row['id_alert']; + $moderators[] = $row['id_member']; + } + if (!empty($alerts)) + { + // Flag 'em as read + $smcFunc['db_query']('', ' + UPDATE {db_prefix}user_alerts + SET is_read = {int:time} + WHERE id_alert IN ({array_int:alerts})', + array( + 'time' => time(), + 'alerts' => $alerts, + ) + ); + // Decrement counter for each moderator who had an unread alert + updateMemberData($moderators, array('alerts' => '-')); + } +} + +/** + * Counts how many reports are in total. Used for creating pagination. + * + * @param int $closed 1 for counting closed reports, 0 for open ones. + * @return integer How many reports. + */ +function countReports($closed = 0) +{ + global $smcFunc, $user_info, $context; + + // Skip entries with id_board = 0 if we're viewing member reports + if ($context['report_type'] == 'members') + { + $and = 'lr.id_board = 0'; + } + else + { + if ($user_info['mod_cache']['bq'] == '1=1' || $user_info['mod_cache']['bq'] == '0=1') + { + $bq = $user_info['mod_cache']['bq']; + } + else + { + $bq = 'lr.' . $user_info['mod_cache']['bq']; + } + + $and = $bq . ' AND lr.id_board != 0'; + } + + // How many entries are we viewing? + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}log_reported AS lr + WHERE lr.closed = {int:view_closed} + AND ' . $and, + array( + 'view_closed' => (int) $closed, + ) + ); + list ($total_reports) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + return $total_reports; +} + +/** + * Get all possible reports the current user can see. + * + * @param int $closed 1 for closed reports, 0 for open ones. + * @return array the reports data with the report ID as key. + */ +function getReports($closed = 0) +{ + global $smcFunc, $context, $user_info, $scripturl, $txt; + + // Lonely, standalone var. + $reports = array(); + + // By George, that means we are in a position to get the reports, golly good. + if ($context['report_type'] == 'members') + { + $request = $smcFunc['db_query']('', ' + SELECT lr.id_report, lr.id_member, + lr.time_started, lr.time_updated, lr.num_reports, lr.closed, lr.ignore_all, + COALESCE(mem.real_name, lr.membername) AS user_name, COALESCE(mem.id_member, 0) AS id_user + FROM {db_prefix}log_reported AS lr + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lr.id_member) + WHERE lr.closed = {int:view_closed} + AND lr.id_board = 0 + ORDER BY lr.time_updated DESC + LIMIT {int:start}, {int:max}', + array( + 'view_closed' => (int) $closed, + 'start' => $context['start'], + 'max' => 10, + ) + ); + } + else + { + $request = $smcFunc['db_query']('', ' + SELECT lr.id_report, lr.id_msg, lr.id_topic, lr.id_board, lr.id_member, lr.subject, lr.body, + lr.time_started, lr.time_updated, lr.num_reports, lr.closed, lr.ignore_all, + COALESCE(mem.real_name, lr.membername) AS author_name, COALESCE(mem.id_member, 0) AS id_author + FROM {db_prefix}log_reported AS lr + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lr.id_member) + WHERE lr.closed = {int:view_closed} + AND lr.id_board != 0 + AND ' . ($user_info['mod_cache']['bq'] == '1=1' || $user_info['mod_cache']['bq'] == '0=1' ? $user_info['mod_cache']['bq'] : 'lr.' . $user_info['mod_cache']['bq']) . ' + ORDER BY lr.time_updated DESC + LIMIT {int:start}, {int:max}', + array( + 'view_closed' => (int) $closed, + 'start' => $context['start'], + 'max' => 10, + ) + ); + } + + $report_ids = array(); + $report_boards_ids = array(); + $i = 0; + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $report_ids[] = $row['id_report']; + $reports[$row['id_report']] = array( + 'id' => $row['id_report'], + 'report_href' => $scripturl . '?action=moderate;area=reported' . $context['report_type'] . ';sa=details;rid=' . $row['id_report'], + 'comments' => array(), + 'time_started' => timeformat($row['time_started']), + 'last_updated' => timeformat($row['time_updated']), + 'num_reports' => $row['num_reports'], + 'closed' => $row['closed'], + 'ignore' => $row['ignore_all'] + ); + + if ($context['report_type'] == 'members') + { + $extraDetails = array( + 'user' => array( + 'id' => $row['id_user'], + 'name' => $row['user_name'], + 'link' => $row['id_user'] ? '' . $row['user_name'] . '' : $row['user_name'], + 'href' => $scripturl . '?action=profile;u=' . $row['id_user'], + ), + ); + } + else + { + $report_boards_ids[] = $row['id_board']; + $extraDetails = array( + 'topic' => array( + 'id' => $row['id_topic'], + 'id_msg' => $row['id_msg'], + 'id_board' => $row['id_board'], + 'href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . '#msg' . $row['id_msg'], + ), + 'author' => array( + 'id' => $row['id_author'], + 'name' => $row['author_name'], + 'link' => $row['id_author'] ? '' . $row['author_name'] . '' : $row['author_name'], + 'href' => $scripturl . '?action=profile;u=' . $row['id_author'], + ), + 'subject' => $row['subject'], + 'body' => parse_bbc($row['body']), + ); + } + + $reports[$row['id_report']] = array_merge($reports[$row['id_report']], $extraDetails); + $i++; + } + $smcFunc['db_free_result']($request); + + // Get the names of boards those topics are in. Slightly faster this way. + if (!empty($report_boards_ids)) + { + $report_boards_ids = array_unique($report_boards_ids); + $board_names = array(); + $request = $smcFunc['db_query']('', ' + SELECT id_board, name + FROM {db_prefix}boards + WHERE id_board IN ({array_int:boards})', + array( + 'boards' => $report_boards_ids, + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + $board_names[$row['id_board']] = $row['name']; + + $smcFunc['db_free_result']($request); + + foreach ($reports as $id_report => $report) + if (!empty($board_names[$report['topic']['id_board']])) + $reports[$id_report]['topic']['board_name'] = $board_names[$report['topic']['id_board']]; + } + + // Now get all the people who reported it. + if (!empty($report_ids)) + { + $request = $smcFunc['db_query']('', ' + SELECT lrc.id_comment, lrc.id_report, lrc.time_sent, lrc.comment, + COALESCE(mem.id_member, 0) AS id_member, COALESCE(mem.real_name, lrc.membername) AS reporter + FROM {db_prefix}log_reported_comments AS lrc + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lrc.id_member) + WHERE lrc.id_report IN ({array_int:report_list})', + array( + 'report_list' => $report_ids, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $reports[$row['id_report']]['comments'][] = array( + 'id' => $row['id_comment'], + 'message' => $row['comment'], + 'time' => timeformat($row['time_sent']), + 'member' => array( + 'id' => $row['id_member'], + 'name' => empty($row['reporter']) ? $txt['guest'] : $row['reporter'], + 'link' => $row['id_member'] ? '' . $row['reporter'] . '' : (empty($row['reporter']) ? $txt['guest'] : $row['reporter']), + 'href' => $row['id_member'] ? $scripturl . '?action=profile;u=' . $row['id_member'] : '', + ), + ); + } + $smcFunc['db_free_result']($request); + } + + // Get the boards where the current user can remove any message. + $context['report_remove_any_boards'] = $user_info['is_admin'] ? $report_boards_ids : array_intersect($report_boards_ids, boardsAllowedTo('remove_any')); + $context['report_manage_bans'] = allowedTo('manage_bans'); + + return $reports; +} + +/** + * Recount all open reports. Sets a SESSION var with the updated info. + * + * @param string $type the type of reports to count + * @return int the update open report count. + */ +function recountOpenReports($type) +{ + global $user_info, $smcFunc; + + if ($type == 'members') + $bq = ''; + else + $bq = ' AND ' . $user_info['mod_cache']['bq']; + + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}log_reported + WHERE closed = {int:not_closed} + AND ignore_all = {int:not_ignored} + AND id_board' . ($type == 'members' ? '' : '!') . '= {int:not_a_reported_post}' + . $bq, + array( + 'not_closed' => 0, + 'not_ignored' => 0, + 'not_a_reported_post' => 0, + ) + ); + list ($open_reports) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + $arr = ($type == 'members' ? 'member_reports' : 'reports'); + $_SESSION['rc'] = array_merge(!empty($_SESSION['rc']) ? $_SESSION['rc'] : array(), + array( + 'id' => $user_info['id'], + 'time' => time(), + $arr => $open_reports, + )); + + return $open_reports; +} + +/** + * Gets additional information for a specific report. + * + * @param int $report_id The report ID to get the info from. + * @return array|bool the report data. Boolean false if no report_id was provided. + */ +function getReportDetails($report_id) +{ + global $smcFunc, $user_info, $context; + + if (empty($report_id)) + return false; + + // We don't need all this info if we're only getting user info + if ($context['report_type'] == 'members') + { + $request = $smcFunc['db_query']('', ' + SELECT lr.id_report, lr.id_member, + lr.time_started, lr.time_updated, lr.num_reports, lr.closed, lr.ignore_all, + COALESCE(mem.real_name, lr.membername) AS user_name, COALESCE(mem.id_member, 0) AS id_user + FROM {db_prefix}log_reported AS lr + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lr.id_member) + WHERE lr.id_report = {int:id_report} + AND lr.id_board = 0 + LIMIT 1', + array( + 'id_report' => $report_id, + ) + ); + } + else + { + // Get the report details, need this so we can limit access to a particular board. + $request = $smcFunc['db_query']('', ' + SELECT lr.id_report, lr.id_msg, lr.id_topic, lr.id_board, lr.id_member, lr.subject, lr.body, + lr.time_started, lr.time_updated, lr.num_reports, lr.closed, lr.ignore_all, + COALESCE(mem.real_name, lr.membername) AS author_name, COALESCE(mem.id_member, 0) AS id_author + FROM {db_prefix}log_reported AS lr + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lr.id_member) + WHERE lr.id_report = {int:id_report} + AND ' . ($user_info['mod_cache']['bq'] == '1=1' || $user_info['mod_cache']['bq'] == '0=1' ? $user_info['mod_cache']['bq'] : 'lr.' . $user_info['mod_cache']['bq']) . ' + LIMIT 1', + array( + 'id_report' => $report_id, + ) + ); + } + + // So did we find anything? + if (!$smcFunc['db_num_rows']($request)) + return false; + + // Woohoo we found a report and they can see it! + $row = $smcFunc['db_fetch_assoc']($request); + $smcFunc['db_free_result']($request); + + return $row; +} + +/** + * Gets both report comments as well as any moderator comment. + * + * @param int $report_id The report ID to get the info from. + * @return array|bool an associative array with 2 keys comments and mod_comments. Boolean false if no report_id was provided. + */ +function getReportComments($report_id) +{ + global $smcFunc, $scripturl, $user_info, $txt; + + if (empty($report_id)) + return false; + + $report = array( + 'comments' => array(), + 'mod_comments' => array() + ); + + // So what bad things do the reporters have to say about it? + $request = $smcFunc['db_query']('', ' + SELECT lrc.id_comment, lrc.id_report, lrc.time_sent, lrc.comment, lrc.member_ip, + COALESCE(mem.id_member, 0) AS id_member, COALESCE(mem.real_name, lrc.membername) AS reporter + FROM {db_prefix}log_reported_comments AS lrc + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lrc.id_member) + WHERE lrc.id_report = {int:id_report}', + array( + 'id_report' => $report_id, + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $report['comments'][] = array( + 'id' => $row['id_comment'], + 'message' => strtr($row['comment'], array("\n" => '
')), + 'time' => timeformat($row['time_sent']), + 'member' => array( + 'id' => $row['id_member'], + 'name' => empty($row['reporter']) ? $txt['guest'] : $row['reporter'], + 'link' => $row['id_member'] ? '' . $row['reporter'] . '' : (empty($row['reporter']) ? $txt['guest'] : $row['reporter']), + 'href' => $row['id_member'] ? $scripturl . '?action=profile;u=' . $row['id_member'] : '', + 'ip' => !empty($row['member_ip']) && allowedTo('moderate_forum') ? '' . inet_dtop($row['member_ip']) . '' : '', + ), + ); + } + $smcFunc['db_free_result']($request); + + // Hang about old chap, any comments from moderators on this one? + $request = $smcFunc['db_query']('', ' + SELECT lc.id_comment, lc.id_notice, lc.log_time, lc.body, + COALESCE(mem.id_member, 0) AS id_member, COALESCE(mem.real_name, lc.member_name) AS moderator + FROM {db_prefix}log_comments AS lc + LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lc.id_member) + WHERE lc.id_notice = {int:id_report} + AND lc.comment_type = {literal:reportc}', + array( + 'id_report' => $report_id, + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $report['mod_comments'][] = array( + 'id' => $row['id_comment'], + 'message' => parse_bbc($row['body']), + 'time' => timeformat($row['log_time']), + 'can_edit' => allowedTo('admin_forum') || (($user_info['id'] == $row['id_member'])), + 'member' => array( + 'id' => $row['id_member'], + 'name' => $row['moderator'], + 'link' => $row['id_member'] ? '' . $row['moderator'] . '' : $row['moderator'], + 'href' => $scripturl . '?action=profile;u=' . $row['id_member'], + ), + ); + } + + $smcFunc['db_free_result']($request); + + return $report; +} + +/** + * Gets specific details about a moderator comment. It also adds a permission for editing/deleting the comment, + * by default only admins and the author of the comment can edit/delete it. + * + * @param int $comment_id The moderator comment ID to get the info from. + * @return array|bool an array with the fetched data. Boolean false if no report_id was provided. + */ +function getCommentModDetails($comment_id) +{ + global $smcFunc, $user_info; + + if (empty($comment_id)) + return false; + + $request = $smcFunc['db_query']('', ' + SELECT id_comment, id_notice, log_time, body, id_member + FROM {db_prefix}log_comments + WHERE id_comment = {int:id_comment} + AND comment_type = {literal:reportc}', + array( + 'id_comment' => $comment_id, + ) + ); + + $comment = $smcFunc['db_fetch_assoc']($request); + + $smcFunc['db_free_result']($request); + + // Add the permission + if (!empty($comment)) + $comment['can_edit'] = allowedTo('admin_forum') || (($user_info['id'] == $comment['id_member'])); + + return $comment; +} + +/** + * Inserts a new moderator comment to the DB. + * + * @param int $report_id The report ID is used to fire a notification about the event. + * @param array $data a formatted array of data to be inserted. Should be already properly sanitized. + * @return bool Boolean false if no data was provided. + */ +function saveModComment($report_id, $data) +{ + global $smcFunc, $user_info, $context; + + if (empty($data)) + return false; + + $data = array_merge(array($user_info['id'], $user_info['name'], 'reportc', ''), $data); + + $last_comment = $smcFunc['db_insert']('', + '{db_prefix}log_comments', + array( + 'id_member' => 'int', 'member_name' => 'string', 'comment_type' => 'string', 'recipient_name' => 'string', + 'id_notice' => 'int', 'body' => 'string', 'log_time' => 'int', + ), + $data, + array('id_comment'), + 1 + ); + + $report = getReportDetails($report_id); + + if ($context['report_type'] == 'members') + { + $prefix = 'Member'; + $data = array( + 'report_id' => $report_id, + 'user_id' => $report['id_user'], + 'user_name' => $report['user_name'], + 'sender_id' => $context['user']['id'], + 'sender_name' => $context['user']['name'], + 'comment_id' => $last_comment, + 'time' => time(), + ); + } + else + { + $prefix = 'Msg'; + $data = array( + 'report_id' => $report_id, + 'comment_id' => $last_comment, + 'msg_id' => $report['id_msg'], + 'topic_id' => $report['id_topic'], + 'board_id' => $report['id_board'], + 'sender_id' => $user_info['id'], + 'sender_name' => $user_info['name'], + 'time' => time(), + ); + } + + // And get ready to notify people. + if (!empty($report)) + $smcFunc['db_insert']('insert', + '{db_prefix}background_tasks', + array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'), + array('$sourcedir/tasks/' . $prefix . 'ReportReply-Notify.php', $prefix . 'ReportReply_Notify_Background', $smcFunc['json_encode']($data), 0), + array('id_task') + ); +} + +/** + * Saves the new information whenever a moderator comment is edited. + * + * @param int $comment_id The edited moderator comment ID. + * @param string $edited_comment The edited moderator comment text. + * @return bool Boolean false if no data or no comment ID was provided. + */ +function editModComment($comment_id, $edited_comment) +{ + global $smcFunc; + + if (empty($comment_id) || empty($edited_comment)) + return false; + + $smcFunc['db_query']('', ' + UPDATE {db_prefix}log_comments + SET body = {string:body} + WHERE id_comment = {int:id_comment}', + array( + 'body' => $edited_comment, + 'id_comment' => $comment_id, + ) + ); + return true; +} + +/** + * Deletes a moderator comment from the DB. + * + * @param int $comment_id The moderator comment ID used to identify which report will be deleted. + * @return bool Boolean false if no data was provided. + */ +function deleteModComment($comment_id) +{ + global $smcFunc; + + if (empty($comment_id)) + return false; + + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}log_comments + WHERE id_comment = {int:comment_id}', + array( + 'comment_id' => $comment_id, + ) + ); + +} + +?> \ No newline at end of file diff --git a/Sources/Subs-Sound.php b/Sources/Subs-Sound.php new file mode 100644 index 0000000..fbdc332 --- /dev/null +++ b/Sources/Subs-Sound.php @@ -0,0 +1,136 @@ + 2 || ($ip2 = cache_get_data('wave_file/' . $user_info['ip2'], 20)) > 2) + die(send_http_status(400)); + cache_put_data('wave_file/' . $user_info['ip'], $ip ? $ip + 1 : 1, 20); + cache_put_data('wave_file/' . $user_info['ip2'], $ip2 ? $ip2 + 1 : 1, 20); + + // Fixate randomization for this word. + $tmp = unpack('n', md5($word . session_id())); + mt_srand(end($tmp)); + + // Try to see if there's a sound font in the user's language. + if (file_exists($settings['default_theme_dir'] . '/fonts/sound/a.' . $user_info['language'] . '.wav')) + $sound_language = $user_info['language']; + + // English should be there. + elseif (file_exists($settings['default_theme_dir'] . '/fonts/sound/a.english.wav')) + $sound_language = 'english'; + + // Guess not... + else + return false; + + // File names are in lower case so lets make sure that we are only using a lower case string + $word = strtolower($word); + + // Loop through all letters of the word $word. + $sound_word = ''; + for ($i = 0; $i < strlen($word); $i++) + { + $sound_letter = implode('', file($settings['default_theme_dir'] . '/fonts/sound/' . $word[$i] . '.' . $sound_language . '.wav')); + if (strpos($sound_letter, 'data') === false) + return false; + + $sound_letter = substr($sound_letter, strpos($sound_letter, 'data') + 8); + switch ($word[$i] === 's' ? 0 : mt_rand(0, 2)) + { + case 0 : + for ($j = 0, $n = strlen($sound_letter); $j < $n; $j++) + for ($k = 0, $m = round(mt_rand(15, 25) / 10); $k < $m; $k++) + $sound_word .= $word[$i] === 's' ? $sound_letter[$j] : chr(mt_rand(max(ord($sound_letter[$j]) - 1, 0x00), min(ord($sound_letter[$j]) + 1, 0xFF))); + break; + + case 1: + for ($j = 0, $n = strlen($sound_letter) - 1; $j < $n; $j += 2) + $sound_word .= (mt_rand(0, 3) == 0 ? '' : $sound_letter[$j]) . (mt_rand(0, 3) === 0 ? $sound_letter[$j + 1] : $sound_letter[$j]) . (mt_rand(0, 3) === 0 ? $sound_letter[$j] : $sound_letter[$j + 1]) . $sound_letter[$j + 1] . (mt_rand(0, 3) == 0 ? $sound_letter[$j + 1] : ''); + $sound_word .= str_repeat($sound_letter[$n], 2); + break; + + case 2: + $shift = 0; + for ($j = 0, $n = strlen($sound_letter); $j < $n; $j++) + { + if (mt_rand(0, 10) === 0) + $shift += mt_rand(-3, 3); + for ($k = 0, $m = round(mt_rand(15, 25) / 10); $k < $m; $k++) + $sound_word .= chr(min(max(ord($sound_letter[$j]) + $shift, 0x00), 0xFF)); + } + break; + } + + $sound_word .= str_repeat(chr(0x80), mt_rand(10000, 10500)); + } + + $data_size = strlen($sound_word); + $file_size = $data_size + 0x24; + $content_length = $file_size + 0x08; + $sample_rate = 16000; + + // Disable compression. + ob_end_clean(); + header_remove('content-encoding'); + header('content-encoding: none'); + header('accept-ranges: bytes'); + header('connection: close'); + header('cache-control: no-cache'); + + // Output the wav. + header('content-type: audio/x-wav'); + header('expires: ' . gmdate('D, d M Y H:i:s', time() + 525600 * 60) . ' GMT'); + + if (isset($_SERVER['HTTP_RANGE'])) + { + list($a, $range) = explode("=", $_SERVER['HTTP_RANGE'], 2); + list($range) = explode(",", $range, 2); + list($range, $range_end) = explode("-", $range); + $range = intval($range); + $range_end = !$range_end ? $content_length - 1 : intval($range_end); + $new_length = $range_end - $range + 1; + + send_http_status(206); + header("content-length: $new_length"); + header("content-range: bytes $range-$range_end/$content_length"); + } + else + header("content-length: " . $content_length); + + echo pack('nnVnnnnnnnnVVnnnnV', 0x5249, 0x4646, $file_size, 0x5741, 0x5645, 0x666D, 0x7420, 0x1000, 0x0000, 0x0100, 0x0100, $sample_rate, $sample_rate, 0x0100, 0x0800, 0x6461, 0x7461, $data_size), $sound_word; + + // Noting more to add. + die(); +} + +?> \ No newline at end of file diff --git a/Sources/Subs-Themes.php b/Sources/Subs-Themes.php new file mode 100644 index 0000000..84ca26e --- /dev/null +++ b/Sources/Subs-Themes.php @@ -0,0 +1,634 @@ + $id, + ); + + // Make our known/enable themes a little easier to work with. + $knownThemes = !empty($modSettings['knownThemes']) ? explode(',', $modSettings['knownThemes']) : array(); + $enableThemes = !empty($modSettings['enableThemes']) ? explode(',', $modSettings['enableThemes']) : array(); + + $request = $smcFunc['db_query']('', ' + SELECT id_theme, variable, value + FROM {db_prefix}themes + WHERE id_theme = ({int:id_theme}) + AND id_member = {int:no_member}' . (!empty($variables) ? ' + AND variable IN ({array_string:variables})' : ''), + array( + 'variables' => $variables, + 'id_theme' => $id, + 'no_member' => 0, + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $single[$row['variable']] = $row['value']; + + // Fix the path and tell if its a valid one. + if ($row['variable'] == 'theme_dir') + { + $single['theme_dir'] = realpath($row['value']); + $single['valid_path'] = file_exists($row['value']) && is_dir($row['value']); + } + } + + // Is this theme installed and enabled? + $single['known'] = in_array($single['id'], $knownThemes); + $single['enable'] = in_array($single['id'], $enableThemes); + + // It should at least return if the theme is a known one or if its enable. + return $single; +} + +/** + * Loads and returns all installed themes. + * + * Stores all themes on $context['themes'] for easier use. + * + * $modSettings['knownThemes'] stores themes that the user is able to select. + * + * @param bool $enable_only Whether to fetch only enabled themes. Default is false. + */ +function get_all_themes($enable_only = false) +{ + global $modSettings, $context, $smcFunc; + + // Make our known/enable themes a little easier to work with. + $knownThemes = !empty($modSettings['knownThemes']) ? explode(',', $modSettings['knownThemes']) : array(); + $enableThemes = !empty($modSettings['enableThemes']) ? explode(',', $modSettings['enableThemes']) : array(); + + // List of all possible themes values. + $themeValues = array( + 'theme_dir', + 'images_url', + 'theme_url', + 'name', + 'theme_layers', + 'theme_templates', + 'version', + 'install_for', + 'based_on', + ); + + // Make changes if you really want it. + call_integration_hook('integrate_get_all_themes', array(&$themeValues, $enable_only)); + + // So, what is it going to be? + $query_where = $enable_only ? $enableThemes : $knownThemes; + + // Perform the query as requested. + $request = $smcFunc['db_query']('', ' + SELECT id_theme, variable, value + FROM {db_prefix}themes + WHERE variable IN ({array_string:theme_values}) + AND id_theme IN ({array_string:query_where}) + AND id_member = {int:no_member}', + array( + 'query_where' => $query_where, + 'theme_values' => $themeValues, + 'no_member' => 0, + ) + ); + + $context['themes'] = array(); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (!isset($context['themes'][$row['id_theme']])) + $context['themes'][$row['id_theme']] = array( + 'id' => (int) $row['id_theme'], + 'known' => in_array($row['id_theme'], $knownThemes), + 'enable' => in_array($row['id_theme'], $enableThemes) + ); + + // Fix the path and tell if its a valid one. + if ($row['variable'] == 'theme_dir') + { + $row['value'] = realpath($row['value']); + $context['themes'][$row['id_theme']]['valid_path'] = file_exists($row['value']) && is_dir($row['value']); + } + $context['themes'][$row['id_theme']][$row['variable']] = $row['value']; + } + + $smcFunc['db_free_result']($request); +} + +/** + * Loads and returns all installed themes. + * + * Stores all themes on $context['themes'] for easier use. + * + * $modSettings['knownThemes'] stores themes that the user is able to select. + */ +function get_installed_themes() +{ + global $modSettings, $context, $smcFunc; + + // Make our known/enable themes a little easier to work with. + $knownThemes = !empty($modSettings['knownThemes']) ? explode(',', $modSettings['knownThemes']) : array(); + $enableThemes = !empty($modSettings['enableThemes']) ? explode(',', $modSettings['enableThemes']) : array(); + + // List of all possible themes values. + $themeValues = array( + 'theme_dir', + 'images_url', + 'theme_url', + 'name', + 'theme_layers', + 'theme_templates', + 'version', + 'install_for', + 'based_on', + ); + + // Make changes if you really want it. + call_integration_hook('integrate_get_installed_themes', array(&$themeValues)); + + // Perform the query as requested. + $request = $smcFunc['db_query']('', ' + SELECT id_theme, variable, value + FROM {db_prefix}themes + WHERE variable IN ({array_string:theme_values}) + AND id_member = {int:no_member}', + array( + 'theme_values' => $themeValues, + 'no_member' => 0, + ) + ); + + $context['themes'] = array(); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (!isset($context['themes'][$row['id_theme']])) + $context['themes'][$row['id_theme']] = array( + 'id' => (int) $row['id_theme'], + 'known' => in_array($row['id_theme'], $knownThemes), + 'enable' => in_array($row['id_theme'], $enableThemes) + ); + + // Fix the path and tell if its a valid one. + if ($row['variable'] == 'theme_dir') + { + $row['value'] = realpath($row['value']); + $context['themes'][$row['id_theme']]['valid_path'] = file_exists($row['value']) && is_dir($row['value']); + } + $context['themes'][$row['id_theme']][$row['variable']] = $row['value']; + } + + $smcFunc['db_free_result']($request); +} + +/** + * Reads an .xml file and returns the data as an array + * + * Removes the entire theme if the .xml file couldn't be found or read. + * + * @param string $path The absolute path to the xml file. + * @return array An array with all the info extracted from the xml file. + */ +function get_theme_info($path) +{ + global $smcFunc, $sourcedir, $txt, $scripturl, $context; + global $explicit_images; + + if (empty($path)) + return false; + + $xml_data = array(); + $explicit_images = false; + + // Perhaps they are trying to install a mod, lets tell them nicely this is the wrong function. + if (file_exists($path . '/package-info.xml')) + { + loadLanguage('Errors'); + + // We need to delete the dir otherwise the next time you try to install a theme you will get the same error. + remove_dir($path); + + $txt['package_get_error_is_mod'] = str_replace('{MANAGEMODURL}', $scripturl . '?action=admin;area=packages;' . $context['session_var'] . '=' . $context['session_id'], $txt['package_get_error_is_mod']); + fatal_lang_error('package_theme_upload_error_broken', false, $txt['package_get_error_is_mod']); + } + + // Parse theme-info.xml into an xmlArray. + require_once($sourcedir . '/Class-Package.php'); + $theme_info_xml = new xmlArray(file_get_contents($path . '/theme_info.xml')); + + // Error message, there isn't any valid info. + if (!$theme_info_xml->exists('theme-info[0]')) + { + remove_dir($path); + fatal_lang_error('package_get_error_packageinfo_corrupt', false); + } + + // Check for compatibility with 2.1 or greater. + if (!$theme_info_xml->exists('theme-info/install')) + { + remove_dir($path); + fatal_lang_error('package_get_error_theme_not_compatible', false, SMF_FULL_VERSION); + } + + // So, we have an install tag which is cool and stuff but we also need to check it and match your current SMF version... + $the_version = SMF_VERSION; + $install_versions = $theme_info_xml->fetch('theme-info/install/@for'); + + // The theme isn't compatible with the current SMF version. + require_once($sourcedir . '/Subs-Package.php'); + if (!$install_versions || !matchPackageVersion($the_version, $install_versions)) + { + remove_dir($path); + fatal_lang_error('package_get_error_theme_not_compatible', false, SMF_FULL_VERSION); + } + + $theme_info_xml = $theme_info_xml->to_array('theme-info[0]'); + + $xml_elements = array( + 'theme_layers' => 'layers', + 'theme_templates' => 'templates', + 'based_on' => 'based-on', + 'version' => 'version', + ); + + // Assign the values to be stored. + foreach ($xml_elements as $var => $name) + if (!empty($theme_info_xml[$name])) + $xml_data[$var] = $theme_info_xml[$name]; + + // Add the supported versions. + $xml_data['install_for'] = $install_versions; + + // Overwrite the default images folder. + if (!empty($theme_info_xml['images'])) + { + $xml_data['images_url'] = $path . '/' . $theme_info_xml['images']; + $explicit_images = true; + } + + if (!empty($theme_info_xml['extra'])) + $xml_data += $smcFunc['json_decode']($theme_info_xml['extra'], true); + + return $xml_data; +} + +/** + * Inserts a theme's data to the DataBase. + * + * Ends execution with fatal_lang_error() if an error appears. + * + * @param array $to_install An array containing all values to be stored into the DB. + * @return int The newly created theme ID. + */ +function theme_install($to_install = array()) +{ + global $smcFunc, $context, $modSettings; + global $settings, $explicit_images; + + // External use? no problem! + if (!empty($to_install)) + $context['to_install'] = $to_install; + + // One last check. + if (empty($context['to_install']['theme_dir']) || basename($context['to_install']['theme_dir']) == 'Themes') + fatal_lang_error('theme_install_invalid_dir', false); + + // OK, is this a newer version of an already installed theme? + if (!empty($context['to_install']['version'])) + { + $request = $smcFunc['db_query']('', ' + SELECT id_theme + FROM {db_prefix}themes + WHERE id_member = {int:no_member} + AND variable = {literal:name} + AND value LIKE {string:name_value} + LIMIT 1', + array( + 'no_member' => 0, + 'name_value' => '%' . $context['to_install']['name'] . '%', + ) + ); + + list ($id_to_update) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + $to_update = get_single_theme($id_to_update, array('version')); + + // Got something, lets figure it out what to do next. + if (!empty($id_to_update) && !empty($to_update['version'])) + switch (compareVersions($context['to_install']['version'], $to_update['version'])) + { + case 1: // Got a newer version, update the old entry. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}themes + SET value = {string:new_value} + WHERE variable = {literal:version} + AND id_theme = {int:id_theme}', + array( + 'new_value' => $context['to_install']['version'], + 'id_theme' => $id_to_update, + ) + ); + + // Done with the update, tell the user about it. + $context['to_install']['updated'] = true; + + return $id_to_update; + + case 0: // This is exactly the same theme. + case -1: // The one being installed is older than the one already installed. + default: // Any other possible result. + fatal_lang_error('package_get_error_theme_no_new_version', false, array($context['to_install']['version'], $to_update['version'])); + } + } + + if (!empty($context['to_install']['based_on'])) + { + // No need for elaborated stuff when the theme is based on the default one. + if ($context['to_install']['based_on'] == 'default') + { + $context['to_install']['theme_url'] = $settings['default_theme_url']; + $context['to_install']['images_url'] = $settings['default_images_url']; + } + + // Custom theme based on another custom theme, lets get some info. + elseif ($context['to_install']['based_on'] != '') + { + $context['to_install']['based_on'] = preg_replace('~[^A-Za-z0-9\-_ ]~', '', $context['to_install']['based_on']); + + // Get the theme info first. + $request = $smcFunc['db_query']('', ' + SELECT id_theme + FROM {db_prefix}themes + WHERE id_member = {int:no_member} + AND (value LIKE {string:based_on} OR value LIKE {string:based_on_path}) + LIMIT 1', + array( + 'no_member' => 0, + 'based_on' => '%/' . $context['to_install']['based_on'], + 'based_on_path' => '%' . "\\" . $context['to_install']['based_on'], + ) + ); + + list ($id_based_on) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + $temp = get_single_theme($id_based_on, array('theme_dir', 'images_url', 'theme_url')); + + // Found the based on theme info, add it to the current one being installed. + if (!empty($temp)) + { + $context['to_install']['base_theme_url'] = $temp['theme_url']; + $context['to_install']['base_theme_dir'] = $temp['theme_dir']; + + if (empty($explicit_images) && !empty($context['to_install']['base_theme_url'])) + $context['to_install']['theme_url'] = $context['to_install']['base_theme_url']; + } + + // Nope, sorry, couldn't find any theme already installed. + else + fatal_lang_error('package_get_error_theme_no_based_on_found', false, $context['to_install']['based_on']); + } + + unset($context['to_install']['based_on']); + } + + // Find the newest id_theme. + $result = $smcFunc['db_query']('', ' + SELECT MAX(id_theme) + FROM {db_prefix}themes', + array( + ) + ); + list ($id_theme) = $smcFunc['db_fetch_row']($result); + $smcFunc['db_free_result']($result); + + // This will be theme number... + $id_theme++; + + // Last minute changes? although, the actual array is a context value you might want to use the new ID. + call_integration_hook('integrate_theme_install', array(&$context['to_install'], $id_theme)); + + $inserts = array(); + foreach ($context['to_install'] as $var => $val) + $inserts[] = array($id_theme, $var, $val); + + if (!empty($inserts)) + $smcFunc['db_insert']('insert', + '{db_prefix}themes', + array('id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'), + $inserts, + array('id_theme', 'variable') + ); + + // Update the known and enable Theme's settings. + $known = strtr($modSettings['knownThemes'] . ',' . $id_theme, array(',,' => ',')); + $enable = strtr($modSettings['enableThemes'] . ',' . $id_theme, array(',,' => ',')); + updateSettings(array('knownThemes' => $known, 'enableThemes' => $enable)); + + return $id_theme; +} + +/** + * Removes a directory from the themes dir. + * + * This is a recursive function, it will call itself if there are subdirs inside the main directory. + * + * @param string $path The absolute path to the directory to be removed + * @return bool true when success, false on error. + */ +function remove_dir($path) +{ + if (empty($path)) + return false; + + if (is_dir($path)) + { + $objects = scandir($path); + + foreach ($objects as $object) + if ($object != '.' && $object != '..') + { + if (filetype($path . '/' . $object) == 'dir') + remove_dir($path . '/' . $object); + + else + unlink($path . '/' . $object); + } + } + + reset($objects); + rmdir($path); +} + +/** + * Removes a theme from the DB, includes all possible places where the theme might be used. + * + * @param int $themeID The theme ID + * @return bool true when success, false on error. + */ +function remove_theme($themeID) +{ + global $smcFunc, $modSettings; + + // Can't delete the default theme, sorry! + if (empty($themeID) || $themeID == 1) + return false; + + $known = explode(',', $modSettings['knownThemes']); + $enable = explode(',', $modSettings['enableThemes']); + + // Remove it from the themes table. + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}themes + WHERE id_theme = {int:current_theme}', + array( + 'current_theme' => $themeID, + ) + ); + + // Update users preferences. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}members + SET id_theme = {int:default_theme} + WHERE id_theme = {int:current_theme}', + array( + 'default_theme' => 0, + 'current_theme' => $themeID, + ) + ); + + // Some boards may have it as preferred theme. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}boards + SET id_theme = {int:default_theme} + WHERE id_theme = {int:current_theme}', + array( + 'default_theme' => 0, + 'current_theme' => $themeID, + ) + ); + + // Remove it from the list of known themes. + $known = array_diff($known, array($themeID)); + + // And the enable list too. + $enable = array_diff($enable, array($themeID)); + + // Back to good old comma separated string. + $known = strtr(implode(',', $known), array(',,' => ',')); + $enable = strtr(implode(',', $enable), array(',,' => ',')); + + // Update the enableThemes list. + updateSettings(array('enableThemes' => $enable, 'knownThemes' => $known)); + + // Fix it if the theme was the overall default theme. + if ($modSettings['theme_guests'] == $themeID) + updateSettings(array('theme_guests' => '1')); + + return true; +} + +/** + * Generates a file listing for a given directory + * + * @param string $path The full path to the directory + * @param string $relative The relative path (relative to the Themes directory) + * @return array An array of information about the files and directories found + */ +function get_file_listing($path, $relative) +{ + global $scripturl, $txt, $context; + + // Is it even a directory? + if (!is_dir($path)) + fatal_lang_error('error_invalid_dir', 'critical'); + + $dir = dir($path); + $entries = array(); + while ($entry = $dir->read()) + $entries[] = $entry; + $dir->close(); + + natcasesort($entries); + + $listing1 = array(); + $listing2 = array(); + + foreach ($entries as $entry) + { + // Skip all dot files, including .htaccess. + if (substr($entry, 0, 1) == '.' || $entry == 'CVS') + continue; + + if (is_dir($path . '/' . $entry)) + $listing1[] = array( + 'filename' => $entry, + 'is_writable' => is_writable($path . '/' . $entry), + 'is_directory' => true, + 'is_template' => false, + 'is_image' => false, + 'is_editable' => false, + 'href' => $scripturl . '?action=admin;area=theme;th=' . $_GET['th'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=edit;directory=' . $relative . $entry, + 'size' => '', + ); + else + { + $size = filesize($path . '/' . $entry); + if ($size > 2048 || $size == 1024) + $size = comma_format($size / 1024) . ' ' . $txt['themeadmin_edit_kilobytes']; + else + $size = comma_format($size) . ' ' . $txt['themeadmin_edit_bytes']; + + $listing2[] = array( + 'filename' => $entry, + 'is_writable' => is_writable($path . '/' . $entry), + 'is_directory' => false, + 'is_template' => preg_match('~\.template\.php$~', $entry) != 0, + 'is_image' => preg_match('~\.(jpg|jpeg|gif|bmp|png)$~', $entry) != 0, + 'is_editable' => is_writable($path . '/' . $entry) && preg_match('~\.(php|pl|css|js|vbs|xml|xslt|txt|xsl|html|htm|shtm|shtml|asp|aspx|cgi|py)$~', $entry) != 0, + 'href' => $scripturl . '?action=admin;area=theme;th=' . $_GET['th'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=edit;filename=' . $relative . $entry, + 'size' => $size, + 'last_modified' => timeformat(filemtime($path . '/' . $entry)), + ); + } + } + + return array_merge($listing1, $listing2); +} + +?> \ No newline at end of file diff --git a/Sources/Subs-Timezones.php b/Sources/Subs-Timezones.php new file mode 100644 index 0000000..23d1436 --- /dev/null +++ b/Sources/Subs-Timezones.php @@ -0,0 +1,2130 @@ + 'GMT', + + // No DST + 'Africa/Algiers' => 'Europe_Central', + + // Uses DST + 'Africa/Casablanca' => 'Africa_Morocco', + + // No DST + 'Africa/Johannesburg' => 'Africa_South', + + // No DST + 'Africa/Lagos' => 'Africa_West', + + // No DST + 'Africa/Maputo' => 'Africa_Central', + + // No DST + 'Africa/Nairobi' => 'Africa_East', + + // Uses DST + 'America/Adak' => 'North_America_Hawaii_Aleutian', + + // Uses DST + 'America/Anchorage' => 'North_America_Alaska', + + // No DST + 'America/Argentina/Buenos_Aires' => 'South_America_Argentina', + + // Uses DST + 'America/Asuncion' => 'South_America_Paraguay', + + // No DST + 'America/Belize' => 'North_America_Central', + + // No DST + 'America/Bogota' => 'South_America_Colombia', + + // No DST + 'America/Caracas' => 'South_America_Venezuela', + + // No DST + 'America/Cayenne' => 'South_America_French_Guiana', + + // Uses DST + 'America/Chicago' => 'North_America_Central', + + // Uses DST + 'America/Chihuahua' => 'North_America_Mexico_Pacific', + + // Uses DST + 'America/Denver' => 'North_America_Mountain', + + // Uses DST + 'America/Nuuk' => 'North_America_Greenland_Western', + + // No DST + 'America/Guayaquil' => 'South_America_Ecuador', + + // No DST + 'America/Guyana' => 'South_America_Guyana', + + // Uses DST + 'America/Halifax' => 'North_America_Atlantic', + + // Uses DST + 'America/Havana' => 'North_America_Cuba', + + // No DST + 'America/Jamaica' => 'North_America_Eastern', + + // No DST + 'America/La_Paz' => 'South_America_Bolivia', + + // No DST + 'America/Lima' => 'South_America_Peru', + + // Uses DST + 'America/Los_Angeles' => 'North_America_Pacific', + + // No DST + 'America/Manaus' => 'South_America_Amazon', + + // Uses DST + 'America/Mexico_City' => 'North_America_Mexico_Central', + + // Uses DST + 'America/Miquelon' => 'North_America_St_Pierre_Miquelon', + + // No DST + 'America/Montevideo' => 'South_America_Uruguay', + + // Uses DST + 'America/New_York' => 'North_America_Eastern', + + // No DST + 'America/Noronha' => 'South_America_Noronha', + + // No DST + 'America/Paramaribo' => 'South_America_Suriname', + + // No DST + 'America/Phoenix' => 'North_America_Mountain', + + // No DST + 'America/Port_of_Spain' => 'North_America_Atlantic', + + // No DST + 'America/Punta_Arenas' => 'South_America_Chile_Magallanes', + + // No DST + 'America/Rio_Branco' => 'South_America_Acre', + + // Uses DST + 'America/Santiago' => 'South_America_Chile', + + // No DST + 'America/Sao_Paulo' => 'South_America_Brasilia', + + // Uses DST + 'America/Scoresbysund' => 'North_America_Greenland_Eastern', + + // Uses DST + 'America/St_Johns' => 'North_America_Newfoundland', + + // No DST + 'Antarctica/Casey' => 'Antarctica_Casey', + + // No DST + 'Antarctica/Davis' => 'Antarctica_Davis', + + // No DST + 'Antarctica/DumontDUrville' => 'Antarctica_DumontDUrville', + + // No DST + 'Antarctica/Macquarie' => 'Antarctica_Macquarie', + + // No DST + 'Antarctica/Mawson' => 'Antarctica_Mawson', + + // Uses DST + 'Antarctica/McMurdo' => 'Antarctica_McMurdo', + + // No DST + 'Antarctica/Palmer' => 'Antarctica_Palmer', + + // No DST + 'Antarctica/Rothera' => 'Antarctica_Rothera', + + // No DST + 'Antarctica/Syowa' => 'Antarctica_Syowa', + + // Uses DST + 'Antarctica/Troll' => 'Antarctica_Troll', + + // No DST + 'Antarctica/Vostok' => 'Antarctica_Vostok', + + // No DST + 'Asia/Almaty' => 'Asia_Kazakhstan_Eastern', + + // Uses DST + 'Asia/Amman' => 'Asia_Jordan', + + // No DST + 'Asia/Aqtau' => 'Asia_Kazakhstan_Western', + + // No DST + 'Asia/Ashgabat' => 'Asia_Turkmenistan', + + // No DST + 'Asia/Baku' => 'Asia_Azerbaijan', + + // No DST + 'Asia/Bangkok' => 'Asia_Southeast', + + // Uses DST + 'Asia/Beirut' => 'Asia_Libya', + + // No DST + 'Asia/Bishkek' => 'Asia_Kyrgystan', + + // No DST + 'Asia/Brunei' => 'Asia_Brunei', + + // Uses DST + 'Asia/Damascus' => 'Asia_Damascus', + + // No DST + 'Asia/Dhaka' => 'Asia_Bangladesh', + + // No DST + 'Asia/Dili' => 'Asia_East_Timor', + + // No DST + 'Asia/Dubai' => 'Asia_Gulf', + + // No DST + 'Asia/Dushanbe' => 'Asia_Tajikistan', + + // Uses DST + 'Asia/Gaza' => 'Asia_Palestine', + + // No DST + 'Asia/Hong_Kong' => 'Asia_Hong_Kong', + + // No DST + 'Asia/Hovd' => 'Asia_Mongolia_Western', + + // No DST + 'Asia/Irkutsk' => 'Asia_Irkutsk', + + // No DST + 'Asia/Jakarta' => 'Asia_Indonesia_Western', + + // No DST + 'Asia/Jayapura' => 'Asia_Indonesia_Eastern', + + // Uses DST + 'Asia/Jerusalem' => 'Asia_Israel', + + // No DST + 'Asia/Kabul' => 'Asia_Afghanistan', + + // No DST + 'Asia/Kamchatka' => 'Asia_Kamchatka', + + // No DST + 'Asia/Karachi' => 'Asia_Pakistan', + + // No DST + 'Asia/Kathmandu' => 'Asia_Nepal', + + // No DST + 'Asia/Kolkata' => 'Asia_India', + + // No DST + 'Asia/Krasnoyarsk' => 'Asia_Krasnoyarsk', + + // No DST + 'Asia/Kuala_Lumpur' => 'Asia_Malaysia', + + // No DST + 'Asia/Magadan' => 'Asia_Magadan', + + // No DST + 'Asia/Makassar' => 'Asia_Indonesia_Central', + + // No DST + 'Asia/Manila' => 'Asia_Philippines', + + // No DST + 'Asia/Omsk' => 'Asia_Omsk', + + // No DST + 'Asia/Riyadh' => 'Asia_Arabia', + + // No DST + 'Asia/Seoul' => 'Asia_Korea', + + // No DST + 'Asia/Shanghai' => 'Asia_China', + + // No DST + 'Asia/Singapore' => 'Asia_Singapore', + + // No DST + 'Asia/Taipei' => 'Asia_Taiwan', + + // No DST + 'Asia/Tashkent' => 'Asia_Uzbekistan', + + // No DST + 'Asia/Tbilisi' => 'Asia_Georgia', + + // Uses DST + 'Asia/Tehran' => 'Asia_Iran', + + // No DST + 'Asia/Thimphu' => 'Asia_Bhutan', + + // No DST + 'Asia/Tokyo' => 'Asia_Japan', + + // No DST + 'Asia/Ulaanbaatar' => 'Asia_Mongolia_Eastern', + + // No DST + 'Asia/Vladivostok' => 'Asia_Vladivostok', + + // No DST + 'Asia/Yakutsk' => 'Asia_Yakutsk', + + // No DST + 'Asia/Yangon' => 'Asia_Myanmar', + + // No DST + 'Asia/Yekaterinburg' => 'Asia_Yekaterinburg', + + // No DST + 'Asia/Yerevan' => 'Asia_Armenia', + + // Uses DST + 'Atlantic/Azores' => 'Atlantic_Azores', + + // No DST + 'Atlantic/Cape_Verde' => 'Atlantic_Cape_Verde', + + // No DST + 'Atlantic/South_Georgia' => 'Atlantic_South_Georgia', + + // No DST + 'Atlantic/Stanley' => 'Atlantic_Falkland', + + // Uses DST + 'Australia/Adelaide' => 'Australia_Central', + + // No DST + 'Australia/Brisbane' => 'Australia_Eastern', + + // No DST + 'Australia/Darwin' => 'Australia_Central', + + // No DST + 'Australia/Eucla' => 'Australia_CentralWestern', + + // Uses DST + 'Australia/Lord_Howe' => 'Australia_Lord_Howe', + + // Uses DST + 'Australia/Melbourne' => 'Australia_Eastern', + + // No DST + 'Australia/Perth' => 'Australia_Western', + + // Uses DST + 'Europe/Berlin' => 'Europe_Central', + + // Uses DST + 'Europe/Chisinau' => 'Europe_Moldova', + + // Uses DST + 'Europe/Dublin' => 'Europe_Eire', + + // Uses DST + 'Europe/Helsinki' => 'Europe_Eastern', + + // No DST + 'Europe/Istanbul' => 'Asia_Turkey', + + // No DST + 'Europe/Kaliningrad' => 'Europe_Eastern', + + // Uses DST + 'Europe/Lisbon' => 'Europe_Western', + + // Uses DST + 'Europe/London' => 'Europe_UK', + + // No DST + 'Europe/Minsk' => 'Europe_Minsk', + + // No DST + 'Europe/Moscow' => 'Europe_Moscow', + + // No DST + 'Europe/Samara' => 'Europe_Samara', + + // No DST + 'Europe/Volgograd' => 'Europe_Volgograd', + + // No DST + 'Indian/Chagos' => 'Indian_Chagos', + + // No DST + 'Indian/Christmas' => 'Indian_Christmas', + + // No DST + 'Indian/Cocos' => 'Indian_Cocos', + + // No DST + 'Indian/Kerguelen' => 'Indian_Kerguelen', + + // No DST + 'Indian/Mahe' => 'Indian_Seychelles', + + // No DST + 'Indian/Maldives' => 'Indian_Maldives', + + // No DST + 'Indian/Mauritius' => 'Indian_Mauritius', + + // No DST + 'Indian/Reunion' => 'Indian_Reunion', + + // Uses DST + 'Pacific/Apia' => 'Pacific_Apia', + + // Uses DST + 'Pacific/Auckland' => 'Pacific_New_Zealand', + + // No DST + 'Pacific/Bougainville' => 'Pacific_Bougainville', + + // Uses DST + 'Pacific/Chatham' => 'Pacific_Chatham', + + // No DST + 'Pacific/Chuuk' => 'Pacific_Chuuk', + + // Uses DST + 'Pacific/Easter' => 'Pacific_Easter', + + // No DST + 'Pacific/Efate' => 'Pacific_Vanuatu', + + // No DST + 'Pacific/Kanton' => 'Pacific_Phoenix_Islands', + + // No DST + 'Pacific/Fakaofo' => 'Pacific_Tokelau', + + // Uses DST + 'Pacific/Fiji' => 'Pacific_Fiji', + + // No DST + 'Pacific/Funafuti' => 'Pacific_Tuvalu', + + // No DST + 'Pacific/Galapagos' => 'Pacific_Galapagos', + + // No DST + 'Pacific/Gambier' => 'Pacific_Gambier', + + // No DST + 'Pacific/Guadalcanal' => 'Pacific_Solomon', + + // No DST + 'Pacific/Guam' => 'Pacific_Chamorro', + + // No DST + 'Pacific/Honolulu' => 'Pacific_Hawaii', + + // No DST + 'Pacific/Kiritimati' => 'Pacific_Line', + + // No DST + 'Pacific/Kwajalein' => 'Pacific_Marshall', + + // No DST + 'Pacific/Marquesas' => 'Pacific_Marquesas', + + // No DST + 'Pacific/Nauru' => 'Pacific_Nauru', + + // No DST + 'Pacific/Niue' => 'Pacific_Niue', + + // No DST + 'Pacific/Norfolk' => 'Pacific_Norfolk', + + // No DST + 'Pacific/Noumea' => 'Pacific_New_Caledonia', + + // No DST + 'Pacific/Pago_Pago' => 'Pacific_Samoa', + + // No DST + 'Pacific/Palau' => 'Pacific_Palau', + + // No DST + 'Pacific/Pitcairn' => 'Pacific_Pitcairn', + + // No DST + 'Pacific/Pohnpei' => 'Pacific_Pohnpei', + + // No DST + 'Pacific/Port_Moresby' => 'Pacific_Papua_New_Guinea', + + // No DST + 'Pacific/Rarotonga' => 'Pacific_Cook', + + // No DST + 'Pacific/Tahiti' => 'Pacific_Tahiti', + + // No DST + 'Pacific/Tarawa' => 'Pacific_Gilbert', + + // No DST + 'Pacific/Tongatapu' => 'Pacific_Tonga', + + // No DST + 'Pacific/Wake' => 'Pacific_Wake', + + // No DST + 'Pacific/Wallis' => 'Pacific_Wallis', + ); + + call_integration_hook('integrate_metazones', array(&$tzid_metazones, $when)); + + // Fallbacks in case the server has an old version of the TZDB. + $tzids = array_keys($tzid_metazones); + $tzid_fallbacks = get_tzid_fallbacks($tzids, $when); + foreach ($tzid_fallbacks as $orig_tzid => $alt_tzid) + { + // Skip any that are unchanged. + if ($orig_tzid == $alt_tzid) + continue; + + // Use fallback where possible. + if (!empty($alt_tzid) && empty($tzid_metazones[$alt_tzid])) + { + $tzid_metazones[$alt_tzid] = $tzid_metazones[$orig_tzid]; + $txt[$alt_tzid] = $txt[$orig_tzid]; + } + + // Either way, get rid of the unknown time zone. + unset($tzid_metazones[$orig_tzid]); + } + + return $tzid_metazones; +} + +/** + * Returns an array of all the time zones in a country, ranked according + * to population and/or political significance. + * + * @param string $country_code The two-character ISO-3166 code for a country. + * @param string $when The date/time used to determine fallback values. + * May be a Unix timestamp or any string that strtotime() can understand. + * Defaults to 'now'. + * @return array An array relating time zones to "meta-zones" + */ +function get_sorted_tzids_for_country($country_code, $when = 'now') +{ + static $country_tzids = array(); + + /* + This array lists all the individual time zones in each country, + sorted by population (as reported in statistics available on + Wikipedia in November 2020). Sorting this way enables us to + consistently select the most appropriate individual time zone to + represent all others that share its DST transition rules and values. + For example, this ensures that New York will be preferred over + random small towns in Indiana. + + If future versions of the time zone database add new time zone + identifiers beyond those included here, they should be added to this + list as appropriate. However, SMF will gracefully handle unexpected + new time zones, so nothing will break in the meantime. + */ + $sorted_tzids = array( + // '??' means international. + '??' => array( + 'UTC', + ), + 'AD' => array( + 'Europe/Andorra', + ), + 'AE' => array( + 'Asia/Dubai', + ), + 'AF' => array( + 'Asia/Kabul', + ), + 'AG' => array( + 'America/Antigua', + ), + 'AI' => array( + 'America/Anguilla', + ), + 'AL' => array( + 'Europe/Tirane', + ), + 'AM' => array( + 'Asia/Yerevan', + ), + 'AO' => array( + 'Africa/Luanda', + ), + 'AQ' => array( + // Sorted based on summer population. + 'Antarctica/McMurdo', + 'Antarctica/Casey', + 'Antarctica/Davis', + 'Antarctica/Mawson', + 'Antarctica/Rothera', + 'Antarctica/Syowa', + 'Antarctica/Palmer', + 'Antarctica/Troll', + 'Antarctica/DumontDUrville', + 'Antarctica/Vostok', + ), + 'AR' => array( + 'America/Argentina/Buenos_Aires', + 'America/Argentina/Cordoba', + 'America/Argentina/Tucuman', + 'America/Argentina/Salta', + 'America/Argentina/Jujuy', + 'America/Argentina/La_Rioja', + 'America/Argentina/San_Luis', + 'America/Argentina/Catamarca', + 'America/Argentina/Mendoza', + 'America/Argentina/San_Juan', + 'America/Argentina/Rio_Gallegos', + 'America/Argentina/Ushuaia', + ), + 'AS' => array( + 'Pacific/Pago_Pago', + ), + 'AT' => array( + 'Europe/Vienna', + ), + 'AU' => array( + 'Australia/Sydney', + 'Australia/Melbourne', + 'Australia/Brisbane', + 'Australia/Perth', + 'Australia/Adelaide', + 'Australia/Hobart', + 'Australia/Darwin', + 'Australia/Broken_Hill', + 'Australia/Currie', + 'Australia/Lord_Howe', + 'Australia/Eucla', + 'Australia/Lindeman', + 'Antarctica/Macquarie', + ), + 'AW' => array( + 'America/Aruba', + ), + 'AX' => array( + 'Europe/Mariehamn', + ), + 'AZ' => array( + 'Asia/Baku', + ), + 'BA' => array( + 'Europe/Sarajevo', + ), + 'BB' => array( + 'America/Barbados', + ), + 'BD' => array( + 'Asia/Dhaka', + ), + 'BE' => array( + 'Europe/Brussels', + ), + 'BF' => array( + 'Africa/Ouagadougou', + ), + 'BG' => array( + 'Europe/Sofia', + ), + 'BH' => array( + 'Asia/Bahrain', + ), + 'BI' => array( + 'Africa/Bujumbura', + ), + 'BJ' => array( + 'Africa/Porto-Novo', + ), + 'BL' => array( + 'America/St_Barthelemy', + ), + 'BM' => array( + 'Atlantic/Bermuda', + ), + 'BN' => array( + 'Asia/Brunei', + ), + 'BO' => array( + 'America/La_Paz', + ), + 'BQ' => array( + 'America/Kralendijk', + ), + 'BR' => array( + 'America/Sao_Paulo', + 'America/Bahia', + 'America/Fortaleza', + 'America/Manaus', + 'America/Recife', + 'America/Belem', + 'America/Maceio', + 'America/Campo_Grande', + 'America/Cuiaba', + 'America/Porto_Velho', + 'America/Rio_Branco', + 'America/Boa_Vista', + 'America/Santarem', + 'America/Araguaina', + 'America/Eirunepe', + 'America/Noronha', + ), + 'BS' => array( + 'America/Nassau', + ), + 'BT' => array( + 'Asia/Thimphu', + ), + 'BW' => array( + 'Africa/Gaborone', + ), + 'BY' => array( + 'Europe/Minsk', + ), + 'BZ' => array( + 'America/Belize', + ), + 'CA' => array( + 'America/Toronto', + 'America/Vancouver', + 'America/Edmonton', + 'America/Winnipeg', + 'America/Halifax', + 'America/Regina', + 'America/St_Johns', + 'America/Moncton', + 'America/Thunder_Bay', + 'America/Whitehorse', + 'America/Glace_Bay', + 'America/Yellowknife', + 'America/Swift_Current', + 'America/Dawson_Creek', + 'America/Goose_Bay', + 'America/Iqaluit', + 'America/Creston', + 'America/Fort_Nelson', + 'America/Inuvik', + 'America/Atikokan', + 'America/Rankin_Inlet', + 'America/Nipigon', + 'America/Cambridge_Bay', + 'America/Pangnirtung', + 'America/Dawson', + 'America/Blanc-Sablon', + 'America/Rainy_River', + 'America/Resolute', + ), + 'CC' => array( + 'Indian/Cocos', + ), + 'CD' => array( + 'Africa/Kinshasa', + 'Africa/Lubumbashi', + ), + 'CF' => array( + 'Africa/Bangui', + ), + 'CG' => array( + 'Africa/Brazzaville', + ), + 'CH' => array( + 'Europe/Zurich', + ), + 'CI' => array( + 'Africa/Abidjan', + ), + 'CK' => array( + 'Pacific/Rarotonga', + ), + 'CL' => array( + 'America/Santiago', + 'America/Punta_Arenas', + 'Pacific/Easter', + ), + 'CM' => array( + 'Africa/Douala', + ), + 'CN' => array( + 'Asia/Shanghai', + 'Asia/Urumqi', + ), + 'CO' => array( + 'America/Bogota', + ), + 'CR' => array( + 'America/Costa_Rica', + ), + 'CU' => array( + 'America/Havana', + ), + 'CV' => array( + 'Atlantic/Cape_Verde', + ), + 'CW' => array( + 'America/Curacao', + ), + 'CX' => array( + 'Indian/Christmas', + ), + 'CY' => array( + 'Asia/Nicosia', + 'Asia/Famagusta', + ), + 'CZ' => array( + 'Europe/Prague', + ), + 'DE' => array( + 'Europe/Berlin', + 'Europe/Busingen', + ), + 'DJ' => array( + 'Africa/Djibouti', + ), + 'DK' => array( + 'Europe/Copenhagen', + ), + 'DM' => array( + 'America/Dominica', + ), + 'DO' => array( + 'America/Santo_Domingo', + ), + 'DZ' => array( + 'Africa/Algiers', + ), + 'EC' => array( + 'America/Guayaquil', + 'Pacific/Galapagos', + ), + 'EE' => array( + 'Europe/Tallinn', + ), + 'EG' => array( + 'Africa/Cairo', + ), + 'EH' => array( + 'Africa/El_Aaiun', + ), + 'ER' => array( + 'Africa/Asmara', + ), + 'ES' => array( + 'Europe/Madrid', + 'Atlantic/Canary', + 'Africa/Ceuta', + ), + 'ET' => array( + 'Africa/Addis_Ababa', + ), + 'FI' => array( + 'Europe/Helsinki', + ), + 'FJ' => array( + 'Pacific/Fiji', + ), + 'FK' => array( + 'Atlantic/Stanley', + ), + 'FM' => array( + 'Pacific/Chuuk', + 'Pacific/Kosrae', + 'Pacific/Pohnpei', + ), + 'FO' => array( + 'Atlantic/Faroe', + ), + 'FR' => array( + 'Europe/Paris', + ), + 'GA' => array( + 'Africa/Libreville', + ), + 'GB' => array( + 'Europe/London', + ), + 'GD' => array( + 'America/Grenada', + ), + 'GE' => array( + 'Asia/Tbilisi', + ), + 'GF' => array( + 'America/Cayenne', + ), + 'GG' => array( + 'Europe/Guernsey', + ), + 'GH' => array( + 'Africa/Accra', + ), + 'GI' => array( + 'Europe/Gibraltar', + ), + 'GL' => array( + 'America/Nuuk', + 'America/Thule', + 'America/Scoresbysund', + 'America/Danmarkshavn', + ), + 'GM' => array( + 'Africa/Banjul', + ), + 'GN' => array( + 'Africa/Conakry', + ), + 'GP' => array( + 'America/Guadeloupe', + ), + 'GQ' => array( + 'Africa/Malabo', + ), + 'GR' => array( + 'Europe/Athens', + ), + 'GS' => array( + 'Atlantic/South_Georgia', + ), + 'GT' => array( + 'America/Guatemala', + ), + 'GU' => array( + 'Pacific/Guam', + ), + 'GW' => array( + 'Africa/Bissau', + ), + 'GY' => array( + 'America/Guyana', + ), + 'HK' => array( + 'Asia/Hong_Kong', + ), + 'HN' => array( + 'America/Tegucigalpa', + ), + 'HR' => array( + 'Europe/Zagreb', + ), + 'HT' => array( + 'America/Port-au-Prince', + ), + 'HU' => array( + 'Europe/Budapest', + ), + 'ID' => array( + 'Asia/Jakarta', + 'Asia/Makassar', + 'Asia/Pontianak', + 'Asia/Jayapura', + ), + 'IE' => array( + 'Europe/Dublin', + ), + 'IL' => array( + 'Asia/Jerusalem', + ), + 'IM' => array( + 'Europe/Isle_of_Man', + ), + 'IN' => array( + 'Asia/Kolkata', + ), + 'IO' => array( + 'Indian/Chagos', + ), + 'IQ' => array( + 'Asia/Baghdad', + ), + 'IR' => array( + 'Asia/Tehran', + ), + 'IS' => array( + 'Atlantic/Reykjavik', + ), + 'IT' => array( + 'Europe/Rome', + ), + 'JE' => array( + 'Europe/Jersey', + ), + 'JM' => array( + 'America/Jamaica', + ), + 'JO' => array( + 'Asia/Amman', + ), + 'JP' => array( + 'Asia/Tokyo', + ), + 'KE' => array( + 'Africa/Nairobi', + ), + 'KG' => array( + 'Asia/Bishkek', + ), + 'KH' => array( + 'Asia/Phnom_Penh', + ), + 'KI' => array( + 'Pacific/Tarawa', + 'Pacific/Kiritimati', + 'Pacific/Kanton', + 'Pacific/Enderbury', + ), + 'KM' => array( + 'Indian/Comoro', + ), + 'KN' => array( + 'America/St_Kitts', + ), + 'KP' => array( + 'Asia/Pyongyang', + ), + 'KR' => array( + 'Asia/Seoul', + ), + 'KW' => array( + 'Asia/Kuwait', + ), + 'KY' => array( + 'America/Cayman', + ), + 'KZ' => array( + 'Asia/Almaty', + 'Asia/Aqtobe', + 'Asia/Atyrau', + 'Asia/Qostanay', + 'Asia/Qyzylorda', + 'Asia/Aqtau', + 'Asia/Oral', + ), + 'LA' => array( + 'Asia/Vientiane', + ), + 'LB' => array( + 'Asia/Beirut', + ), + 'LC' => array( + 'America/St_Lucia', + ), + 'LI' => array( + 'Europe/Vaduz', + ), + 'LK' => array( + 'Asia/Colombo', + ), + 'LR' => array( + 'Africa/Monrovia', + ), + 'LS' => array( + 'Africa/Maseru', + ), + 'LT' => array( + 'Europe/Vilnius', + ), + 'LU' => array( + 'Europe/Luxembourg', + ), + 'LV' => array( + 'Europe/Riga', + ), + 'LY' => array( + 'Africa/Tripoli', + ), + 'MA' => array( + 'Africa/Casablanca', + ), + 'MC' => array( + 'Europe/Monaco', + ), + 'MD' => array( + 'Europe/Chisinau', + ), + 'ME' => array( + 'Europe/Podgorica', + ), + 'MF' => array( + 'America/Marigot', + ), + 'MG' => array( + 'Indian/Antananarivo', + ), + 'MH' => array( + 'Pacific/Majuro', + 'Pacific/Kwajalein', + ), + 'MK' => array( + 'Europe/Skopje', + ), + 'ML' => array( + 'Africa/Bamako', + ), + 'MM' => array( + 'Asia/Yangon', + ), + 'MN' => array( + 'Asia/Ulaanbaatar', + 'Asia/Choibalsan', + 'Asia/Hovd', + ), + 'MO' => array( + 'Asia/Macau', + ), + 'MP' => array( + 'Pacific/Saipan', + ), + 'MQ' => array( + 'America/Martinique', + ), + 'MR' => array( + 'Africa/Nouakchott', + ), + 'MS' => array( + 'America/Montserrat', + ), + 'MT' => array( + 'Europe/Malta', + ), + 'MU' => array( + 'Indian/Mauritius', + ), + 'MV' => array( + 'Indian/Maldives', + ), + 'MW' => array( + 'Africa/Blantyre', + ), + 'MX' => array( + 'America/Mexico_City', + 'America/Tijuana', + 'America/Monterrey', + 'America/Chihuahua', + 'America/Merida', + 'America/Hermosillo', + 'America/Cancun', + 'America/Matamoros', + 'America/Mazatlan', + 'America/Bahia_Banderas', + 'America/Ojinaga', + ), + 'MY' => array( + 'Asia/Kuala_Lumpur', + 'Asia/Kuching', + ), + 'MZ' => array( + 'Africa/Maputo', + ), + 'NA' => array( + 'Africa/Windhoek', + ), + 'NC' => array( + 'Pacific/Noumea', + ), + 'NE' => array( + 'Africa/Niamey', + ), + 'NF' => array( + 'Pacific/Norfolk', + ), + 'NG' => array( + 'Africa/Lagos', + ), + 'NI' => array( + 'America/Managua', + ), + 'NL' => array( + 'Europe/Amsterdam', + ), + 'NO' => array( + 'Europe/Oslo', + ), + 'NP' => array( + 'Asia/Kathmandu', + ), + 'NR' => array( + 'Pacific/Nauru', + ), + 'NU' => array( + 'Pacific/Niue', + ), + 'NZ' => array( + 'Pacific/Auckland', + 'Pacific/Chatham', + ), + 'OM' => array( + 'Asia/Muscat', + ), + 'PA' => array( + 'America/Panama', + ), + 'PE' => array( + 'America/Lima', + ), + 'PF' => array( + 'Pacific/Tahiti', + 'Pacific/Marquesas', + 'Pacific/Gambier', + ), + 'PG' => array( + 'Pacific/Port_Moresby', + 'Pacific/Bougainville', + ), + 'PH' => array( + 'Asia/Manila', + ), + 'PK' => array( + 'Asia/Karachi', + ), + 'PL' => array( + 'Europe/Warsaw', + ), + 'PM' => array( + 'America/Miquelon', + ), + 'PN' => array( + 'Pacific/Pitcairn', + ), + 'PR' => array( + 'America/Puerto_Rico', + ), + 'PS' => array( + 'Asia/Gaza', + 'Asia/Hebron', + ), + 'PT' => array( + 'Europe/Lisbon', + 'Atlantic/Madeira', + 'Atlantic/Azores', + ), + 'PW' => array( + 'Pacific/Palau', + ), + 'PY' => array( + 'America/Asuncion', + ), + 'QA' => array( + 'Asia/Qatar', + ), + 'RE' => array( + 'Indian/Reunion', + ), + 'RO' => array( + 'Europe/Bucharest', + ), + 'RS' => array( + 'Europe/Belgrade', + ), + 'RU' => array( + 'Europe/Moscow', + 'Asia/Novosibirsk', + 'Asia/Yekaterinburg', + 'Europe/Samara', + 'Asia/Omsk', + 'Asia/Krasnoyarsk', + 'Europe/Volgograd', + 'Europe/Saratov', + 'Asia/Barnaul', + 'Europe/Ulyanovsk', + 'Asia/Irkutsk', + 'Asia/Vladivostok', + 'Asia/Tomsk', + 'Asia/Novokuznetsk', + 'Europe/Astrakhan', + 'Europe/Kirov', + 'Europe/Kaliningrad', + 'Asia/Chita', + 'Asia/Yakutsk', + 'Asia/Sakhalin', + 'Asia/Kamchatka', + 'Asia/Magadan', + 'Asia/Anadyr', + 'Asia/Khandyga', + 'Asia/Ust-Nera', + 'Asia/Srednekolymsk', + ), + 'RW' => array( + 'Africa/Kigali', + ), + 'SA' => array( + 'Asia/Riyadh', + ), + 'SB' => array( + 'Pacific/Guadalcanal', + ), + 'SC' => array( + 'Indian/Mahe', + ), + 'SD' => array( + 'Africa/Khartoum', + ), + 'SE' => array( + 'Europe/Stockholm', + ), + 'SG' => array( + 'Asia/Singapore', + ), + 'SH' => array( + 'Atlantic/St_Helena', + ), + 'SI' => array( + 'Europe/Ljubljana', + ), + 'SJ' => array( + 'Arctic/Longyearbyen', + ), + 'SK' => array( + 'Europe/Bratislava', + ), + 'SL' => array( + 'Africa/Freetown', + ), + 'SM' => array( + 'Europe/San_Marino', + ), + 'SN' => array( + 'Africa/Dakar', + ), + 'SO' => array( + 'Africa/Mogadishu', + ), + 'SR' => array( + 'America/Paramaribo', + ), + 'SS' => array( + 'Africa/Juba', + ), + 'ST' => array( + 'Africa/Sao_Tome', + ), + 'SV' => array( + 'America/El_Salvador', + ), + 'SX' => array( + 'America/Lower_Princes', + ), + 'SY' => array( + 'Asia/Damascus', + ), + 'SZ' => array( + 'Africa/Mbabane', + ), + 'TC' => array( + 'America/Grand_Turk', + ), + 'TD' => array( + 'Africa/Ndjamena', + ), + 'TF' => array( + 'Indian/Kerguelen', + ), + 'TG' => array( + 'Africa/Lome', + ), + 'TH' => array( + 'Asia/Bangkok', + ), + 'TJ' => array( + 'Asia/Dushanbe', + ), + 'TK' => array( + 'Pacific/Fakaofo', + ), + 'TL' => array( + 'Asia/Dili', + ), + 'TM' => array( + 'Asia/Ashgabat', + ), + 'TN' => array( + 'Africa/Tunis', + ), + 'TO' => array( + 'Pacific/Tongatapu', + ), + 'TR' => array( + 'Europe/Istanbul', + ), + 'TT' => array( + 'America/Port_of_Spain', + ), + 'TV' => array( + 'Pacific/Funafuti', + ), + 'TW' => array( + 'Asia/Taipei', + ), + 'TZ' => array( + 'Africa/Dar_es_Salaam', + ), + 'UA' => array( + 'Europe/Kyiv', + 'Europe/Zaporozhye', + 'Europe/Simferopol', + 'Europe/Uzhgorod', + ), + 'UG' => array( + 'Africa/Kampala', + ), + 'UM' => array( + 'Pacific/Midway', + 'Pacific/Wake', + ), + 'US' => array( + 'America/New_York', + 'America/Los_Angeles', + 'America/Chicago', + 'America/Denver', + 'America/Phoenix', + 'America/Indiana/Indianapolis', + 'America/Detroit', + 'America/Kentucky/Louisville', + 'Pacific/Honolulu', + 'America/Anchorage', + 'America/Boise', + 'America/Juneau', + 'America/Indiana/Vincennes', + 'America/Sitka', + 'America/Menominee', + 'America/Indiana/Tell_City', + 'America/Kentucky/Monticello', + 'America/Nome', + 'America/Indiana/Knox', + 'America/North_Dakota/Beulah', + 'America/Indiana/Winamac', + 'America/Indiana/Petersburg', + 'America/Indiana/Vevay', + 'America/Metlakatla', + 'America/North_Dakota/New_Salem', + 'America/Indiana/Marengo', + 'America/Yakutat', + 'America/North_Dakota/Center', + 'America/Adak', + ), + 'UY' => array( + 'America/Montevideo', + ), + 'UZ' => array( + 'Asia/Tashkent', + 'Asia/Samarkand', + ), + 'VA' => array( + 'Europe/Vatican', + ), + 'VC' => array( + 'America/St_Vincent', + ), + 'VE' => array( + 'America/Caracas', + ), + 'VG' => array( + 'America/Tortola', + ), + 'VI' => array( + 'America/St_Thomas', + ), + 'VN' => array( + 'Asia/Ho_Chi_Minh', + ), + 'VU' => array( + 'Pacific/Efate', + ), + 'WF' => array( + 'Pacific/Wallis', + ), + 'WS' => array( + 'Pacific/Apia', + ), + 'YE' => array( + 'Asia/Aden', + ), + 'YT' => array( + 'Indian/Mayotte', + ), + 'ZA' => array( + 'Africa/Johannesburg', + ), + 'ZM' => array( + 'Africa/Lusaka', + ), + 'ZW' => array( + 'Africa/Harare', + ), + ); + + // Just in case... + $country_code = strtoupper(trim($country_code)); + + // Avoid unnecessary repetition. + if (!isset($country_tzids[$country_code])) + { + call_integration_hook('integrate_country_timezones', array(&$sorted_tzids, $country_code, $when)); + + $country_tzids[$country_code] = isset($sorted_tzids[$country_code]) ? $sorted_tzids[$country_code] : array(); + + // If something goes wrong, we want an empty array, not false. + $recognized_country_tzids = array_filter((array) @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, $country_code)); + + // Make sure that no time zones are missing. + $country_tzids[$country_code] = array_unique(array_merge($country_tzids[$country_code], array_intersect($recognized_country_tzids, timezone_identifiers_list()))); + + // Get fallbacks where necessary. + $country_tzids[$country_code] = array_unique(array_values(get_tzid_fallbacks($country_tzids[$country_code], $when))); + + // Filter out any time zones that are still undefined. + $country_tzids[$country_code] = array_intersect(array_filter($country_tzids[$country_code]), timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)); + } + + return $country_tzids[$country_code]; +} + +/** + * Checks a list of time zone identifiers to make sure they are all defined in + * the installed version of the time zone database, and returns an array of + * key-value substitution pairs. + * + * For defined time zone identifiers, the substitution value will be identical + * to the original value. For undefined ones, the substitute will be a time zone + * identifier that was equivalent to the missing one at the specified time, or + * an empty string if there was no equivalent at that time. + * + * Note: These fallbacks do not need to include every new time zone ever. They + * only need to cover any that are used in $tzid_metazones. + * + * To find the date & time when a new time zone comes into effect, check + * the TZDB changelog at https://data.iana.org/time-zones/tzdb/NEWS + * + * @param array $tzids The time zone identifiers to check. + * @param string $when The date/time used to determine substitute values. + * May be a Unix timestamp or any string that strtotime() can understand. + * Defaults to 'now'. + * @return array Substitute values for any missing time zone identifiers. + */ +function get_tzid_fallbacks($tzids, $when = 'now') +{ + $tzids = (array) $tzids; + + $when = is_numeric($when) ? intval($when) : (is_int(@strtotime($when)) ? strtotime($when) : time()); + + // 'ts' is the timestamp when the substitution first becomes valid. + // 'tzid' is the alternative time zone identifier to use. + $fallbacks = array( + // 1. Simple renames. PHP_INT_MIN because these are valid for all dates. + 'Asia/Kolkata' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => 'Asia/Calcutta', + ), + ), + 'Pacific/Chuuk' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => 'Pacific/Truk', + ), + ), + 'Pacific/Kanton' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => 'Pacific/Enderbury', + ), + ), + 'Pacific/Pohnpei' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => 'Pacific/Ponape', + ), + ), + 'Asia/Yangon' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => 'Asia/Rangoon', + ), + ), + 'America/Nuuk' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => 'America/Godthab', + ), + ), + 'Europe/Busingen' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => 'Europe/Zurich', + ), + ), + 'Europe/Kyiv' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => 'Europe/Kiev', + ), + ), + + // 2. Newly created time zones. + + // The initial entry in many of the following zones is set to '' because + // the records go back to eras before the adoption of standardized time + // zones, which means no substitutes are possible then. + + // The same as Tasmania, except it stayed on DST all year in 2010. + // Australia/Tasmania is an otherwise unused backwards compatibility + // link to Australia/Hobart, so we can borrow it here without conflict. + 'Antarctica/Macquarie' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => 'Australia/Tasmania', + ), + array( + 'ts' => strtotime('2010-04-03T16:00:00+0000'), + 'tzid' => 'Etc/GMT-11', + ), + array( + 'ts' => strtotime('2011-04-07T17:00:00+0000'), + 'tzid' => 'Australia/Tasmania', + ), + ), + + // Added in version 2013a. + 'Asia/Khandyga' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + array( + 'ts' => strtotime('1919-12-14T14:57:47+0000'), + 'tzid' => 'Etc/GMT-8', + ), + array( + 'ts' => strtotime('1930-06-20T16:00:00+0000'), + 'tzid' => 'Asia/Yakutsk', + ), + array( + 'ts' => strtotime('2003-12-31T15:00:00+0000'), + 'tzid' => 'Asia/Vladivostok', + ), + array( + 'ts' => strtotime('2011-09-12T13:00:00+0000'), + 'tzid' => 'Asia/Yakutsk', + ), + ), + + // Added in version 2013a. + 'Asia/Ust-Nera' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + array( + 'ts' => strtotime('1919-12-14T14:27:06+0000'), + 'tzid' => 'Etc/GMT-8', + ), + array( + 'ts' => strtotime('1930-06-20T16:00:00+0000'), + 'tzid' => 'Asia/Yakutsk', + ), + array( + 'ts' => strtotime('1981-03-31T15:00:00+0000'), + 'tzid' => 'Asia/Magadan', + ), + array( + 'ts' => strtotime('2011-09-12T12:00:00+0000'), + 'tzid' => 'Asia/Vladivostok', + ), + ), + + // Created in version 2014b. + // This place uses two hours for DST. No substitutes are possible. + 'Antarctica/Troll' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + ), + + // Diverged from Asia/Yakustsk in version 2014f. + 'Asia/Chita' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + array( + 'ts' => strtotime('1919-12-14T16:26:08+0000'), + 'tzid' => 'Asia/Yakutsk', + ), + array( + 'ts' => strtotime('2014-10-25T16:00:00+0000'), + 'tzid' => 'Etc/GMT-8', + ), + array( + 'ts' => strtotime('2016-03-26T18:00:00+0000'), + 'tzid' => 'Asia/Yakutsk', + ), + ), + + // Diverged from Asia/Magadan in version 2014f. + 'Asia/Srednekolymsk' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + array( + 'ts' => strtotime('1924-05-01T13:45:08+0000'), + 'tzid' => 'Etc/GMT-10', + ), + array( + 'ts' => strtotime('1930-06-20T14:00:00+0000'), + 'tzid' => 'Asia/Magadan', + ), + array( + 'ts' => strtotime('2014-10-25T14:00:00+0000'), + 'tzid' => 'Etc/GMT-11', + ), + ), + + // Diverged from Pacific/Port_Moresby in version 2014i. + 'Pacific/Bougainville' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + // Pacific/Yap is an unused link to Pacific/Port_Moresby. + array( + 'ts' => strtotime('1879-12-31T14:11:20+0000'), + 'tzid' => 'Pacific/Yap', + ), + // Apparently this was different for a while in World War II. + array( + 'ts' => strtotime('1942-06-30T14:00:00+0000'), + 'tzid' => 'Singapore', + ), + array( + 'ts' => strtotime('1945-08-20T15:00:00+0000'), + 'tzid' => 'Pacific/Yap', + ), + // For dates after divergence, it is the same as Pacific/Kosrae. + // If this ever ceases to be true, add another entry. + array( + 'ts' => strtotime('2014-12-27T16:00:00+0000'), + 'tzid' => 'Pacific/Kosrae', + ), + ), + + // Added in version 2015g. + 'America/Fort_Nelson' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + array( + 'ts' => strtotime('1884-01-01T08:12:28+0000'), + 'tzid' => 'Canada/Pacific', + ), + array( + 'ts' => strtotime('1946-01-01T08:00:00+0000'), + 'tzid' => 'Etc/GMT+8', + ), + array( + 'ts' => strtotime('1947-01-01T08:00:00+0000'), + 'tzid' => 'Canada/Pacific', + ), + array( + 'ts' => strtotime('2015-03-08T10:00:00+0000'), + 'tzid' => 'MST', + ), + ), + + // Created in version 2016b. + 'Europe/Astrakhan' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + array( + 'ts' => strtotime('1935-01-26T20:00:00+0000'), + 'tzid' => 'Europe/Samara', + ), + array( + 'ts' => strtotime('1989-03-25T22:00:00+0000'), + 'tzid' => 'Europe/Volgograd', + ), + array( + 'ts' => strtotime('2016-03-26T23:00:00+0000'), + 'tzid' => 'Europe/Samara', + ), + ), + + // Created in version 2016b. + 'Europe/Ulyanovsk' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + array( + 'ts' => strtotime('1935-01-26T20:00:00+0000'), + 'tzid' => 'Europe/Samara', + ), + array( + 'ts' => strtotime('1989-03-25T22:00:00+0000'), + 'tzid' => 'W-SU', + ), + array( + 'ts' => strtotime('2016-03-26T23:00:00+0000'), + 'tzid' => 'Europe/Samara', + ), + ), + + // Created in version 2016b. + 'Asia/Barnaul' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + array( + 'ts' => strtotime('1919-12-09T18:25:00+0000'), + 'tzid' => 'Etc/GMT-6', + ), + array( + 'ts' => strtotime('1930-06-20T18:00:00+0000'), + 'tzid' => 'Asia/Novokuznetsk', + ), + array( + 'ts' => strtotime('1995-05-27T17:00:00+0000'), + 'tzid' => 'Asia/Novosibirsk', + ), + array( + 'ts' => strtotime('2016-03-26T20:00:00+0000'), + 'tzid' => 'Asia/Novokuznetsk', + ), + ), + + // Created in version 2016b. + 'Asia/Tomsk' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + array( + 'ts' => strtotime('1919-12-21T18:20:09+0000'), + 'tzid' => 'Asia/Novosibirsk', + ), + array( + 'ts' => strtotime('1930-06-20T18:00:00+0000'), + 'tzid' => 'Asia/Novokuznetsk', + ), + array( + 'ts' => strtotime('2002-04-30T20:00:00+0000'), + 'tzid' => 'Asia/Novosibirsk', + ), + array( + 'ts' => strtotime('2016-05-28T20:00:00+0000'), + 'tzid' => 'Asia/Novokuznetsk', + ), + ), + + // Created in version 2016d. + 'Europe/Kirov' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + array( + 'ts' => strtotime('1935-01-26T20:00:00+0000'), + 'tzid' => 'Europe/Samara', + ), + array( + 'ts' => strtotime('1989-03-25T22:00:00+0000'), + 'tzid' => 'Europe/Volgograd', + ), + array( + 'ts' => strtotime('1992-03-28T22:00:00+0000'), + 'tzid' => 'W-SU', + ), + ), + + // Diverged from Asia/Nicosia in version 2016i. + 'Asia/Famagusta' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + // Europe/Nicosia is an otherwise unused link to Asia/Nicosia. + array( + 'ts' => strtotime('1921-11-13T21:46:32+0000'), + 'tzid' => 'Europe/Nicosia', + ), + // Became same as Europe/Istanbul. + // Turkey is an otherwise unused link to Europe/Istanbul. + array( + 'ts' => strtotime('2016-09-07T21:00:00+0000'), + 'tzid' => 'Turkey', + ), + // Became same as Asia/Nicosia again. + array( + 'ts' => strtotime('2017-10-29T01:00:00+0000'), + 'tzid' => 'Europe/Nicosia', + ), + ), + + // Created in version 2016j. + 'Asia/Atyrau' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + array( + 'ts' => strtotime('1924-05-01T20:32:16+0000'), + 'tzid' => 'Etc/GMT-3', + ), + array( + 'ts' => strtotime('1930-06-20T21:00:00+0000'), + 'tzid' => 'Asia/Aqtau', + ), + array( + 'ts' => strtotime('1981-09-30T19:00:00+0000'), + 'tzid' => 'Asia/Aqtobe', + ), + array( + 'tz' => strtotime('1999-03-27T21:00:00+0000'), + 'tzid' => 'Etc/GMT-5' + ), + ), + + // Diverged from Europe/Volgograd in version 2016j. + 'Europe/Saratov' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + array( + 'ts' => strtotime('1935-01-26T20:00:00+0000'), + 'tzid' => 'Europe/Samara', + ), + array( + 'ts' => strtotime('1988-03-26T22:00:00+0000'), + 'tzid' => 'Europe/Volgograd', + ), + array( + 'ts' => strtotime('2016-12-03T23:00:00+0000'), + 'tzid' => 'Europe/Samara', + ), + ), + + // Diverged from America/Santiago in version 2017a. + 'America/Punta_Arenas' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + // Chile/Continental is an otherwise unused link to America/Santiago. + array( + 'ts' => strtotime('1890-01-01T04:43:40+0000'), + 'tzid' => 'Chile/Continental', + ), + array( + 'ts' => strtotime('1942-08-01T05:00:00+0000'), + 'tzid' => 'Etc/GMT+4', + ), + array( + 'ts' => strtotime('1946-08-29T04:00:00+0000'), + 'tzid' => 'Chile/Continental', + ), + // America/Mendoza is an otherwise unused link to America/Argentina/Mendoza. + array( + 'ts' => strtotime('2016-12-04T03:00:00+0000'), + 'tzid' => 'America/Mendoza', + ), + ), + + // Diverged from Asia/Qyzylorda in version 2018h. + 'Asia/Qostanay' => array( + array( + 'ts' => PHP_INT_MIN, + 'tzid' => '', + ), + array( + 'ts' => strtotime('1924-05-01T19:45:32+0000'), + 'tzid' => 'Asia/Qyzylorda', + ), + array( + 'ts' => strtotime('1930-06-20T20:00:00+0000'), + 'tzid' => 'Asia/Aqtobe', + ), + array( + 'ts' => strtotime('2004-10-30T21:00:00+0000'), + 'tzid' => 'Asia/Almaty', + ), + ), + ); + + $missing = array_diff($tzids, timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)); + + call_integration_hook('integrate_timezone_fallbacks', array(&$fallbacks, &$missing, $tzids, $when)); + + $replacements = array(); + + foreach ($tzids as $tzid) + { + // Not missing. + if (!in_array($tzid, $missing)) + $replacements[$tzid] = $tzid; + + // Missing and we have no fallback. + elseif (empty($fallbacks[$tzid])) + $replacements[$tzid] = ''; + + // Missing, but we have a fallback. + else + { + usort( + $fallbacks[$tzid], + function ($a, $b) + { + return $a['ts'] > $b['ts']; + } + ); + + foreach ($fallbacks[$tzid] as $alt) + { + if ($when < $alt['ts']) + break; + + $replacements[$tzid] = $alt['tzid']; + } + + // Replacement is already in use. + if (in_array($alt['tzid'], $replacements) || (in_array($alt['tzid'], $tzids) && strpos($alt['tzid'], 'Etc/') === false)) + $replacements[$tzid] = ''; + + if (empty($replacements[$tzid])) + $replacements[$tzid] = ''; + } + } + + return $replacements; +} + +/** + * Validates a set of two-character ISO 3166-1 country codes. + * + * @param array|string $country_codes Array or CSV string of country codes. + * @param bool $as_csv If true, return CSV string instead of array. + * @return array|string Array or CSV string of valid country codes. + */ +function validate_iso_country_codes($country_codes, $as_csv = false) +{ + if (is_string($country_codes)) + $country_codes = explode(',', $country_codes); + else + $country_codes = array_map('strval', (array) $country_codes); + + foreach ($country_codes as $key => $country_code) + { + $country_code = strtoupper(trim($country_code)); + $country_tzids = strlen($country_code) !== 2 ? null : @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, $country_code); + $country_codes[$key] = empty($country_tzids) ? null : $country_code; + } + + $country_codes = array_filter($country_codes); + + if (!empty($as_csv)) + $country_codes = implode(',', $country_codes); + + return $country_codes; +} + +?> \ No newline at end of file diff --git a/Sources/Subs.php b/Sources/Subs.php new file mode 100644 index 0000000..d71eee5 --- /dev/null +++ b/Sources/Subs.php @@ -0,0 +1,8437 @@ + 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
' elsewise. + + disabled_before: used in place of before when disabled. Defaults + to '
' if block_level, '' if not. + + disabled_after: used in place of after when disabled. Defaults + to '
' if block_level, '' if not. + + block_level: set to true the tag is a "block level" tag, similar + to HTML. Block level tags cannot be nested inside tags that are + not block level, and will not be implicitly closed as easily. + One break following a block level tag may also be removed. + + trim: if set, and 'inside' whitespace after the begin tag will be + removed. If set to 'outside', whitespace after the end tag will + meet the same fate. + + validate: except when type is missing or 'closed', a callback to + validate the data as $data. Depending on the tag's type, $data + may be a string or an array of strings (corresponding to the + replacement.) + + quoted: when type is 'unparsed_equals' or 'parsed_equals' only, + may be not set, 'optional', or 'required' corresponding to if + the content may be quoted. This allows the parser to read + [tag="abc]def[esdf]"] properly. + + require_parents: an array of tag names, or not set. If set, the + enclosing tag *must* be one of the listed tags, or parsing won't + occur. + + require_children: similar to require_parents, if set children + won't be parsed if they are not in the list. + + disallow_children: similar to, but very different from, + require_children, if it is set the listed tags will not be + parsed inside the tag. + + parsed_tags_allowed: an array restricting what BBC can be in the + parsed_equals parameter, if desired. + */ + + $codes = array( + array( + 'tag' => 'abbr', + 'type' => 'unparsed_equals', + 'before' => '', + 'after' => '', + 'quoted' => 'optional', + 'disabled_after' => ' ($1)', + ), + // Legacy (and just an alias for [abbr] even when enabled) + array( + 'tag' => 'acronym', + 'type' => 'unparsed_equals', + 'before' => '', + 'after' => '', + 'quoted' => 'optional', + 'disabled_after' => ' ($1)', + ), + array( + 'tag' => 'anchor', + 'type' => 'unparsed_equals', + 'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]', + 'before' => '', + 'after' => '', + ), + array( + 'tag' => 'attach', + 'type' => 'unparsed_content', + 'parameters' => array( + 'id' => array('match' => '(\d+)'), + 'alt' => array('optional' => true), + 'width' => array('optional' => true, 'match' => '(\d+)'), + 'height' => array('optional' => true, 'match' => '(\d+)'), + 'display' => array('optional' => true, 'match' => '(link|embed)'), + ), + 'content' => '$1', + 'validate' => function(&$tag, &$data, $disabled, $params) use ($modSettings, $context, $sourcedir, $txt, $smcFunc) + { + $returnContext = ''; + + // BBC or the entire attachments feature is disabled + if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach'])) + return $data; + + // Save the attach ID. + $attachID = $params['{id}']; + + // Kinda need this. + require_once($sourcedir . '/Subs-Attachments.php'); + + $currentAttachment = parseAttachBBC($attachID); + + // parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do. + if (is_string($currentAttachment)) + return $data = '' . (!empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment) . ''; + + // We need a display mode. + if (empty($params['{display}'])) + { + // Images, video, and audio are embedded by default. + if (!empty($currentAttachment['is_image']) || strpos($currentAttachment['mime_type'], 'video/') === 0 || strpos($currentAttachment['mime_type'], 'audio/') === 0) + $params['{display}'] = 'embed'; + // Anything else shows a link by default. + else + $params['{display}'] = 'link'; + } + + // Embedded file. + if ($params['{display}'] == 'embed') + { + $alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"'; + $title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : ''; + + // Image. + if (!empty($currentAttachment['is_image'])) + { + if (empty($params['{width}']) && empty($params['{height}'])) + $returnContext .= ''; + else + { + $width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"': ''; + $height = !empty($params['{height}']) ? 'height="' . $params['{height}'] . '"' : ''; + $returnContext .= ''; + } + } + // Video. + elseif (strpos($currentAttachment['mime_type'], 'video/') === 0) + { + $width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : ''; + $height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : ''; + + $returnContext .= '' . (!empty($data) && $data != $currentAttachment['name'] ? '
' . $data . '
' : ''); + } + // Audio. + elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0) + { + $width = 'max-width:100%; width: ' . (!empty($params['{width}']) ? $params['{width}'] : '400') . 'px;'; + $height = !empty($params['{height}']) ? 'height: ' . $params['{height}'] . 'px;' : ''; + + $returnContext .= (!empty($data) && $data != $currentAttachment['name'] ? $data . ' ' : '') . ''; + } + // Anything else. + else + { + $width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : ''; + $height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : ''; + + $returnContext .= '' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . ''; + } + } + + // No image. Show a link. + else + $returnContext .= '' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . ''; + + // Use this hook to adjust the HTML output of the attach BBCode. + // If you want to work with the attachment data itself, use one of these: + // - integrate_pre_parseAttachBBC + // - integrate_post_parseAttachBBC + call_integration_hook('integrate_attach_bbc_validate', array(&$returnContext, $currentAttachment, $tag, $data, $disabled, $params)); + + // Gotta append what we just did. + $data = $returnContext; + }, + ), + array( + 'tag' => 'b', + 'before' => '', + 'after' => '', + ), + // Legacy (equivalent to [ltr] or [rtl]) + array( + 'tag' => 'bdo', + 'type' => 'unparsed_equals', + 'before' => '', + 'after' => '', + 'test' => '(rtl|ltr)\]', + 'block_level' => true, + ), + // Legacy (alias of [color=black]) + array( + 'tag' => 'black', + 'before' => '', + 'after' => '', + ), + // Legacy (alias of [color=blue]) + array( + 'tag' => 'blue', + 'before' => '', + 'after' => '', + ), + array( + 'tag' => 'br', + 'type' => 'closed', + 'content' => '
', + ), + array( + 'tag' => 'center', + 'before' => '
', + 'after' => '
', + 'block_level' => true, + ), + array( + 'tag' => 'code', + 'type' => 'unparsed_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, -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('
' => '')); + }, + ), + array( + 'tag' => 'email', + 'type' => 'unparsed_equals', + 'before' => '', + 'after' => '', + // @todo Should this respect guest_hideContacts? + 'disallow_children' => array('email', 'ftp', 'url', 'iurl'), + 'disabled_after' => ' ($1)', + ), + // Legacy (and just a link even when not disabled) + array( + 'tag' => 'flash', + 'type' => 'unparsed_commas_content', + 'test' => '\d+,\d+\]', + 'content' => '$1', + 'validate' => function (&$tag, &$data, $disabled) + { + $data[0] = normalize_iri(strtr(trim($data[0]), array('
' => '', ' ' => '%20'))); + + $scheme = parse_iri($data[0], PHP_URL_SCHEME); + if (empty($scheme)) + $data[0] = '//' . ltrim($data[0], ':/'); + + $ascii_url = iri_to_url($data[0]); + if ($ascii_url !== $data[0]) + $tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']); + }, + ), + array( + 'tag' => 'float', + 'type' => 'unparsed_equals', + 'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]', + 'before' => '
', + 'after' => '
', + 'validate' => function(&$tag, &$data, $disabled) + { + $class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"'; + + if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches)) + $css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"'; + else + $css = ''; + + $data = $class . $css; + }, + 'trim' => 'outside', + 'block_level' => true, + ), + // Legacy (alias of [url] with an FTP URL) + array( + 'tag' => 'ftp', + 'type' => 'unparsed_content', + 'content' => '$1', + 'validate' => function(&$tag, &$data, $disabled) + { + $data = normalize_iri(strtr(trim($data), array('
' => '', ' ' => '%20'))); + + $scheme = parse_iri($data, PHP_URL_SCHEME); + if (empty($scheme)) + $data = 'ftp://' . ltrim($data, ':/'); + + $ascii_url = iri_to_url($data); + if ($ascii_url !== $data) + $tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']); + }, + ), + // Legacy (alias of [url] with an FTP URL) + array( + 'tag' => 'ftp', + 'type' => 'unparsed_equals', + 'before' => '', + 'after' => '', + 'validate' => function(&$tag, &$data, $disabled) + { + $data = iri_to_url(strtr(trim($data), array('
' => '', ' ' => '%20'))); + + $scheme = parse_iri($data, PHP_URL_SCHEME); + if (empty($scheme)) + $data = 'ftp://' . ltrim($data, ':/'); + }, + 'disallow_children' => array('email', 'ftp', 'url', 'iurl'), + 'disabled_after' => ' ($1)', + ), + array( + 'tag' => 'font', + 'type' => 'unparsed_equals', + 'test' => '[A-Za-z0-9_,\-\s]+?\]', + 'before' => '', + 'after' => '', + ), + // Legacy (one of those things that should not be done) + array( + 'tag' => 'glow', + 'type' => 'unparsed_commas', + 'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]', + 'before' => '', + 'after' => '', + ), + // Legacy (alias of [color=green]) + array( + 'tag' => 'green', + 'before' => '', + 'after' => '', + ), + array( + 'tag' => 'html', + 'type' => 'unparsed_content', + 'content' => '
$1
', + 'block_level' => true, + 'disabled_content' => '$1', + ), + array( + 'tag' => 'hr', + 'type' => 'closed', + 'content' => '
', + 'block_level' => true, + ), + array( + 'tag' => 'i', + 'before' => '', + 'after' => '', + ), + array( + 'tag' => 'img', + 'type' => 'unparsed_content', + 'parameters' => array( + 'alt' => array('optional' => true), + 'title' => array('optional' => true), + 'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'), + 'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'), + ), + 'content' => '$1', + 'validate' => function(&$tag, &$data, $disabled, $params) + { + $url = iri_to_url(strtr(trim($data), array('
' => '', ' ' => '%20'))); + + if (parse_iri($url, PHP_URL_SCHEME) === null) + $url = '//' . ltrim($url, ':/'); + else + $url = get_proxied_url($url); + + $alt = !empty($params['{alt}']) ? ' alt="' . $params['{alt}']. '"' : ' alt=""'; + $title = !empty($params['{title}']) ? ' title="' . $params['{title}']. '"' : ''; + + $data = isset($disabled[$tag['tag']]) ? $url : ''; + }, + 'disabled_content' => '($1)', + ), + array( + 'tag' => 'iurl', + 'type' => 'unparsed_content', + 'content' => '$1', + 'validate' => function(&$tag, &$data, $disabled) + { + $data = normalize_iri(strtr(trim($data), array('
' => '', ' ' => '%20'))); + + $scheme = parse_iri($data, PHP_URL_SCHEME); + if (empty($scheme)) + $data = '//' . ltrim($data, ':/'); + + $ascii_url = iri_to_url($data); + if ($ascii_url !== $data) + $tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']); + }, + ), + array( + 'tag' => 'iurl', + 'type' => 'unparsed_equals', + 'quoted' => 'optional', + 'before' => '', + 'after' => '', + 'validate' => function(&$tag, &$data, $disabled) + { + if (substr($data, 0, 1) == '#') + $data = '#post_' . substr($data, 1); + else + { + $data = iri_to_url(strtr(trim($data), array('
' => '', ' ' => '%20'))); + + $scheme = parse_iri($data, PHP_URL_SCHEME); + if (empty($scheme)) + $data = '//' . ltrim($data, ':/'); + } + }, + 'disallow_children' => array('email', 'ftp', 'url', 'iurl'), + 'disabled_after' => ' ($1)', + ), + array( + 'tag' => 'justify', + 'before' => '
', + 'after' => '
', + 'block_level' => true, + ), + array( + 'tag' => 'left', + 'before' => '
', + 'after' => '
', + 'block_level' => true, + ), + array( + 'tag' => 'li', + 'before' => '
  • ', + 'after' => '
  • ', + 'trim' => 'outside', + 'require_parents' => array('list'), + 'block_level' => true, + 'disabled_before' => '', + 'disabled_after' => '
    ', + ), + array( + 'tag' => 'list', + 'before' => '
      ', + 'after' => '
    ', + 'trim' => 'inside', + 'require_children' => array('li', 'list'), + 'block_level' => true, + ), + array( + 'tag' => 'list', + 'parameters' => array( + 'type' => array('match' => '(none|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|upper-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha)'), + ), + 'before' => '
      ', + 'after' => '
    ', + 'trim' => 'inside', + 'require_children' => array('li'), + 'block_level' => true, + ), + array( + 'tag' => 'ltr', + 'before' => '', + 'after' => '', + 'block_level' => true, + ), + array( + 'tag' => 'me', + 'type' => 'unparsed_equals', + 'before' => '
    * $1 ', + 'after' => '
    ', + 'quoted' => 'optional', + 'block_level' => true, + 'disabled_before' => '/me ', + 'disabled_after' => '
    ', + ), + array( + 'tag' => 'member', + 'type' => 'unparsed_equals', + 'before' => '@', + 'after' => '', + ), + // Legacy (horrible memories of the 1990s) + array( + 'tag' => 'move', + 'before' => '', + 'after' => '', + 'block_level' => true, + 'disallow_children' => array('move'), + ), + array( + 'tag' => 'nobbc', + 'type' => 'unparsed_content', + 'content' => '$1', + ), + array( + 'tag' => 'php', + 'type' => 'unparsed_content', + 'content' => '$1', + 'validate' => isset($disabled['php']) ? null : function(&$tag, &$data, $disabled) + { + if (!isset($disabled['php'])) + { + $add_begin = substr(trim($data), 0, 5) != '<?'; + $data = highlight_php_code($add_begin ? '<?php ' . $data . '?>' : $data); + if ($add_begin) + $data = preg_replace(array('~^(.+?)<\?.{0,40}?php(?: |\s)~', '~\?>((?:)*)$~'), '$1', $data, 2); + } + }, + 'block_level' => false, + 'disabled_content' => '$1', + ), + array( + 'tag' => 'pre', + 'before' => '
    ',
    +				'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' => '
    ', + 'after' => '
    ', + 'block_level' => true, + ), + array( + 'tag' => 'rtl', + 'before' => '', + 'after' => '', + 'block_level' => true, + ), + array( + 'tag' => 's', + 'before' => '', + 'after' => '', + ), + // Legacy (never a good idea) + array( + 'tag' => 'shadow', + 'type' => 'unparsed_commas', + 'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]', + 'before' => '', + 'after' => '', + 'validate' => function(&$tag, &$data, $disabled) + { + + if ($data[1] == 'top' || (is_numeric($data[1]) && $data[1] < 50)) + $data[1] = '0 -2px 1px'; + + elseif ($data[1] == 'right' || (is_numeric($data[1]) && $data[1] < 100)) + $data[1] = '2px 0 1px'; + + elseif ($data[1] == 'bottom' || (is_numeric($data[1]) && $data[1] < 190)) + $data[1] = '0 2px 1px'; + + elseif ($data[1] == 'left' || (is_numeric($data[1]) && $data[1] < 280)) + $data[1] = '-2px 0 1px'; + + else + $data[1] = '1px 1px 1px'; + }, + ), + array( + 'tag' => 'size', + 'type' => 'unparsed_equals', + 'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]', + 'before' => '', + 'after' => '', + ), + array( + 'tag' => 'size', + 'type' => 'unparsed_equals', + 'test' => '[1-7]\]', + 'before' => '', + 'after' => '', + 'validate' => function(&$tag, &$data, $disabled) + { + $sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95); + $data = $sizes[$data] . 'em'; + }, + ), + array( + 'tag' => 'sub', + 'before' => '', + 'after' => '', + ), + array( + 'tag' => 'sup', + 'before' => '', + 'after' => '', + ), + array( + 'tag' => 'table', + 'before' => '', + 'after' => '
    ', + 'trim' => 'inside', + 'require_children' => array('tr'), + 'block_level' => true, + ), + array( + 'tag' => 'td', + 'before' => '', + 'after' => '', + 'require_parents' => array('tr'), + 'trim' => 'outside', + 'block_level' => true, + 'disabled_before' => '', + 'disabled_after' => '', + ), + array( + 'tag' => 'time', + 'type' => 'unparsed_content', + 'content' => '$1', + 'validate' => function(&$tag, &$data, $disabled) + { + if (is_numeric($data)) + $data = timeformat($data); + + $tag['content'] = '$1'; + }, + ), + array( + 'tag' => 'tr', + 'before' => '', + 'after' => '', + 'require_parents' => array('table'), + 'require_children' => array('td'), + 'trim' => 'both', + 'block_level' => true, + 'disabled_before' => '', + 'disabled_after' => '', + ), + // Legacy (the element is dead) + array( + 'tag' => 'tt', + 'before' => '', + 'after' => '', + ), + array( + 'tag' => 'u', + 'before' => '', + 'after' => '', + ), + array( + 'tag' => 'url', + 'type' => 'unparsed_content', + 'content' => '$1', + 'validate' => function(&$tag, &$data, $disabled) + { + $data = normalize_iri(strtr(trim($data), array('
    ' => '', ' ' => '%20'))); + + $scheme = parse_iri($data, PHP_URL_SCHEME); + if (empty($scheme)) + $data = '//' . ltrim($data, ':/'); + + $ascii_url = iri_to_url($data); + if ($ascii_url !== $data) + $tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']); + }, + ), + array( + 'tag' => 'url', + 'type' => 'unparsed_equals', + 'quoted' => 'optional', + 'before' => '', + 'after' => '', + 'validate' => function(&$tag, &$data, $disabled) + { + $data = iri_to_url(strtr(trim($data), array('
    ' => '', ' ' => '%20'))); + + $scheme = parse_iri($data, PHP_URL_SCHEME); + if (empty($scheme)) + $data = '//' . ltrim($data, ':/'); + }, + 'disallow_children' => array('email', 'ftp', 'url', 'iurl'), + 'disabled_after' => ' ($1)', + ), + // Legacy (alias of [color=white]) + array( + 'tag' => 'white', + 'before' => '', + 'after' => '', + ), + array( + 'tag' => 'youtube', + 'type' => 'unparsed_content', + 'content' => '
    ', + 'disabled_content' => 'https://www.youtube.com/watch?v=$1', + 'block_level' => true, + ), + ); + + // Inside these tags autolink is not recommendable. + $no_autolink_tags = array( + 'url', + 'iurl', + 'email', + 'img', + 'html', + 'attach', + 'ftp', + 'flash', + 'member', + 'code', + 'php', + 'nobbc', + ); + + // Let mods add new BBC without hassle. + call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags)); + + // This is mainly for the bbc manager, so it's easy to add tags above. Custom BBC should be added above this line. + if ($message === false) + { + usort( + $codes, + function($a, $b) + { + return strcmp($a['tag'], $b['tag']); + } + ); + return $codes; + } + + // So the parser won't skip them. + $itemcodes = array( + '*' => 'disc', + '@' => 'disc', + '+' => 'square', + 'x' => 'square', + '#' => 'square', + 'o' => 'circle', + 'O' => 'circle', + '0' => 'circle', + ); + if (!isset($disabled['li']) && !isset($disabled['list'])) + { + foreach ($itemcodes as $c => $dummy) + $bbc_codes[$c] = array(); + } + + // Shhhh! + if (!isset($disabled['color'])) + { + $codes[] = array( + 'tag' => 'chrissy', + 'before' => '', + 'after' => ' :-*', + ); + $codes[] = array( + 'tag' => 'kissy', + 'before' => '', + 'after' => ' :-*', + ); + } + $codes[] = array( + 'tag' => 'cowsay', + 'parameters' => array( + 'e' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => 'oo', 'validate' => function ($eyes) use ($smcFunc) + { + return $smcFunc['substr']($eyes . 'oo', 0, 2); + }, + ), + 't' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => ' ', 'validate' => function ($tongue) use ($smcFunc) + { + return $smcFunc['substr']($tongue . ' ', 0, 2); + }, + ), + ), + 'before' => '
    ', + 'after' => '
    ', + '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" => '
    ')); + + if (!empty($parse_tags)) + { + $real_alltags_regex = $alltags_regex; + $alltags_regex = ''; + } + if (empty($alltags_regex)) + { + $alltags = array(); + foreach ($bbc_codes as $section) + { + foreach ($section as $code) + $alltags[] = $code['tag']; + } + $alltags_regex = '(?' . '>\b' . build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')'; + } + + $pos = -1; + while ($pos !== false) + { + $last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos; + preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1); + $pos = isset($matches[0][1]) ? $matches[0][1] : false; + + // Failsafe. + if ($pos === false || $last_pos > $pos) + $pos = strlen($message) + 1; + + // Can't have a one letter smiley, URL, or email! (Sorry.) + if ($last_pos < $pos - 1) + { + // Make sure the $last_pos is not negative. + $last_pos = max($last_pos, 0); + + // Pick a block of data to do some raw fixing on. + $data = substr($message, $last_pos, $pos - $last_pos); + + $placeholders = array(); + $placeholders_counter = 0; + // Wrap in "private use" Unicode characters to ensure there will be no conflicts. + $placeholder_template = html_entity_decode('') . '%1$s' . html_entity_decode(''); + + // Take care of some HTML! + if (!empty($modSettings['enablePostHTML']) && strpos($data, '<') !== false) + { + $data = preg_replace('~<a\s+href=((?:")?)((?:https?://|ftps?://|mailto:|tel:)\S+?)\\1>(.*?)</a>~i', '[url="$2"]$3[/url]', $data); + + //
    should be empty. + $empty_tags = array('br', 'hr'); + foreach ($empty_tags as $tag) + $data = str_replace(array('<' . $tag . '>', '<' . $tag . '/>', '<' . $tag . ' />'), '<' . $tag . '>', $data); + + // b, u, i, s, pre... basic tags. + $closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong'); + foreach ($closable_tags as $tag) + { + $diff = substr_count($data, '<' . $tag . '>') - substr_count($data, '</' . $tag . '>'); + $data = strtr($data, array('<' . $tag . '>' => '<' . $tag . '>', '</' . $tag . '>' => '')); + + if ($diff > 0) + $data = substr($data, 0, -1) . str_repeat('', $diff) . substr($data, -1); + } + + // Do - with security... action= -> action-. + preg_match_all('~<img\s+src=((?:")?)((?:https?://|ftps?://)\S+?)\\1(?:\s+alt=(".*?"|\S*?))?(?:\s?/)?>~i', $data, $matches, PREG_PATTERN_ORDER); + if (!empty($matches[0])) + { + $replaces = array(); + foreach ($matches[2] as $match => $imgtag) + { + $alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^"|"$~', '', $matches[3][$match]); + + // Remove action= from the URL - no funny business, now. + // @todo Testing this preg_match seems pointless + if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0) + $imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag); + + $placeholder = sprintf($placeholder_template, ++$placeholders_counter); + $placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]'; + + $replaces[$matches[0][$match]] = $placeholder; + } + + $data = strtr($data, $replaces); + } + } + + if (!empty($modSettings['autoLinkUrls'])) + { + // Are we inside tags that should be auto linked? + $no_autolink_area = false; + if (!empty($open_tags)) + { + foreach ($open_tags as $open_tag) + if (in_array($open_tag['tag'], $no_autolink_tags)) + $no_autolink_area = true; + } + + // Don't go backwards. + // @todo Don't think is the real solution.... + $lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0; + if ($pos < $lastAutoPos) + $no_autolink_area = true; + $lastAutoPos = $pos; + + if (!$no_autolink_area) + { + // An   right after a URL can break the autolinker + if (strpos($data, ' ') !== false) + { + $placeholders[html_entity_decode(' ', 0, $context['character_set'])] = ' '; + $data = strtr($data, array(' ' => html_entity_decode(' ', 0, $context['character_set']))); + } + + // Some reusable character classes + $excluded_trailing_chars = '!;:.,?'; + $domain_label_chars = '0-9A-Za-z\-' . ($context['utf8'] ? implode('', array( + '\x{A0}-\x{D7FF}', '\x{F900}-\x{FDCF}', '\x{FDF0}-\x{FFEF}', + '\x{10000}-\x{1FFFD}', '\x{20000}-\x{2FFFD}', '\x{30000}-\x{3FFFD}', + '\x{40000}-\x{4FFFD}', '\x{50000}-\x{5FFFD}', '\x{60000}-\x{6FFFD}', + '\x{70000}-\x{7FFFD}', '\x{80000}-\x{8FFFD}', '\x{90000}-\x{9FFFD}', + '\x{A0000}-\x{AFFFD}', '\x{B0000}-\x{BFFFD}', '\x{C0000}-\x{CFFFD}', + '\x{D0000}-\x{DFFFD}', '\x{E1000}-\x{EFFFD}', + )) : ''); + + // Parse any URLs + if (!isset($disabled['url']) && strpos($data, '[url') === false) + { + // URI schemes that require some sort of special handling. + $schemes = array( + // Schemes whose URI definitions require a domain name in the + // authority (or whatever the next part of the URI is). + 'need_domain' => array( + 'aaa', 'aaas', 'acap', 'acct', 'afp', 'cap', 'cid', 'coap', + 'coap+tcp', 'coap+ws', 'coaps', 'coaps+tcp', 'coaps+ws', 'crid', + 'cvs', 'dict', 'dns', 'feed', 'fish', 'ftp', 'git', 'go', + 'gopher', 'h323', 'http', 'https', 'iax', 'icap', 'im', 'imap', + 'ipp', 'ipps', 'irc', 'irc6', 'ircs', 'ldap', 'ldaps', 'mailto', + 'mid', 'mupdate', 'nfs', 'nntp', 'pop', 'pres', 'reload', + 'rsync', 'rtsp', 'sftp', 'sieve', 'sip', 'sips', 'smb', 'snmp', + 'soap.beep', 'soap.beeps', 'ssh', 'svn', 'stun', 'stuns', + 'telnet', 'tftp', 'tip', 'tn3270', 'turn', 'turns', 'tv', 'udp', + 'vemmi', 'vnc', 'webcal', 'ws', 'wss', 'xmlrpc.beep', + 'xmlrpc.beeps', 'xmpp', 'z39.50', 'z39.50r', 'z39.50s', + ), + // Schemes that allow an empty authority ("://" followed by "/") + 'empty_authority' => array( + 'file', 'ni', 'nih', + ), + // Schemes that do not use an authority but still have a reasonable + // chance of working as clickable links. + 'no_authority' => array( + 'about', 'callto', 'geo', 'gg', 'leaptofrogans', 'magnet', + 'mailto', 'maps', 'news', 'ni', 'nih', 'service', 'skype', + 'sms', 'tel', 'tv', + ), + // Schemes that we should never link. + 'forbidden' => array( + 'javascript', 'data', + ), + ); + + // In case a mod wants to control behaviour for a special URI scheme. + call_integration_hook('integrate_autolinker_schemes', array(&$schemes)); + + // Don't repeat this unnecessarily. + if (empty($url_regex)) + { + // PCRE subroutines for efficiency. + $pcre_subroutines = array( + 'tlds' => $modSettings['tld_regex'], + 'pct' => '%[0-9A-Fa-f]{2}', + 'domain_label_char' => '[' . $domain_label_chars . ']', + 'not_domain_label_char' => '[^' . $domain_label_chars . ']', + 'domain' => '(?:(?P>domain_label_char)+\.)+(?P>tlds)(?!\.(?P>domain_label_char))', + 'no_domain' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:@]|(?P>pct))+', + 'scheme_need_domain' => build_regex($schemes['need_domain'], '~'), + 'scheme_empty_authority' => build_regex($schemes['empty_authority'], '~'), + 'scheme_no_authority' => build_regex($schemes['no_authority'], '~'), + 'scheme_any' => '[A-Za-z][0-9A-Za-z+\-.]*', + 'user_info' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:]|(?P>pct))+', + 'dec_octet' => '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)', + 'h16' => '[0-9A-Fa-f]{1,4}', + 'ipv4' => '(?:\b(?:(?P>dec_octet)\.){3}(?P>dec_octet)\b)', + 'ipv6' => '\[(?:' . implode('|', array( + '(?:(?P>h16):){7}(?P>h16)', + '(?:(?P>h16):){1,7}:', + '(?:(?P>h16):){1,6}(?::(?P>h16))', + '(?:(?P>h16):){1,5}(?::(?P>h16)){1,2}', + '(?:(?P>h16):){1,4}(?::(?P>h16)){1,3}', + '(?:(?P>h16):){1,3}(?::(?P>h16)){1,4}', + '(?:(?P>h16):){1,2}(?::(?P>h16)){1,5}', + '(?P>h16):(?::(?P>h16)){1,6}', + ':(?:(?::(?P>h16)){1,7}|:)', + 'fe80:(?::(?P>h16)){0,4}%[0-9A-Za-z]+', + '::(ffff(:0{1,4})?:)?(?P>ipv4)', + '(?:(?P>h16):){1,4}:(?P>ipv4)', + )) . ')\]', + 'host' => '(?:' . implode('|', array( + 'localhost', + '(?P>domain)', + '(?P>ipv4)', + '(?P>ipv6)', + )) . ')', + 'authority' => '(?:(?P>user_info)@)?(?P>host)(?::\d+)?', + ); + + // Brackets and quotation marks are problematic at the end of an IRI. + // E.g.: `http://foo.com/baz(qux)` vs. `(http://foo.com/baz_qux)` + // In the first case, the user probably intended the `)` as part of the + // IRI, but not in the second case. To account for this, we test for + // balanced pairs within the IRI. + $balanced_pairs = array( + // Brackets and parentheses + '(' => ')', '[' => ']', '{' => '}', + // Double quotation marks + '"' => '"', + html_entity_decode('“', 0, $context['character_set']) => html_entity_decode('”', 0, $context['character_set']), + html_entity_decode('„', 0, $context['character_set']) => html_entity_decode('”', 0, $context['character_set']), + html_entity_decode('‟', 0, $context['character_set']) => html_entity_decode('”', 0, $context['character_set']), + html_entity_decode('«', 0, $context['character_set']) => html_entity_decode('»', 0, $context['character_set']), + // Single quotation marks + '\'' => '\'', + html_entity_decode('‘', 0, $context['character_set']) => html_entity_decode('’', 0, $context['character_set']), + html_entity_decode('‚', 0, $context['character_set']) => html_entity_decode('’', 0, $context['character_set']), + html_entity_decode('‛', 0, $context['character_set']) => html_entity_decode('’', 0, $context['character_set']), + html_entity_decode('‹', 0, $context['character_set']) => html_entity_decode('›', 0, $context['character_set']), + ); + foreach ($balanced_pairs as $pair_opener => $pair_closer) + $balanced_pairs[$smcFunc['htmlspecialchars']($pair_opener)] = $smcFunc['htmlspecialchars']($pair_closer); + + $bracket_quote_chars = ''; + $bracket_quote_entities = array(); + foreach ($balanced_pairs as $pair_opener => $pair_closer) + { + if ($pair_opener == $pair_closer) + $pair_closer = ''; + + foreach (array($pair_opener, $pair_closer) as $bracket_quote) + { + if (strpos($bracket_quote, '&') === false) + $bracket_quote_chars .= $bracket_quote; + else + $bracket_quote_entities[] = substr($bracket_quote, 1); + } + } + $bracket_quote_chars = str_replace(array('[', ']'), array('\[', '\]'), $bracket_quote_chars); + + $pcre_subroutines['bracket_quote'] = '[' . $bracket_quote_chars . ']|&' . build_regex($bracket_quote_entities, '~'); + $pcre_subroutines['allowed_entities'] = '&(?!' . build_regex(array_merge($bracket_quote_entities, array('lt;', 'gt;')), '~') . ')'; + $pcre_subroutines['excluded_lookahead'] = '(?![' . $excluded_trailing_chars . ']*(?' . '>[\h\v]|
    |$))'; + + foreach (array('path', 'query', 'fragment') as $part) + { + switch ($part) { + case 'path': + $part_disallowed_chars = '\h\v<>' . $bracket_quote_chars . $excluded_trailing_chars . '/#&'; + $part_excluded_trailing_chars = str_replace('?', '', $excluded_trailing_chars); + break; + + case 'query': + $part_disallowed_chars = '\h\v<>' . $bracket_quote_chars . $excluded_trailing_chars . '#&'; + $part_excluded_trailing_chars = $excluded_trailing_chars; + break; + + default: + $part_disallowed_chars = '\h\v<>' . $bracket_quote_chars . $excluded_trailing_chars . '&'; + $part_excluded_trailing_chars = $excluded_trailing_chars; + break; + } + $pcre_subroutines[$part . '_allowed'] = '[^' . $part_disallowed_chars . ']|(?P>allowed_entities)|[' . $part_excluded_trailing_chars . '](?P>excluded_lookahead)'; + + $balanced_construct_regex = array(); + + foreach ($balanced_pairs as $pair_opener => $pair_closer) + $balanced_construct_regex[] = preg_quote($pair_opener) . '(?P>' . $part . '_recursive)*+' . preg_quote($pair_closer); + + $pcre_subroutines[$part . '_balanced'] = '(?:' . implode('|', $balanced_construct_regex) . ')(?P>' . $part . '_allowed)*+'; + $pcre_subroutines[$part . '_recursive'] = '(?' . '>(?P>' . $part . '_allowed)|(?P>' . $part . '_balanced))'; + + $pcre_subroutines[$part . '_segment'] = + // Allowed characters besides brackets and quotation marks + '(?P>' . $part . '_allowed)*+' . + // Brackets and quotation marks that are either... + '(?:' . + // part of a balanced construct + '(?P>' . $part . '_balanced)' . + // or + '|' . + // unpaired but not at the end + '(?P>bracket_quote)(?=(?P>' . $part . '_allowed))' . + ')*+'; + } + + // Time to build this monster! + $url_regex = + // 1. IRI scheme and domain components + '(?:' . + // 1a. IRIs with a scheme, or at least an opening "//" + '(?:' . + + // URI scheme (or lack thereof for schemeless URLs) + '(?' . '>' . + // URI scheme and colon + '\b' . + '(?:' . + // Either a scheme that need a domain in the authority + // (Remember for later that we need a domain) + '(?P(?P>scheme_need_domain)):' . + // or + '|' . + // a scheme that allows an empty authority + // (Remember for later that the authority can be empty) + '(?P(?P>scheme_empty_authority)):' . + // or + '|' . + // a scheme that uses no authority + '(?P>scheme_no_authority):(?!//)' . + // or + '|' . + // another scheme, but only if it is followed by "://" + '(?P>scheme_any):(?=//)' . + ')' . + + // or + '|' . + + // An empty string followed by "//" for schemeless URLs + '(?P(?=//))' . + ')' . + + // IRI authority chunk (maybe) + '(?:' . + // (Keep track of whether we find a valid authority or not) + '(?P' . + // 2 slashes before the authority itself + '//' . + '(?:' . + // If there was no scheme... + '(?()' . + // require an authority that contains a domain. + '(?P>authority)' . + + // Else if a domain is needed... + '|(?()' . + // require an authority with a domain. + '(?P>authority)' . + + // Else if an empty authority is allowed... + '|(?()' . + // then require either + '(?:' . + // empty string, followed by a "/" + '(?=/)' . + // or + '|' . + // an authority with a domain. + '(?P>authority)' . + ')' . + + // Else just a run of IRI characters. + '|(?P>no_domain)' . + ')' . + ')' . + ')' . + ')' . + // Followed by a non-domain character or end of line + '(?=(?P>not_domain_label_char)|$)' . + ')' . + + // or, if there is a scheme but no authority + // (e.g. "mailto:" URLs)... + '|' . + + // A run of IRI characters + '(?P>no_domain)' . + // If scheme needs a domain, require a dot and a TLD + '(?()\.(?P>tlds))' . + // Followed by a non-domain character or end of line + '(?=(?P>not_domain_label_char)|$)' . + ')' . + ')' . + + // Or, if there is neither a scheme nor an authority... + '|' . + + // 1b. Naked domains + // (e.g. "example.com" in "Go to example.com for an example.") + '(?P' . + // Preceded by start of line or a space + '(?<=^|
    |[\h\v])' . + // A domain name + '(?P>domain)' . + // Followed by a non-domain character or end of line + '(?=(?P>not_domain_label_char)|$)' . + ')' . + ')' . + + // 2. IRI path, query, and fragment components (if present) + '(?:' . + // If the IRI has an authority or is a naked domain and any of these + // components exist, the path must start with a single "/". + // Note: technically, it is valid to append a query or fragment + // directly to the authority chunk without a "/", but supporting + // that in the autolinker would produce a lot of false positives, + // so we don't. + '(?=' . + // If we found an authority above... + '(?()' . + // require a "/" + '/' . + // Else if we found a naked domain above... + '|(?()' . + // require a "/" + '/' . + ')' . + ')' . + ')' . + + // 2.a. Path component, if any. + '(?:' . + // Can have one or more segments + '(?:' . + // Not preceded by a "/", except in the special case of an + // empty authority immediately before the path. + '(?()' . + '(?:(?<=://)|(?path_segment)*+' . + ')*+' . + ')' . + + // 2.b. Query component, if any. + '(?:' . + // Initial "?" that is not last character. + '\?' . '(?=(?P>bracket_quote)*(?P>query_allowed))' . + // Then a run of allowed query characters + '(?P>query_segment)*+' . + ')?' . + + // 2.c. Fragment component, if any. + '(?:' . + // Initial "#" that is not last character. + '#' . '(?=(?P>bracket_quote)*(?P>fragment_allowed))' . + // Then a run of allowed fragment characters + '(?P>fragment_segment)*+' . + ')?' . + ')?+'; + + // Finally, define the PCRE subroutines in the regex. + $url_regex .= '(?(DEFINE)'; + + foreach ($pcre_subroutines as $name => $subroutine) + $url_regex .= '(?<' . $name . '>' . $subroutine . ')'; + + $url_regex .= ')'; + } + + $tmp_data = preg_replace_callback( + '~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''), + function($matches) use ($schemes) + { + $url = array_shift($matches); + + // If this isn't a clean URL, bail out + if ($url != sanitize_iri($url)) + return $url; + + // Ensure the host name is in its canonical form. + $url = normalize_iri($url); + + $parsedurl = parse_iri($url); + + if (!isset($parsedurl['scheme'])) + $parsedurl['scheme'] = ''; + + if ($parsedurl['scheme'] == 'mailto') + { + if (isset($disabled['email'])) + return $url; + + // Is this version of PHP capable of validating this email address? + $can_validate = defined('FILTER_FLAG_EMAIL_UNICODE') || strlen($parsedurl['path']) == strspn(strtolower($parsedurl['path']), 'abcdefghijklmnopqrstuvwxyz0123456789!#$%&\'*+-/=?^_`{|}~.@'); + + $flags = defined('FILTER_FLAG_EMAIL_UNICODE') ? FILTER_FLAG_EMAIL_UNICODE : null; + + if (!$can_validate || filter_var($parsedurl['path'], FILTER_VALIDATE_EMAIL, $flags) !== false) + return '[email=' . str_replace('mailto:', '', $url) . ']' . $url . '[/email]'; + else + return $url; + } + + // Are we linking a schemeless URL or naked domain name (e.g. "example.com")? + if (empty($parsedurl['scheme'])) + $fullUrl = '//' . ltrim($url, ':/'); + else + $fullUrl = $url; + + // Make sure that $fullUrl really is valid + if (in_array($parsedurl['scheme'], $schemes['forbidden']) || (!in_array($parsedurl['scheme'], $schemes['no_authority']) && validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false)) + return $url; + + return '[url="' . str_replace(array('[', ']'), array('[', ']'), iri_to_url($fullUrl)) . '"]' . $url . '[/url]'; + }, + $data + ); + + if (!is_null($tmp_data)) + $data = $tmp_data; + } + + // Next, emails... Must be careful not to step on enablePostHTML logic above... + if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false) + { + // Preceded by a space or start of line + $email_regex = '(?<=^|
    |[\h\v])' . + + // An email address + '[' . $domain_label_chars . '_.]{1,80}' . + '@' . + '[' . $domain_label_chars . '.]+' . + '\.' . $modSettings['tld_regex'] . + + // Followed by a non-domain character or end of line + '(?=[^' . $domain_label_chars . ']|$)'; + + $tmp_data = preg_replace('~' . $email_regex . '~i' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data); + + if (!is_null($tmp_data)) + $data = $tmp_data; + } + + // Save a little memory. + unset($tmp_data); + } + } + + // Restore any placeholders + $data = strtr($data, $placeholders); + + $data = strtr($data, array("\t" => '   ')); + + // If it wasn't changed, no copying or other boring stuff has to happen! + if ($data != substr($message, $last_pos, $pos - $last_pos)) + { + $message = substr($message, 0, $last_pos) . $data . substr($message, $pos); + + // Since we changed it, look again in case we added or removed a tag. But we don't want to skip any. + $old_pos = strlen($data) + $last_pos; + $pos = strpos($message, '[', $last_pos); + $pos = $pos === false ? $old_pos : min($pos, $old_pos); + } + } + + // Are we there yet? Are we there yet? + if ($pos >= strlen($message) - 1) + break; + + $tag_character = strtolower($message[$pos + 1]); + + if ($tag_character == '/' && !empty($open_tags)) + { + $pos2 = strpos($message, ']', $pos + 1); + if ($pos2 == $pos + 2) + continue; + + $look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2)); + + // A closing tag that doesn't match any open tags? Skip it. + if (!in_array($look_for, array_map(function($code) { return $code['tag']; }, $open_tags))) + continue; + + $to_close = array(); + $block_level = null; + + do + { + $tag = array_pop($open_tags); + if (!$tag) + break; + + if (!empty($tag['block_level'])) + { + // Only find out if we need to. + if ($block_level === false) + { + array_push($open_tags, $tag); + break; + } + + // The idea is, if we are LOOKING for a block level tag, we can close them on the way. + if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]])) + { + foreach ($bbc_codes[$look_for[0]] as $temp) + if ($temp['tag'] == $look_for) + { + $block_level = !empty($temp['block_level']); + break; + } + } + + if ($block_level !== true) + { + $block_level = false; + array_push($open_tags, $tag); + break; + } + } + + $to_close[] = $tag; + } + while ($tag['tag'] != $look_for); + + // Did we just eat through everything and not find it? + if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for))) + { + $open_tags = $to_close; + continue; + } + elseif (!empty($to_close) && $tag['tag'] != $look_for) + { + if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]])) + { + foreach ($bbc_codes[$look_for[0]] as $temp) + if ($temp['tag'] == $look_for) + { + $block_level = !empty($temp['block_level']); + break; + } + } + + // We're not looking for a block level tag (or maybe even a tag that exists...) + if (!$block_level) + { + foreach ($to_close as $tag) + array_push($open_tags, $tag); + continue; + } + } + + foreach ($to_close as $tag) + { + $message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1); + $pos += strlen($tag['after']) + 2; + $pos2 = $pos - 1; + + // See the comment at the end of the big loop - just eating whitespace ;). + $whitespace_regex = ''; + if (!empty($tag['block_level'])) + $whitespace_regex .= '( |\s)*()?'; + // Trim one line of whitespace after unnested tags, but all of it after nested ones + 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])); + } + + if (!empty($to_close)) + { + $to_close = array(); + $pos--; + } + + continue; + } + + // No tags for this character, so just keep going (fastest possible course.) + if (!isset($bbc_codes[$tag_character])) + continue; + + $inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1]; + $tag = null; + foreach ($bbc_codes[$tag_character] as $possible) + { + $pt_strlen = strlen($possible['tag']); + + // Not a match? + if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag']) + continue; + + $next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : ''; + + // A tag is the last char maybe + if ($next_c == '') + break; + + // A test validation? + if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0) + continue; + // Do we want parameters? + elseif (!empty($possible['parameters'])) + { + // Are all the parameters optional? + $param_required = false; + foreach ($possible['parameters'] as $param) + { + if (empty($param['optional'])) + { + $param_required = true; + break; + } + } + + if ($param_required && $next_c != ' ') + continue; + } + elseif (isset($possible['type'])) + { + // Do we need an equal sign? + if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=') + continue; + // Maybe we just want a /... + if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]') + continue; + // An immediate ]? + if ($possible['type'] == 'unparsed_content' && $next_c != ']') + continue; + } + // No type means 'parsed_content', which demands an immediate ] without parameters! + elseif ($next_c != ']') + continue; + + // Check allowed tree? + if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents']))) + continue; + elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children'])) + continue; + // If this is in the list of disallowed child tags, don't parse it. + elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children'])) + continue; + + $pos1 = $pos + 1 + $pt_strlen + 1; + + // Quotes can have alternate styling, we do this php-side due to all the permutations of quotes. + if ($possible['tag'] == 'quote') + { + // Start with standard + $quote_alt = false; + foreach ($open_tags as $open_quote) + { + // Every parent quote this quote has flips the styling + if ($open_quote['tag'] == 'quote') + $quote_alt = !$quote_alt; + } + // Add a class to the quote to style alternating blockquotes + $possible['before'] = strtr($possible['before'], array('
    ' => '
    ')); + } + + // 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 .= ''; + $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'] = '
    '; + } + // 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']) ? '
    ' : ''; + $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 = '' . strtr($specialChars, array(':' => ':', '(' => '(', ')' => ')', '$' => '$', '[' => '[')) . ''; + + $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 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 .) + if (!isset($_REQUEST['xml'])) + displayDebug(); + } + } + + // Remember this URL in case someone doesn't like sending HTTP_REFERER. + if ($should_log) + $_SESSION['old_url'] = $_SERVER['REQUEST_URL']; + + // For session check verification.... don't switch browsers... + $_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT']; + + // Hand off the output to the portal, etc. we're integrated with. + call_integration_hook('integrate_exit', array($do_footer)); + + // Don't exit if we're coming from index.php; that will pass through normally. + if (!$from_index) + exit; +} + +/** + * Get the size of a specified image with better error handling. + * + * @todo see if it's better in Subs-Graphics, but one step at the time. + * Uses getimagesize() to determine the size of a file. + * Attempts to connect to the server first so it won't time out. + * + * @param string $url The URL of the image + * @return array|false The image size as array (width, height), or false on failure + */ +function url_image_size($url) +{ + global $sourcedir; + + // Make sure it is a proper URL. + $url = str_replace(' ', '%20', $url); + + // Can we pull this from the cache... please please? + if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null) + return $temp; + $t = microtime(true); + + // Get the host to pester... + preg_match('~^\w+://(.+?)/(.*)$~', $url, $match); + + // Can't figure it out, just try the image size. + if ($url == '' || $url == 'http://' || $url == 'https://') + { + return false; + } + elseif (!isset($match[1])) + { + $size = @getimagesize($url); + } + else + { + // Try to connect to the server... give it half a second. + $temp = 0; + $fp = @fsockopen($match[1], 80, $temp, $temp, 0.5); + + // Successful? Continue... + if ($fp != false) + { + // Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.) + fwrite($fp, 'HEAD /' . $match[2] . ' HTTP/1.1' . "\r\n" . 'Host: ' . $match[1] . "\r\n" . 'user-agent: '. SMF_USER_AGENT . "\r\n" . 'Connection: close' . "\r\n\r\n"); + + // Read in the HTTP/1.1 or whatever. + $test = substr(fgets($fp, 11), -1); + fclose($fp); + + // See if it returned a 404/403 or something. + if ($test < 4) + { + $size = @getimagesize($url); + + // This probably means allow_url_fopen is off, let's try GD. + if ($size === false && function_exists('imagecreatefromstring')) + { + // It's going to hate us for doing this, but another request... + $image = @imagecreatefromstring(fetch_web_data($url)); + if ($image !== false) + { + $size = array(imagesx($image), imagesy($image)); + imagedestroy($image); + } + } + } + } + } + + // If we didn't get it, we failed. + if (!isset($size)) + $size = false; + + // If this took a long time, we may never have to do it again, but then again we might... + if (microtime(true) - $t > 0.8) + cache_put_data('url_image_size-' . md5($url), $size, 240); + + // Didn't work. + return $size; +} + +/** + * Sets up the basic theme context stuff. + * + * @param bool $forceload Whether to load the theme even if it's already loaded + */ +function setupThemeContext($forceload = false) +{ + global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance; + global $smcFunc; + static $loaded = false; + + // Under SSI this function can be called more then once. That can cause some problems. + // So only run the function once unless we are forced to run it again. + if ($loaded && !$forceload) + return; + + $loaded = true; + + $context['in_maintenance'] = !empty($maintenance); + $context['current_time'] = timeformat(time(), false); + $context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : ''; + $context['random_news_line'] = array(); + + // Get some news... + $context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news']))))); + for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++) + { + if (trim($context['news_lines'][$i]) == '') + continue; + + // Clean it up for presentation ;). + $context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i); + } + + if (!empty($context['news_lines']) && (!empty($modSettings['allow_guestAccess']) || $context['user']['is_logged'])) + $context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)]; + + if (!$user_info['is_guest']) + { + $context['user']['messages'] = &$user_info['messages']; + $context['user']['unread_messages'] = &$user_info['unread_messages']; + $context['user']['alerts'] = &$user_info['alerts']; + + // Personal message popup... + if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0)) + $context['user']['popup_messages'] = true; + else + $context['user']['popup_messages'] = false; + $_SESSION['unread_messages'] = $user_info['unread_messages']; + + if (allowedTo('moderate_forum')) + $context['unapproved_members'] = !empty($modSettings['unapprovedMembers']) ? $modSettings['unapprovedMembers'] : 0; + + $context['user']['avatar'] = set_avatar_data(array( + 'filename' => $user_info['avatar']['filename'], + 'avatar' => $user_info['avatar']['url'], + 'email' => $user_info['email'], + )); + + // Figure out how long they've been logged in. + $context['user']['total_time_logged_in'] = array( + 'days' => floor($user_info['total_time_logged_in'] / 86400), + 'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600), + 'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60) + ); + } + else + { + $context['user']['messages'] = 0; + $context['user']['unread_messages'] = 0; + $context['user']['avatar'] = array(); + $context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0); + $context['user']['popup_messages'] = false; + + // If we've upgraded recently, go easy on the passwords. + if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime'])) + $context['disable_login_hashing'] = true; + } + + // Setup the main menu items. + setupMenuContext(); + + // This is here because old index templates might still use it. + $context['show_news'] = !empty($settings['enable_news']); + + // This is done to allow theme authors to customize it as they want. + $context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm'); + + // 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array. + if ($context['show_pm_popup']) + addInlineJavaScript(' + jQuery(document).ready(function($) { + new smc_Popup({ + heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ', + content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ', + icon_class: \'main_icons mail_new\' + }); + });'); + + // Add a generic "Are you sure?" confirmation message. + addInlineJavaScript(' + var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) . ';'); + + // Now add the capping code for avatars. + if (!empty($modSettings['avatar_max_width_external']) && !empty($modSettings['avatar_max_height_external']) && !empty($modSettings['avatar_action_too_large']) && $modSettings['avatar_action_too_large'] == 'option_css_resize') + addInlineCss(' + img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px !important; max-height: ' . $modSettings['avatar_max_height_external'] . 'px !important; }'); + + // Add max image limits + if (!empty($modSettings['max_image_width'])) + addInlineCss(' + .postarea .bbc_img, .list_posts .bbc_img, .post .inner .bbc_img, form#reported_posts .bbc_img, #preview_body .bbc_img { max-width: min(100%,' . $modSettings['max_image_width'] . 'px); }'); + + if (!empty($modSettings['max_image_height'])) + addInlineCss(' + .postarea .bbc_img, .list_posts .bbc_img, .post .inner .bbc_img, form#reported_posts .bbc_img, #preview_body .bbc_img { max-height: ' . $modSettings['max_image_height'] . 'px; }'); + + // This looks weird, but it's because BoardIndex.php references the variable. + $context['common_stats']['latest_member'] = array( + 'id' => $modSettings['latestMember'], + 'name' => $modSettings['latestRealName'], + 'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'], + 'link' => '' . $modSettings['latestRealName'] . '', + ); + $context['common_stats'] = array( + 'total_posts' => comma_format($modSettings['totalMessages']), + 'total_topics' => comma_format($modSettings['totalTopics']), + 'total_members' => comma_format($modSettings['totalMembers']), + 'latest_member' => $context['common_stats']['latest_member'], + ); + $context['common_stats']['boardindex_total_posts'] = sprintf($txt['boardindex_total_posts'], $context['common_stats']['total_posts'], $context['common_stats']['total_topics'], $context['common_stats']['total_members']); + + if (empty($settings['theme_version'])) + addJavaScriptVar('smf_scripturl', $scripturl); + + if (!isset($context['page_title'])) + $context['page_title'] = ''; + + // Set some specific vars. + $context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](html_entity_decode($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : ''); + $context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : ''; + + // Content related meta tags, including Open Graph + $context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']); + $context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']); + + if (!empty($context['meta_keywords'])) + $context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']); + + if (!empty($context['canonical_url'])) + $context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']); + + if (!empty($settings['og_image'])) + $context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']); + + if (!empty($context['meta_description'])) + { + $context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']); + $context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']); + } + else + { + $context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']); + $context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']); + } + + call_integration_hook('integrate_theme_context'); +} + +/** + * Helper function to set the system memory to a needed value + * - If the needed memory is greater than current, will attempt to get more + * - if in_use is set to true, will also try to take the current memory usage in to account + * + * @param string $needed The amount of memory to request, if needed, like 256M + * @param bool $in_use Set to true to account for current memory usage of the script + * @return boolean True if we have at least the needed memory + */ +function setMemoryLimit($needed, $in_use = false) +{ + // everything in bytes + $memory_current = memoryReturnBytes(ini_get('memory_limit')); + $memory_needed = memoryReturnBytes($needed); + + // should we account for how much is currently being used? + if ($in_use) + $memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576); + + // if more is needed, request it + if ($memory_current < $memory_needed) + { + @ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M'); + $memory_current = memoryReturnBytes(ini_get('memory_limit')); + } + + $memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit'))); + + // return success or not + return (bool) ($memory_current >= $memory_needed); +} + +/** + * Helper function to convert memory string settings to bytes + * + * @param string $val The byte string, like 256M or 1G + * @return integer The string converted to a proper integer in bytes + */ +function memoryReturnBytes($val) +{ + if (is_integer($val)) + return $val; + + // Separate the number from the designator + $val = trim($val); + $num = intval(substr($val, 0, strlen($val) - 1)); + $last = strtolower(substr($val, -1)); + + // convert to bytes + switch ($last) + { + case 'g': + $num *= 1024; + case 'm': + $num *= 1024; + case 'k': + $num *= 1024; + } + return $num; +} + +/** + * The header template + */ +function template_header() +{ + global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir, $cache_enable, $language; + + setupThemeContext(); + + // Print stuff to prevent caching of pages (except on attachment errors, etc.) + if (empty($context['no_last_modified'])) + { + header('expires: Mon, 26 Jul 1997 05:00:00 GMT'); + header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + + // Are we debugging the template/html content? + if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie')) + header('content-type: application/xhtml+xml'); + elseif (!isset($_REQUEST['xml'])) + header('content-type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set'])); + } + + header('content-type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set'])); + + // We need to splice this in after the body layer, or after the main layer for older stuff. + if ($context['in_maintenance'] && $context['user']['is_admin']) + { + $position = array_search('body', $context['template_layers']); + if ($position === false) + $position = array_search('main', $context['template_layers']); + + if ($position !== false) + { + $before = array_slice($context['template_layers'], 0, $position + 1); + $after = array_slice($context['template_layers'], $position + 1); + $context['template_layers'] = array_merge($before, array('maint_warning'), $after); + } + } + + $checked_securityFiles = false; + $showed_banned = false; + foreach ($context['template_layers'] as $layer) + { + loadSubTemplate($layer . '_above', true); + + // May seem contrived, but this is done in case the body and main layer aren't there... + if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles) + { + $checked_securityFiles = true; + + $securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~'); + + // Add your own files. + call_integration_hook('integrate_security_files', array(&$securityFiles)); + + foreach ($securityFiles as $i => $securityFile) + { + if (!file_exists($boarddir . '/' . $securityFile)) + unset($securityFiles[$i]); + } + + // We are already checking so many files...just few more doesn't make any difference! :P + if (!empty($modSettings['currentAttachmentUploadDir'])) + $path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']]; + + else + $path = $modSettings['attachmentUploadDir']; + + secureDirectory($path, true); + secureDirectory($cachedir); + + // If agreement is enabled, at least the english version shall exist + if (!empty($modSettings['requireAgreement'])) + $agreement = !file_exists($boarddir . '/agreement.txt'); + + // If privacy policy is enabled, at least the default language version shall exist + if (!empty($modSettings['requirePolicyAgreement'])) + $policy_agreement = empty($modSettings['policy_' . $language]); + + if (!empty($securityFiles) || + (!empty($cache_enable) && !is_writable($cachedir)) || + !empty($agreement) || + !empty($policy_agreement) || + !empty($context['auth_secret_missing'])) + { + echo ' +
    +

    !!

    +

    ', empty($securityFiles) && empty($context['auth_secret_missing']) ? $txt['generic_warning'] : $txt['security_risk'], '

    +

    '; + + foreach ($securityFiles as $securityFile) + { + echo ' + ', $txt['not_removed'], '', $securityFile, '!
    '; + + if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~') + echo ' + ', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '
    '; + } + + if (!empty($cache_enable) && !is_writable($cachedir)) + echo ' + ', $txt['cache_writable'], '
    '; + + if (!empty($agreement)) + echo ' + ', $txt['agreement_missing'], '
    '; + + if (!empty($policy_agreement)) + echo ' + ', $txt['policy_agreement_missing'], '
    '; + + if (!empty($context['auth_secret_missing'])) + echo ' + ', $txt['auth_secret_missing'], '
    '; + + echo ' +

    +
    '; + } + } + // If the user is banned from posting inform them of it. + elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned) + { + $showed_banned = true; + echo ' +
    + ', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']); + + if (!empty($_SESSION['ban']['cannot_post']['reason'])) + echo ' +
    ', $_SESSION['ban']['cannot_post']['reason'], '
    '; + + if (!empty($_SESSION['ban']['expire_time'])) + echo ' +
    ', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '
    '; + else + echo ' +
    ', $txt['your_ban_expires_never'], '
    '; + + echo ' +
    '; + } + } +} + +/** + * Show the copyright. + */ +function theme_copyright() +{ + global $forum_copyright, $scripturl; + + // Don't display copyright for things like SSI. + if (SMF !== 1) + return; + + // Put in the version... + printf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR, $scripturl); +} + +/** + * The template footer + */ +function template_footer() +{ + global $context, $modSettings, $db_count; + + // Show the load time? (only makes sense for the footer.) + $context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']); + $context['load_time'] = round(microtime(true) - TIME_START, 3); + $context['load_queries'] = $db_count; + + if (!empty($context['template_layers']) && is_array($context['template_layers'])) + foreach (array_reverse($context['template_layers']) as $layer) + loadSubTemplate($layer . '_below', true); +} + +/** + * Output the Javascript files + * - tabbing in this function is to make the HTML source look good and proper + * - if deferred is set function will output all JS set to load at page end + * + * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag) + */ +function template_javascript($do_deferred = false) +{ + global $context, $modSettings, $settings; + + // Use this hook to minify/optimize Javascript files and vars + call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred)); + + $toMinify = array( + 'standard' => array(), + 'defer' => array(), + 'async' => array(), + ); + + // Ouput the declared Javascript variables. + if (!empty($context['javascript_vars']) && !$do_deferred) + { + echo ' + '; + } + + // In the dark days before HTML5, deferred JS files needed to be loaded at the end of the body. + // Now we load them in the head and use 'async' and/or 'defer' attributes. Much better performance. + if (!$do_deferred) + { + // While we have JavaScript files to place in the template. + foreach ($context['javascript_files'] as $id => $js_file) + { + // Last minute call! allow theme authors to disable single files. + if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files'])) + continue; + + // By default files don't get minimized unless the file explicitly says so! + if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files'])) + { + if (!empty($js_file['options']['async'])) + $toMinify['async'][] = $js_file; + + elseif (!empty($js_file['options']['defer'])) + $toMinify['defer'][] = $js_file; + + else + $toMinify['standard'][] = $js_file; + + // Grab a random seed. + if (!isset($minSeed) && isset($js_file['options']['seed'])) + $minSeed = $js_file['options']['seed']; + } + + else + { + echo ' + '; + } + } + + foreach ($toMinify as $js_files) + { + if (!empty($js_files)) + { + $result = custMinify($js_files, 'js'); + + $minSuccessful = array_keys($result) === array('smf_minified'); + + foreach ($result as $minFile) + echo ' + '; + } + } + } + + // Inline JavaScript - Actually useful some times! + if (!empty($context['javascript_inline'])) + { + if (!empty($context['javascript_inline']['defer']) && $do_deferred) + { + echo ' +'; + } + + if (!empty($context['javascript_inline']['standard']) && !$do_deferred) + { + echo ' + '; + } + } +} + +/** + * Output the CSS files + */ +function template_css() +{ + global $context, $db_show_debug, $boardurl, $settings, $modSettings; + + // Use this hook to minify/optimize CSS files + call_integration_hook('integrate_pre_css_output'); + + $toMinify = array(); + $normal = array(); + + uasort( + $context['css_files'], + function ($a, $b) + { + return $a['options']['order_pos'] < $b['options']['order_pos'] ? -1 : ($a['options']['order_pos'] > $b['options']['order_pos'] ? 1 : 0); + } + ); + + foreach ($context['css_files'] as $id => $file) + { + // Last minute call! allow theme authors to disable single files. + if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files'])) + continue; + + // Files are minimized unless they explicitly opt out. + if (!isset($file['options']['minimize'])) + $file['options']['minimize'] = true; + + if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']) && !isset($_REQUEST['normalcss'])) + { + $toMinify[] = $file; + + // Grab a random seed. + if (!isset($minSeed) && isset($file['options']['seed'])) + $minSeed = $file['options']['seed']; + } + else + $normal[] = array( + 'url' => $file['fileUrl'] . (isset($file['options']['seed']) ? $file['options']['seed'] : ''), + 'attributes' => !empty($file['options']['attributes']) ? $file['options']['attributes'] : array() + ); + } + + if (!empty($toMinify)) + { + $result = custMinify($toMinify, 'css'); + + $minSuccessful = array_keys($result) === array('smf_minified'); + + foreach ($result as $minFile) + echo ' + '; + } + + // Print the rest after the minified files. + if (!empty($normal)) + foreach ($normal as $nf) + { + echo ' + $value) + { + if (is_bool($value)) + echo !empty($value) ? ' ' . $key : ''; + else + echo ' ', $key, '="', $value, '"'; + } + + echo '>'; + } + + if ($db_show_debug === true) + { + // Try to keep only what's useful. + $repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => ''); + foreach ($context['css_files'] as $file) + $context['debug']['sheets'][] = strtr($file['fileName'], $repl); + } + + if (!empty($context['css_header'])) + { + echo ' + '; + } +} + +/** + * Get an array of previously defined files and adds them to our main minified files. + * Sets a one day cache to avoid re-creating a file on every request. + * + * @param array $data The files to minify. + * @param string $type either css or js. + * @return array Info about the minified file, or about the original files if the minify process failed. + */ +function custMinify($data, $type) +{ + global $settings, $txt; + + $types = array('css', 'js'); + $type = !empty($type) && in_array($type, $types) ? $type : false; + $data = is_array($data) ? $data : array(); + + if (empty($type) || empty($data)) + return $data; + + // Different pages include different files, so we use a hash to label the different combinations + $hash = md5(implode(' ', array_map( + function($file) + { + return $file['filePath'] . '-' . $file['mtime']; + }, + $data + ))); + + // Is this a deferred or asynchronous JavaScript file? + $async = $type === 'js'; + $defer = $type === 'js'; + if ($type === 'js') + { + foreach ($data as $id => $file) + { + // A minified script should only be loaded asynchronously if all its components wanted to be. + if (empty($file['options']['async'])) + $async = false; + + // A minified script should only be deferred if all its components wanted to be. + if (empty($file['options']['defer'])) + $defer = false; + } + } + + // Did we already do this? + $minified_file = $settings['theme_dir'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/minified_' . $hash . '.' . $type; + $already_exists = file_exists($minified_file); + + // Already done? + if ($already_exists) + { + return array('smf_minified' => array( + 'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file), + 'filePath' => $minified_file, + 'fileName' => basename($minified_file), + 'options' => array('async' => !empty($async), 'defer' => !empty($defer)), + )); + } + // File has to exist. If it doesn't, try to create it. + elseif (@fopen($minified_file, 'w') === false || !smf_chmod($minified_file)) + { + loadLanguage('Errors'); + log_error(sprintf($txt['file_not_created'], $minified_file), 'general'); + + // The process failed, so roll back to print each individual file. + return $data; + } + + // No namespaces, sorry! + $classType = 'MatthiasMullie\\Minify\\' . strtoupper($type); + + $minifier = new $classType(); + + foreach ($data as $id => $file) + { + $toAdd = !empty($file['filePath']) && file_exists($file['filePath']) ? $file['filePath'] : false; + + // The file couldn't be located so it won't be added. Log this error. + if (empty($toAdd)) + { + loadLanguage('Errors'); + log_error(sprintf($txt['file_minimize_fail'], !empty($file['fileName']) ? $file['fileName'] : $id), 'general'); + continue; + } + + // Add this file to the list. + $minifier->add($toAdd); + } + + // Create the file. + $minifier->minify($minified_file); + unset($minifier); + clearstatcache(); + + // Minify process failed. + if (!filesize($minified_file)) + { + loadLanguage('Errors'); + log_error(sprintf($txt['file_not_created'], $minified_file), 'general'); + + // The process failed so roll back to print each individual file. + return $data; + } + + return array('smf_minified' => array( + 'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file), + 'filePath' => $minified_file, + 'fileName' => basename($minified_file), + 'options' => array('async' => $async, 'defer' => $defer), + )); +} + +/** + * Clears out old minimized CSS and JavaScript files and ensures $modSettings['browser_cache'] is up to date + */ +function deleteAllMinified() +{ + global $smcFunc, $txt, $modSettings; + + $not_deleted = array(); + $most_recent = 0; + + // Kinda sucks that we need to do another query to get all the theme dirs, but c'est la vie. + $request = $smcFunc['db_query']('', ' + SELECT id_theme AS id, value AS dir + FROM {db_prefix}themes + WHERE variable = {string:var}', + array( + 'var' => 'theme_dir', + ) + ); + while ($theme = $smcFunc['db_fetch_assoc']($request)) + { + foreach (array('css', 'js') as $type) + { + foreach (glob(rtrim($theme['dir'], '/') . '/' . ($type == 'css' ? 'css' : 'scripts') . '/*.' . $type) as $filename) + { + // We want to find the most recent mtime of non-minified files + if (strpos(pathinfo($filename, PATHINFO_BASENAME), 'minified') === false) + $most_recent = max($most_recent, (int) @filemtime($filename)); + + // Try to delete minified files. Add them to our error list if that fails. + elseif (!@unlink($filename)) + $not_deleted[] = $filename; + } + } + } + $smcFunc['db_free_result']($request); + + // This setting tracks the most recent modification time of any of our CSS and JS files + if ($most_recent != $modSettings['browser_cache']) + updateSettings(array('browser_cache' => $most_recent)); + + // If any of the files could not be deleted, log an error about it. + if (!empty($not_deleted)) + { + loadLanguage('Errors'); + log_error(sprintf($txt['unlink_minimized_fail'], implode('
    ', $not_deleted)), 'general'); + } +} + +/** + * Get an attachment's encrypted filename. If $new is true, won't check for file existence. + * + * @todo this currently returns the hash if new, and the full filename otherwise. + * Something messy like that. + * @todo and of course everything relies on this behavior and work around it. :P. + * Converters included. + * + * @param string $filename The name of the file + * @param int $attachment_id The ID of the attachment + * @param string|null $dir Which directory it should be in (null to use current one) + * @param bool $new Whether this is a new attachment + * @param string $file_hash The file hash + * @return string The path to the file + */ +function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '') +{ + global $modSettings, $smcFunc; + + // Just make up a nice hash... + if ($new) + return sha1(md5($filename . time()) . mt_rand()); + + // Just make sure that attachment id is only a int + $attachment_id = (int) $attachment_id; + + // Grab the file hash if it wasn't added. + // Left this for legacy. + if ($file_hash === '') + { + $request = $smcFunc['db_query']('', ' + SELECT file_hash + FROM {db_prefix}attachments + WHERE id_attach = {int:id_attach}', + array( + 'id_attach' => $attachment_id, + ) + ); + + if ($smcFunc['db_num_rows']($request) === 0) + return false; + + list ($file_hash) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + } + + // Still no hash? mmm... + if (empty($file_hash)) + $file_hash = sha1(md5($filename . time()) . mt_rand()); + + // Are we using multiple directories? + if (is_array($modSettings['attachmentUploadDir'])) + $path = $modSettings['attachmentUploadDir'][$dir]; + + else + $path = $modSettings['attachmentUploadDir']; + + return $path . '/' . $attachment_id . '_' . $file_hash . '.dat'; +} + +/** + * Convert a single IP to a ranged IP. + * internal function used to convert a user-readable format to a format suitable for the database. + * + * @param string $fullip The full IP + * @return array An array of IP parts + */ +function ip2range($fullip) +{ + // Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.) + if ($fullip == 'unknown') + $fullip = '255.255.255.255'; + + $ip_parts = explode('-', $fullip); + $ip_array = array(); + + // if ip 22.12.31.21 + if (count($ip_parts) == 1 && isValidIP($fullip)) + { + $ip_array['low'] = $fullip; + $ip_array['high'] = $fullip; + return $ip_array; + } // if ip 22.12.* -> 22.12.*-22.12.* + elseif (count($ip_parts) == 1) + { + $ip_parts[0] = $fullip; + $ip_parts[1] = $fullip; + } + + // if ip 22.12.31.21-12.21.31.21 + if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1])) + { + $ip_array['low'] = $ip_parts[0]; + $ip_array['high'] = $ip_parts[1]; + return $ip_array; + } + elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.* + { + $valid_low = isValidIP($ip_parts[0]); + $valid_high = isValidIP($ip_parts[1]); + $count = 0; + $mode = (preg_match('/:/', $ip_parts[0]) > 0 ? ':' : '.'); + $max = ($mode == ':' ? 'ffff' : '255'); + $min = 0; + if (!$valid_low) + { + $ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]); + $valid_low = isValidIP($ip_parts[0]); + while (!$valid_low) + { + $ip_parts[0] .= $mode . $min; + $valid_low = isValidIP($ip_parts[0]); + $count++; + if ($count > 9) break; + } + } + + $count = 0; + if (!$valid_high) + { + $ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]); + $valid_high = isValidIP($ip_parts[1]); + while (!$valid_high) + { + $ip_parts[1] .= $mode . $max; + $valid_high = isValidIP($ip_parts[1]); + $count++; + if ($count > 9) break; + } + } + + if ($valid_high && $valid_low) + { + $ip_array['low'] = $ip_parts[0]; + $ip_array['high'] = $ip_parts[1]; + } + } + + return $ip_array; +} + +/** + * Lookup an IP; try shell_exec first because we can do a timeout on it. + * + * @param string $ip The IP to get the hostname from + * @return string The hostname + */ +function host_from_ip($ip) +{ + global $modSettings; + + if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null) + return $host; + $t = microtime(true); + + $exists = function_exists('shell_exec'); + + // Try the Linux host command, perhaps? + if ($exists && !isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1) + { + if (!isset($modSettings['host_to_dis'])) + $test = @shell_exec('host -W 1 ' . @escapeshellarg($ip)); + else + $test = @shell_exec('host ' . @escapeshellarg($ip)); + + // Did host say it didn't find anything? + if (strpos($test, 'not found') !== false) + $host = ''; + // Invalid server option? + elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis'])) + updateSettings(array('host_to_dis' => 1)); + // Maybe it found something, after all? + elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1) + $host = $match[1]; + } + + // This is nslookup; usually only Windows, but possibly some Unix? + if ($exists && !isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1) + { + $test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip)); + if (strpos($test, 'Non-existent domain') !== false) + $host = ''; + elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1) + $host = $match[1]; + } + + // This is the last try :/. + if (!isset($host) || $host === false) + $host = @gethostbyaddr($ip); + + // It took a long time, so let's cache it! + if (microtime(true) - $t > 0.5) + cache_put_data('hostlookup-' . $ip, $host, 600); + + return $host; +} + +/** + * Chops a string into words and prepares them to be inserted into (or searched from) the database. + * + * @param string $text The text to split into words + * @param int $max_chars The maximum number of characters per word + * @param bool $encrypt Whether to encrypt the results + * @return array An array of ints or words depending on $encrypt + */ +function text2words($text, $max_chars = 20, $encrypt = false) +{ + global $smcFunc, $context; + + // Upgrader may be working on old DBs... + if (!isset($context['utf8'])) + $context['utf8'] = false; + + // Step 1: Remove entities/things we don't consider words: + $words = preg_replace('~(?:[\x0B\0' . ($context['utf8'] ? '\x{A0}' : '\xA0') . '\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~' . ($context['utf8'] ? 'u' : ''), ' ', strtr($text, array('
    ' => ' '))); + + // Step 2: Entities we left to letters, where applicable, lowercase. + $words = un_htmlspecialchars($smcFunc['strtolower']($words)); + + // Step 3: Ready to split apart and index! + $words = explode(' ', $words); + + if ($encrypt) + { + $possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122))); + $returned_ints = array(); + foreach ($words as $word) + { + if (($word = trim($word, '-_\'')) !== '') + { + $encrypted = substr(crypt($word, 'uk'), 2, $max_chars); + $total = 0; + for ($i = 0; $i < $max_chars; $i++) + $total += $possible_chars[ord($encrypted[$i])] * pow(63, $i); + $returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total; + } + } + return array_unique($returned_ints); + } + else + { + // Trim characters before and after and add slashes for database insertion. + $returned_words = array(); + foreach ($words as $word) + if (($word = trim($word, '-_\'')) !== '') + $returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars); + + // Filter out all words that occur more than once. + return array_unique($returned_words); + } +} + +/** + * Creates an image/text button + * + * @deprecated since 2.1 + * @param string $name The name of the button (should be a main_icons class or the name of an image) + * @param string $alt The alt text + * @param string $label The $txt string to use as the label + * @param string $custom Custom text/html to add to the img tag (only when using an actual image) + * @param boolean $force_use Whether to force use of this when template_create_button is available + * @return string The HTML to display the button + */ +function create_button($name, $alt, $label = '', $custom = '', $force_use = false) +{ + global $settings, $txt; + + // Does the current loaded theme have this and we are not forcing the usage of this function? + if (function_exists('template_create_button') && !$force_use) + return template_create_button($name, $alt, $label = '', $custom = ''); + + if (!$settings['use_image_buttons']) + return $txt[$alt]; + elseif (!empty($settings['use_buttons'])) + return '' . ($label != '' ? ' ' . $txt[$label] . '' : ''); + else + return '' . $txt[$alt] . ''; +} + +/** + * Sets up all of the top menu buttons + * Saves them in the cache if it is available and on + * Places the results in $context + */ +function setupMenuContext() +{ + global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings, $smcFunc, $cache_enable; + + // Set up the menu privileges. + $context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts')); + $context['allow_admin'] = allowedTo(array('admin_forum', 'manage_boards', 'manage_permissions', 'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news', 'manage_attachments', 'manage_smileys')); + + $context['allow_memberlist'] = allowedTo('view_mlist'); + $context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']); + $context['allow_moderation_center'] = $context['user']['can_mod']; + $context['allow_pm'] = allowedTo('pm_read'); + + $cacheTime = $modSettings['lastActive'] * 60; + + // Initial "can you post an event in the calendar" option - but this might have been set in the calendar already. + if (!isset($context['allow_calendar_event'])) + { + $context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post'); + + // If you don't allow events not linked to posts and you're not an admin, we have more work to do... + if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin']) + { + $boards_can_post = boardsAllowedTo('post_new'); + $context['allow_calendar_event'] &= !empty($boards_can_post); + } + } + + // There is some menu stuff we need to do if we're coming at this from a non-guest perspective. + if (!$context['user']['is_guest']) + { + addInlineJavaScript(' + var user_menus = new smc_PopupMenu(); + user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup"); + user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u=' . $context['user']['id'] . '");', true); + if ($context['allow_pm']) + addInlineJavaScript(' + user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true); + + if (!empty($modSettings['enable_ajax_alerts'])) + { + require_once($sourcedir . '/Subs-Notify.php'); + + $timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true); + $timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000; + + addInlineJavaScript(' + var new_alert_title = "' . $context['forum_name_html_safe'] . '"; + var alert_timeout = ' . $timeout . ';'); + loadJavaScriptFile('alerts.js', array('minimize' => true), 'smf_alerts'); + } + } + + // All the buttons we can possible want and then some, try pulling the final list of buttons from cache first. + if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated']) + { + $buttons = array( + 'home' => array( + 'title' => $txt['home'], + 'href' => $scripturl, + 'show' => true, + 'sub_buttons' => array( + ), + 'is_last' => $context['right_to_left'], + ), + 'search' => array( + 'title' => $txt['search'], + 'href' => $scripturl . '?action=search', + 'show' => $context['allow_search'], + 'sub_buttons' => array( + ), + ), + 'admin' => array( + 'title' => $txt['admin'], + 'href' => $scripturl . '?action=admin', + 'show' => $context['allow_admin'], + 'sub_buttons' => array( + 'featuresettings' => array( + 'title' => $txt['modSettings_title'], + 'href' => $scripturl . '?action=admin;area=featuresettings', + 'show' => allowedTo('admin_forum'), + ), + 'packages' => array( + 'title' => $txt['package'], + 'href' => $scripturl . '?action=admin;area=packages', + 'show' => allowedTo('admin_forum'), + ), + 'errorlog' => array( + 'title' => $txt['errorlog'], + 'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc', + 'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']), + ), + 'permissions' => array( + 'title' => $txt['edit_permissions'], + 'href' => $scripturl . '?action=admin;area=permissions', + 'show' => allowedTo('manage_permissions'), + ), + 'memberapprove' => array( + 'title' => $txt['approve_members_waiting'], + 'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve', + 'show' => !empty($context['unapproved_members']), + 'is_last' => true, + ), + ), + ), + 'moderate' => array( + 'title' => $txt['moderate'], + 'href' => $scripturl . '?action=moderate', + 'show' => $context['allow_moderation_center'], + 'sub_buttons' => array( + 'modlog' => array( + 'title' => $txt['modlog_view'], + 'href' => $scripturl . '?action=moderate;area=modlog', + 'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1', + ), + 'poststopics' => array( + 'title' => $txt['mc_unapproved_poststopics'], + 'href' => $scripturl . '?action=moderate;area=postmod;sa=posts', + 'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']), + ), + 'attachments' => array( + 'title' => $txt['mc_unapproved_attachments'], + 'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments', + 'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']), + ), + 'reports' => array( + 'title' => $txt['mc_reported_posts'], + 'href' => $scripturl . '?action=moderate;area=reportedposts', + 'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1', + ), + 'reported_members' => array( + 'title' => $txt['mc_reported_members'], + 'href' => $scripturl . '?action=moderate;area=reportedmembers', + 'show' => allowedTo('moderate_forum'), + 'is_last' => true, + ) + ), + ), + 'calendar' => array( + 'title' => $txt['calendar'], + 'href' => $scripturl . '?action=calendar', + 'show' => $context['allow_calendar'], + 'sub_buttons' => array( + 'view' => array( + 'title' => $txt['calendar_menu'], + 'href' => $scripturl . '?action=calendar', + 'show' => $context['allow_calendar_event'], + ), + 'post' => array( + 'title' => $txt['calendar_post_event'], + 'href' => $scripturl . '?action=calendar;sa=post', + 'show' => $context['allow_calendar_event'], + 'is_last' => true, + ), + ), + ), + 'mlist' => array( + 'title' => $txt['members_title'], + 'href' => $scripturl . '?action=mlist', + 'show' => $context['allow_memberlist'], + 'sub_buttons' => array( + 'mlist_view' => array( + 'title' => $txt['mlist_menu_view'], + 'href' => $scripturl . '?action=mlist', + 'show' => true, + ), + 'mlist_search' => array( + 'title' => $txt['mlist_search'], + 'href' => $scripturl . '?action=mlist;sa=search', + 'show' => true, + 'is_last' => true, + ), + ), + 'is_last' => !$context['right_to_left'] && empty($settings['login_main_menu']), + ), + // Theme authors: If you want the login and register buttons to appear in + // the main forum menu on your theme, set $settings['login_main_menu'] to + // true in your theme's template_init() function in index.template.php. + 'login' => array( + 'title' => $txt['login'], + 'href' => $scripturl . '?action=login', + 'onclick' => 'return reqOverlayDiv(this.href, ' . JavaScriptEscape($txt['login']) . ', \'login\');', + 'show' => $user_info['is_guest'] && !empty($settings['login_main_menu']), + 'sub_buttons' => array( + ), + 'is_last' => !$context['right_to_left'], + ), + 'logout' => array( + 'title' => $txt['logout'], + 'href' => $scripturl . '?action=logout;' . $context['session_var'] . '=' . $context['session_id'], + 'show' => !$user_info['is_guest'] && !empty($settings['login_main_menu']), + 'sub_buttons' => array( + ), + 'is_last' => !$context['right_to_left'], + ), + 'signup' => array( + 'title' => $txt['register'], + 'href' => $scripturl . '?action=signup', + 'icon' => 'regcenter', + 'show' => $user_info['is_guest'] && $context['can_register'] && !empty($settings['login_main_menu']), + 'sub_buttons' => array( + ), + 'is_last' => !$context['right_to_left'], + ), + ); + + // Allow editing menu buttons easily. + call_integration_hook('integrate_menu_buttons', array(&$buttons)); + + // Now we put the buttons in the context so the theme can use them. + $menu_buttons = array(); + foreach ($buttons as $act => $button) + if (!empty($button['show'])) + { + $button['active_button'] = false; + + // Make sure the last button truly is the last button. + if (!empty($button['is_last'])) + { + if (isset($last_button)) + unset($menu_buttons[$last_button]['is_last']); + $last_button = $act; + } + + // Go through the sub buttons if there are any. + if (!empty($button['sub_buttons'])) + foreach ($button['sub_buttons'] as $key => $subbutton) + { + if (empty($subbutton['show'])) + unset($button['sub_buttons'][$key]); + + // 2nd level sub buttons next... + if (!empty($subbutton['sub_buttons'])) + { + foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2) + { + if (empty($sub_button2['show'])) + unset($button['sub_buttons'][$key]['sub_buttons'][$key2]); + } + } + } + + // Does this button have its own icon? + if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon'])) + $button['icon'] = ''; + elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon'])) + $button['icon'] = ''; + elseif (isset($button['icon'])) + $button['icon'] = ''; + else + $button['icon'] = ''; + + $menu_buttons[$act] = $button; + } + + if (!empty($cache_enable) && $cache_enable >= 2) + cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime); + } + + $context['menu_buttons'] = $menu_buttons; + + // Logging out requires the session id in the url. + if (isset($context['menu_buttons']['logout'])) + $context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']); + + // Figure out which action we are doing so we can set the active tab. + // Default to home. + $current_action = 'home'; + + if (isset($context['menu_buttons'][$context['current_action']])) + $current_action = $context['current_action']; + elseif ($context['current_action'] == 'search2') + $current_action = 'search'; + elseif ($context['current_action'] == 'theme') + $current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin'; + elseif ($context['current_action'] == 'signup2') + $current_action = 'signup'; + elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder')) + $current_action = 'login'; + elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center']) + $current_action = 'moderate'; + + // There are certain exceptions to the above where we don't want anything on the menu highlighted. + if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner'])) + { + $current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile'; + $context[$current_action] = true; + } + elseif ($context['current_action'] == 'pm') + { + $current_action = 'self_pm'; + $context['self_pm'] = true; + } + + $context['total_mod_reports'] = 0; + $context['total_admin_reports'] = 0; + + if (!empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1' && !empty($context['open_mod_reports']) && !empty($context['menu_buttons']['moderate']['sub_buttons']['reports'])) + { + $context['total_mod_reports'] = $context['open_mod_reports']; + $context['menu_buttons']['moderate']['sub_buttons']['reports']['amt'] = $context['open_mod_reports']; + } + + // Show how many errors there are + if (!empty($context['menu_buttons']['admin']['sub_buttons']['errorlog'])) + { + // Get an error count, if necessary + if (!isset($context['num_errors'])) + { + $query = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}log_errors', + array() + ); + + list($context['num_errors']) = $smcFunc['db_fetch_row']($query); + $smcFunc['db_free_result']($query); + } + + if (!empty($context['num_errors'])) + { + $context['total_admin_reports'] += $context['num_errors']; + $context['menu_buttons']['admin']['sub_buttons']['errorlog']['amt'] = $context['num_errors']; + } + } + + // Show number of reported members + if (!empty($context['open_member_reports']) && !empty($context['menu_buttons']['moderate']['sub_buttons']['reported_members'])) + { + $context['total_mod_reports'] += $context['open_member_reports']; + $context['menu_buttons']['moderate']['sub_buttons']['reported_members']['amt'] = $context['open_member_reports']; + } + + if (!empty($context['unapproved_members']) && !empty($context['menu_buttons']['admin'])) + { + $context['menu_buttons']['admin']['sub_buttons']['memberapprove']['amt'] = $context['unapproved_members']; + $context['total_admin_reports'] += $context['unapproved_members']; + } + + if ($context['total_admin_reports'] > 0 && !empty($context['menu_buttons']['admin'])) + { + $context['menu_buttons']['admin']['amt'] = $context['total_admin_reports']; + } + + // Do we have any open reports? + if ($context['total_mod_reports'] > 0 && !empty($context['menu_buttons']['moderate'])) + { + $context['menu_buttons']['moderate']['amt'] = $context['total_mod_reports']; + } + + // Not all actions are simple. + call_integration_hook('integrate_current_action', array(&$current_action)); + + if (isset($context['menu_buttons'][$current_action])) + $context['menu_buttons'][$current_action]['active_button'] = true; +} + +/** + * Generate a random seed and ensure it's stored in settings. + */ +function smf_seed_generator() +{ + updateSettings(array('rand_seed' => microtime(true))); +} + +/** + * Process functions of an integration hook. + * calls all functions of the given hook. + * supports static class method calls. + * + * @param string $hook The hook name + * @param array $parameters An array of parameters this hook implements + * @return array The results of the functions + */ +function call_integration_hook($hook, $parameters = array()) +{ + global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug; + global $context, $txt; + + if ($db_show_debug === true) + $context['debug']['hooks'][] = $hook; + + // Need to have some control. + if (!isset($context['instances'])) + $context['instances'] = array(); + + $results = array(); + if (empty($modSettings[$hook])) + return $results; + + $functions = explode(',', $modSettings[$hook]); + // Loop through each function. + foreach ($functions as $function) + { + // Hook has been marked as "disabled". Skip it! + if (strpos($function, '!') !== false) + continue; + + $call = call_helper($function, true); + + // Is it valid? + if (!empty($call)) + $results[$function] = call_user_func_array($call, $parameters); + // This failed, but we want to do so silently. + elseif (!empty($function) && !empty($context['ignore_hook_errors'])) + return $results; + // Whatever it was suppose to call, it failed :( + elseif (!empty($function)) + { + loadLanguage('Errors'); + + // Get a full path to show on error. + if (strpos($function, '|') !== false) + { + list ($file, $string) = explode('|', $function); + $absPath = empty($settings['theme_dir']) ? (strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir))) : (strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']))); + log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general'); + } + // "Assume" the file resides on $boarddir somewhere... + else + log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general'); + } + } + + return $results; +} + +/** + * Add a function for integration hook. + * Does nothing if the function is already added. + * Cleans up enabled/disabled variants before taking requested action. + * + * @param string $hook The complete hook name. + * @param string $function The function name. Can be a call to a method via Class::method. + * @param bool $permanent If true, updates the value in settings table. + * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php + * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method. + */ +function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false) +{ + global $smcFunc, $modSettings, $context; + + // Any objects? + if ($object) + $function = $function . '#'; + + // Any files to load? + if (!empty($file) && is_string($file)) + $function = $file . (!empty($function) ? '|' . $function : ''); + + // Get the correct string. + $integration_call = $function; + $enabled_call = rtrim($function, '!'); + $disabled_call = $enabled_call . '!'; + + // Is it going to be permanent? + if ($permanent) + { + $request = $smcFunc['db_query']('', ' + SELECT value + FROM {db_prefix}settings + WHERE variable = {string:variable}', + array( + 'variable' => $hook, + ) + ); + list ($current_functions) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + if (!empty($current_functions)) + { + $current_functions = explode(',', $current_functions); + + // Cleanup enabled/disabled variants before taking action. + $current_functions = array_diff($current_functions, array($enabled_call, $disabled_call)); + + $permanent_functions = array_unique(array_merge($current_functions, array($integration_call))); + } + else + $permanent_functions = array($integration_call); + + updateSettings(array($hook => implode(',', $permanent_functions))); + } + + // Make current function list usable. + $functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]); + + // Cleanup enabled/disabled variants before taking action. + $functions = array_diff($functions, array($enabled_call, $disabled_call)); + + $functions = array_unique(array_merge($functions, array($integration_call))); + $modSettings[$hook] = implode(',', $functions); + + // It is handy to be able to know which hooks are temporary... + if ($permanent !== true) + { + if (!isset($context['integration_hooks_temporary'])) + $context['integration_hooks_temporary'] = array(); + $context['integration_hooks_temporary'][$hook][$function] = true; + } +} + +/** + * Remove an integration hook function. + * Removes the given function from the given hook. + * Does nothing if the function is not available. + * Cleans up enabled/disabled variants before taking requested action. + * + * @param string $hook The complete hook name. + * @param string $function The function name. Can be a call to a method via Class::method. + * @param boolean $permanent Irrelevant for the function itself but need to declare it to match + * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php + * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method. + * @see add_integration_function + */ +function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false) +{ + global $smcFunc, $modSettings; + + // Any objects? + if ($object) + $function = $function . '#'; + + // Any files to load? + if (!empty($file) && is_string($file)) + $function = $file . '|' . $function; + + // Get the correct string. + $integration_call = $function; + $enabled_call = rtrim($function, '!'); + $disabled_call = $enabled_call . '!'; + + // Get the permanent functions. + $request = $smcFunc['db_query']('', ' + SELECT value + FROM {db_prefix}settings + WHERE variable = {string:variable}', + array( + 'variable' => $hook, + ) + ); + list ($current_functions) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + if (!empty($current_functions)) + { + $current_functions = explode(',', $current_functions); + + // Cleanup enabled and disabled variants. + $current_functions = array_unique(array_diff($current_functions, array($enabled_call, $disabled_call))); + + updateSettings(array($hook => implode(',', $current_functions))); + } + + // Turn the function list into something usable. + $functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]); + + // Cleanup enabled and disabled variants. + $functions = array_unique(array_diff($functions, array($enabled_call, $disabled_call))); + + $modSettings[$hook] = implode(',', $functions); +} + +/** + * Receives a string and tries to figure it out if its a method or a function. + * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class. + * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array. + * Prepare and returns a callable depending on the type of method/function found. + * + * @param mixed $string The string containing a function name or a static call. The function can also accept a closure, object or a callable array (object/class, valid_callable) + * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string. + * @return string|array|boolean Either a string or an array that contains a callable function name or an array with a class and method to call. Boolean false if the given string cannot produce a callable var. + */ +function call_helper($string, $return = false) +{ + global $context, $smcFunc, $txt, $db_show_debug; + + // Really? + if (empty($string)) + return false; + + // An array? should be a "callable" array IE array(object/class, valid_callable). + // A closure? should be a callable one. + if (is_array($string) || $string instanceof Closure) + return $return ? $string : (is_callable($string) ? call_user_func($string) : false); + + // No full objects, sorry! pass a method or a property instead! + if (is_object($string)) + return false; + + // Stay vitaminized my friends... + $string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string)); + + // Is there a file to load? + $string = load_file($string); + + // Loaded file failed + if (empty($string)) + return false; + + // Found a method. + if (strpos($string, '::') !== false) + { + list ($class, $method) = explode('::', $string); + + // Check if a new object will be created. + if (strpos($method, '#') !== false) + { + // Need to remove the # thing. + $method = str_replace('#', '', $method); + + // Don't need to create a new instance for every method. + if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class)) + { + $context['instances'][$class] = new $class; + + // Add another one to the list. + if ($db_show_debug === true) + { + if (!isset($context['debug']['instances'])) + $context['debug']['instances'] = array(); + + $context['debug']['instances'][$class] = $class; + } + } + + $func = array($context['instances'][$class], $method); + } + + // Right then. This is a call to a static method. + else + $func = array($class, $method); + } + + // Nope! just a plain regular function. + else + $func = $string; + + // We can't call this helper, but we want to silently ignore this. + if (!is_callable($func, false, $callable_name) && !empty($context['ignore_hook_errors'])) + return false; + + // Right, we got what we need, time to do some checks. + elseif (!is_callable($func, false, $callable_name)) + { + loadLanguage('Errors'); + log_error(sprintf($txt['sub_action_fail'], $callable_name), 'general'); + + // Gotta tell everybody. + return false; + } + + // Everything went better than expected. + else + { + // What are we gonna do about it? + if ($return) + return $func; + + // If this is a plain function, avoid the heat of calling call_user_func(). + else + { + if (is_array($func)) + call_user_func($func); + + else + $func(); + } + } +} + +/** + * Receives a string and tries to figure it out if it contains info to load a file. + * Checks for a | (pipe) symbol and tries to load a file with the info given. + * The string should be format as follows File.php|. You can use the following wildcards: $boarddir, $sourcedir and if available at the moment of execution, $themedir. + * + * @param string $string The string containing a valid format. + * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded. + */ +function load_file($string) +{ + global $sourcedir, $txt, $boarddir, $settings, $context; + + if (empty($string)) + return false; + + if (strpos($string, '|') !== false) + { + list ($file, $string) = explode('|', $string); + + // Match the wildcards to their regular vars. + if (empty($settings['theme_dir'])) + $absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir)); + + else + $absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir'])); + + // Load the file if it can be loaded. + if (file_exists($absPath)) + require_once($absPath); + + // No? try a fallback to $sourcedir + else + { + $absPath = $sourcedir . '/' . $file; + + if (file_exists($absPath)) + require_once($absPath); + + // Sorry, can't do much for you at this point. + elseif (empty($context['uninstalling'])) + { + loadLanguage('Errors'); + log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general'); + + // File couldn't be loaded. + return false; + } + } + } + + return $string; +} + +/** + * Get the contents of a URL, irrespective of allow_url_fopen. + * + * - reads the contents of an http or ftp address and returns the page in a string + * - will accept up to 3 page redirections (redirectio_level in the function call is private) + * - if post_data is supplied, the value and length is posted to the given url as form data + * - URL must be supplied in lowercase + * + * @param string $url The URL + * @param string $post_data The data to post to the given URL + * @param bool $keep_alive Whether to send keepalive info + * @param int $redirection_level How many levels of redirection + * @return string|false The fetched data or false on failure + */ +function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 0) +{ + global $webmaster_email, $sourcedir, $txt; + static $keep_alive_dom = null, $keep_alive_fp = null; + + preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', iri_to_url($url), $match); + + // No scheme? No data for you! + if (empty($match[1])) + return false; + + // An FTP url. We should try connecting and RETRieving it... + elseif ($match[1] == 'ftp') + { + // Include the file containing the ftp_connection class. + require_once($sourcedir . '/Class-Package.php'); + + // Establish a connection and attempt to enable passive mode. + $ftp = new ftp_connection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email); + if ($ftp->error !== false || !$ftp->passive()) + return false; + + // I want that one *points*! + fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n"); + + // Since passive mode worked (or we would have returned already!) open the connection. + $fp = @fsockopen($ftp->pasv['ip'], $ftp->pasv['port'], $err, $err, 5); + if (!$fp) + return false; + + // The server should now say something in acknowledgement. + $ftp->check_response(150); + + $data = ''; + while (!feof($fp)) + $data .= fread($fp, 4096); + fclose($fp); + + // All done, right? Good. + $ftp->check_response(226); + $ftp->close(); + } + + // This is more likely; a standard HTTP URL. + elseif (isset($match[1]) && $match[1] == 'http') + { + // First try to use fsockopen, because it is fastest. + if ($keep_alive && $match[3] == $keep_alive_dom) + $fp = $keep_alive_fp; + if (empty($fp)) + { + // Open the socket on the port we want... + $fp = @fsockopen(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? ($match[2] ? 443 : 80) : $match[5], $err, $err, 5); + } + if (!empty($fp)) + { + if ($keep_alive) + { + $keep_alive_dom = $match[3]; + $keep_alive_fp = $fp; + } + + // I want this, from there, and I'm not going to be bothering you for more (probably.) + if (empty($post_data)) + { + fwrite($fp, 'GET ' . ($match[6] !== '/' ? str_replace(' ', '%20', $match[6]) : '') . ' HTTP/1.0' . "\r\n"); + fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n"); + fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n"); + if ($keep_alive) + fwrite($fp, 'connection: Keep-Alive' . "\r\n\r\n"); + else + fwrite($fp, 'connection: close' . "\r\n\r\n"); + } + else + { + fwrite($fp, 'POST ' . ($match[6] !== '/' ? $match[6] : '') . ' HTTP/1.0' . "\r\n"); + fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n"); + fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n"); + if ($keep_alive) + fwrite($fp, 'connection: Keep-Alive' . "\r\n"); + else + fwrite($fp, 'connection: close' . "\r\n"); + fwrite($fp, 'content-type: application/x-www-form-urlencoded' . "\r\n"); + fwrite($fp, 'content-length: ' . strlen($post_data) . "\r\n\r\n"); + fwrite($fp, $post_data); + } + + $response = fgets($fp, 768); + + // Redirect in case this location is permanently or temporarily moved. + if ($redirection_level < 3 && preg_match('~^HTTP/\S+\s+30[127]~i', $response) === 1) + { + $header = ''; + $location = ''; + while (!feof($fp) && trim($header = fgets($fp, 4096)) != '') + if (stripos($header, 'location:') !== false) + $location = trim(substr($header, strpos($header, ':') + 1)); + + if (empty($location)) + return false; + else + { + if (!$keep_alive) + fclose($fp); + return fetch_web_data($location, $post_data, $keep_alive, $redirection_level + 1); + } + } + + // Make sure we get a 200 OK. + elseif (preg_match('~^HTTP/\S+\s+20[01]~i', $response) === 0) + return false; + + // Skip the headers... + while (!feof($fp) && trim($header = fgets($fp, 4096)) != '') + { + if (preg_match('~content-length:\s*(\d+)~i', $header, $match) != 0) + $content_length = $match[1]; + elseif (preg_match('~connection:\s*close~i', $header) != 0) + { + $keep_alive_dom = null; + $keep_alive = false; + } + + continue; + } + + $data = ''; + if (isset($content_length)) + { + while (!feof($fp) && strlen($data) < $content_length) + $data .= fread($fp, $content_length - strlen($data)); + } + else + { + while (!feof($fp)) + $data .= fread($fp, 4096); + } + + if (!$keep_alive) + fclose($fp); + } + + // If using fsockopen didn't work, try to use cURL if available. + elseif (function_exists('curl_init')) + { + // Include the file containing the curl_fetch_web_data class. + require_once($sourcedir . '/Class-CurlFetchWeb.php'); + + $fetch_data = new curl_fetch_web_data(); + $fetch_data->get_url_data($url, $post_data); + + // no errors and a 200 result, then we have a good dataset, well we at least have data. ;) + if ($fetch_data->result('code') == 200 && !$fetch_data->result('error')) + $data = $fetch_data->result('body'); + else + return false; + } + + // Neither fsockopen nor curl are available. Well, phooey. + else + return false; + } + else + { + // Umm, this shouldn't happen? + loadLanguage('Errors'); + trigger_error($txt['fetch_web_data_bad_url'], E_USER_NOTICE); + $data = false; + } + + return $data; +} + +/** + * Attempts to determine the MIME type of some data or a file. + * + * @param string $data The data to check, or the path or URL of a file to check. + * @param string $is_path If true, $data is a path or URL to a file. + * @return string|bool A MIME type, or false if we cannot determine it. + */ +function get_mime_type($data, $is_path = false) +{ + global $cachedir; + + $finfo_loaded = extension_loaded('fileinfo'); + $exif_loaded = extension_loaded('exif') && function_exists('image_type_to_mime_type'); + + // Oh well. We tried. + if (!$finfo_loaded && !$exif_loaded) + return false; + + // Start with the 'empty' MIME type. + $mime_type = 'application/x-empty'; + + if ($finfo_loaded) + { + // Just some nice, simple data to analyze. + if (empty($is_path)) + $mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data); + + // A file, or maybe a URL? + else + { + // Local file. + if (file_exists($data)) + $mime_type = mime_content_type($data); + + // URL. + elseif ($data = fetch_web_data($data)) + $mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data); + } + } + // Workaround using Exif requires a local file. + else + { + // If $data is a URL to fetch, do so. + if (!empty($is_path) && !file_exists($data) && url_exists($data)) + { + $data = fetch_web_data($data); + $is_path = false; + } + + // If we don't have a local file, create one and use it. + if (empty($is_path)) + { + $temp_file = tempnam($cachedir, md5($data)); + file_put_contents($temp_file, $data); + $is_path = true; + $data = $temp_file; + } + + $imagetype = @exif_imagetype($data); + + if (isset($temp_file)) + unlink($temp_file); + + // Unfortunately, this workaround only works for image files. + if ($imagetype !== false) + $mime_type = image_type_to_mime_type($imagetype); + } + + return $mime_type; +} + +/** + * Checks whether a file or data has the expected MIME type. + * + * @param string $data The data to check, or the path or URL of a file to check. + * @param string $type_pattern A regex pattern to match the acceptable MIME types. + * @param string $is_path If true, $data is a path or URL to a file. + * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check. + */ +function check_mime_type($data, $type_pattern, $is_path = false) +{ + // Get the MIME type. + $mime_type = get_mime_type($data, $is_path); + + // Couldn't determine it. + if ($mime_type === false) + return 2; + + // Check whether the MIME type matches expectations. + return (int) @preg_match('~' . $type_pattern . '~', $mime_type); +} + +/** + * Prepares an array of "likes" info for the topic specified by $topic + * + * @param integer $topic The topic ID to fetch the info from. + * @return array An array of IDs of messages in the specified topic that the current user likes + */ +function prepareLikesContext($topic) +{ + global $user_info, $smcFunc; + + // Make sure we have something to work with. + if (empty($topic)) + return array(); + + // We already know the number of likes per message, we just want to know whether the current user liked it or not. + $user = $user_info['id']; + $cache_key = 'likes_topic_' . $topic . '_' . $user; + $ttl = 180; + + if (($temp = cache_get_data($cache_key, $ttl)) === null) + { + $temp = array(); + $request = $smcFunc['db_query']('', ' + SELECT content_id + FROM {db_prefix}user_likes AS l + INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg) + WHERE l.id_member = {int:current_user} + AND l.content_type = {literal:msg} + AND m.id_topic = {int:topic}', + array( + 'current_user' => $user, + 'topic' => $topic, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $temp[] = (int) $row['content_id']; + + cache_put_data($cache_key, $temp, $ttl); + } + + return $temp; +} + +/** + * Microsoft uses their own character set Code Page 1252 (CP1252), which is a + * superset of ISO 8859-1, defining several characters between DEC 128 and 159 + * that are not normally displayable. This converts the popular ones that + * appear from a cut and paste from windows. + * + * @param string $string The string + * @return string The sanitized string + */ +function sanitizeMSCutPaste($string) +{ + global $context; + + if (empty($string)) + return $string; + + // UTF-8 occurences of MS special characters + $findchars_utf8 = array( + "\xe2\x80\x9a", // single low-9 quotation mark + "\xe2\x80\x9e", // double low-9 quotation mark + "\xe2\x80\xa6", // horizontal ellipsis + "\xe2\x80\x98", // left single curly quote + "\xe2\x80\x99", // right single curly quote + "\xe2\x80\x9c", // left double curly quote + "\xe2\x80\x9d", // right double curly quote + ); + + // windows 1252 / iso equivalents + $findchars_iso = array( + chr(130), + chr(132), + chr(133), + chr(145), + chr(146), + chr(147), + chr(148), + ); + + // safe replacements + $replacechars = array( + ',', // ‚ + ',,', // „ + '...', // … + "'", // ‘ + "'", // ’ + '"', // “ + '"', // ” + ); + + if ($context['utf8']) + $string = str_replace($findchars_utf8, $replacechars, $string); + else + $string = str_replace($findchars_iso, $replacechars, $string); + + return $string; +} + +/** + * Decode numeric html entities to their ascii or UTF8 equivalent character. + * + * Callback function for preg_replace_callback in subs-members + * Uses capture group 2 in the supplied array + * Does basic scan to ensure characters are inside a valid range + * + * @param array $matches An array of matches (relevant info should be the 3rd item) + * @return string A fixed string + */ +function replaceEntities__callback($matches) +{ + global $context; + + if (!isset($matches[2])) + return ''; + + $num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2]; + + // remove left to right / right to left overrides + if ($num === 0x202D || $num === 0x202E) + return ''; + + // Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced + if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E))) + return '&#' . $num . ';'; + + if (empty($context['utf8'])) + { + // no control characters + if ($num < 0x20) + return ''; + // text is text + elseif ($num < 0x80) + return chr($num); + // all others get html-ised + else + return '&#' . $matches[2] . ';'; + } + else + { + // <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set + // 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text) + if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF)) + return ''; + // <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation + elseif ($num < 0x80) + return chr($num); + // <0x800 (2048) + elseif ($num < 0x800) + return chr(($num >> 6) + 192) . chr(($num & 63) + 128); + // < 0x10000 (65536) + elseif ($num < 0x10000) + return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128); + // <= 0x10FFFF (1114111) + else + return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128); + } +} + +/** + * Converts html entities to utf8 equivalents + * + * Callback function for preg_replace_callback + * Uses capture group 1 in the supplied array + * Does basic checks to keep characters inside a viewable range. + * + * @param array $matches An array of matches (relevant info should be the 2nd item in the array) + * @return string The fixed string + */ +function fixchar__callback($matches) +{ + if (!isset($matches[1])) + return ''; + + $num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1]; + + // <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set + // 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides + if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E) + return ''; + // <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation + elseif ($num < 0x80) + return chr($num); + // <0x800 (2048) + elseif ($num < 0x800) + return chr(($num >> 6) + 192) . chr(($num & 63) + 128); + // < 0x10000 (65536) + elseif ($num < 0x10000) + return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128); + // <= 0x10FFFF (1114111) + else + return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128); +} + +/** + * Strips out invalid html entities, replaces others with html style { codes + * + * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example + * strpos, strlen, substr etc + * + * @param array $matches An array of matches (relevant info should be the 3rd item in the array) + * @return string The fixed string + */ +function entity_fix__callback($matches) +{ + if (!isset($matches[2])) + return ''; + + $num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2]; + + // we don't allow control characters, characters out of range, byte markers, etc + if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E) + return ''; + else + return '&#' . $num . ';'; +} + +/** + * Return a Gravatar URL based on + * - the supplied email address, + * - the global maximum rating, + * - the global default fallback, + * - maximum sizes as set in the admin panel. + * + * It is SSL aware, and caches most of the parameters. + * + * @param string $email_address The user's email address + * @return string The gravatar URL + */ +function get_gravatar_url($email_address) +{ + global $modSettings, $smcFunc; + static $url_params = null; + + if ($url_params === null) + { + $ratings = array('G', 'PG', 'R', 'X'); + $defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank'); + $url_params = array(); + if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings)) + $url_params[] = 'rating=' . $modSettings['gravatarMaxRating']; + if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults)) + $url_params[] = 'default=' . $modSettings['gravatarDefault']; + if (!empty($modSettings['avatar_max_width_external'])) + $size_string = (int) $modSettings['avatar_max_width_external']; + if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string)) + if ((int) $modSettings['avatar_max_height_external'] < $size_string) + $size_string = $modSettings['avatar_max_height_external']; + + if (!empty($size_string)) + $url_params[] = 's=' . $size_string; + } + $http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www'; + + return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params); +} + +/** + * Get a list of time zones. + * + * @param string $when The date/time for which to calculate the time zone values. + * May be a Unix timestamp or any string that strtotime() can understand. + * Defaults to 'now'. + * @return array An array of time zone identifiers and label text. + */ +function smf_list_timezones($when = 'now') +{ + global $modSettings, $tztxt, $txt, $context, $cur_profile, $sourcedir; + static $timezones_when = array(); + + require_once($sourcedir . '/Subs-Timezones.php'); + + // Parseable datetime string? + if (is_int($timestamp = strtotime($when))) + $when = $timestamp; + + // A Unix timestamp? + elseif (is_numeric($when)) + $when = intval($when); + + // Invalid value? Just get current Unix timestamp. + else + $when = time(); + + // No point doing this over if we already did it once + if (isset($timezones_when[$when])) + return $timezones_when[$when]; + + // We'll need these too + $date_when = date_create('@' . $when); + $later = strtotime('@' . $when . ' + 1 year'); + + // Load up any custom time zone descriptions we might have + loadLanguage('Timezones'); + + $tzid_metazones = get_tzid_metazones($later); + + // Should we put time zones from certain countries at the top of the list? + $priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array(); + + $priority_tzids = array(); + foreach ($priority_countries as $country) + { + $country_tzids = get_sorted_tzids_for_country($country); + + if (!empty($country_tzids)) + $priority_tzids = array_merge($priority_tzids, $country_tzids); + } + + // Antarctic research stations should be listed last, unless you're running a penguin forum + $low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array(); + + $normal_priority_tzids = array_diff(array_unique(array_merge(array_keys($tzid_metazones), timezone_identifiers_list())), $priority_tzids, $low_priority_tzids); + + // Process them in order of importance. + $tzids = array_merge($priority_tzids, $normal_priority_tzids, $low_priority_tzids); + + // Idea here is to get exactly one representative identifier for each and every unique set of time zone rules. + $dst_types = array(); + $labels = array(); + $offsets = array(); + foreach ($tzids as $tzid) + { + // We don't want UTC right now + if ($tzid == 'UTC') + continue; + + $tz = @timezone_open($tzid); + + if ($tz == null) + continue; + + // First, get the set of transition rules for this tzid + $tzinfo = timezone_transitions_get($tz, $when, $later); + + // Use the entire set of transition rules as the array *key* so we can avoid duplicates + $tzkey = serialize($tzinfo); + + // ...But make sure to include all explicitly defined meta-zones. + if (isset($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid])) + $tzkey = serialize(array_merge($tzinfo, array('metazone' => $tzid_metazones[$tzid]))); + + // Don't overwrite our preferred tzids + if (empty($zones[$tzkey]['tzid'])) + { + $zones[$tzkey]['tzid'] = $tzid; + $zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0); + + foreach ($tzinfo as $transition) { + $zones[$tzkey]['abbrs'][] = $transition['abbr']; + } + + if (isset($tzid_metazones[$tzid])) + $zones[$tzkey]['metazone'] = $tzid_metazones[$tzid]; + else + { + $tzgeo = timezone_location_get($tz); + $country_tzids = get_sorted_tzids_for_country($tzgeo['country_code']); + + if (count($country_tzids) === 1) + $zones[$tzkey]['metazone'] = $txt['iso3166'][$tzgeo['country_code']]; + } + } + + // A time zone from a prioritized country? + if (in_array($tzid, $priority_tzids)) + $priority_zones[$tzkey] = true; + + // Keep track of the location for this tzid. + if (!empty($txt[$tzid])) + $zones[$tzkey]['locations'][] = $txt[$tzid]; + else + { + $tzid_parts = explode('/', $tzid); + $zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts)); + } + + // Keep track of the current offset for this tzid. + $offsets[$tzkey] = $tzinfo[0]['offset']; + + // Keep track of the Standard Time offset for this tzid. + foreach ($tzinfo as $transition) + { + if (!$transition['isdst']) + { + $std_offsets[$tzkey] = $transition['offset']; + break; + } + } + if (!isset($std_offsets[$tzkey])) + $std_offsets[$tzkey] = $tzinfo[0]['offset']; + + // Figure out the "meta-zone" info for the label + if (empty($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid])) + { + $zones[$tzkey]['metazone'] = $tzid_metazones[$tzid]; + $zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0); + } + $dst_types[$tzkey] = count($tzinfo) > 1 ? 'c' : ($tzinfo[0]['isdst'] ? 't' : 'f'); + $labels[$tzkey] = !empty($zones[$tzkey]['metazone']) && !empty($tztxt[$zones[$tzkey]['metazone']]) ? $tztxt[$zones[$tzkey]['metazone']] : ''; + + // Remember this for later + if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid) + $member_tzkey = $tzkey; + if (isset($context['event']['tz']) && $context['event']['tz'] == $tzid) + $event_tzkey = $tzkey; + if ($modSettings['default_timezone'] == $tzid) + $default_tzkey = $tzkey; + } + + // Sort by current offset, then standard offset, then DST type, then label. + array_multisort($offsets, SORT_DESC, SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones); + + // Build the final array of formatted values + $priority_timezones = array(); + $timezones = array(); + foreach ($zones as $tzkey => $tzvalue) + { + date_timezone_set($date_when, timezone_open($tzvalue['tzid'])); + + // Use the human friendly time zone name, if there is one. + $desc = ''; + if (!empty($tzvalue['metazone'])) + { + if (!empty($tztxt[$tzvalue['metazone']])) + $metazone = $tztxt[$tzvalue['metazone']]; + else + $metazone = sprintf($tztxt['generic_timezone'], $tzvalue['metazone'], '%1$s'); + + switch ($tzvalue['dst_type']) + { + case 0: + $desc = sprintf($metazone, $tztxt['daylight_saving_time_false']); + break; + + case 1: + $desc = sprintf($metazone, ''); + break; + + case 2: + $desc = sprintf($metazone, $tztxt['daylight_saving_time_true']); + break; + } + } + // Otherwise, use the list of locations (max 5, so things don't get silly) + else + $desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : ''); + + // We don't want abbreviations like '+03' or '-11'. + $abbrs = array_filter( + $tzvalue['abbrs'], + function ($abbr) + { + return !strspn($abbr, '+-'); + } + ); + $abbrs = count($abbrs) == count($tzvalue['abbrs']) ? array_unique($abbrs) : array(); + + // Show the UTC offset and abbreviation(s). + $desc = '[UTC' . date_format($date_when, 'P') . '] - ' . str_replace(' ', ' ', $desc) . (!empty($abbrs) ? ' (' . implode('/', $abbrs) . ')' : ''); + + if (isset($priority_zones[$tzkey])) + $priority_timezones[$tzvalue['tzid']] = $desc; + else + $timezones[$tzvalue['tzid']] = $desc; + + // Automatically fix orphaned time zones. + if (isset($member_tzkey) && $member_tzkey == $tzkey) + $cur_profile['timezone'] = $tzvalue['tzid']; + if (isset($event_tzkey) && $event_tzkey == $tzkey) + $context['event']['tz'] = $tzvalue['tzid']; + if (isset($default_tzkey) && $default_tzkey == $tzkey && $modSettings['default_timezone'] != $tzvalue['tzid']) + updateSettings(array('default_timezone' => $tzvalue['tzid'])); + } + + if (!empty($priority_timezones)) + $priority_timezones[] = '-----'; + + $timezones = array_merge( + $priority_timezones, + array('UTC' => 'UTC' . (!empty($tztxt['UTC']) ? ' - ' . $tztxt['UTC'] : ''), '-----'), + $timezones + ); + + $timezones_when[$when] = $timezones; + + return $timezones_when[$when]; +} + +/** + * Gets a member's selected time zone identifier + * + * @param int $id_member The member id to look up. If not provided, the current user's id will be used. + * @return string The time zone identifier string for the user's time zone. + */ +function getUserTimezone($id_member = null) +{ + global $smcFunc, $user_info, $modSettings, $user_settings; + static $member_cache = array(); + + if (is_null($id_member)) + $id_member = empty($user_info['id']) ? 0 : (int) $user_info['id']; + else + $id_member = (int) $id_member; + + // Did we already look this up? + if (isset($member_cache[$id_member])) + return $member_cache[$id_member]; + + // Check if we already have this in $user_settings. + if (isset($user_settings['id_member']) && $user_settings['id_member'] == $id_member && !empty($user_settings['timezone'])) + { + $member_cache[$id_member] = $user_settings['timezone']; + return $user_settings['timezone']; + } + + if (!empty($id_member)) + { + // Look it up in the database. + $request = $smcFunc['db_query']('', ' + SELECT timezone + FROM {db_prefix}members + WHERE id_member = {int:id_member}', + array( + 'id_member' => $id_member, + ) + ); + list($timezone) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + } + + // If it is invalid, fall back to the default. + if (empty($timezone) || !in_array($timezone, timezone_identifiers_list(DateTimeZone::ALL_WITH_BC))) + $timezone = isset($modSettings['default_timezone']) ? $modSettings['default_timezone'] : date_default_timezone_get(); + + // Save for later. + $member_cache[$id_member] = $timezone; + + return $timezone; +} + +/** + * Converts an IP address into binary + * + * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation + * @return string|false The IP address in binary or false + */ +function inet_ptod($ip_address) +{ + if (!isValidIP($ip_address)) + return $ip_address; + + $bin = inet_pton($ip_address); + return $bin; +} + +/** + * Converts a binary version of an IP address into a readable format + * + * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases)) + * @return string|false The IP address in presentation format or false on error + */ +function inet_dtop($bin) +{ + global $db_type; + + if (empty($bin)) + return ''; + elseif ($db_type == 'postgresql') + return $bin; + // Already a String? + elseif (isValidIP($bin)) + return $bin; + return inet_ntop($bin); +} + +/** + * Safe serialize() and unserialize() replacements + * + * @license Public Domain + * + * @author anthon (dot) pang (at) gmail (dot) com + */ + +/** + * Safe serialize() replacement. Recursive + * - output a strict subset of PHP's native serialized representation + * - does not serialize objects + * + * @param mixed $value + * @return string + */ +function _safe_serialize($value) +{ + if (is_null($value)) + return 'N;'; + + if (is_bool($value)) + return 'b:' . (int) $value . ';'; + + if (is_int($value)) + return 'i:' . $value . ';'; + + if (is_float($value)) + return 'd:' . str_replace(',', '.', $value) . ';'; + + if (is_string($value)) + return 's:' . strlen($value) . ':"' . $value . '";'; + + if (is_array($value)) + { + // Check for nested objects or resources. + $contains_invalid = false; + array_walk_recursive( + $value, + function($v) use (&$contains_invalid) + { + if (is_object($v) || is_resource($v)) + $contains_invalid = true; + } + ); + if ($contains_invalid) + return false; + + $out = ''; + foreach ($value as $k => $v) + $out .= _safe_serialize($k) . _safe_serialize($v); + + return 'a:' . count($value) . ':{' . $out . '}'; + } + + // safe_serialize cannot serialize resources or objects. + return false; +} + +/** + * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues. + * + * @param mixed $value + * @return string + */ +function safe_serialize($value) +{ + // Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen() + if (function_exists('mb_internal_encoding') && + (((int) ini_get('mbstring.func_overload')) & 2)) + { + $mbIntEnc = mb_internal_encoding(); + mb_internal_encoding('ASCII'); + } + + $out = _safe_serialize($value); + + if (isset($mbIntEnc)) + mb_internal_encoding($mbIntEnc); + + return $out; +} + +/** + * Safe unserialize() replacement + * - accepts a strict subset of PHP's native serialized representation + * - does not unserialize objects + * + * @param string $str + * @return mixed + * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects) + */ +function _safe_unserialize($str) +{ + // Input is not a string. + if (empty($str) || !is_string($str)) + return false; + + // The substring 'O:' is used to serialize objects. + // If it is not present, then there are none in the serialized data. + if (strpos($str, 'O:') === false) + return unserialize($str); + + $stack = array(); + $expected = array(); + + /* + * states: + * 0 - initial state, expecting a single value or array + * 1 - terminal state + * 2 - in array, expecting end of array or a key + * 3 - in array, expecting value or another array + */ + $state = 0; + while ($state != 1) + { + $type = isset($str[0]) ? $str[0] : ''; + if ($type == '}') + $str = substr($str, 1); + + elseif ($type == 'N' && $str[1] == ';') + { + $value = null; + $str = substr($str, 2); + } + elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches)) + { + $value = $matches[1] == '1' ? true : false; + $str = substr($str, 4); + } + elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches)) + { + $value = (int) $matches[1]; + $str = $matches[2]; + } + elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches)) + { + $value = (float) $matches[1]; + $str = $matches[3]; + } + elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";') + { + $value = substr($matches[2], 0, (int) $matches[1]); + $str = substr($matches[2], (int) $matches[1] + 2); + } + elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches)) + { + $expectedLength = (int) $matches[1]; + $str = $matches[2]; + } + + // Object or unknown/malformed type. + else + return false; + + switch ($state) + { + case 3: // In array, expecting value or another array. + if ($type == 'a') + { + $stack[] = &$list; + $list[$key] = array(); + $list = &$list[$key]; + $expected[] = $expectedLength; + $state = 2; + break; + } + if ($type != '}') + { + $list[$key] = $value; + $state = 2; + break; + } + + // Missing array value. + return false; + + case 2: // in array, expecting end of array or a key + if ($type == '}') + { + // Array size is less than expected. + if (count($list) < end($expected)) + return false; + + unset($list); + $list = &$stack[count($stack) - 1]; + array_pop($stack); + + // Go to terminal state if we're at the end of the root array. + array_pop($expected); + + if (count($expected) == 0) + $state = 1; + + break; + } + + if ($type == 'i' || $type == 's') + { + // Array size exceeds expected length. + if (count($list) >= end($expected)) + return false; + + $key = $value; + $state = 3; + break; + } + + // Illegal array index type. + return false; + + // Expecting array or value. + case 0: + if ($type == 'a') + { + $data = array(); + $list = &$data; + $expected[] = $expectedLength; + $state = 2; + break; + } + + if ($type != '}') + { + $data = $value; + $state = 1; + break; + } + + // Not in array. + return false; + } + } + + // Trailing data in input. + if (!empty($str)) + return false; + + return $data; +} + +/** + * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue + * + * @param string $str + * @return mixed + */ +function safe_unserialize($str) +{ + // Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen() + if (function_exists('mb_internal_encoding') && + (((int) ini_get('mbstring.func_overload')) & 0x02)) + { + $mbIntEnc = mb_internal_encoding(); + mb_internal_encoding('ASCII'); + } + + $out = _safe_unserialize($str); + + if (isset($mbIntEnc)) + mb_internal_encoding($mbIntEnc); + + return $out; +} + +/** + * Tries different modes to make file/dirs writable. Wrapper function for chmod() + * + * @param string $file The file/dir full path. + * @param int $value Not needed, added for legacy reasons. + * @return boolean true if the file/dir is already writable or the function was able to make it writable, false if the function couldn't make the file/dir writable. + */ +function smf_chmod($file, $value = 0) +{ + // No file? no checks! + if (empty($file)) + return false; + + // Already writable? + if (is_writable($file)) + return true; + + // Do we have a file or a dir? + $isDir = is_dir($file); + $isWritable = false; + + // Set different modes. + $chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666); + + foreach ($chmodValues as $val) + { + // If it's writable, break out of the loop. + if (is_writable($file)) + { + $isWritable = true; + break; + } + + else + @chmod($file, $val); + } + + return $isWritable; +} + +/** + * Wrapper function for json_decode() with error handling. + * + * @param string $json The string to decode. + * @param bool $returnAsArray To return the decoded string as an array or an object, SMF only uses Arrays but to keep on compatibility with json_decode its set to false as default. + * @param bool $logIt To specify if the error will be logged if theres any. + * @return array Either an empty array or the decoded data as an array. + */ +function smf_json_decode($json, $returnAsArray = false, $logIt = true) +{ + global $txt; + + // Come on... + if (empty($json) || !is_string($json)) + return array(); + + $returnArray = @json_decode($json, $returnAsArray); + + // PHP 5.3 so no json_last_error_msg() + switch (json_last_error()) + { + case JSON_ERROR_NONE: + $jsonError = false; + break; + case JSON_ERROR_DEPTH: + $jsonError = 'JSON_ERROR_DEPTH'; + break; + case JSON_ERROR_STATE_MISMATCH: + $jsonError = 'JSON_ERROR_STATE_MISMATCH'; + break; + case JSON_ERROR_CTRL_CHAR: + $jsonError = 'JSON_ERROR_CTRL_CHAR'; + break; + case JSON_ERROR_SYNTAX: + $jsonError = 'JSON_ERROR_SYNTAX'; + break; + case JSON_ERROR_UTF8: + $jsonError = 'JSON_ERROR_UTF8'; + break; + default: + $jsonError = 'unknown'; + break; + } + + // Something went wrong! + if (!empty($jsonError) && $logIt) + { + // Being a wrapper means we lost our smf_error_handler() privileges :( + $jsonDebug = debug_backtrace(); + $jsonDebug = $jsonDebug[0]; + loadLanguage('Errors'); + + if (!empty($jsonDebug)) + log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']); + + else + log_error($txt['json_' . $jsonError], 'critical'); + + // Everyone expects an array. + return array(); + } + + return $returnArray; +} + +/** + * Check the given String if he is a valid IPv4 or IPv6 + * return true or false + * + * @param string $IPString + * + * @return bool + */ +function isValidIP($IPString) +{ + return filter_var($IPString, FILTER_VALIDATE_IP) !== false; +} + +/** + * Outputs a response. + * It assumes the data is already a string. + * + * @param string $data The data to print + * @param string $type The content type. Defaults to Json. + * @return void + */ +function smf_serverResponse($data = '', $type = 'content-type: application/json') +{ + global $db_show_debug, $modSettings; + + // Defensive programming anyone? + if (empty($data)) + return false; + + // Don't need extra stuff... + $db_show_debug = false; + + // Kill anything else. + ob_end_clean(); + + if (!empty($modSettings['enableCompressedOutput'])) + @ob_start('ob_gzhandler'); + else + ob_start(); + + // Set the header. + header($type); + + // Echo! + echo $data; + + // Done. + obExit(false); +} + +/** + * Creates an optimized regex to match all known top level domains. + * + * The optimized regex is stored in $modSettings['tld_regex']. + * + * To update the stored version of the regex to use the latest list of valid + * TLDs from iana.org, set the $update parameter to true. Updating can take some + * time, based on network connectivity, so it should normally only be done by + * calling this function from a background or scheduled task. + * + * If $update is not true, but the regex is missing or invalid, the regex will + * be regenerated from a hard-coded list of TLDs. This regenerated regex will be + * overwritten on the next scheduled update. + * + * @param bool $update If true, fetch and process the latest official list of TLDs from iana.org. + */ +function set_tld_regex($update = false) +{ + global $sourcedir, $smcFunc, $modSettings; + static $done = false; + + // If we don't need to do anything, don't + if (!$update && $done) + return; + + // Should we get a new copy of the official list of TLDs? + if ($update) + { + $tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt'); + $tlds_md5 = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt.md5'); + + /** + * If the Internet Assigned Numbers Authority can't be reached, the Internet is GONE! + * We're probably running on a server hidden in a bunker deep underground to protect + * it from marauding bandits roaming on the surface. We don't want to waste precious + * electricity on pointlessly repeating background tasks, so we'll wait until the next + * regularly scheduled update to see if civilization has been restored. + */ + if ($tlds === false || $tlds_md5 === false) + $postapocalypticNightmare = true; + + // Make sure nothing went horribly wrong along the way. + if (md5($tlds) != substr($tlds_md5, 0, 32)) + $tlds = array(); + } + // If we aren't updating and the regex is valid, we're done + elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', '') !== false) + { + $done = true; + return; + } + + // If we successfully got an update, process the list into an array + if (!empty($tlds)) + { + // Clean $tlds and convert it to an array + $tlds = array_filter( + explode("\n", strtolower($tlds)), + function($line) + { + $line = trim($line); + if (empty($line) || strlen($line) != strspn($line, 'abcdefghijklmnopqrstuvwxyz0123456789-')) + return false; + else + return true; + } + ); + + // Convert Punycode to Unicode + if (!function_exists('idn_to_utf8')) + require_once($sourcedir . '/Subs-Compat.php'); + + foreach ($tlds as &$tld) + $tld = idn_to_utf8($tld, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46); + } + // Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update + else + { + $tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz', + 'cat', 'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post', + 'pro', 'tel', 'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al', + 'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', + 'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', + 'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', + 'cn', 'co', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', + 'dz', 'ec', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', + 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', + 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', + 'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo', + 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', + 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', + 'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt', + 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl', + 'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', + 'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', + 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', + 'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', + 'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', + 'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', + 'ws', 'ye', 'yt', 'za', 'zm', 'zw', + ); + + // Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues. + if (empty($postapocalypticNightmare)) + { + $smcFunc['db_insert']('insert', '{db_prefix}background_tasks', + array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'), + array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array() + ); + } + } + + // Tack on some "special use domain names" that aren't in DNS but may possibly resolve. + // See https://www.iana.org/assignments/special-use-domain-names/ for more info. + $tlds = array_merge($tlds, array('local', 'onion', 'test')); + + // Get an optimized regex to match all the TLDs + $tld_regex = build_regex($tlds); + + // Remember the new regex in $modSettings + updateSettings(array('tld_regex' => $tld_regex)); + + // Redundant repetition is redundant + $done = true; +} + +/** + * Creates optimized regular expressions from an array of strings. + * + * An optimized regex built using this function will be much faster than a + * simple regex built using `implode('|', $strings)` --- anywhere from several + * times to several orders of magnitude faster. + * + * However, the time required to build the optimized regex is approximately + * equal to the time it takes to execute the simple regex. Therefore, it is only + * worth calling this function if the resulting regex will be used more than + * once. + * + * Because PHP places an upper limit on the allowed length of a regex, very + * large arrays of $strings may not fit in a single regex. Normally, the excess + * strings will simply be dropped. However, if the $returnArray parameter is set + * to true, this function will build as many regexes as necessary to accommodate + * everything in $strings and return them in an array. You will need to iterate + * through all elements of the returned array in order to test all possible + * matches. + * + * @param array $strings An array of strings to make a regex for. + * @param string $delim An optional delimiter character to pass to preg_quote(). + * @param bool $returnArray If true, returns an array of regexes. + * @return string|array One or more regular expressions to match any of the input strings. + */ +function build_regex($strings, $delim = null, $returnArray = false) +{ + global $smcFunc; + static $regexes = array(); + + // If it's not an array, there's not much to do. ;) + if (!is_array($strings)) + return preg_quote(@strval($strings), $delim); + + $regex_key = md5(json_encode(array($strings, $delim, $returnArray))); + + if (isset($regexes[$regex_key])) + return $regexes[$regex_key]; + + // The mb_* functions are faster than the $smcFunc ones, but may not be available + if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr')) + { + if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false) + { + $current_encoding = mb_internal_encoding(); + mb_internal_encoding($string_encoding); + } + + $strlen = 'mb_strlen'; + $substr = 'mb_substr'; + } + else + { + $strlen = $smcFunc['strlen']; + $substr = $smcFunc['substr']; + } + + // This recursive function creates the index array from the strings + $add_string_to_index = function($string, $index) use (&$strlen, &$substr, &$add_string_to_index) + { + static $depth = 0; + $depth++; + + $first = (string) @$substr($string, 0, 1); + + // No first character? That's no good. + if ($first === '') + { + // A nested array? Really? Ugh. Fine. + if (is_array($string) && $depth < 20) + { + foreach ($string as $str) + $index = $add_string_to_index($str, $index); + } + + $depth--; + return $index; + } + + if (empty($index[$first])) + $index[$first] = array(); + + if ($strlen($string) > 1) + { + // Sanity check on recursion + if ($depth > 99) + $index[$first][$substr($string, 1)] = ''; + + else + $index[$first] = $add_string_to_index($substr($string, 1), $index[$first]); + } + else + $index[$first][''] = ''; + + $depth--; + return $index; + }; + + // This recursive function turns the index array into a regular expression + $index_to_regex = function(&$index, $delim) use (&$strlen, &$index_to_regex) + { + static $depth = 0; + $depth++; + + // Absolute max length for a regex is 32768, but we might need wiggle room + $max_length = 30000; + + $regex = array(); + $length = 0; + + foreach ($index as $key => $value) + { + $key_regex = preg_quote($key, $delim); + $new_key = $key; + + if (empty($value)) + $sub_regex = ''; + else + { + $sub_regex = $index_to_regex($value, $delim); + + if (count(array_keys($value)) == 1) + { + $new_key_array = explode('(?' . '>', $sub_regex); + $new_key .= $new_key_array[0]; + } + else + $sub_regex = '(?' . '>' . $sub_regex . ')'; + } + + if ($depth > 1) + $regex[$new_key] = $key_regex . $sub_regex; + else + { + if (($length += strlen($key_regex . $sub_regex) + 1) < $max_length || empty($regex)) + { + $regex[$new_key] = $key_regex . $sub_regex; + unset($index[$key]); + } + else + break; + } + } + + // Sort by key length and then alphabetically + uksort( + $regex, + function($k1, $k2) use (&$strlen) + { + $l1 = $strlen($k1); + $l2 = $strlen($k2); + + if ($l1 == $l2) + return strcmp($k1, $k2) > 0 ? 1 : -1; + else + return $l1 > $l2 ? -1 : 1; + } + ); + + $depth--; + return implode('|', $regex); + }; + + // Now that the functions are defined, let's do this thing + $index = array(); + $regex = ''; + + foreach ($strings as $string) + $index = $add_string_to_index($string, $index); + + if ($returnArray === true) + { + $regex = array(); + while (!empty($index)) + $regex[] = '(?' . '>' . $index_to_regex($index, $delim) . ')'; + } + else + $regex = '(?' . '>' . $index_to_regex($index, $delim) . ')'; + + // Restore PHP's internal character encoding to whatever it was originally + if (!empty($current_encoding)) + mb_internal_encoding($current_encoding); + + $regexes[$regex_key] = $regex; + return $regex; +} + +/** + * Check if the passed url has an SSL certificate. + * + * Returns true if a cert was found & false if not. + * + * @param string $url to check, in $boardurl format (no trailing slash). + */ +function ssl_cert_found($url) +{ + // This check won't work without OpenSSL + if (!extension_loaded('openssl')) + return true; + + // First, strip the subfolder from the passed url, if any + $parsedurl = parse_iri($url); + $url = 'ssl://' . $parsedurl['host'] . ':443'; + + // Next, check the ssl stream context for certificate info + if (version_compare(PHP_VERSION, '5.6.0', '<')) + $ssloptions = array("capture_peer_cert" => true); + else + $ssloptions = array("capture_peer_cert" => true, "verify_peer" => true, "allow_self_signed" => true); + + $result = false; + $context = stream_context_create(array("ssl" => $ssloptions)); + $stream = @stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context); + if ($stream !== false) + { + $params = stream_context_get_params($stream); + $result = isset($params["options"]["ssl"]["peer_certificate"]) ? true : false; + } + return $result; +} + +/** + * Check if the passed url has a redirect to https:// by querying headers. + * + * Returns true if a redirect was found & false if not. + * Note that when force_ssl = 2, SMF issues its own redirect... So if this + * returns true, it may be caused by SMF, not necessarily an .htaccess redirect. + * + * @param string $url to check, in $boardurl format (no trailing slash). + */ +function https_redirect_active($url) +{ + // Ask for the headers for the passed url, but via http... + // Need to add the trailing slash, or it puts it there & thinks there's a redirect when there isn't... + $url = str_ireplace('https://', 'http://', $url) . '/'; + $headers = @get_headers($url); + if ($headers === false) + return false; + + // Now to see if it came back https... + // First check for a redirect status code in first row (301, 302, 307) + if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false) + return false; + + // Search for the location entry to confirm https + $result = false; + foreach ($headers as $header) + { + if (stristr($header, 'Location: https://') !== false) + { + $result = true; + break; + } + } + return $result; +} + +/** + * Build query_wanna_see_board and query_see_board for a userid + * + * Returns array with keys query_wanna_see_board and query_see_board + * + * @param int $userid of the user + */ +function build_query_board($userid) +{ + global $user_info, $modSettings, $smcFunc, $db_prefix; + + $query_part = array(); + + // If we come from cron, we can't have a $user_info. + if (isset($user_info['id']) && $user_info['id'] == $userid && SMF != 'BACKGROUND') + { + $groups = $user_info['groups']; + $can_see_all_boards = $user_info['is_admin'] || $user_info['can_manage_boards']; + $ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null; + } + else + { + $request = $smcFunc['db_query']('', ' + SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group + FROM {db_prefix}members AS mem + WHERE mem.id_member = {int:id_member} + LIMIT 1', + array( + 'id_member' => $userid, + ) + ); + + $row = $smcFunc['db_fetch_assoc']($request); + + if (empty($row['additional_groups'])) + $groups = array($row['id_group'], $row['id_post_group']); + else + $groups = array_merge( + array($row['id_group'], $row['id_post_group']), + explode(',', $row['additional_groups']) + ); + + // Because history has proven that it is possible for groups to go bad - clean up in case. + foreach ($groups as $k => $v) + $groups[$k] = (int) $v; + + $can_see_all_boards = in_array(1, $groups) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($groups, explode(',', $modSettings['board_manager_groups']))) > 0); + + $ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array(); + } + + // Just build this here, it makes it easier to change/use - administrators can see all boards. + if ($can_see_all_boards) + $query_part['query_see_board'] = '1=1'; + // Otherwise just the groups in $user_info['groups']. + else + { + $query_part['query_see_board'] = ' + EXISTS ( + SELECT bpv.id_board + FROM ' . $db_prefix . 'board_permissions_view AS bpv + WHERE bpv.id_group IN ('. implode(',', $groups) .') + AND bpv.deny = 0 + AND bpv.id_board = b.id_board + )'; + + if (!empty($modSettings['deny_boards_access'])) + $query_part['query_see_board'] .= ' + AND NOT EXISTS ( + SELECT bpv.id_board + FROM ' . $db_prefix . 'board_permissions_view AS bpv + WHERE bpv.id_group IN ( '. implode(',', $groups) .') + AND bpv.deny = 1 + AND bpv.id_board = b.id_board + )'; + } + + $query_part['query_see_message_board'] = str_replace('b.', 'm.', $query_part['query_see_board']); + $query_part['query_see_topic_board'] = str_replace('b.', 't.', $query_part['query_see_board']); + + // Build the list of boards they WANT to see. + // This will take the place of query_see_boards in certain spots, so it better include the boards they can see also + + // If they aren't ignoring any boards then they want to see all the boards they can see + if (empty($ignoreboards)) + { + $query_part['query_wanna_see_board'] = $query_part['query_see_board']; + $query_part['query_wanna_see_message_board'] = $query_part['query_see_message_board']; + $query_part['query_wanna_see_topic_board'] = $query_part['query_see_topic_board']; + } + // Ok I guess they don't want to see all the boards + else + { + $query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))'; + $query_part['query_wanna_see_message_board'] = '(' . $query_part['query_see_message_board'] . ' AND m.id_board NOT IN (' . implode(',', $ignoreboards) . '))'; + $query_part['query_wanna_see_topic_board'] = '(' . $query_part['query_see_topic_board'] . ' AND t.id_board NOT IN (' . implode(',', $ignoreboards) . '))'; + } + + return $query_part; +} + +/** + * Check if the connection is using https. + * + * @return boolean true if connection used https + */ +function httpsOn() +{ + $secure = false; + + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') + $secure = true; + elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' || !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on') + $secure = true; + + return $secure; +} + +/** + * A wrapper for `parse_url($url)` that can handle URLs with international + * characters (a.k.a. IRIs) + * + * @param string $iri The IRI to parse. + * @param int $component Optional parameter to pass to parse_url(). + * @return mixed Same as parse_url(), but with unmangled Unicode. + */ +function parse_iri($iri, $component = -1) +{ + $iri = preg_replace_callback( + '~[^\x00-\x7F\pZ\pC]|%~u', + function($matches) + { + return rawurlencode($matches[0]); + }, + $iri + ); + + $parsed = parse_url($iri, $component); + + if (is_array($parsed)) + { + foreach ($parsed as &$part) + $part = rawurldecode($part); + } + elseif (is_string($parsed)) + $parsed = rawurldecode($parsed); + + return $parsed; +} + +/** + * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs + * with international characters (a.k.a. IRIs) + * + * @param string $iri The IRI to test. + * @param int $flags Optional flags to pass to filter_var() + * @return string|bool Either the original IRI, or false if the IRI was invalid. + */ +function validate_iri($iri, $flags = 0) +{ + $url = iri_to_url($iri); + + // PHP 5 doesn't recognize IPv6 addresses in the URL host. + if (version_compare(phpversion(), '7.0.0', '<')) + { + $host = parse_url((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST); + + if (strpos($host, '[') === 0 && strpos($host, ']') === strlen($host) - 1 && strpos($host, ':') !== false) + $url = str_replace($host, '127.0.0.1', $url); + } + + if (filter_var($url, FILTER_VALIDATE_URL, $flags) !== false) + return $iri; + else + return false; +} + +/** + * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs + * with international characters (a.k.a. IRIs) + * + * Note: The returned value will still be an IRI, not a URL. To convert to URL, + * feed the result of this function to iri_to_url() + * + * @param string $iri The IRI to sanitize. + * @return string|bool The sanitized version of the IRI + */ +function sanitize_iri($iri) +{ + // Encode any non-ASCII characters (but not space or control characters of any sort) + // Also encode '%' in order to preserve anything that is already percent-encoded. + $iri = preg_replace_callback( + '~[^\x00-\x7F\pZ\pC]|%~u', + function($matches) + { + return rawurlencode($matches[0]); + }, + $iri + ); + + // Perform normal sanitization + $iri = filter_var($iri, FILTER_SANITIZE_URL); + + // Decode the non-ASCII characters + $iri = rawurldecode($iri); + + return $iri; +} + +/** + * Performs Unicode normalization on IRIs. + * + * Internally calls sanitize_iri(), then performs Unicode normalization on the + * IRI as a whole, using NFKC normalization for the domain name (see RFC 3491) + * and NFC normalization for the rest. + * + * @param string $iri The IRI to normalize. + * @return string|bool The normalized version of the IRI. + */ +function normalize_iri($iri) +{ + global $sourcedir, $context, $txt, $db_character_set; + + // If we are not using UTF-8, just sanitize and return. + if (isset($context['utf8']) ? !$context['utf8'] : (isset($txt['lang_character_set']) ? $txt['lang_character_set'] != 'UTF-8' : (isset($db_character_set) && $db_character_set != 'utf8'))) + return sanitize_iri($iri); + + require_once($sourcedir . '/Subs-Charset.php'); + + $iri = sanitize_iri(utf8_normalize_c($iri)); + + $host = parse_iri((strpos($iri, '//') === 0 ? 'http:' : '') . $iri, PHP_URL_HOST); + + if (!empty($host)) + { + $normalized_host = utf8_normalize_kc_casefold($host); + $pos = strpos($iri, $host); + } + else + { + $host = ''; + $normalized_host = ''; + $pos = 0; + } + + $before_host = substr($iri, 0, $pos); + $after_host = substr($iri, $pos + strlen($host)); + + return $before_host . $normalized_host . $after_host; +} + +/** + * Converts a URL with international characters (an IRI) into a pure ASCII URL + * + * Uses Punycode to encode any non-ASCII characters in the domain name, and uses + * standard URL encoding on the rest. + * + * @param string $iri A IRI that may or may not contain non-ASCII characters. + * @return string|bool The URL version of the IRI. + */ +function iri_to_url($iri) +{ + global $sourcedir, $context, $txt, $db_character_set; + + // Sanity check: must be using UTF-8 to do this. + if (isset($context['utf8']) ? !$context['utf8'] : (isset($txt['lang_character_set']) ? $txt['lang_character_set'] != 'UTF-8' : (isset($db_character_set) && $db_character_set != 'utf8'))) + return $iri; + + require_once($sourcedir . '/Subs-Charset.php'); + + $iri = sanitize_iri(utf8_normalize_c($iri)); + + $host = parse_iri((strpos($iri, '//') === 0 ? 'http:' : '') . $iri, PHP_URL_HOST); + + if (!empty($host)) + { + if (!function_exists('idn_to_ascii')) + require_once($sourcedir . '/Subs-Compat.php'); + + // Convert the host using the Punycode algorithm + $encoded_host = idn_to_ascii($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46); + + $pos = strpos($iri, $host); + } + else + { + $host = ''; + $encoded_host = ''; + $pos = 0; + } + + $before_host = substr($iri, 0, $pos); + $after_host = substr($iri, $pos + strlen($host)); + + // Encode any disallowed characters in the rest of the URL + $unescaped = array( + '%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&', + '%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*', + '%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':', + '%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@', + '%25' => '%', + ); + + $before_host = strtr(rawurlencode($before_host), $unescaped); + $after_host = strtr(rawurlencode($after_host), $unescaped); + + return $before_host . $encoded_host . $after_host; +} + +/** + * Decodes a URL containing encoded international characters to UTF-8 + * + * Decodes any Punycode encoded characters in the domain name, then uses + * standard URL decoding on the rest. + * + * @param string $url The pure ASCII version of a URL. + * @return string|bool The UTF-8 version of the URL. + */ +function url_to_iri($url) +{ + global $sourcedir, $context, $txt, $db_character_set; + + // Sanity check: must be using UTF-8 to do this. + if (isset($context['utf8']) ? !$context['utf8'] : (isset($txt['lang_character_set']) ? $txt['lang_character_set'] != 'UTF-8' : (isset($db_character_set) && $db_character_set != 'utf8'))) + return $url; + + $host = parse_iri((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST); + + if (!empty($host)) + { + if (!function_exists('idn_to_utf8')) + require_once($sourcedir . '/Subs-Compat.php'); + + // Decode the domain from Punycode + $decoded_host = idn_to_utf8($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46); + + $pos = strpos($url, $host); + } + else + { + $decoded_host = ''; + $pos = 0; + } + + $before_host = substr($url, 0, $pos); + $after_host = substr($url, $pos + strlen($host)); + + // Decode the rest of the URL, but preserve escaped URL syntax characters. + $double_escaped = array( + '%21' => '%2521', '%23' => '%2523', '%24' => '%2524', '%26' => '%2526', + '%27' => '%2527', '%28' => '%2528', '%29' => '%2529', '%2A' => '%252A', + '%2B' => '%252B', '%2C' => '%252C', '%2F' => '%252F', '%3A' => '%253A', + '%3B' => '%253B', '%3D' => '%253D', '%3F' => '%253F', '%40' => '%2540', + '%25' => '%2525', + ); + + $before_host = rawurldecode(strtr($before_host, $double_escaped)); + $after_host = rawurldecode(strtr($after_host, $double_escaped)); + + return $before_host . $decoded_host . $after_host; +} + +/** + * Ensures SMF's scheduled tasks are being run as intended + * + * If the admin activated the cron_is_real_cron setting, but the cron job is + * not running things at least once per day, we need to go back to SMF's default + * behaviour using "web cron" JavaScript calls. + */ +function check_cron() +{ + global $modSettings, $smcFunc, $txt; + + if (!empty($modSettings['cron_is_real_cron']) && time() - @intval($modSettings['cron_last_checked']) > 84600) + { + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}scheduled_tasks + WHERE disabled = {int:not_disabled} + AND next_time < {int:yesterday}', + array( + 'not_disabled' => 0, + 'yesterday' => time() - 84600, + ) + ); + list($overdue) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // If we have tasks more than a day overdue, cron isn't doing its job. + if (!empty($overdue)) + { + loadLanguage('ManageScheduledTasks'); + log_error($txt['cron_not_working']); + updateSettings(array('cron_is_real_cron' => 0)); + } + else + updateSettings(array('cron_last_checked' => time())); + } +} + +/** + * Sends an appropriate HTTP status header based on a given status code + * + * @param int $code The status code + * @param string $status The string for the status. Set automatically if not provided. + */ +function send_http_status($code, $status = '') +{ + global $sourcedir; + + // This will fail anyways if headers have been sent. + if (headers_sent()) + return; + + $statuses = array( + 204 => 'No Content', + 206 => 'Partial Content', + 304 => 'Not Modified', + 400 => 'Bad Request', + 403 => 'Forbidden', + 404 => 'Not Found', + 410 => 'Gone', + 500 => 'Internal Server Error', + 503 => 'Service Unavailable', + ); + + $protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0'; + + // Typically during these requests, we have cleaned the response (ob_*clean), ensure these headers exist. + require_once($sourcedir . '/Security.php'); + frameOptionsHeader(); + corsPolicyHeader(); + + if (!isset($statuses[$code]) && empty($status)) + header($protocol . ' 500 Internal Server Error'); + else + header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code])); +} + +/** + * Concatenates an array of strings into a grammatically correct sentence list + * + * Uses formats defined in the language files to build the list appropropriately + * for the currently loaded language. + * + * @param array $list An array of strings to concatenate. + * @return string The localized sentence list. + */ +function sentence_list($list) +{ + global $txt; + + // Make sure the bare necessities are defined + if (empty($txt['sentence_list_format']['n'])) + $txt['sentence_list_format']['n'] = '{series}'; + if (!isset($txt['sentence_list_separator'])) + $txt['sentence_list_separator'] = ', '; + if (!isset($txt['sentence_list_separator_alt'])) + $txt['sentence_list_separator_alt'] = '; '; + + // Which format should we use? + if (isset($txt['sentence_list_format'][count($list)])) + $format = $txt['sentence_list_format'][count($list)]; + else + $format = $txt['sentence_list_format']['n']; + + // Do we want the normal separator or the alternate? + $separator = $txt['sentence_list_separator']; + foreach ($list as $item) + { + if (strpos($item, $separator) !== false) + { + $separator = $txt['sentence_list_separator_alt']; + $format = strtr($format, trim($txt['sentence_list_separator']), trim($separator)); + break; + } + } + + $replacements = array(); + + // Special handling for the last items on the list + $i = 0; + while (empty($done)) + { + if (strpos($format, '{'. --$i . '}') !== false) + $replacements['{'. $i . '}'] = array_pop($list); + else + $done = true; + } + unset($done); + + // Special handling for the first items on the list + $i = 0; + while (empty($done)) + { + if (strpos($format, '{'. ++$i . '}') !== false) + $replacements['{'. $i . '}'] = array_shift($list); + else + $done = true; + } + unset($done); + + // Whatever is left + $replacements['{series}'] = implode($separator, $list); + + // Do the deed + return strtr($format, $replacements); +} + +/** + * Truncate an array to a specified length + * + * @param array $array The array to truncate + * @param int $max_length The upperbound on the length + * @param int $deep How levels in an multidimensional array should the function take into account. + * @return array The truncated array + */ +function truncate_array($array, $max_length = 1900, $deep = 3) +{ + $array = (array) $array; + + $curr_length = array_length($array, $deep); + + if ($curr_length <= $max_length) + return $array; + + else + { + // Truncate each element's value to a reasonable length + $param_max = floor($max_length / count($array)); + + $current_deep = $deep - 1; + + foreach ($array as $key => &$value) + { + if (is_array($value)) + if ($current_deep > 0) + $value = truncate_array($value, $current_deep); + + else + $value = substr($value, 0, $param_max - strlen($key) - 5); + } + + return $array; + } +} + +/** + * array_length Recursive + * @param array $array + * @param int $deep How many levels should the function + * @return int + */ +function array_length($array, $deep = 3) +{ + // Work with arrays + $array = (array) $array; + $length = 0; + + $deep_count = $deep - 1; + + foreach ($array as $value) + { + // Recursive? + if (is_array($value)) + { + // No can't do + if ($deep_count <= 0) + continue; + + $length += array_length($value, $deep_count); + } + else + $length += strlen($value); + } + + return $length; +} + +/** + * Compares existance request variables against an array. + * + * The input array is associative, where keys denote accepted values + * in a request variable denoted by `$req_val`. Values can be: + * + * - another associative array where at least one key must be found + * in the request and their values are accepted request values. + * - A scalar value, in which case no furthur checks are done. + * + * @param array $array + * @param string $req_var request variable + * + * @return bool whether any of the criteria was satisfied + */ +function is_filtered_request(array $array, $req_var) +{ + $matched = false; + if (isset($_REQUEST[$req_var], $array[$_REQUEST[$req_var]])) + { + if (is_array($array[$_REQUEST[$req_var]])) + { + foreach ($array[$_REQUEST[$req_var]] as $subtype => $subnames) + $matched |= isset($_REQUEST[$subtype]) && in_array($_REQUEST[$subtype], $subnames); + } + else + $matched = true; + } + + return (bool) $matched; +} + +/** + * Clean up the XML to make sure it doesn't contain invalid characters. + * + * See https://www.w3.org/TR/xml/#charsets + * + * @param string $string The string to clean + * @return string The cleaned string + */ +function cleanXml($string) +{ + global $context; + + $illegal_chars = array( + // Remove all ASCII control characters except \t, \n, and \r. + "\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08", + "\x0B", "\x0C", "\x0E", "\x0F", "\x10", "\x11", "\x12", "\x13", "\x14", + "\x15", "\x16", "\x17", "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D", + "\x1E", "\x1F", + // Remove \xFFFE and \xFFFF + "\xEF\xBF\xBE", "\xEF\xBF\xBF", + ); + + $string = str_replace($illegal_chars, '', $string); + + // The Unicode surrogate pair code points should never be present in our + // strings to begin with, but if any snuck in, they need to be removed. + if (!empty($context['utf8']) && strpos($string, "\xED") !== false) + $string = preg_replace('/\xED[\xA0-\xBF][\x80-\xBF]/', '', $string); + + return $string; +} + +/** + * Escapes (replaces) characters in strings to make them safe for use in JavaScript + * + * @param string $string The string to escape + * @param bool $as_json If true, escape as double-quoted string. Default false. + * @return string The escaped string + */ +function JavaScriptEscape($string, $as_json = false) +{ + global $scripturl; + + $q = !empty($as_json) ? '"' : '\''; + + return $q . strtr($string, array( + "\r" => '', + "\n" => '\\n', + "\t" => '\\t', + '\\' => '\\\\', + $q => addslashes($q), + ' '<' . $q . ' + ' . $q . '/', + ' '' => '', + ' ' $q . ' + smf_scripturl + ' . $q, + )) . $q; +} + +function tokenTxtReplace($stringSubject = '') +{ + global $txt; + + if (empty($stringSubject)) + return ''; + + $translatable_tokens = preg_match_all('/{(.*?)}/' , $stringSubject, $matches); + $toFind = array(); + $replaceWith = array(); + + if (!empty($matches[1])) + foreach ($matches[1] as $token) { + $toFind[] = '{' . $token . '}'; + $replaceWith[] = isset($txt[$token]) ? $txt[$token] : $token; + } + + return str_replace($toFind, $replaceWith, $stringSubject); +} + +?> \ No newline at end of file diff --git a/Sources/Subscriptions-PayPal.php b/Sources/Subscriptions-PayPal.php new file mode 100644 index 0000000..6b1ded4 --- /dev/null +++ b/Sources/Subscriptions-PayPal.php @@ -0,0 +1,453 @@ + $txt['paypal_email_desc'], + 'size' => 60 + ), + array( + 'email', 'paypal_additional_emails', + 'subtext' => $txt['paypal_additional_emails_desc'], + 'size' => 60 + ), + array( + 'email', 'paypal_sandbox_email', + 'subtext' => $txt['paypal_sandbox_email_desc'], + 'size' => 60 + ), + ); + + return $setting_data; + } + + /** + * Is this enabled for new payments? + * + * @return boolean Whether this gateway is enabled (for PayPal, whether the PayPal email is set) + */ + public function gatewayEnabled() + { + global $modSettings; + + return !empty($modSettings['paypal_email']); + } + + /** + * What do we want? + * + * Called from Profile-Actions.php to return a unique set of fields for the given gateway + * plus all the standard ones for the subscription form + * + * @param string $unique_id The unique ID of this gateway + * @param array $sub_data Subscription data + * @param int|float $value The amount of the subscription + * @param string $period + * @param string $return_url The URL to return the user to after processing the payment + * @return array An array of data for the form + */ + public function fetchGatewayFields($unique_id, $sub_data, $value, $period, $return_url) + { + global $modSettings, $txt, $boardurl; + + $return_data = array( + 'form' => 'https://www.' . (!empty($modSettings['paidsubs_test']) ? 'sandbox.' : '') . 'paypal.com/cgi-bin/webscr', + 'id' => 'paypal', + 'hidden' => array(), + 'title' => $txt['paypal'], + 'desc' => $txt['paid_confirm_paypal'], + 'submit' => $txt['paid_paypal_order'], + 'javascript' => '', + ); + + // All the standard bits. + $return_data['hidden']['business'] = $modSettings['paypal_email']; + $return_data['hidden']['item_name'] = $sub_data['name'] . ' ' . $txt['subscription']; + $return_data['hidden']['item_number'] = $unique_id; + $return_data['hidden']['currency_code'] = strtoupper($modSettings['paid_currency_code']); + $return_data['hidden']['no_shipping'] = 1; + $return_data['hidden']['no_note'] = 1; + $return_data['hidden']['amount'] = $value; + $return_data['hidden']['cmd'] = !$sub_data['repeatable'] ? '_xclick' : '_xclick-subscriptions'; + $return_data['hidden']['return'] = $return_url; + $return_data['hidden']['a3'] = $value; + $return_data['hidden']['src'] = 1; + $return_data['hidden']['notify_url'] = $boardurl . '/subscriptions.php'; + + // If possible let's use the language we know we need. + $return_data['hidden']['lc'] = !empty($txt['lang_paypal']) ? $txt['lang_paypal'] : 'US'; + + // Now stuff dependant on what we're doing. + if ($sub_data['flexible']) + { + $return_data['hidden']['p3'] = 1; + $return_data['hidden']['t3'] = strtoupper(substr($period, 0, 1)); + } + else + { + preg_match('~(\d*)(\w)~', $sub_data['real_length'], $match); + $unit = $match[1]; + $period = $match[2]; + + $return_data['hidden']['p3'] = $unit; + $return_data['hidden']['t3'] = $period; + } + + // If it's repeatable do some javascript to respect this idea. + if (!empty($sub_data['repeatable'])) + $return_data['javascript'] = ' + document.write(\'
    \'); + + function switchPaypalRecur() + { + document.getElementById("paypal_cmd").value = document.getElementById("do_paypal_recur").checked ? "_xclick-subscriptions" : "_xclick"; + }'; + + return $return_data; + } +} + +/** + * Class of functions to validate a IPN response and provide details of the payment + */ +class paypal_payment +{ + /** + * @var string $return_data The data to return + */ + private $return_data; + + /** + * This function returns true/false for whether this gateway thinks the data is intended for it. + * + * @return boolean Whether this gateway things the data is valid + */ + public function isValid() + { + global $modSettings; + + // Has the user set up an email address? + if ((empty($modSettings['paidsubs_test']) && empty($modSettings['paypal_email'])) || (!empty($modSettings['paidsubs_test']) && empty($modSettings['paypal_sandbox_email']))) + return false; + // Check the correct transaction types are even here. + if ((!isset($_POST['txn_type']) && !isset($_POST['payment_status'])) || (!isset($_POST['business']) && !isset($_POST['receiver_email']))) + return false; + // Correct email address? + if (!isset($_POST['business'])) + $_POST['business'] = $_POST['receiver_email']; + + // Are we testing? + if (!empty($modSettings['paidsubs_test']) && strtolower($modSettings['paypal_sandbox_email']) != strtolower($_POST['business']) && (empty($modSettings['paypal_additional_emails']) || !in_array(strtolower($_POST['business']), explode(',', strtolower($modSettings['paypal_additional_emails']))))) + return false; + elseif (strtolower($modSettings['paypal_email']) != strtolower($_POST['business']) && (empty($modSettings['paypal_additional_emails']) || !in_array(strtolower($_POST['business']), explode(',', $modSettings['paypal_additional_emails'])))) + return false; + return true; + } + + /** + * Post the IPN data received back to paypal for validation + * Sends the complete unaltered message back to PayPal. The message must contain the same fields + * in the same order and be encoded in the same way as the original message + * PayPal will respond back with a single word, which is either VERIFIED if the message originated with PayPal or INVALID + * + * If valid returns the subscription and member IDs we are going to process if it passes + * + * @return string A string containing the subscription ID and member ID, separated by a + + */ + public function precheck() + { + global $modSettings, $txt; + + // Put this to some default value. + if (!isset($_POST['txn_type'])) + $_POST['txn_type'] = ''; + + // Build the request string - starting with the minimum requirement. + $requestString = 'cmd=_notify-validate'; + + // Now my dear, add all the posted bits in the order we got them + foreach ($_POST as $k => $v) + $requestString .= '&' . $k . '=' . urlencode($v); + + // Can we use curl? + if (function_exists('curl_init') && $curl = curl_init((!empty($modSettings['paidsubs_test']) ? 'https://www.sandbox.' : 'https://www.') . 'paypal.com/cgi-bin/webscr')) + { + // Set the post data. + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $requestString); + + // Set up the headers so paypal will accept the post + curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($curl, CURLOPT_FORBID_REUSE, 1); + curl_setopt($curl, CURLOPT_HTTPHEADER, array( + 'Host: www.' . (!empty($modSettings['paidsubs_test']) ? 'sandbox.' : '') . 'paypal.com', + 'Connection: close' + )); + + // Fetch the data returned as a string. + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + + // Fetch the data. + $this->return_data = curl_exec($curl); + + // Close the session. + curl_close($curl); + } + // Otherwise good old HTTP. + else + { + // Setup the headers. + $header = 'POST /cgi-bin/webscr HTTP/1.1' . "\r\n"; + $header .= 'content-type: application/x-www-form-urlencoded' . "\r\n"; + $header .= 'Host: www.' . (!empty($modSettings['paidsubs_test']) ? 'sandbox.' : '') . 'paypal.com' . "\r\n"; + $header .= 'content-length: ' . strlen($requestString) . "\r\n"; + $header .= 'connection: close' . "\r\n\r\n"; + + // Open the connection. + if (!empty($modSettings['paidsubs_test'])) + $fp = fsockopen('ssl://www.sandbox.paypal.com', 443, $errno, $errstr, 30); + else + $fp = fsockopen('www.paypal.com', 80, $errno, $errstr, 30); + + // Did it work? + if (!$fp) + generateSubscriptionError($txt['paypal_could_not_connect']); + + // Put the data to the port. + fputs($fp, $header . $requestString); + + // Get the data back... + while (!feof($fp)) + { + $this->return_data = fgets($fp, 1024); + if (strcmp(trim($this->return_data), 'VERIFIED') === 0) + break; + } + + // Clean up. + fclose($fp); + } + + // If this isn't verified then give up... + if (strcmp(trim($this->return_data), 'VERIFIED') !== 0) + exit; + + // Check that this is intended for us. + if (strtolower($modSettings['paypal_email']) != strtolower($_POST['business']) && (empty($modSettings['paypal_additional_emails']) || !in_array(strtolower($_POST['business']), explode(',', strtolower($modSettings['paypal_additional_emails']))))) + exit; + + // Is this a subscription - and if so is it a secondary payment that we need to process? + // If so, make sure we get it in the expected format. Seems PayPal sometimes sends it without urlencoding. + if (!empty($_POST['item_number']) && strpos($_POST['item_number'], ' ') !== false) + $_POST['item_number'] = str_replace(' ', '+', $_POST['item_number']); + if ($this->isSubscription() && (empty($_POST['item_number']) || strpos($_POST['item_number'], '+') === false)) + // Calculate the subscription it relates to! + $this->_findSubscription(); + + // Verify the currency! + if (strtolower($_POST['mc_currency']) !== strtolower($modSettings['paid_currency_code'])) + exit; + + // Can't exist if it doesn't contain anything. + if (empty($_POST['item_number'])) + exit; + + // Return the id_sub and id_member + return explode('+', $_POST['item_number']); + } + + /** + * Is this a refund? + * + * @return boolean Whether this is a refund + */ + public function isRefund() + { + if ($_POST['payment_status'] === 'Refunded' || $_POST['payment_status'] === 'Reversed' || $_POST['txn_type'] === 'Refunded' || ($_POST['txn_type'] === 'reversal' && $_POST['payment_status'] === 'Completed')) + return true; + else + return false; + } + + /** + * Is this a subscription? + * + * @return boolean Whether this is a subscription + */ + public function isSubscription() + { + if (substr($_POST['txn_type'], 0, 14) === 'subscr_payment' && $_POST['payment_status'] === 'Completed') + return true; + else + return false; + } + + /** + * Is this a normal payment? + * + * @return boolean Whether this is a normal payment + */ + public function isPayment() + { + if ($_POST['payment_status'] === 'Completed' && $_POST['txn_type'] === 'web_accept') + return true; + else + return false; + } + + /** + * Is this a cancellation? + * + * @return boolean Whether this is a cancellation + */ + public function isCancellation() + { + // subscr_cancel is sent when the user cancels, subscr_eot is sent when the subscription reaches final payment + // Neither require us to *do* anything as per performCancel(). + // subscr_eot, if sent, indicates an end of payments term. + if (substr($_POST['txn_type'], 0, 13) === 'subscr_cancel' || substr($_POST['txn_type'], 0, 10) === 'subscr_eot') + return true; + else + return false; + } + + /** + * Things to do in the event of a cancellation + * + * @param string $subscription_id + * @param int $member_id + * @param array $subscription_info + */ + public function performCancel($subscription_id, $member_id, $subscription_info) + { + // PayPal doesn't require SMF to notify it every time the subscription is up for renewal. + // A cancellation should not cause the user to be immediately dropped from their subscription, but + // let it expire normally. Some systems require taking action in the database to deal with this, but + // PayPal does not, so we actually just do nothing. But this is a nice prototype/example just in case. + } + + /** + * How much was paid? + * + * @return float The amount paid + */ + public function getCost() + { + return (isset($_POST['tax']) ? $_POST['tax'] : 0) + $_POST['mc_gross']; + } + + /** + * Record the transaction reference to finish up. + * + */ + public function close() + { + global $smcFunc, $subscription_id; + + // If it's a subscription record the reference. + if ($_POST['txn_type'] == 'subscr_payment' && !empty($_POST['subscr_id'])) + { + $smcFunc['db_query']('', ' + UPDATE {db_prefix}log_subscribed + SET vendor_ref = {string:vendor_ref} + WHERE id_sublog = {int:current_subscription}', + array( + 'current_subscription' => $subscription_id, + 'vendor_ref' => $_POST['subscr_id'], + ) + ); + } + } + + /** + * A private function to find out the subscription details. + * + * @access private + * @return boolean|void False on failure, otherwise just sets $_POST['item_number'] + */ + private function _findSubscription() + { + global $smcFunc; + + // Assume we have this? + if (empty($_POST['subscr_id'])) + return false; + + // Do we have this in the database? + $request = $smcFunc['db_query']('', ' + SELECT id_member, id_subscribe + FROM {db_prefix}log_subscribed + WHERE vendor_ref = {string:vendor_ref} + LIMIT 1', + array( + 'vendor_ref' => $_POST['subscr_id'], + ) + ); + // No joy? + if ($smcFunc['db_num_rows']($request) == 0) + { + // Can we identify them by email? + if (!empty($_POST['payer_email'])) + { + $smcFunc['db_free_result']($request); + $request = $smcFunc['db_query']('', ' + SELECT ls.id_member, ls.id_subscribe + FROM {db_prefix}log_subscribed AS ls + INNER JOIN {db_prefix}members AS mem ON (mem.id_member = ls.id_member) + WHERE mem.email_address = {string:payer_email} + LIMIT 1', + array( + 'payer_email' => $_POST['payer_email'], + ) + ); + if ($smcFunc['db_num_rows']($request) === 0) + return false; + } + else + return false; + } + list ($member_id, $subscription_id) = $smcFunc['db_fetch_row']($request); + $_POST['item_number'] = $member_id . '+' . $subscription_id; + $smcFunc['db_free_result']($request); + } +} + +?> \ No newline at end of file diff --git a/Sources/Themes.php b/Sources/Themes.php new file mode 100644 index 0000000..dd45d4c --- /dev/null +++ b/Sources/Themes.php @@ -0,0 +1,1925 @@ + value).) + * - tar and gzip the directory - and you're done! + * - please include any special license in a license.txt file. + * + * Simple Machines Forum (SMF) + * + * @package SMF + * @author Simple Machines https://www.simplemachines.org + * @copyright 2022 Simple Machines and individual contributors + * @license https://www.simplemachines.org/about/smf/license.php BSD + * + * @version 2.1.0 + */ + +if (!defined('SMF')) + die('No direct access...'); + +/** + * Subaction handler - manages the action and delegates control to the proper + * sub-action. + * It loads both the Themes and Settings language files. + * Checks the session by GET or POST to verify the sent data. + * Requires the user not be a guest. (@todo what?) + * Accessed via ?action=admin;area=theme. + */ +function ThemesMain() +{ + global $txt, $context, $sourcedir; + + // Load the important language files... + loadLanguage('Themes'); + loadLanguage('Settings'); + loadLanguage('Drafts'); + + // No funny business - guests only. + is_not_guest(); + + require_once($sourcedir . '/Subs-Themes.php'); + + // Default the page title to Theme Administration by default. + $context['page_title'] = $txt['themeadmin_title']; + + // Theme administration, removal, choice, or installation... + $subActions = array( + 'admin' => 'ThemeAdmin', + 'list' => 'ThemeList', + 'reset' => 'SetThemeOptions', + 'options' => 'SetThemeOptions', + 'install' => 'ThemeInstall', + 'remove' => 'RemoveTheme', + 'pick' => 'PickTheme', + 'edit' => 'EditTheme', + 'enable' => 'EnableTheme', + 'copy' => 'CopyTemplate', + ); + + // @todo Layout Settings? huh? + if (!empty($context['admin_menu_name'])) + { + $context[$context['admin_menu_name']]['tab_data'] = array( + 'title' => $txt['themeadmin_title'], + 'description' => $txt['themeadmin_description'], + 'tabs' => array( + 'admin' => array( + 'description' => $txt['themeadmin_admin_desc'], + ), + 'list' => array( + 'description' => $txt['themeadmin_list_desc'], + ), + 'reset' => array( + 'description' => $txt['themeadmin_reset_desc'], + ), + 'edit' => array( + 'description' => $txt['themeadmin_edit_desc'], + ), + ), + ); + } + + // CRUD $subActions as needed. + call_integration_hook('integrate_manage_themes', array(&$subActions)); + + // Whatever you decide to do, clean the minify cache. + cache_put_data('minimized_css', null); + + // Follow the sa or just go to administration. + if (isset($_GET['sa']) && !empty($subActions[$_GET['sa']])) + call_helper($subActions[$_GET['sa']]); + + else + call_helper($subActions['admin']); +} + +/** + * This function allows administration of themes and their settings, + * as well as global theme settings. + * - sets the settings theme_allow, theme_guests, and knownThemes. + * - requires the admin_forum permission. + * - accessed with ?action=admin;area=theme;sa=admin. + * + * Uses Themes template + * Uses Admin language file + */ +function ThemeAdmin() +{ + global $context, $boarddir; + + // Are handling any settings? + if (isset($_POST['save'])) + { + checkSession(); + validateToken('admin-tm'); + + if (isset($_POST['options']['known_themes'])) + foreach ($_POST['options']['known_themes'] as $key => $id) + $_POST['options']['known_themes'][$key] = (int) $id; + + else + fatal_lang_error('themes_none_selectable', false); + + if (!in_array($_POST['options']['theme_guests'], $_POST['options']['known_themes'])) + fatal_lang_error('themes_default_selectable', false); + + // Commit the new settings. + updateSettings(array( + 'theme_allow' => $_POST['options']['theme_allow'], + 'theme_guests' => $_POST['options']['theme_guests'], + 'knownThemes' => implode(',', $_POST['options']['known_themes']), + )); + if ((int) $_POST['theme_reset'] == 0 || in_array($_POST['theme_reset'], $_POST['options']['known_themes'])) + updateMemberData(null, array('id_theme' => (int) $_POST['theme_reset'])); + + redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=admin'); + } + + loadLanguage('Admin'); + isAllowedTo('admin_forum'); + loadTemplate('Themes'); + + // List all enabled themes. + get_all_themes(true); + + // Can we create a new theme? + $context['can_create_new'] = is_writable($boarddir . '/Themes'); + $context['new_theme_dir'] = substr(realpath($boarddir . '/Themes/default'), 0, -7); + + // Look for a non existent theme directory. (ie theme87.) + $theme_dir = $boarddir . '/Themes/theme'; + $i = 1; + while (file_exists($theme_dir . $i)) + $i++; + + $context['new_theme_name'] = 'theme' . $i; + + // A bunch of tokens for a bunch of forms. + createToken('admin-tm'); + createToken('admin-t-file'); + createToken('admin-t-copy'); + createToken('admin-t-dir'); +} + +/** + * This function lists the available themes and provides an interface to reset + * the paths of all the installed themes. + */ +function ThemeList() +{ + global $context, $boarddir, $boardurl, $smcFunc; + + loadLanguage('Admin'); + isAllowedTo('admin_forum'); + + if (isset($_REQUEST['th'])) + return SetThemeSettings(); + + if (isset($_POST['save'])) + { + checkSession(); + validateToken('admin-tl'); + + // Calling the almighty power of global vars! + get_installed_themes(); + + $setValues = array(); + foreach ($context['themes'] as $id => $theme) + { + if (file_exists($_POST['reset_dir'] . '/' . basename($theme['theme_dir']))) + { + $setValues[] = array($id, 0, 'theme_dir', realpath($_POST['reset_dir'] . '/' . basename($theme['theme_dir']))); + $setValues[] = array($id, 0, 'theme_url', $_POST['reset_url'] . '/' . basename($theme['theme_dir'])); + $setValues[] = array($id, 0, 'images_url', $_POST['reset_url'] . '/' . basename($theme['theme_dir']) . '/' . basename($theme['images_url'])); + } + + if (isset($theme['base_theme_dir']) && file_exists($_POST['reset_dir'] . '/' . basename($theme['base_theme_dir']))) + { + $setValues[] = array($id, 0, 'base_theme_dir', realpath($_POST['reset_dir'] . '/' . basename($theme['base_theme_dir']))); + $setValues[] = array($id, 0, 'base_theme_url', $_POST['reset_url'] . '/' . basename($theme['base_theme_dir'])); + $setValues[] = array($id, 0, 'base_images_url', $_POST['reset_url'] . '/' . basename($theme['base_theme_dir']) . '/' . basename($theme['base_images_url'])); + } + + cache_put_data('theme_settings-' . $id, null, 90); + } + + if (!empty($setValues)) + { + $smcFunc['db_insert']('replace', + '{db_prefix}themes', + array('id_theme' => 'int', 'id_member' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'), + $setValues, + array('id_theme', 'variable', 'id_member') + ); + } + + redirectexit('action=admin;area=theme;sa=list;' . $context['session_var'] . '=' . $context['session_id']); + } + + loadTemplate('Themes'); + + // Get all installed themes. + get_installed_themes(); + + $context['reset_dir'] = realpath($boarddir . '/Themes'); + $context['reset_url'] = $boardurl . '/Themes'; + + $context['sub_template'] = 'list_themes'; + createToken('admin-tl'); + createToken('admin-tr', 'request'); + createToken('admin-tre', 'request'); +} + +/** + * Administrative global settings. + */ +function SetThemeOptions() +{ + global $txt, $context, $settings, $modSettings, $smcFunc; + + $_GET['th'] = isset($_GET['th']) ? (int) $_GET['th'] : (isset($_GET['id']) ? (int) $_GET['id'] : 0); + + isAllowedTo('admin_forum'); + + if (empty($_GET['th']) && empty($_GET['id'])) + { + $request = $smcFunc['db_query']('', ' + SELECT id_theme, variable, value + FROM {db_prefix}themes + WHERE variable IN ({string:name}, {string:theme_dir}) + AND id_member = {int:no_member}', + array( + 'no_member' => 0, + 'name' => 'name', + 'theme_dir' => 'theme_dir', + ) + ); + $context['themes'] = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (!isset($context['themes'][$row['id_theme']])) + $context['themes'][$row['id_theme']] = array( + 'id' => $row['id_theme'], + 'num_default_options' => 0, + 'num_members' => 0, + ); + $context['themes'][$row['id_theme']][$row['variable']] = $row['value']; + } + $smcFunc['db_free_result']($request); + + $request = $smcFunc['db_query']('', ' + SELECT id_theme, COUNT(*) AS value + FROM {db_prefix}themes + WHERE id_member = {int:guest_member} + GROUP BY id_theme', + array( + 'guest_member' => -1, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $context['themes'][$row['id_theme']]['num_default_options'] = $row['value']; + $smcFunc['db_free_result']($request); + + // Need to make sure we don't do custom fields. + $request = $smcFunc['db_query']('', ' + SELECT col_name + FROM {db_prefix}custom_fields', + array( + ) + ); + $customFields = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $customFields[] = $row['col_name']; + $smcFunc['db_free_result']($request); + $customFieldsQuery = empty($customFields) ? '' : ('AND variable NOT IN ({array_string:custom_fields})'); + + $request = $smcFunc['db_query']('themes_count', ' + SELECT COUNT(DISTINCT id_member) AS value, id_theme + FROM {db_prefix}themes + WHERE id_member > {int:no_member} + ' . $customFieldsQuery . ' + GROUP BY id_theme', + array( + 'no_member' => 0, + 'custom_fields' => empty($customFields) ? array() : $customFields, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $context['themes'][$row['id_theme']]['num_members'] = $row['value']; + $smcFunc['db_free_result']($request); + + // There has to be a Settings template! + foreach ($context['themes'] as $k => $v) + if (empty($v['theme_dir']) || (!file_exists($v['theme_dir'] . '/Settings.template.php') && empty($v['num_members']))) + unset($context['themes'][$k]); + + loadTemplate('Themes'); + $context['sub_template'] = 'reset_list'; + + createToken('admin-stor', 'request'); + return; + } + + // Submit? + if (isset($_POST['submit']) && empty($_POST['who'])) + { + checkSession(); + validateToken('admin-sto'); + + if (empty($_POST['options'])) + $_POST['options'] = array(); + if (empty($_POST['default_options'])) + $_POST['default_options'] = array(); + + // Set up the sql query. + $setValues = array(); + + foreach ($_POST['options'] as $opt => $val) + $setValues[] = array(-1, $_GET['th'], $opt, is_array($val) ? implode(',', $val) : $val); + + $old_settings = array(); + foreach ($_POST['default_options'] as $opt => $val) + { + $old_settings[] = $opt; + + $setValues[] = array(-1, 1, $opt, is_array($val) ? implode(',', $val) : $val); + } + + // If we're actually inserting something.. + if (!empty($setValues)) + { + // Are there options in non-default themes set that should be cleared? + if (!empty($old_settings)) + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}themes + WHERE id_theme != {int:default_theme} + AND id_member = {int:guest_member} + AND variable IN ({array_string:old_settings})', + array( + 'default_theme' => 1, + 'guest_member' => -1, + 'old_settings' => $old_settings, + ) + ); + + $smcFunc['db_insert']('replace', + '{db_prefix}themes', + array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'), + $setValues, + array('id_theme', 'variable', 'id_member') + ); + } + + cache_put_data('theme_settings-' . $_GET['th'], null, 90); + cache_put_data('theme_settings-1', null, 90); + + redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=reset'); + } + elseif (isset($_POST['submit']) && $_POST['who'] == 1) + { + checkSession(); + validateToken('admin-sto'); + + $_POST['options'] = empty($_POST['options']) ? array() : $_POST['options']; + $_POST['options_master'] = empty($_POST['options_master']) ? array() : $_POST['options_master']; + $_POST['default_options'] = empty($_POST['default_options']) ? array() : $_POST['default_options']; + $_POST['default_options_master'] = empty($_POST['default_options_master']) ? array() : $_POST['default_options_master']; + + $old_settings = array(); + foreach ($_POST['default_options'] as $opt => $val) + { + if ($_POST['default_options_master'][$opt] == 0) + continue; + elseif ($_POST['default_options_master'][$opt] == 1) + { + // Delete then insert for ease of database compatibility! + $smcFunc['db_query']('substring', ' + DELETE FROM {db_prefix}themes + WHERE id_theme = {int:default_theme} + AND id_member > {int:no_member} + AND variable = SUBSTRING({string:option}, 1, 255)', + array( + 'default_theme' => 1, + 'no_member' => 0, + 'option' => $opt, + ) + ); + $smcFunc['db_query']('substring', ' + INSERT INTO {db_prefix}themes + (id_member, id_theme, variable, value) + SELECT id_member, 1, SUBSTRING({string:option}, 1, 255), SUBSTRING({string:value}, 1, 65534) + FROM {db_prefix}members', + array( + 'option' => $opt, + 'value' => (is_array($val) ? implode(',', $val) : $val), + ) + ); + + $old_settings[] = $opt; + } + elseif ($_POST['default_options_master'][$opt] == 2) + { + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}themes + WHERE variable = {string:option_name} + AND id_member > {int:no_member}', + array( + 'no_member' => 0, + 'option_name' => $opt, + ) + ); + } + } + + // Delete options from other themes. + if (!empty($old_settings)) + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}themes + WHERE id_theme != {int:default_theme} + AND id_member > {int:no_member} + AND variable IN ({array_string:old_settings})', + array( + 'default_theme' => 1, + 'no_member' => 0, + 'old_settings' => $old_settings, + ) + ); + + foreach ($_POST['options'] as $opt => $val) + { + if ($_POST['options_master'][$opt] == 0) + continue; + elseif ($_POST['options_master'][$opt] == 1) + { + // Delete then insert for ease of database compatibility - again! + $smcFunc['db_query']('substring', ' + DELETE FROM {db_prefix}themes + WHERE id_theme = {int:current_theme} + AND id_member > {int:no_member} + AND variable = SUBSTRING({string:option}, 1, 255)', + array( + 'current_theme' => $_GET['th'], + 'no_member' => 0, + 'option' => $opt, + ) + ); + $smcFunc['db_query']('substring', ' + INSERT INTO {db_prefix}themes + (id_member, id_theme, variable, value) + SELECT id_member, {int:current_theme}, SUBSTRING({string:option}, 1, 255), SUBSTRING({string:value}, 1, 65534) + FROM {db_prefix}members', + array( + 'current_theme' => $_GET['th'], + 'option' => $opt, + 'value' => (is_array($val) ? implode(',', $val) : $val), + ) + ); + } + elseif ($_POST['options_master'][$opt] == 2) + { + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}themes + WHERE variable = {string:option} + AND id_member > {int:no_member} + AND id_theme = {int:current_theme}', + array( + 'no_member' => 0, + 'current_theme' => $_GET['th'], + 'option' => $opt, + ) + ); + } + } + + redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=reset'); + } + elseif (!empty($_GET['who']) && $_GET['who'] == 2) + { + checkSession('get'); + validateToken('admin-stor', 'request'); + + // Don't delete custom fields!! + if ($_GET['th'] == 1) + { + $request = $smcFunc['db_query']('', ' + SELECT col_name + FROM {db_prefix}custom_fields', + array( + ) + ); + $customFields = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $customFields[] = $row['col_name']; + $smcFunc['db_free_result']($request); + } + $customFieldsQuery = empty($customFields) ? '' : ('AND variable NOT IN ({array_string:custom_fields})'); + + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}themes + WHERE id_member > {int:no_member} + AND id_theme = {int:current_theme} + ' . $customFieldsQuery, + array( + 'no_member' => 0, + 'current_theme' => $_GET['th'], + 'custom_fields' => empty($customFields) ? array() : $customFields, + ) + ); + + redirectexit('action=admin;area=theme;' . $context['session_var'] . '=' . $context['session_id'] . ';sa=reset'); + } + + $old_id = $settings['theme_id']; + $old_settings = $settings; + + loadTheme($_GET['th'], false); + + loadLanguage('Profile'); + // @todo Should we just move these options so they are no longer theme dependant? + loadLanguage('PersonalMessage'); + + // Let the theme take care of the settings. + loadTemplate('Settings'); + loadSubTemplate('options'); + + // Let mods hook into the theme options. + call_integration_hook('integrate_theme_options'); + + $context['sub_template'] = 'set_options'; + $context['page_title'] = $txt['theme_settings']; + + $context['options'] = $context['theme_options']; + $context['theme_settings'] = $settings; + + if (empty($_REQUEST['who'])) + { + $request = $smcFunc['db_query']('', ' + SELECT variable, value + FROM {db_prefix}themes + WHERE id_theme IN (1, {int:current_theme}) + AND id_member = {int:guest_member}', + array( + 'current_theme' => $_GET['th'], + 'guest_member' => -1, + ) + ); + $context['theme_options'] = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $context['theme_options'][$row['variable']] = $row['value']; + $smcFunc['db_free_result']($request); + + $context['theme_options_reset'] = false; + } + else + { + $context['theme_options'] = array(); + $context['theme_options_reset'] = true; + } + + foreach ($context['options'] as $i => $setting) + { + // Just skip separators + if (!is_array($setting)) + continue; + + // Is this disabled? + if (isset($setting['enabled']) && $setting['enabled'] === false) + { + unset($context['options'][$i]); + continue; + } + + if (!isset($setting['type']) || $setting['type'] == 'bool') + $context['options'][$i]['type'] = 'checkbox'; + elseif ($setting['type'] == 'int' || $setting['type'] == 'integer') + $context['options'][$i]['type'] = 'number'; + elseif ($setting['type'] == 'string') + $context['options'][$i]['type'] = 'text'; + + if (isset($setting['options'])) + $context['options'][$i]['type'] = 'list'; + + $context['options'][$i]['value'] = !isset($context['theme_options'][$setting['id']]) ? '' : $context['theme_options'][$setting['id']]; + } + + // Restore the existing theme. + loadTheme($old_id, false); + $settings = $old_settings; + + loadTemplate('Themes'); + createToken('admin-sto'); +} + +/** + * Administrative global settings. + * - saves and requests global theme settings. ($settings) + * - loads the Admin language file. + * - calls ThemeAdmin() if no theme is specified. (the theme center.) + * - requires admin_forum permission. + * - accessed with ?action=admin;area=theme;sa=list&th=xx. + */ +function SetThemeSettings() +{ + global $txt, $context, $settings, $modSettings, $smcFunc; + + if (empty($_GET['th']) && empty($_GET['id'])) + return ThemeAdmin(); + + $_GET['th'] = isset($_GET['th']) ? (int) $_GET['th'] : (int) $_GET['id']; + + // Select the best fitting tab. + $context[$context['admin_menu_name']]['current_subsection'] = 'list'; + + loadLanguage('Admin'); + isAllowedTo('admin_forum'); + + // Validate inputs/user. + if (empty($_GET['th'])) + fatal_lang_error('no_theme', false); + + // Fetch the smiley sets... + $sets = explode(',', 'none,' . $modSettings['smiley_sets_known']); + $set_names = explode("\n", $txt['smileys_none'] . "\n" . $modSettings['smiley_sets_names']); + $context['smiley_sets'] = array( + '' => $txt['smileys_no_default'] + ); + foreach ($sets as $i => $set) + $context['smiley_sets'][$set] = $smcFunc['htmlspecialchars']($set_names[$i]); + + $old_id = $settings['theme_id']; + $old_settings = $settings; + + loadTheme($_GET['th'], false); + + // Sadly we really do need to init the template. + loadSubTemplate('init', 'ignore'); + + // Also load the actual themes language file - in case of special settings. + loadLanguage('Settings', '', true, true); + + // And the custom language strings... + loadLanguage('ThemeStrings', '', false, true); + + // Let the theme take care of the settings. + loadTemplate('Settings'); + loadSubTemplate('settings'); + + // Load the variants separately... + $settings['theme_variants'] = array(); + if (file_exists($settings['theme_dir'] . '/index.template.php')) + { + $file_contents = implode('', file($settings['theme_dir'] . '/index.template.php')); + if (preg_match('~\$settings\[\'theme_variants\'\]\s*=(.+?);~', $file_contents, $matches)) + eval('global $settings;' . $matches[0]); + } + + // Let mods hook into the theme settings. + call_integration_hook('integrate_theme_settings'); + + // Submitting! + if (isset($_POST['save'])) + { + checkSession(); + validateToken('admin-sts'); + + if (empty($_POST['options'])) + $_POST['options'] = array(); + if (empty($_POST['default_options'])) + $_POST['default_options'] = array(); + + // Make sure items are cast correctly. + foreach ($context['theme_settings'] as $item) + { + // Disregard this item if this is just a separator. + if (!is_array($item)) + continue; + + foreach (array('options', 'default_options') as $option) + { + if (!isset($_POST[$option][$item['id']])) + continue; + // Checkbox. + elseif (empty($item['type'])) + $_POST[$option][$item['id']] = $_POST[$option][$item['id']] ? 1 : 0; + // Number + elseif ($item['type'] == 'number') + $_POST[$option][$item['id']] = (int) $_POST[$option][$item['id']]; + } + } + + // Set up the sql query. + $inserts = array(); + foreach ($_POST['options'] as $opt => $val) + $inserts[] = array(0, $_GET['th'], $opt, is_array($val) ? implode(',', $val) : $val); + foreach ($_POST['default_options'] as $opt => $val) + $inserts[] = array(0, 1, $opt, is_array($val) ? implode(',', $val) : $val); + // If we're actually inserting something.. + if (!empty($inserts)) + { + $smcFunc['db_insert']('replace', + '{db_prefix}themes', + array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'), + $inserts, + array('id_member', 'id_theme', 'variable') + ); + } + + cache_put_data('theme_settings-' . $_GET['th'], null, 90); + cache_put_data('theme_settings-1', null, 90); + + // Invalidate the cache. + updateSettings(array('settings_updated' => time())); + + redirectexit('action=admin;area=theme;sa=list;th=' . $_GET['th'] . ';' . $context['session_var'] . '=' . $context['session_id']); + } + + $context['sub_template'] = 'set_settings'; + $context['page_title'] = $txt['theme_settings']; + + foreach ($settings as $setting => $dummy) + { + if (!in_array($setting, array('theme_url', 'theme_dir', 'images_url', 'template_dirs'))) + $settings[$setting] = htmlspecialchars__recursive($settings[$setting]); + } + + $context['settings'] = $context['theme_settings']; + $context['theme_settings'] = $settings; + + foreach ($context['settings'] as $i => $setting) + { + // Separators are dummies, so leave them alone. + if (!is_array($setting)) + continue; + + if (!isset($setting['type']) || $setting['type'] == 'bool') + $context['settings'][$i]['type'] = 'checkbox'; + elseif ($setting['type'] == 'int' || $setting['type'] == 'integer') + $context['settings'][$i]['type'] = 'number'; + elseif ($setting['type'] == 'string') + $context['settings'][$i]['type'] = 'text'; + + if (isset($setting['options'])) + $context['settings'][$i]['type'] = 'list'; + + $context['settings'][$i]['value'] = !isset($settings[$setting['id']]) ? '' : $settings[$setting['id']]; + } + + // Do we support variants? + if (!empty($settings['theme_variants'])) + { + $context['theme_variants'] = array(); + foreach ($settings['theme_variants'] as $variant) + { + // Have any text, old chap? + $context['theme_variants'][$variant] = array( + 'label' => isset($txt['variant_' . $variant]) ? $txt['variant_' . $variant] : $variant, + 'thumbnail' => !file_exists($settings['theme_dir'] . '/images/thumbnail.png') || file_exists($settings['theme_dir'] . '/images/thumbnail_' . $variant . '.png') ? $settings['images_url'] . '/thumbnail_' . $variant . '.png' : ($settings['images_url'] . '/thumbnail.png'), + ); + } + $context['default_variant'] = !empty($settings['default_variant']) && isset($context['theme_variants'][$settings['default_variant']]) ? $settings['default_variant'] : $settings['theme_variants'][0]; + } + + // Restore the current theme. + loadTheme($old_id, false); + + // Reinit just incase. + loadSubTemplate('init', 'ignore'); + + $settings = $old_settings; + + loadTemplate('Themes'); + + // We like Kenny better than Token. + createToken('admin-sts'); +} + +/** + * Remove a theme from the database. + * - removes an installed theme. + * - requires an administrator. + * - accessed with ?action=admin;area=theme;sa=remove. + */ +function RemoveTheme() +{ + global $context; + + checkSession('get'); + + isAllowedTo('admin_forum'); + validateToken('admin-tr', 'request'); + + // The theme's ID must be an integer. + $themeID = isset($_GET['th']) ? (int) $_GET['th'] : (int) $_GET['id']; + + // You can't delete the default theme! + if ($themeID == 1) + fatal_lang_error('no_access', false); + + $theme_info = get_single_theme($themeID, array('theme_dir')); + + // Remove it from the DB. + remove_theme($themeID); + + // And remove all its files and folders too. + if (!empty($theme_info) && !empty($theme_info['theme_dir'])) + remove_dir($theme_info['theme_dir']); + + // Go back to the list page. + redirectexit('action=admin;area=theme;sa=list;' . $context['session_var'] . '=' . $context['session_id'] . ';done=removing'); +} + +/** + * Handles enabling/disabling a theme from the admin center + */ +function EnableTheme() +{ + global $modSettings, $context; + + checkSession('get'); + + isAllowedTo('admin_forum'); + validateToken('admin-tre', 'request'); + + // The theme's ID must be an string. + $themeID = isset($_GET['th']) ? (string) trim($_GET['th']) : (string) trim($_GET['id']); + + // Get the current list. + $enableThemes = explode(',', $modSettings['enableThemes']); + + // Are we disabling it? + if (isset($_GET['disabled'])) + $enableThemes = array_diff($enableThemes, array($themeID)); + + // Nope? then enable it! + else + $enableThemes[] = (string) $themeID; + + // Update the setting. + $enableThemes = strtr(implode(',', $enableThemes), array(',,' => ',')); + updateSettings(array('enableThemes' => $enableThemes)); + + // Done! + redirectexit('action=admin;area=theme;sa=list;' . $context['session_var'] . '=' . $context['session_id'] . ';done=' . (isset($_GET['disabled']) ? 'disabling' : 'enabling')); +} + +/** + * Determines if a user can change their theme. + * + * @param int $id_member + * @param int $id_theme + * + * @return bool + */ +function canPickTheme($id_member, $id_theme) +{ + global $modSettings, $user_info; + + return + allowedTo($user_info['id'] == $id_member ? 'profile_extra_own' : 'profile_extra_any') + && ($id_theme == 0 || (allowedTo('admin_forum') || in_array($id_theme, explode(',', $modSettings['knownThemes']))) && in_array($id_theme, explode(',', $modSettings['enableThemes']))) + && (!empty($modSettings['theme_allow']) || allowedTo('admin_forum')); +} + +/** + * Choose a theme from a list. + * allows a user to pick a new theme with an interface. + * - uses the Themes template. (pick sub template.) + * - accessed with ?action=theme;sa=pick. + */ +function PickTheme() +{ + global $txt, $context, $modSettings, $user_info, $language, $smcFunc, $settings, $scripturl; + + loadLanguage('Profile'); + loadTemplate('Themes'); + + // Build the link tree. + $context['linktree'][] = array( + 'url' => $scripturl . '?action=theme;sa=pick;u=' . (!empty($_REQUEST['u']) ? (int) $_REQUEST['u'] : 0), + 'name' => $txt['theme_pick'], + ); + $context['default_theme_id'] = $modSettings['theme_default']; + $_SESSION['id_theme'] = 0; + if (!isset($_REQUEST['u'])) + $_REQUEST['u'] = $user_info['id']; + + // Have we made a decision, or are we just browsing? + if (isset($_POST['save'])) + { + checkSession(); + validateToken('pick-th'); + + $id_theme = (int) key($_POST['save']); + if (isset($_POST['vrt'][$id_theme])) + $variant = $_POST['vrt'][$id_theme]; + + if (canPickTheme((int) $_REQUEST['u'], $id_theme)) + { + // An identifier of zero means that the user wants the forum default theme. + updateMemberData((int) $_REQUEST['u'], array('id_theme' => $id_theme)); + + if (!empty($variant)) + { + // Set the identifier to the forum default. + if (isset($id_theme) && $id_theme == 0) + $id_theme = $modSettings['theme_guests']; + + $smcFunc['db_insert']('replace', + '{db_prefix}themes', + array('id_theme' => 'int', 'id_member' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'), + array($id_theme, (int) $_REQUEST['u'], 'theme_variant', $variant), + array('id_theme', 'id_member', 'variable') + ); + cache_put_data('theme_settings-' . $id_theme . ':' . (int) $_REQUEST['u'], null, 90); + + if ($user_info['id'] == $_REQUEST['u']) + $_SESSION['id_variant'] = 0; + } + + redirectexit('action=profile;u=' . (int) $_REQUEST['u'] . ';area=theme'); + } + } + + // Figure out who the member of the minute is, and what theme they've chosen. + if (!isset($_REQUEST['u']) || !allowedTo('admin_forum')) + { + $context['current_member'] = $user_info['id']; + $context['current_theme'] = $user_info['theme']; + } + else + { + $context['current_member'] = (int) $_REQUEST['u']; + + $request = $smcFunc['db_query']('', ' + SELECT id_theme + FROM {db_prefix}members + WHERE id_member = {int:current_member} + LIMIT 1', + array( + 'current_member' => $context['current_member'], + ) + ); + list ($context['current_theme']) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + } + + // Get the theme name and descriptions. + $context['available_themes'] = array(); + if (!empty($modSettings['knownThemes'])) + { + $request = $smcFunc['db_query']('', ' + SELECT id_theme, variable, value + FROM {db_prefix}themes + WHERE variable IN ({literal:name}, {literal:theme_url}, {literal:theme_dir}, {literal:images_url}, {literal:disable_user_variant})' . (!allowedTo('admin_forum') ? ' + AND id_theme IN ({array_int:known_themes})' : '') . ' + AND id_theme != {int:default_theme} + AND id_member = {int:no_member} + AND id_theme IN ({array_int:enable_themes})', + array( + 'default_theme' => 0, + 'no_member' => 0, + 'known_themes' => explode(',', $modSettings['knownThemes']), + 'enable_themes' => explode(',', $modSettings['enableThemes']), + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if (!isset($context['available_themes'][$row['id_theme']])) + $context['available_themes'][$row['id_theme']] = array( + 'id' => $row['id_theme'], + 'selected' => $context['current_theme'] == $row['id_theme'], + 'num_users' => 0 + ); + $context['available_themes'][$row['id_theme']][$row['variable']] = $row['value']; + } + $smcFunc['db_free_result']($request); + } + + // Okay, this is a complicated problem: the default theme is 1, but they aren't allowed to access 1! + if (!isset($context['available_themes'][$modSettings['theme_guests']])) + { + $context['available_themes'][0] = array( + 'num_users' => 0 + ); + $guest_theme = 0; + } + else + $guest_theme = $modSettings['theme_guests']; + + $request = $smcFunc['db_query']('', ' + SELECT id_theme, COUNT(*) AS the_count + FROM {db_prefix}members + GROUP BY id_theme + ORDER BY id_theme DESC', + array( + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // Figure out which theme it is they are REALLY using. + if (!empty($modSettings['knownThemes']) && !in_array($row['id_theme'], explode(',', $modSettings['knownThemes']))) + $row['id_theme'] = $guest_theme; + elseif (empty($modSettings['theme_allow'])) + $row['id_theme'] = $guest_theme; + + if (isset($context['available_themes'][$row['id_theme']])) + $context['available_themes'][$row['id_theme']]['num_users'] += $row['the_count']; + else + $context['available_themes'][$guest_theme]['num_users'] += $row['the_count']; + } + $smcFunc['db_free_result']($request); + + // Get any member variant preferences. + $variant_preferences = array(); + if ($context['current_member'] > 0) + { + $request = $smcFunc['db_query']('', ' + SELECT id_theme, value + FROM {db_prefix}themes + WHERE variable = {string:theme_variant} + AND id_member IN ({array_int:id_member}) + ORDER BY id_member ASC', + array( + 'theme_variant' => 'theme_variant', + 'id_member' => isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? array(-1, $context['current_member']) : array(-1), + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + $variant_preferences[$row['id_theme']] = $row['value']; + $smcFunc['db_free_result']($request); + } + + // Save the setting first. + $current_images_url = $settings['images_url']; + $current_theme_variants = !empty($settings['theme_variants']) ? $settings['theme_variants'] : array(); + + foreach ($context['available_themes'] as $id_theme => $theme_data) + { + // Don't try to load the forum or board default theme's data... it doesn't have any! + if ($id_theme == 0) + continue; + + // The thumbnail needs the correct path. + $settings['images_url'] = &$theme_data['images_url']; + + if (file_exists($theme_data['theme_dir'] . '/languages/Settings.' . $user_info['language'] . '.php')) + include($theme_data['theme_dir'] . '/languages/Settings.' . $user_info['language'] . '.php'); + elseif (file_exists($theme_data['theme_dir'] . '/languages/Settings.' . $language . '.php')) + include($theme_data['theme_dir'] . '/languages/Settings.' . $language . '.php'); + else + { + $txt['theme_thumbnail_href'] = $theme_data['images_url'] . '/thumbnail.png'; + $txt['theme_description'] = ''; + } + + $context['available_themes'][$id_theme]['thumbnail_href'] = sprintf($txt['theme_thumbnail_href'], $settings['images_url']); + $context['available_themes'][$id_theme]['description'] = $txt['theme_description']; + + // Are there any variants? + $context['available_themes'][$id_theme]['variants'] = array(); + if (file_exists($theme_data['theme_dir'] . '/index.template.php') && (empty($theme_data['disable_user_variant']) || allowedTo('admin_forum'))) + { + $file_contents = implode('', file($theme_data['theme_dir'] . '/index.template.php')); + if (preg_match('~\$settings\[\'theme_variants\'\]\s*=(.+?);~', $file_contents, $matches)) + { + $settings['theme_variants'] = array(); + + // Fill settings up. + eval('global $settings;' . $matches[0]); + + if (!empty($settings['theme_variants'])) + { + loadLanguage('Settings'); + + foreach ($settings['theme_variants'] as $variant) + $context['available_themes'][$id_theme]['variants'][$variant] = array( + 'label' => isset($txt['variant_' . $variant]) ? $txt['variant_' . $variant] : $variant, + 'thumbnail' => !file_exists($theme_data['theme_dir'] . '/images/thumbnail.png') || file_exists($theme_data['theme_dir'] . '/images/thumbnail_' . $variant . '.png') ? $theme_data['images_url'] . '/thumbnail_' . $variant . '.png' : ($theme_data['images_url'] . '/thumbnail.png'), + ); + + $context['available_themes'][$id_theme]['selected_variant'] = isset($_GET['vrt']) ? $_GET['vrt'] : (!empty($variant_preferences[$id_theme]) ? $variant_preferences[$id_theme] : (!empty($settings['default_variant']) ? $settings['default_variant'] : $settings['theme_variants'][0])); + if (!isset($context['available_themes'][$id_theme]['variants'][$context['available_themes'][$id_theme]['selected_variant']]['thumbnail'])) + $context['available_themes'][$id_theme]['selected_variant'] = $settings['theme_variants'][0]; + + $context['available_themes'][$id_theme]['thumbnail_href'] = $context['available_themes'][$id_theme]['variants'][$context['available_themes'][$id_theme]['selected_variant']]['thumbnail']; + // Allow themes to override the text. + $context['available_themes'][$id_theme]['pick_label'] = isset($txt['variant_pick']) ? $txt['variant_pick'] : $txt['theme_pick_variant']; + } + } + } + } + // Then return it. + addJavaScriptVar( + 'oThemeVariants', + json_encode(array_map( + function($theme) + { + return $theme['variants']; + }, + $context['available_themes'] + )) + ); + loadJavaScriptFile('profile.js', array('defer' => false, 'minimize' => true), 'smf_profile'); + $settings['images_url'] = $current_images_url; + $settings['theme_variants'] = $current_theme_variants; + + // As long as we're not doing the default theme... + if (!isset($_REQUEST['u']) || $_REQUEST['u'] >= 0) + { + if ($guest_theme != 0) + $context['available_themes'][0] = $context['available_themes'][$guest_theme]; + + $context['available_themes'][0]['id'] = 0; + $context['available_themes'][0]['name'] = $txt['theme_forum_default']; + $context['available_themes'][0]['selected'] = $context['current_theme'] == 0; + $context['available_themes'][0]['description'] = $txt['theme_global_description']; + } + + ksort($context['available_themes']); + + $context['page_title'] = $txt['theme_pick']; + $context['sub_template'] = 'pick'; + createToken('pick-th'); +} + +/** + * Installs new themes, calls the respective function according to the install type. + * - puts themes in $boardurl/Themes. + * - assumes the gzip has a root directory in it. (ie default.) + * Requires admin_forum. + * Accessed with ?action=admin;area=theme;sa=install. + */ +function ThemeInstall() +{ + global $sourcedir, $txt, $context, $boarddir, $boardurl; + global $themedir, $themeurl, $smcFunc; + + checkSession('request'); + isAllowedTo('admin_forum'); + + require_once($sourcedir . '/Subs-Package.php'); + + // Make it easier to change the path and url. + $themedir = $boarddir . '/Themes'; + $themeurl = $boardurl . '/Themes'; + + loadTemplate('Themes'); + + $subActions = array( + 'file' => 'InstallFile', + 'copy' => 'InstallCopy', + 'dir' => 'InstallDir', + ); + + // Is there a function to call? + if (isset($_GET['do']) && !empty($_GET['do']) && isset($subActions[$_GET['do']])) + { + $action = $smcFunc['htmlspecialchars'](trim($_GET['do'])); + + // Got any info from the specific form? + if (!isset($_POST['save_' . $action])) + fatal_lang_error('theme_install_no_action', false); + + validateToken('admin-t-' . $action); + + // Hopefully the themes directory is writable, or we might have a problem. + if (!is_writable($themedir)) + fatal_lang_error('theme_install_write_error', 'critical'); + + // Call the function and handle the result. + $result = $subActions[$action](); + + // Everything went better than expected! + if (!empty($result)) + { + $context['sub_template'] = 'installed'; + $context['page_title'] = $txt['theme_installed']; + $context['installed_theme'] = $result; + } + } + + // Nope, show a nice error. + else + fatal_lang_error('theme_install_no_action', false); +} + +/** + * Installs a theme from a theme package. + * + * Stores the theme files on a temp dir, on success it renames the dir to the new theme's name. Ends execution with fatal_lang_error() on any error. + * + * @return array The newly created theme's info. + */ +function InstallFile() +{ + global $themedir, $themeurl, $context; + + // Set a temp dir for dumping all required files on it. + $dirtemp = $themedir . '/temp'; + + // Make sure the temp dir doesn't already exist + if (file_exists($dirtemp)) + remove_dir($dirtemp); + + // Create the temp dir. + mkdir($dirtemp, 0777); + + // Hopefully the temp directory is writable, or we might have a problem. + if (!is_writable($dirtemp)) + { + // Lets give it a try. + smf_chmod($dirtemp, '0755'); + + // How about now? + if (!is_writable($dirtemp)) + fatal_lang_error('theme_install_write_error', 'critical'); + } + + // This happens when the admin session is gone and the user has to login again. + if (!isset($_FILES) || !isset($_FILES['theme_gz']) || empty($_FILES['theme_gz'])) + redirectexit('action=admin;area=theme;sa=admin;' . $context['session_var'] . '=' . $context['session_id']); + + // Another error check layer, something went wrong with the upload. + if (isset($_FILES['theme_gz']['error']) && $_FILES['theme_gz']['error'] != 0) + fatal_lang_error('theme_install_error_file_' . $_FILES['theme_gz']['error'], false); + + // Get the theme's name. + $name = pathinfo($_FILES['theme_gz']['name'], PATHINFO_FILENAME); + $name = preg_replace(array('/\s/', '/\.[\.]+/', '/[^\w_\.\-]/', '/\.tar$/'), array('_', '.', '', ''), $name); + + // Start setting some vars. + $context['to_install'] = array( + 'theme_dir' => $themedir . '/' . $name, + 'theme_url' => $themeurl . '/' . $name, + 'images_url' => $themeurl . '/' . $name . '/images', + 'name' => $name, + ); + + // Extract the file on the proper themes dir. + $extracted = read_tgz_file($_FILES['theme_gz']['tmp_name'], $dirtemp, false, true); + + if ($extracted) + { + // Read its info form the XML file. + $theme_info = get_theme_info($dirtemp); + $context['to_install'] += $theme_info; + + // Install the theme. theme_install() will return the new installed ID. + $context['to_install']['id'] = theme_install($context['to_install']); + + // Rename the temp dir to the actual theme name. + rename($dirtemp, $context['to_install']['theme_dir']); + + // return all the info. + return $context['to_install']; + } + + else + fatal_lang_error('theme_install_error_title', false); +} + +/** + * Makes a copy from the default theme, assigns a name for it and installs it. + * + * Creates a new .xml file containing all the theme's info. + * + * @return array The newly created theme's info. + */ +function InstallCopy() +{ + global $themedir, $themeurl, $settings, $smcFunc, $context; + + // There's gotta be something to work with. + if (!isset($_REQUEST['copy']) || empty($_REQUEST['copy'])) + fatal_lang_error('theme_install_error_title', false); + + // Get a cleaner version. + $name = preg_replace('~[^A-Za-z0-9_\- ]~', '', $_REQUEST['copy']); + + // Is there a theme already named like this? + if (file_exists($themedir . '/' . $name)) + fatal_lang_error('theme_install_already_dir', false); + + // This is a brand new theme so set all possible values. + $context['to_install'] = array( + 'theme_dir' => $themedir . '/' . $name, + 'theme_url' => $themeurl . '/' . $name, + 'name' => $name, + 'images_url' => $themeurl . '/' . $name . '/images', + 'version' => '1.0', + 'install_for' => '2.1 - 2.1.99, ' . SMF_VERSION, + 'based_on' => '', + 'based_on_dir' => $themedir . '/default', + 'theme_layers' => 'html,body', + 'theme_templates' => 'index', + ); + + // Create the specific dir. + umask(0); + mkdir($context['to_install']['theme_dir'], 0777); + + // Buy some time. + @set_time_limit(600); + if (function_exists('apache_reset_timeout')) + @apache_reset_timeout(); + + // Create subdirectories for css and javascript files. + mkdir($context['to_install']['theme_dir'] . '/css', 0777); + mkdir($context['to_install']['theme_dir'] . '/scripts', 0777); + + // Create subdirectory for language files + mkdir($context['to_install']['theme_dir'] . '/languages', 0777); + + // Copy over the default non-theme files. + $to_copy = array( + '/index.php', + '/index.template.php', + '/css/admin.css', + '/css/calendar.css', + '/css/calendar.rtl.css', + '/css/index.css', + '/css/responsive.css', + '/css/rtl.css', + '/scripts/theme.js', + '/languages/index.php', + '/languages/Settings.english.php', + ); + + foreach ($to_copy as $file) + { + copy($settings['default_theme_dir'] . $file, $context['to_install']['theme_dir'] . $file); + smf_chmod($context['to_install']['theme_dir'] . $file, 0777); + } + + // And now the entire images directory! + copytree($settings['default_theme_dir'] . '/images', $context['to_install']['theme_dir'] . '/images'); + package_flush_cache(); + + // Any data from the default theme that we want? + foreach (get_single_theme(1, array('theme_layers', 'theme_templates')) as $variable => $value) + if ($variable == 'theme_templates' || $variable == 'theme_layers') + $context['to_install'][$variable] = $value; + + // Lets add a theme_info.xml to this theme. + $xml_info = '<' . '?xml version="1.0"?' . '> + + +smf:' . $smcFunc['strtolower']($context['to_install']['name']) . ' + +1.0 + + + +' . $context['to_install']['name'] . ' + +info@simplemachines.org + +https://www.simplemachines.org/ + +' . $context['to_install']['theme_layers'] . ' + +' . $context['to_install']['theme_templates'] . ' + + +'; + + // Now write it. + $fp = @fopen($context['to_install']['theme_dir'] . '/theme_info.xml', 'w+'); + if ($fp) + { + fwrite($fp, $xml_info); + fclose($fp); + } + + // Install the theme. theme_install() will take care of possible errors. + $context['to_install']['id'] = theme_install($context['to_install']); + + // return the info. + return $context['to_install']; +} + +/** + * Install a theme from a specific dir + * + * Assumes the dir is located on the main Themes dir. Ends execution with fatal_lang_error() on any error. + * + * @return array The newly created theme's info. + */ +function InstallDir() +{ + global $themedir, $themeurl, $context; + + // Cannot use the theme dir as a theme dir. + if (!isset($_REQUEST['theme_dir']) || empty($_REQUEST['theme_dir']) || rtrim(realpath($_REQUEST['theme_dir']), '/\\') == realpath($themedir)) + fatal_lang_error('theme_install_invalid_dir', false); + + // Check is there is "something" on the dir. + elseif (!is_dir($_REQUEST['theme_dir']) || !file_exists($_REQUEST['theme_dir'] . '/theme_info.xml')) + fatal_lang_error('theme_install_error', false); + + $name = basename($_REQUEST['theme_dir']); + $name = preg_replace(array('/\s/', '/\.[\.]+/', '/[^\w_\.\-]/'), array('_', '.', ''), $name); + + // All good! set some needed vars. + $context['to_install'] = array( + 'theme_dir' => $_REQUEST['theme_dir'], + 'theme_url' => $themeurl . '/' . $name, + 'name' => $name, + 'images_url' => $themeurl . '/' . $name . '/images', + ); + + // Read its info form the XML file. + $theme_info = get_theme_info($context['to_install']['theme_dir']); + $context['to_install'] += $theme_info; + + // Install the theme. theme_install() will take care of possible errors. + $context['to_install']['id'] = theme_install($context['to_install']); + + // return the info. + return $context['to_install']; +} + +/** + * Possibly the simplest and best example of how to use the template system. + * - allows the theme to take care of actions. + * - happens if $settings['catch_action'] is set and action isn't found + * in the action array. + * - can use a template, layers, sub_template, filename, and/or function. + */ +function WrapAction() +{ + global $context, $settings; + + // Load any necessary template(s)? + if (isset($settings['catch_action']['template'])) + { + // Load both the template and language file. (but don't fret if the language file isn't there...) + loadTemplate($settings['catch_action']['template']); + loadLanguage($settings['catch_action']['template'], '', false); + } + + // Any special layers? + if (isset($settings['catch_action']['layers'])) + $context['template_layers'] = $settings['catch_action']['layers']; + + // Any function to call? + if (isset($settings['catch_action']['function'])) + { + $hook = $settings['catch_action']['function']; + + if (!isset($settings['catch_action']['filename'])) + $settings['catch_action']['filename'] = ''; + + add_integration_function('integrate_wrap_action', $hook, false, $settings['catch_action']['filename'], false); + call_integration_hook('integrate_wrap_action'); + } + // And finally, the main sub template ;). + if (isset($settings['catch_action']['sub_template'])) + $context['sub_template'] = $settings['catch_action']['sub_template']; +} + +/** + * Set an option via javascript. + * - sets a theme option without outputting anything. + * - can be used with javascript, via a dummy image... (which doesn't require + * the page to reload.) + * - requires someone who is logged in. + * - accessed via ?action=jsoption;var=variable;val=value;session_var=sess_id. + * - does not log access to the Who's Online log. (in index.php..) + */ +function SetJavaScript() +{ + global $settings, $user_info, $smcFunc, $options; + + // Check the session id. + checkSession('get'); + + // This good-for-nothing pixel is being used to keep the session alive. + if (empty($_GET['var']) || !isset($_GET['val'])) + redirectexit($settings['images_url'] . '/blank.png'); + + // Sorry, guests can't go any further than this. + if ($user_info['is_guest'] || $user_info['id'] == 0) + obExit(false); + + $reservedVars = array( + 'actual_theme_url', + 'actual_images_url', + 'base_theme_dir', + 'base_theme_url', + 'default_images_url', + 'default_theme_dir', + 'default_theme_url', + 'default_template', + 'images_url', + 'number_recent_posts', + 'smiley_sets_default', + 'theme_dir', + 'theme_id', + 'theme_layers', + 'theme_templates', + 'theme_url', + 'name', + ); + + // Can't change reserved vars. + if (in_array(strtolower($_GET['var']), $reservedVars)) + redirectexit($settings['images_url'] . '/blank.png'); + + // Use a specific theme? + if (isset($_GET['th']) || isset($_GET['id'])) + { + // Invalidate the current themes cache too. + cache_put_data('theme_settings-' . $settings['theme_id'] . ':' . $user_info['id'], null, 60); + + $settings['theme_id'] = isset($_GET['th']) ? (int) $_GET['th'] : (int) $_GET['id']; + } + + // If this is the admin preferences the passed value will just be an element of it. + if ($_GET['var'] == 'admin_preferences') + { + $options['admin_preferences'] = !empty($options['admin_preferences']) ? $smcFunc['json_decode']($options['admin_preferences'], true) : array(); + // New thingy... + if (isset($_GET['admin_key']) && strlen($_GET['admin_key']) < 5) + $options['admin_preferences'][$_GET['admin_key']] = $_GET['val']; + + // Change the value to be something nice, + $_GET['val'] = $smcFunc['json_encode']($options['admin_preferences']); + } + + // Update the option. + $smcFunc['db_insert']('replace', + '{db_prefix}themes', + array('id_theme' => 'int', 'id_member' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'), + array($settings['theme_id'], $user_info['id'], $_GET['var'], is_array($_GET['val']) ? implode(',', $_GET['val']) : $_GET['val']), + array('id_theme', 'id_member', 'variable') + ); + + cache_put_data('theme_settings-' . $settings['theme_id'] . ':' . $user_info['id'], null, 60); + + // Don't output anything... + redirectexit($settings['images_url'] . '/blank.png'); +} + +/** + * Shows an interface for editing the templates. + * - uses the Themes template and edit_template/edit_style sub template. + * - accessed via ?action=admin;area=theme;sa=edit + */ +function EditTheme() +{ + global $context, $scripturl, $boarddir, $smcFunc, $txt; + + // @todo Should this be removed? + if (isset($_REQUEST['preview'])) + die('die() with fire'); + + isAllowedTo('admin_forum'); + loadTemplate('Themes'); + + $_GET['th'] = isset($_GET['th']) ? (int) $_GET['th'] : (int) @$_GET['id']; + + if (empty($_GET['th'])) + { + get_installed_themes(); + + foreach ($context['themes'] as $key => $theme) + { + // There has to be a Settings template! + if (!file_exists($theme['theme_dir'] . '/index.template.php') && !file_exists($theme['theme_dir'] . '/css/index.css')) + unset($context['themes'][$key]); + + else + $context['themes'][$key]['can_edit_style'] = file_exists($theme['theme_dir'] . '/css/index.css'); + } + + $context['sub_template'] = 'edit_list'; + + return 'no_themes'; + } + + $context['session_error'] = false; + + // Get the directory of the theme we are editing. + $currentTheme = get_single_theme($_GET['th']); + $context['theme_id'] = $currentTheme['id']; + $context['browse_title'] = sprintf($txt['themeadmin_browsing_theme'], $currentTheme['name']); + + if (!file_exists($currentTheme['theme_dir'] . '/index.template.php') && !file_exists($currentTheme['theme_dir'] . '/css/index.css')) + fatal_lang_error('theme_edit_missing', false); + + if (!isset($_REQUEST['filename'])) + { + if (isset($_GET['directory'])) + { + if (substr($_GET['directory'], 0, 1) == '.') + $_GET['directory'] = ''; + else + { + $_GET['directory'] = preg_replace(array('~^[\./\\:\0\n\r]+~', '~[\\\\]~', '~/[\./]+~'), array('', '/', '/'), $_GET['directory']); + + $temp = realpath($currentTheme['theme_dir'] . '/' . $_GET['directory']); + if (empty($temp) || substr($temp, 0, strlen(realpath($currentTheme['theme_dir']))) != realpath($currentTheme['theme_dir'])) + $_GET['directory'] = ''; + } + } + + if (isset($_GET['directory']) && $_GET['directory'] != '') + { + $context['theme_files'] = get_file_listing($currentTheme['theme_dir'] . '/' . $_GET['directory'], $_GET['directory'] . '/'); + + $temp = dirname($_GET['directory']); + array_unshift($context['theme_files'], array( + 'filename' => $temp == '.' || $temp == '' ? '/ (..)' : $temp . ' (..)', + 'is_writable' => is_writable($currentTheme['theme_dir'] . '/' . $temp), + 'is_directory' => true, + 'is_template' => false, + 'is_image' => false, + 'is_editable' => false, + 'href' => $scripturl . '?action=admin;area=theme;th=' . $_GET['th'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=edit;directory=' . $temp, + 'size' => '', + )); + } + else + $context['theme_files'] = get_file_listing($currentTheme['theme_dir'], ''); + + // Do not list minified_ files + foreach ($context['theme_files'] as $key => $file) + { + if (strpos($file['filename'], 'minified_') !== false) + unset($context['theme_files'][$key]); + } + + $context['sub_template'] = 'edit_browse'; + + return; + } + else + { + if (substr($_REQUEST['filename'], 0, 1) == '.') + $_REQUEST['filename'] = ''; + else + { + $_REQUEST['filename'] = preg_replace(array('~^[\./\\:\0\n\r]+~', '~[\\\\]~', '~/[\./]+~'), array('', '/', '/'), $_REQUEST['filename']); + + $temp = realpath($currentTheme['theme_dir'] . '/' . $_REQUEST['filename']); + if (empty($temp) || substr($temp, 0, strlen(realpath($currentTheme['theme_dir']))) != realpath($currentTheme['theme_dir'])) + $_REQUEST['filename'] = ''; + } + + if (empty($_REQUEST['filename'])) + fatal_lang_error('theme_edit_missing', false); + } + + if (isset($_POST['save'])) + { + if (checkSession('post', '', false) == '' && validateToken('admin-te-' . md5($_GET['th'] . '-' . $_REQUEST['filename']), 'post', false) == true) + { + if (is_array($_POST['entire_file'])) + $_POST['entire_file'] = implode("\n", $_POST['entire_file']); + + $_POST['entire_file'] = rtrim(strtr($_POST['entire_file'], array("\r" => '', ' ' => "\t"))); + + // Check for a parse error! + if (substr($_REQUEST['filename'], -13) == '.template.php' && is_writable($currentTheme['theme_dir']) && ini_get('display_errors')) + { + $fp = fopen($currentTheme['theme_dir'] . '/tmp_' . session_id() . '.php', 'w'); + fwrite($fp, $_POST['entire_file']); + fclose($fp); + + $error = @file_get_contents($currentTheme['theme_url'] . '/tmp_' . session_id() . '.php'); + if (preg_match('~ (\d+)$~i', $error) != 0) + $error_file = $currentTheme['theme_dir'] . '/tmp_' . session_id() . '.php'; + else + unlink($currentTheme['theme_dir'] . '/tmp_' . session_id() . '.php'); + } + + if (!isset($error_file)) + { + $fp = fopen($currentTheme['theme_dir'] . '/' . $_REQUEST['filename'], 'w'); + fwrite($fp, $_POST['entire_file']); + fclose($fp); + + // Nuke any minified files and update $modSettings['browser_cache'] + deleteAllMinified(); + + redirectexit('action=admin;area=theme;th=' . $_GET['th'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=edit;directory=' . dirname($_REQUEST['filename'])); + } + } + // Session timed out. + else + { + loadLanguage('Errors'); + + $context['session_error'] = true; + $context['sub_template'] = 'edit_file'; + + // Recycle the submitted data. + if (is_array($_POST['entire_file'])) + $context['entire_file'] = $smcFunc['htmlspecialchars'](implode("\n", $_POST['entire_file'])); + else + $context['entire_file'] = $smcFunc['htmlspecialchars']($_POST['entire_file']); + + $context['edit_filename'] = $smcFunc['htmlspecialchars']($_POST['filename']); + + // You were able to submit it, so it's reasonable to assume you are allowed to save. + $context['allow_save'] = true; + + // Re-create the token so that it can be used + createToken('admin-te-' . md5($_GET['th'] . '-' . $_REQUEST['filename'])); + + return; + } + } + + $context['allow_save'] = is_writable($currentTheme['theme_dir'] . '/' . $_REQUEST['filename']); + $context['allow_save_filename'] = strtr($currentTheme['theme_dir'] . '/' . $_REQUEST['filename'], array($boarddir => '...')); + $context['edit_filename'] = $smcFunc['htmlspecialchars']($_REQUEST['filename']); + + if (substr($_REQUEST['filename'], -4) == '.css') + { + $context['sub_template'] = 'edit_style'; + + $context['entire_file'] = $smcFunc['htmlspecialchars'](strtr(file_get_contents($currentTheme['theme_dir'] . '/' . $_REQUEST['filename']), array("\t" => ' '))); + } + elseif (substr($_REQUEST['filename'], -13) == '.template.php') + { + $context['sub_template'] = 'edit_template'; + + if (!isset($error_file)) + $file_data = file($currentTheme['theme_dir'] . '/' . $_REQUEST['filename']); + else + { + if (preg_match('~(.+?:.+?).+?(.+?\d+)$~i', $error, $match) != 0) + $context['parse_error'] = $match[1] . $_REQUEST['filename'] . $match[2]; + $file_data = file($error_file); + unlink($error_file); + } + + $j = 0; + $context['file_parts'] = array(array('lines' => 0, 'line' => 1, 'data' => '')); + for ($i = 0, $n = count($file_data); $i < $n; $i++) + { + if (isset($file_data[$i + 1]) && substr($file_data[$i + 1], 0, 9) == 'function ') + { + // Try to format the functions a little nicer... + $context['file_parts'][$j]['data'] = trim($context['file_parts'][$j]['data']) . "\n"; + + if (empty($context['file_parts'][$j]['lines'])) + unset($context['file_parts'][$j]); + $context['file_parts'][++$j] = array('lines' => 0, 'line' => $i + 1, 'data' => ''); + } + + $context['file_parts'][$j]['lines']++; + $context['file_parts'][$j]['data'] .= $smcFunc['htmlspecialchars'](strtr($file_data[$i], array("\t" => ' '))); + } + + $context['entire_file'] = $smcFunc['htmlspecialchars'](strtr(implode('', $file_data), array("\t" => ' '))); + } + else + { + $context['sub_template'] = 'edit_file'; + + $context['entire_file'] = $smcFunc['htmlspecialchars'](strtr(file_get_contents($currentTheme['theme_dir'] . '/' . $_REQUEST['filename']), array("\t" => ' '))); + } + + // Create a special token to allow editing of multiple files. + createToken('admin-te-' . md5($_GET['th'] . '-' . $_REQUEST['filename'])); +} + +/** + * Makes a copy of a template file in a new location + * + * @uses template_copy_template() + */ +function CopyTemplate() +{ + global $context, $settings; + + isAllowedTo('admin_forum'); + loadTemplate('Themes'); + + $context[$context['admin_menu_name']]['current_subsection'] = 'edit'; + + $_GET['th'] = isset($_GET['th']) ? (int) $_GET['th'] : (int) $_GET['id']; + + if (empty($_GET['th'])) + fatal_lang_error('theme_install_invalid_id'); + + // Get the theme info. + $theme = get_single_theme($_GET['th']); + $context['theme_id'] = $theme['id']; + + if (isset($_REQUEST['template']) && preg_match('~[\./\\\\:\0]~', $_REQUEST['template']) == 0) + { + if (file_exists($settings['default_theme_dir'] . '/' . $_REQUEST['template'] . '.template.php')) + $filename = $settings['default_theme_dir'] . '/' . $_REQUEST['template'] . '.template.php'; + + else + fatal_lang_error('no_access', false); + + $fp = fopen($theme['theme_dir'] . '/' . $_REQUEST['template'] . '.template.php', 'w'); + fwrite($fp, file_get_contents($filename)); + fclose($fp); + + redirectexit('action=admin;area=theme;th=' . $context['theme_id'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=copy'); + } + elseif (isset($_REQUEST['lang_file']) && preg_match('~^[^\./\\\\:\0]\.[^\./\\\\:\0]$~', $_REQUEST['lang_file']) != 0) + { + if (file_exists($settings['default_theme_dir'] . '/languages/' . $_REQUEST['template'] . '.php')) + $filename = $settings['default_theme_dir'] . '/languages/' . $_REQUEST['template'] . '.php'; + + else + fatal_lang_error('no_access', false); + + $fp = fopen($theme['theme_dir'] . '/languages/' . $_REQUEST['lang_file'] . '.php', 'w'); + fwrite($fp, file_get_contents($filename)); + fclose($fp); + + redirectexit('action=admin;area=theme;th=' . $context['theme_id'] . ';' . $context['session_var'] . '=' . $context['session_id'] . ';sa=copy'); + } + + $templates = array(); + $lang_files = array(); + + $dir = dir($settings['default_theme_dir']); + while ($entry = $dir->read()) + { + if (substr($entry, -13) == '.template.php') + $templates[] = substr($entry, 0, -13); + } + $dir->close(); + + $dir = dir($settings['default_theme_dir'] . '/languages'); + while ($entry = $dir->read()) + { + if (preg_match('~^([^\.]+\.[^\.]+)\.php$~', $entry, $matches)) + $lang_files[] = $matches[1]; + } + $dir->close(); + + natcasesort($templates); + natcasesort($lang_files); + + $context['available_templates'] = array(); + foreach ($templates as $template) + $context['available_templates'][$template] = array( + 'filename' => $template . '.template.php', + 'value' => $template, + 'already_exists' => false, + 'can_copy' => is_writable($theme['theme_dir']), + ); + $context['available_language_files'] = array(); + foreach ($lang_files as $file) + $context['available_language_files'][$file] = array( + 'filename' => $file . '.php', + 'value' => $file, + 'already_exists' => false, + 'can_copy' => file_exists($theme['theme_dir'] . '/languages') ? is_writable($theme['theme_dir'] . '/languages') : is_writable($theme['theme_dir']), + ); + + $dir = dir($theme['theme_dir']); + while ($entry = $dir->read()) + { + if (substr($entry, -13) == '.template.php' && isset($context['available_templates'][substr($entry, 0, -13)])) + { + $context['available_templates'][substr($entry, 0, -13)]['already_exists'] = true; + $context['available_templates'][substr($entry, 0, -13)]['can_copy'] = is_writable($theme['theme_dir'] . '/' . $entry); + } + } + $dir->close(); + + if (file_exists($theme['theme_dir'] . '/languages')) + { + $dir = dir($theme['theme_dir'] . '/languages'); + while ($entry = $dir->read()) + { + if (preg_match('~^([^\.]+\.[^\.]+)\.php$~', $entry, $matches) && isset($context['available_language_files'][$matches[1]])) + { + $context['available_language_files'][$matches[1]]['already_exists'] = true; + $context['available_language_files'][$matches[1]]['can_copy'] = is_writable($theme['theme_dir'] . '/languages/' . $entry); + } + } + $dir->close(); + } + + $context['sub_template'] = 'copy_template'; +} + +?> \ No newline at end of file diff --git a/Sources/Topic.php b/Sources/Topic.php new file mode 100644 index 0000000..5cb254f --- /dev/null +++ b/Sources/Topic.php @@ -0,0 +1,169 @@ + $topic, + ) + ); + list ($starter, $locked) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // Can you lock topics here, mister? + $user_lock = !allowedTo('lock_any'); + if ($user_lock && $starter == $user_info['id']) + isAllowedTo('lock_own'); + else + isAllowedTo('lock_any'); + + // Another moderator got the job done first? + if (isset($_GET['sa']) && $_GET['sa'] == 'unlock' && $locked == '0') + fatal_lang_error('error_topic_locked_already', false); + elseif (isset($_GET['sa']) && $_GET['sa'] == 'lock' && ($locked == '1' || $locked == '2')) + fatal_lang_error('error_topic_unlocked_already', false); + + // Locking with high privileges. + if ($locked == '0' && !$user_lock) + $locked = '1'; + // Locking with low privileges. + elseif ($locked == '0') + $locked = '2'; + // Unlocking - make sure you don't unlock what you can't. + elseif ($locked == '2' || ($locked == '1' && !$user_lock)) + $locked = '0'; + // You cannot unlock this! + else + fatal_lang_error('locked_by_admin', 'user'); + + // Actually lock the topic in the database with the new value. + $smcFunc['db_query']('', ' + UPDATE {db_prefix}topics + SET locked = {int:locked} + WHERE id_topic = {int:current_topic}', + array( + 'current_topic' => $topic, + 'locked' => $locked, + ) + ); + + // If they are allowed a "moderator" permission, log it in the moderator log. + if (!$user_lock) + logAction($locked ? 'lock' : 'unlock', array('topic' => $topic, 'board' => $board)); + // Notify people that this topic has been locked? + sendNotifications($topic, empty($locked) ? 'unlock' : 'lock'); + + // Back to the topic! + redirectexit('topic=' . $topic . '.' . $_REQUEST['start'] . ';moderate'); +} + +/** + * Sticky a topic. + * Can't be done by topic starters - that would be annoying! + * What this does: + * - stickies a topic - toggles between sticky and normal. + * - requires the make_sticky permission. + * - adds an entry to the moderator log. + * - when done, sends the user back to the topic. + * - accessed via ?action=sticky. + */ +function Sticky() +{ + global $topic, $board, $sourcedir, $smcFunc; + + // Make sure the user can sticky it, and they are stickying *something*. + isAllowedTo('make_sticky'); + + // You can't sticky a board or something! + if (empty($topic)) + fatal_lang_error('not_a_topic', false); + + checkSession('get'); + + // We need Subs-Post.php for the sendNotifications() function. + require_once($sourcedir . '/Subs-Post.php'); + + // Is this topic already stickied, or no? + $request = $smcFunc['db_query']('', ' + SELECT is_sticky + FROM {db_prefix}topics + WHERE id_topic = {int:current_topic} + LIMIT 1', + array( + 'current_topic' => $topic, + ) + ); + list ($is_sticky) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // Another moderator got the job done first? + if (isset($_GET['sa']) && $_GET['sa'] == 'nonsticky' && $is_sticky == '0') + fatal_lang_error('error_topic_nonsticky_already', false); + elseif (isset($_GET['sa']) && $_GET['sa'] == 'sticky' && $is_sticky == '1') + fatal_lang_error('error_topic_sticky_already', false); + + // Toggle the sticky value.... pretty simple ;). + $smcFunc['db_query']('', ' + UPDATE {db_prefix}topics + SET is_sticky = {int:is_sticky} + WHERE id_topic = {int:current_topic}', + array( + 'current_topic' => $topic, + 'is_sticky' => empty($is_sticky) ? 1 : 0, + ) + ); + + // Log this sticky action - always a moderator thing. + logAction(empty($is_sticky) ? 'sticky' : 'unsticky', array('topic' => $topic, 'board' => $board)); + // Notify people that this topic has been stickied? + if (empty($is_sticky)) + sendNotifications($topic, 'sticky'); + + // Take them back to the now stickied topic. + redirectexit('topic=' . $topic . '.' . $_REQUEST['start'] . ';moderate'); +} + +?> \ No newline at end of file diff --git a/Sources/Unicode/CaseFold.php b/Sources/Unicode/CaseFold.php new file mode 100644 index 0000000..7bc0167 --- /dev/null +++ b/Sources/Unicode/CaseFold.php @@ -0,0 +1,3029 @@ + "\x61", + "\x42" => "\x62", + "\x43" => "\x63", + "\x44" => "\x64", + "\x45" => "\x65", + "\x46" => "\x66", + "\x47" => "\x67", + "\x48" => "\x68", + "\x49" => "\x69", + "\x4A" => "\x6A", + "\x4B" => "\x6B", + "\x4C" => "\x6C", + "\x4D" => "\x6D", + "\x4E" => "\x6E", + "\x4F" => "\x6F", + "\x50" => "\x70", + "\x51" => "\x71", + "\x52" => "\x72", + "\x53" => "\x73", + "\x54" => "\x74", + "\x55" => "\x75", + "\x56" => "\x76", + "\x57" => "\x77", + "\x58" => "\x78", + "\x59" => "\x79", + "\x5A" => "\x7A", + "\xC2\xB5" => "\xCE\xBC", + "\xC3\x80" => "\xC3\xA0", + "\xC3\x81" => "\xC3\xA1", + "\xC3\x82" => "\xC3\xA2", + "\xC3\x83" => "\xC3\xA3", + "\xC3\x84" => "\xC3\xA4", + "\xC3\x85" => "\xC3\xA5", + "\xC3\x86" => "\xC3\xA6", + "\xC3\x87" => "\xC3\xA7", + "\xC3\x88" => "\xC3\xA8", + "\xC3\x89" => "\xC3\xA9", + "\xC3\x8A" => "\xC3\xAA", + "\xC3\x8B" => "\xC3\xAB", + "\xC3\x8C" => "\xC3\xAC", + "\xC3\x8D" => "\xC3\xAD", + "\xC3\x8E" => "\xC3\xAE", + "\xC3\x8F" => "\xC3\xAF", + "\xC3\x90" => "\xC3\xB0", + "\xC3\x91" => "\xC3\xB1", + "\xC3\x92" => "\xC3\xB2", + "\xC3\x93" => "\xC3\xB3", + "\xC3\x94" => "\xC3\xB4", + "\xC3\x95" => "\xC3\xB5", + "\xC3\x96" => "\xC3\xB6", + "\xC3\x98" => "\xC3\xB8", + "\xC3\x99" => "\xC3\xB9", + "\xC3\x9A" => "\xC3\xBA", + "\xC3\x9B" => "\xC3\xBB", + "\xC3\x9C" => "\xC3\xBC", + "\xC3\x9D" => "\xC3\xBD", + "\xC3\x9E" => "\xC3\xBE", + "\xC4\x80" => "\xC4\x81", + "\xC4\x82" => "\xC4\x83", + "\xC4\x84" => "\xC4\x85", + "\xC4\x86" => "\xC4\x87", + "\xC4\x88" => "\xC4\x89", + "\xC4\x8A" => "\xC4\x8B", + "\xC4\x8C" => "\xC4\x8D", + "\xC4\x8E" => "\xC4\x8F", + "\xC4\x90" => "\xC4\x91", + "\xC4\x92" => "\xC4\x93", + "\xC4\x94" => "\xC4\x95", + "\xC4\x96" => "\xC4\x97", + "\xC4\x98" => "\xC4\x99", + "\xC4\x9A" => "\xC4\x9B", + "\xC4\x9C" => "\xC4\x9D", + "\xC4\x9E" => "\xC4\x9F", + "\xC4\xA0" => "\xC4\xA1", + "\xC4\xA2" => "\xC4\xA3", + "\xC4\xA4" => "\xC4\xA5", + "\xC4\xA6" => "\xC4\xA7", + "\xC4\xA8" => "\xC4\xA9", + "\xC4\xAA" => "\xC4\xAB", + "\xC4\xAC" => "\xC4\xAD", + "\xC4\xAE" => "\xC4\xAF", + "\xC4\xB2" => "\xC4\xB3", + "\xC4\xB4" => "\xC4\xB5", + "\xC4\xB6" => "\xC4\xB7", + "\xC4\xB9" => "\xC4\xBA", + "\xC4\xBB" => "\xC4\xBC", + "\xC4\xBD" => "\xC4\xBE", + "\xC4\xBF" => "\xC5\x80", + "\xC5\x81" => "\xC5\x82", + "\xC5\x83" => "\xC5\x84", + "\xC5\x85" => "\xC5\x86", + "\xC5\x87" => "\xC5\x88", + "\xC5\x8A" => "\xC5\x8B", + "\xC5\x8C" => "\xC5\x8D", + "\xC5\x8E" => "\xC5\x8F", + "\xC5\x90" => "\xC5\x91", + "\xC5\x92" => "\xC5\x93", + "\xC5\x94" => "\xC5\x95", + "\xC5\x96" => "\xC5\x97", + "\xC5\x98" => "\xC5\x99", + "\xC5\x9A" => "\xC5\x9B", + "\xC5\x9C" => "\xC5\x9D", + "\xC5\x9E" => "\xC5\x9F", + "\xC5\xA0" => "\xC5\xA1", + "\xC5\xA2" => "\xC5\xA3", + "\xC5\xA4" => "\xC5\xA5", + "\xC5\xA6" => "\xC5\xA7", + "\xC5\xA8" => "\xC5\xA9", + "\xC5\xAA" => "\xC5\xAB", + "\xC5\xAC" => "\xC5\xAD", + "\xC5\xAE" => "\xC5\xAF", + "\xC5\xB0" => "\xC5\xB1", + "\xC5\xB2" => "\xC5\xB3", + "\xC5\xB4" => "\xC5\xB5", + "\xC5\xB6" => "\xC5\xB7", + "\xC5\xB8" => "\xC3\xBF", + "\xC5\xB9" => "\xC5\xBA", + "\xC5\xBB" => "\xC5\xBC", + "\xC5\xBD" => "\xC5\xBE", + "\xC5\xBF" => "\x73", + "\xC6\x81" => "\xC9\x93", + "\xC6\x82" => "\xC6\x83", + "\xC6\x84" => "\xC6\x85", + "\xC6\x86" => "\xC9\x94", + "\xC6\x87" => "\xC6\x88", + "\xC6\x89" => "\xC9\x96", + "\xC6\x8A" => "\xC9\x97", + "\xC6\x8B" => "\xC6\x8C", + "\xC6\x8E" => "\xC7\x9D", + "\xC6\x8F" => "\xC9\x99", + "\xC6\x90" => "\xC9\x9B", + "\xC6\x91" => "\xC6\x92", + "\xC6\x93" => "\xC9\xA0", + "\xC6\x94" => "\xC9\xA3", + "\xC6\x96" => "\xC9\xA9", + "\xC6\x97" => "\xC9\xA8", + "\xC6\x98" => "\xC6\x99", + "\xC6\x9C" => "\xC9\xAF", + "\xC6\x9D" => "\xC9\xB2", + "\xC6\x9F" => "\xC9\xB5", + "\xC6\xA0" => "\xC6\xA1", + "\xC6\xA2" => "\xC6\xA3", + "\xC6\xA4" => "\xC6\xA5", + "\xC6\xA6" => "\xCA\x80", + "\xC6\xA7" => "\xC6\xA8", + "\xC6\xA9" => "\xCA\x83", + "\xC6\xAC" => "\xC6\xAD", + "\xC6\xAE" => "\xCA\x88", + "\xC6\xAF" => "\xC6\xB0", + "\xC6\xB1" => "\xCA\x8A", + "\xC6\xB2" => "\xCA\x8B", + "\xC6\xB3" => "\xC6\xB4", + "\xC6\xB5" => "\xC6\xB6", + "\xC6\xB7" => "\xCA\x92", + "\xC6\xB8" => "\xC6\xB9", + "\xC6\xBC" => "\xC6\xBD", + "\xC7\x84" => "\xC7\x86", + "\xC7\x85" => "\xC7\x86", + "\xC7\x87" => "\xC7\x89", + "\xC7\x88" => "\xC7\x89", + "\xC7\x8A" => "\xC7\x8C", + "\xC7\x8B" => "\xC7\x8C", + "\xC7\x8D" => "\xC7\x8E", + "\xC7\x8F" => "\xC7\x90", + "\xC7\x91" => "\xC7\x92", + "\xC7\x93" => "\xC7\x94", + "\xC7\x95" => "\xC7\x96", + "\xC7\x97" => "\xC7\x98", + "\xC7\x99" => "\xC7\x9A", + "\xC7\x9B" => "\xC7\x9C", + "\xC7\x9E" => "\xC7\x9F", + "\xC7\xA0" => "\xC7\xA1", + "\xC7\xA2" => "\xC7\xA3", + "\xC7\xA4" => "\xC7\xA5", + "\xC7\xA6" => "\xC7\xA7", + "\xC7\xA8" => "\xC7\xA9", + "\xC7\xAA" => "\xC7\xAB", + "\xC7\xAC" => "\xC7\xAD", + "\xC7\xAE" => "\xC7\xAF", + "\xC7\xB1" => "\xC7\xB3", + "\xC7\xB2" => "\xC7\xB3", + "\xC7\xB4" => "\xC7\xB5", + "\xC7\xB6" => "\xC6\x95", + "\xC7\xB7" => "\xC6\xBF", + "\xC7\xB8" => "\xC7\xB9", + "\xC7\xBA" => "\xC7\xBB", + "\xC7\xBC" => "\xC7\xBD", + "\xC7\xBE" => "\xC7\xBF", + "\xC8\x80" => "\xC8\x81", + "\xC8\x82" => "\xC8\x83", + "\xC8\x84" => "\xC8\x85", + "\xC8\x86" => "\xC8\x87", + "\xC8\x88" => "\xC8\x89", + "\xC8\x8A" => "\xC8\x8B", + "\xC8\x8C" => "\xC8\x8D", + "\xC8\x8E" => "\xC8\x8F", + "\xC8\x90" => "\xC8\x91", + "\xC8\x92" => "\xC8\x93", + "\xC8\x94" => "\xC8\x95", + "\xC8\x96" => "\xC8\x97", + "\xC8\x98" => "\xC8\x99", + "\xC8\x9A" => "\xC8\x9B", + "\xC8\x9C" => "\xC8\x9D", + "\xC8\x9E" => "\xC8\x9F", + "\xC8\xA0" => "\xC6\x9E", + "\xC8\xA2" => "\xC8\xA3", + "\xC8\xA4" => "\xC8\xA5", + "\xC8\xA6" => "\xC8\xA7", + "\xC8\xA8" => "\xC8\xA9", + "\xC8\xAA" => "\xC8\xAB", + "\xC8\xAC" => "\xC8\xAD", + "\xC8\xAE" => "\xC8\xAF", + "\xC8\xB0" => "\xC8\xB1", + "\xC8\xB2" => "\xC8\xB3", + "\xC8\xBA" => "\xE2\xB1\xA5", + "\xC8\xBB" => "\xC8\xBC", + "\xC8\xBD" => "\xC6\x9A", + "\xC8\xBE" => "\xE2\xB1\xA6", + "\xC9\x81" => "\xC9\x82", + "\xC9\x83" => "\xC6\x80", + "\xC9\x84" => "\xCA\x89", + "\xC9\x85" => "\xCA\x8C", + "\xC9\x86" => "\xC9\x87", + "\xC9\x88" => "\xC9\x89", + "\xC9\x8A" => "\xC9\x8B", + "\xC9\x8C" => "\xC9\x8D", + "\xC9\x8E" => "\xC9\x8F", + "\xCD\x85" => "\xCE\xB9", + "\xCD\xB0" => "\xCD\xB1", + "\xCD\xB2" => "\xCD\xB3", + "\xCD\xB6" => "\xCD\xB7", + "\xCD\xBF" => "\xCF\xB3", + "\xCE\x86" => "\xCE\xAC", + "\xCE\x88" => "\xCE\xAD", + "\xCE\x89" => "\xCE\xAE", + "\xCE\x8A" => "\xCE\xAF", + "\xCE\x8C" => "\xCF\x8C", + "\xCE\x8E" => "\xCF\x8D", + "\xCE\x8F" => "\xCF\x8E", + "\xCE\x91" => "\xCE\xB1", + "\xCE\x92" => "\xCE\xB2", + "\xCE\x93" => "\xCE\xB3", + "\xCE\x94" => "\xCE\xB4", + "\xCE\x95" => "\xCE\xB5", + "\xCE\x96" => "\xCE\xB6", + "\xCE\x97" => "\xCE\xB7", + "\xCE\x98" => "\xCE\xB8", + "\xCE\x99" => "\xCE\xB9", + "\xCE\x9A" => "\xCE\xBA", + "\xCE\x9B" => "\xCE\xBB", + "\xCE\x9C" => "\xCE\xBC", + "\xCE\x9D" => "\xCE\xBD", + "\xCE\x9E" => "\xCE\xBE", + "\xCE\x9F" => "\xCE\xBF", + "\xCE\xA0" => "\xCF\x80", + "\xCE\xA1" => "\xCF\x81", + "\xCE\xA3" => "\xCF\x83", + "\xCE\xA4" => "\xCF\x84", + "\xCE\xA5" => "\xCF\x85", + "\xCE\xA6" => "\xCF\x86", + "\xCE\xA7" => "\xCF\x87", + "\xCE\xA8" => "\xCF\x88", + "\xCE\xA9" => "\xCF\x89", + "\xCE\xAA" => "\xCF\x8A", + "\xCE\xAB" => "\xCF\x8B", + "\xCF\x82" => "\xCF\x83", + "\xCF\x8F" => "\xCF\x97", + "\xCF\x90" => "\xCE\xB2", + "\xCF\x91" => "\xCE\xB8", + "\xCF\x95" => "\xCF\x86", + "\xCF\x96" => "\xCF\x80", + "\xCF\x98" => "\xCF\x99", + "\xCF\x9A" => "\xCF\x9B", + "\xCF\x9C" => "\xCF\x9D", + "\xCF\x9E" => "\xCF\x9F", + "\xCF\xA0" => "\xCF\xA1", + "\xCF\xA2" => "\xCF\xA3", + "\xCF\xA4" => "\xCF\xA5", + "\xCF\xA6" => "\xCF\xA7", + "\xCF\xA8" => "\xCF\xA9", + "\xCF\xAA" => "\xCF\xAB", + "\xCF\xAC" => "\xCF\xAD", + "\xCF\xAE" => "\xCF\xAF", + "\xCF\xB0" => "\xCE\xBA", + "\xCF\xB1" => "\xCF\x81", + "\xCF\xB4" => "\xCE\xB8", + "\xCF\xB5" => "\xCE\xB5", + "\xCF\xB7" => "\xCF\xB8", + "\xCF\xB9" => "\xCF\xB2", + "\xCF\xBA" => "\xCF\xBB", + "\xCF\xBD" => "\xCD\xBB", + "\xCF\xBE" => "\xCD\xBC", + "\xCF\xBF" => "\xCD\xBD", + "\xD0\x80" => "\xD1\x90", + "\xD0\x81" => "\xD1\x91", + "\xD0\x82" => "\xD1\x92", + "\xD0\x83" => "\xD1\x93", + "\xD0\x84" => "\xD1\x94", + "\xD0\x85" => "\xD1\x95", + "\xD0\x86" => "\xD1\x96", + "\xD0\x87" => "\xD1\x97", + "\xD0\x88" => "\xD1\x98", + "\xD0\x89" => "\xD1\x99", + "\xD0\x8A" => "\xD1\x9A", + "\xD0\x8B" => "\xD1\x9B", + "\xD0\x8C" => "\xD1\x9C", + "\xD0\x8D" => "\xD1\x9D", + "\xD0\x8E" => "\xD1\x9E", + "\xD0\x8F" => "\xD1\x9F", + "\xD0\x90" => "\xD0\xB0", + "\xD0\x91" => "\xD0\xB1", + "\xD0\x92" => "\xD0\xB2", + "\xD0\x93" => "\xD0\xB3", + "\xD0\x94" => "\xD0\xB4", + "\xD0\x95" => "\xD0\xB5", + "\xD0\x96" => "\xD0\xB6", + "\xD0\x97" => "\xD0\xB7", + "\xD0\x98" => "\xD0\xB8", + "\xD0\x99" => "\xD0\xB9", + "\xD0\x9A" => "\xD0\xBA", + "\xD0\x9B" => "\xD0\xBB", + "\xD0\x9C" => "\xD0\xBC", + "\xD0\x9D" => "\xD0\xBD", + "\xD0\x9E" => "\xD0\xBE", + "\xD0\x9F" => "\xD0\xBF", + "\xD0\xA0" => "\xD1\x80", + "\xD0\xA1" => "\xD1\x81", + "\xD0\xA2" => "\xD1\x82", + "\xD0\xA3" => "\xD1\x83", + "\xD0\xA4" => "\xD1\x84", + "\xD0\xA5" => "\xD1\x85", + "\xD0\xA6" => "\xD1\x86", + "\xD0\xA7" => "\xD1\x87", + "\xD0\xA8" => "\xD1\x88", + "\xD0\xA9" => "\xD1\x89", + "\xD0\xAA" => "\xD1\x8A", + "\xD0\xAB" => "\xD1\x8B", + "\xD0\xAC" => "\xD1\x8C", + "\xD0\xAD" => "\xD1\x8D", + "\xD0\xAE" => "\xD1\x8E", + "\xD0\xAF" => "\xD1\x8F", + "\xD1\xA0" => "\xD1\xA1", + "\xD1\xA2" => "\xD1\xA3", + "\xD1\xA4" => "\xD1\xA5", + "\xD1\xA6" => "\xD1\xA7", + "\xD1\xA8" => "\xD1\xA9", + "\xD1\xAA" => "\xD1\xAB", + "\xD1\xAC" => "\xD1\xAD", + "\xD1\xAE" => "\xD1\xAF", + "\xD1\xB0" => "\xD1\xB1", + "\xD1\xB2" => "\xD1\xB3", + "\xD1\xB4" => "\xD1\xB5", + "\xD1\xB6" => "\xD1\xB7", + "\xD1\xB8" => "\xD1\xB9", + "\xD1\xBA" => "\xD1\xBB", + "\xD1\xBC" => "\xD1\xBD", + "\xD1\xBE" => "\xD1\xBF", + "\xD2\x80" => "\xD2\x81", + "\xD2\x8A" => "\xD2\x8B", + "\xD2\x8C" => "\xD2\x8D", + "\xD2\x8E" => "\xD2\x8F", + "\xD2\x90" => "\xD2\x91", + "\xD2\x92" => "\xD2\x93", + "\xD2\x94" => "\xD2\x95", + "\xD2\x96" => "\xD2\x97", + "\xD2\x98" => "\xD2\x99", + "\xD2\x9A" => "\xD2\x9B", + "\xD2\x9C" => "\xD2\x9D", + "\xD2\x9E" => "\xD2\x9F", + "\xD2\xA0" => "\xD2\xA1", + "\xD2\xA2" => "\xD2\xA3", + "\xD2\xA4" => "\xD2\xA5", + "\xD2\xA6" => "\xD2\xA7", + "\xD2\xA8" => "\xD2\xA9", + "\xD2\xAA" => "\xD2\xAB", + "\xD2\xAC" => "\xD2\xAD", + "\xD2\xAE" => "\xD2\xAF", + "\xD2\xB0" => "\xD2\xB1", + "\xD2\xB2" => "\xD2\xB3", + "\xD2\xB4" => "\xD2\xB5", + "\xD2\xB6" => "\xD2\xB7", + "\xD2\xB8" => "\xD2\xB9", + "\xD2\xBA" => "\xD2\xBB", + "\xD2\xBC" => "\xD2\xBD", + "\xD2\xBE" => "\xD2\xBF", + "\xD3\x80" => "\xD3\x8F", + "\xD3\x81" => "\xD3\x82", + "\xD3\x83" => "\xD3\x84", + "\xD3\x85" => "\xD3\x86", + "\xD3\x87" => "\xD3\x88", + "\xD3\x89" => "\xD3\x8A", + "\xD3\x8B" => "\xD3\x8C", + "\xD3\x8D" => "\xD3\x8E", + "\xD3\x90" => "\xD3\x91", + "\xD3\x92" => "\xD3\x93", + "\xD3\x94" => "\xD3\x95", + "\xD3\x96" => "\xD3\x97", + "\xD3\x98" => "\xD3\x99", + "\xD3\x9A" => "\xD3\x9B", + "\xD3\x9C" => "\xD3\x9D", + "\xD3\x9E" => "\xD3\x9F", + "\xD3\xA0" => "\xD3\xA1", + "\xD3\xA2" => "\xD3\xA3", + "\xD3\xA4" => "\xD3\xA5", + "\xD3\xA6" => "\xD3\xA7", + "\xD3\xA8" => "\xD3\xA9", + "\xD3\xAA" => "\xD3\xAB", + "\xD3\xAC" => "\xD3\xAD", + "\xD3\xAE" => "\xD3\xAF", + "\xD3\xB0" => "\xD3\xB1", + "\xD3\xB2" => "\xD3\xB3", + "\xD3\xB4" => "\xD3\xB5", + "\xD3\xB6" => "\xD3\xB7", + "\xD3\xB8" => "\xD3\xB9", + "\xD3\xBA" => "\xD3\xBB", + "\xD3\xBC" => "\xD3\xBD", + "\xD3\xBE" => "\xD3\xBF", + "\xD4\x80" => "\xD4\x81", + "\xD4\x82" => "\xD4\x83", + "\xD4\x84" => "\xD4\x85", + "\xD4\x86" => "\xD4\x87", + "\xD4\x88" => "\xD4\x89", + "\xD4\x8A" => "\xD4\x8B", + "\xD4\x8C" => "\xD4\x8D", + "\xD4\x8E" => "\xD4\x8F", + "\xD4\x90" => "\xD4\x91", + "\xD4\x92" => "\xD4\x93", + "\xD4\x94" => "\xD4\x95", + "\xD4\x96" => "\xD4\x97", + "\xD4\x98" => "\xD4\x99", + "\xD4\x9A" => "\xD4\x9B", + "\xD4\x9C" => "\xD4\x9D", + "\xD4\x9E" => "\xD4\x9F", + "\xD4\xA0" => "\xD4\xA1", + "\xD4\xA2" => "\xD4\xA3", + "\xD4\xA4" => "\xD4\xA5", + "\xD4\xA6" => "\xD4\xA7", + "\xD4\xA8" => "\xD4\xA9", + "\xD4\xAA" => "\xD4\xAB", + "\xD4\xAC" => "\xD4\xAD", + "\xD4\xAE" => "\xD4\xAF", + "\xD4\xB1" => "\xD5\xA1", + "\xD4\xB2" => "\xD5\xA2", + "\xD4\xB3" => "\xD5\xA3", + "\xD4\xB4" => "\xD5\xA4", + "\xD4\xB5" => "\xD5\xA5", + "\xD4\xB6" => "\xD5\xA6", + "\xD4\xB7" => "\xD5\xA7", + "\xD4\xB8" => "\xD5\xA8", + "\xD4\xB9" => "\xD5\xA9", + "\xD4\xBA" => "\xD5\xAA", + "\xD4\xBB" => "\xD5\xAB", + "\xD4\xBC" => "\xD5\xAC", + "\xD4\xBD" => "\xD5\xAD", + "\xD4\xBE" => "\xD5\xAE", + "\xD4\xBF" => "\xD5\xAF", + "\xD5\x80" => "\xD5\xB0", + "\xD5\x81" => "\xD5\xB1", + "\xD5\x82" => "\xD5\xB2", + "\xD5\x83" => "\xD5\xB3", + "\xD5\x84" => "\xD5\xB4", + "\xD5\x85" => "\xD5\xB5", + "\xD5\x86" => "\xD5\xB6", + "\xD5\x87" => "\xD5\xB7", + "\xD5\x88" => "\xD5\xB8", + "\xD5\x89" => "\xD5\xB9", + "\xD5\x8A" => "\xD5\xBA", + "\xD5\x8B" => "\xD5\xBB", + "\xD5\x8C" => "\xD5\xBC", + "\xD5\x8D" => "\xD5\xBD", + "\xD5\x8E" => "\xD5\xBE", + "\xD5\x8F" => "\xD5\xBF", + "\xD5\x90" => "\xD6\x80", + "\xD5\x91" => "\xD6\x81", + "\xD5\x92" => "\xD6\x82", + "\xD5\x93" => "\xD6\x83", + "\xD5\x94" => "\xD6\x84", + "\xD5\x95" => "\xD6\x85", + "\xD5\x96" => "\xD6\x86", + "\xE1\x82\xA0" => "\xE2\xB4\x80", + "\xE1\x82\xA1" => "\xE2\xB4\x81", + "\xE1\x82\xA2" => "\xE2\xB4\x82", + "\xE1\x82\xA3" => "\xE2\xB4\x83", + "\xE1\x82\xA4" => "\xE2\xB4\x84", + "\xE1\x82\xA5" => "\xE2\xB4\x85", + "\xE1\x82\xA6" => "\xE2\xB4\x86", + "\xE1\x82\xA7" => "\xE2\xB4\x87", + "\xE1\x82\xA8" => "\xE2\xB4\x88", + "\xE1\x82\xA9" => "\xE2\xB4\x89", + "\xE1\x82\xAA" => "\xE2\xB4\x8A", + "\xE1\x82\xAB" => "\xE2\xB4\x8B", + "\xE1\x82\xAC" => "\xE2\xB4\x8C", + "\xE1\x82\xAD" => "\xE2\xB4\x8D", + "\xE1\x82\xAE" => "\xE2\xB4\x8E", + "\xE1\x82\xAF" => "\xE2\xB4\x8F", + "\xE1\x82\xB0" => "\xE2\xB4\x90", + "\xE1\x82\xB1" => "\xE2\xB4\x91", + "\xE1\x82\xB2" => "\xE2\xB4\x92", + "\xE1\x82\xB3" => "\xE2\xB4\x93", + "\xE1\x82\xB4" => "\xE2\xB4\x94", + "\xE1\x82\xB5" => "\xE2\xB4\x95", + "\xE1\x82\xB6" => "\xE2\xB4\x96", + "\xE1\x82\xB7" => "\xE2\xB4\x97", + "\xE1\x82\xB8" => "\xE2\xB4\x98", + "\xE1\x82\xB9" => "\xE2\xB4\x99", + "\xE1\x82\xBA" => "\xE2\xB4\x9A", + "\xE1\x82\xBB" => "\xE2\xB4\x9B", + "\xE1\x82\xBC" => "\xE2\xB4\x9C", + "\xE1\x82\xBD" => "\xE2\xB4\x9D", + "\xE1\x82\xBE" => "\xE2\xB4\x9E", + "\xE1\x82\xBF" => "\xE2\xB4\x9F", + "\xE1\x83\x80" => "\xE2\xB4\xA0", + "\xE1\x83\x81" => "\xE2\xB4\xA1", + "\xE1\x83\x82" => "\xE2\xB4\xA2", + "\xE1\x83\x83" => "\xE2\xB4\xA3", + "\xE1\x83\x84" => "\xE2\xB4\xA4", + "\xE1\x83\x85" => "\xE2\xB4\xA5", + "\xE1\x83\x87" => "\xE2\xB4\xA7", + "\xE1\x83\x8D" => "\xE2\xB4\xAD", + "\xE1\x8F\xB8" => "\xE1\x8F\xB0", + "\xE1\x8F\xB9" => "\xE1\x8F\xB1", + "\xE1\x8F\xBA" => "\xE1\x8F\xB2", + "\xE1\x8F\xBB" => "\xE1\x8F\xB3", + "\xE1\x8F\xBC" => "\xE1\x8F\xB4", + "\xE1\x8F\xBD" => "\xE1\x8F\xB5", + "\xE1\xB2\x80" => "\xD0\xB2", + "\xE1\xB2\x81" => "\xD0\xB4", + "\xE1\xB2\x82" => "\xD0\xBE", + "\xE1\xB2\x83" => "\xD1\x81", + "\xE1\xB2\x84" => "\xD1\x82", + "\xE1\xB2\x85" => "\xD1\x82", + "\xE1\xB2\x86" => "\xD1\x8A", + "\xE1\xB2\x87" => "\xD1\xA3", + "\xE1\xB2\x88" => "\xEA\x99\x8B", + "\xE1\xB2\x90" => "\xE1\x83\x90", + "\xE1\xB2\x91" => "\xE1\x83\x91", + "\xE1\xB2\x92" => "\xE1\x83\x92", + "\xE1\xB2\x93" => "\xE1\x83\x93", + "\xE1\xB2\x94" => "\xE1\x83\x94", + "\xE1\xB2\x95" => "\xE1\x83\x95", + "\xE1\xB2\x96" => "\xE1\x83\x96", + "\xE1\xB2\x97" => "\xE1\x83\x97", + "\xE1\xB2\x98" => "\xE1\x83\x98", + "\xE1\xB2\x99" => "\xE1\x83\x99", + "\xE1\xB2\x9A" => "\xE1\x83\x9A", + "\xE1\xB2\x9B" => "\xE1\x83\x9B", + "\xE1\xB2\x9C" => "\xE1\x83\x9C", + "\xE1\xB2\x9D" => "\xE1\x83\x9D", + "\xE1\xB2\x9E" => "\xE1\x83\x9E", + "\xE1\xB2\x9F" => "\xE1\x83\x9F", + "\xE1\xB2\xA0" => "\xE1\x83\xA0", + "\xE1\xB2\xA1" => "\xE1\x83\xA1", + "\xE1\xB2\xA2" => "\xE1\x83\xA2", + "\xE1\xB2\xA3" => "\xE1\x83\xA3", + "\xE1\xB2\xA4" => "\xE1\x83\xA4", + "\xE1\xB2\xA5" => "\xE1\x83\xA5", + "\xE1\xB2\xA6" => "\xE1\x83\xA6", + "\xE1\xB2\xA7" => "\xE1\x83\xA7", + "\xE1\xB2\xA8" => "\xE1\x83\xA8", + "\xE1\xB2\xA9" => "\xE1\x83\xA9", + "\xE1\xB2\xAA" => "\xE1\x83\xAA", + "\xE1\xB2\xAB" => "\xE1\x83\xAB", + "\xE1\xB2\xAC" => "\xE1\x83\xAC", + "\xE1\xB2\xAD" => "\xE1\x83\xAD", + "\xE1\xB2\xAE" => "\xE1\x83\xAE", + "\xE1\xB2\xAF" => "\xE1\x83\xAF", + "\xE1\xB2\xB0" => "\xE1\x83\xB0", + "\xE1\xB2\xB1" => "\xE1\x83\xB1", + "\xE1\xB2\xB2" => "\xE1\x83\xB2", + "\xE1\xB2\xB3" => "\xE1\x83\xB3", + "\xE1\xB2\xB4" => "\xE1\x83\xB4", + "\xE1\xB2\xB5" => "\xE1\x83\xB5", + "\xE1\xB2\xB6" => "\xE1\x83\xB6", + "\xE1\xB2\xB7" => "\xE1\x83\xB7", + "\xE1\xB2\xB8" => "\xE1\x83\xB8", + "\xE1\xB2\xB9" => "\xE1\x83\xB9", + "\xE1\xB2\xBA" => "\xE1\x83\xBA", + "\xE1\xB2\xBD" => "\xE1\x83\xBD", + "\xE1\xB2\xBE" => "\xE1\x83\xBE", + "\xE1\xB2\xBF" => "\xE1\x83\xBF", + "\xE1\xB8\x80" => "\xE1\xB8\x81", + "\xE1\xB8\x82" => "\xE1\xB8\x83", + "\xE1\xB8\x84" => "\xE1\xB8\x85", + "\xE1\xB8\x86" => "\xE1\xB8\x87", + "\xE1\xB8\x88" => "\xE1\xB8\x89", + "\xE1\xB8\x8A" => "\xE1\xB8\x8B", + "\xE1\xB8\x8C" => "\xE1\xB8\x8D", + "\xE1\xB8\x8E" => "\xE1\xB8\x8F", + "\xE1\xB8\x90" => "\xE1\xB8\x91", + "\xE1\xB8\x92" => "\xE1\xB8\x93", + "\xE1\xB8\x94" => "\xE1\xB8\x95", + "\xE1\xB8\x96" => "\xE1\xB8\x97", + "\xE1\xB8\x98" => "\xE1\xB8\x99", + "\xE1\xB8\x9A" => "\xE1\xB8\x9B", + "\xE1\xB8\x9C" => "\xE1\xB8\x9D", + "\xE1\xB8\x9E" => "\xE1\xB8\x9F", + "\xE1\xB8\xA0" => "\xE1\xB8\xA1", + "\xE1\xB8\xA2" => "\xE1\xB8\xA3", + "\xE1\xB8\xA4" => "\xE1\xB8\xA5", + "\xE1\xB8\xA6" => "\xE1\xB8\xA7", + "\xE1\xB8\xA8" => "\xE1\xB8\xA9", + "\xE1\xB8\xAA" => "\xE1\xB8\xAB", + "\xE1\xB8\xAC" => "\xE1\xB8\xAD", + "\xE1\xB8\xAE" => "\xE1\xB8\xAF", + "\xE1\xB8\xB0" => "\xE1\xB8\xB1", + "\xE1\xB8\xB2" => "\xE1\xB8\xB3", + "\xE1\xB8\xB4" => "\xE1\xB8\xB5", + "\xE1\xB8\xB6" => "\xE1\xB8\xB7", + "\xE1\xB8\xB8" => "\xE1\xB8\xB9", + "\xE1\xB8\xBA" => "\xE1\xB8\xBB", + "\xE1\xB8\xBC" => "\xE1\xB8\xBD", + "\xE1\xB8\xBE" => "\xE1\xB8\xBF", + "\xE1\xB9\x80" => "\xE1\xB9\x81", + "\xE1\xB9\x82" => "\xE1\xB9\x83", + "\xE1\xB9\x84" => "\xE1\xB9\x85", + "\xE1\xB9\x86" => "\xE1\xB9\x87", + "\xE1\xB9\x88" => "\xE1\xB9\x89", + "\xE1\xB9\x8A" => "\xE1\xB9\x8B", + "\xE1\xB9\x8C" => "\xE1\xB9\x8D", + "\xE1\xB9\x8E" => "\xE1\xB9\x8F", + "\xE1\xB9\x90" => "\xE1\xB9\x91", + "\xE1\xB9\x92" => "\xE1\xB9\x93", + "\xE1\xB9\x94" => "\xE1\xB9\x95", + "\xE1\xB9\x96" => "\xE1\xB9\x97", + "\xE1\xB9\x98" => "\xE1\xB9\x99", + "\xE1\xB9\x9A" => "\xE1\xB9\x9B", + "\xE1\xB9\x9C" => "\xE1\xB9\x9D", + "\xE1\xB9\x9E" => "\xE1\xB9\x9F", + "\xE1\xB9\xA0" => "\xE1\xB9\xA1", + "\xE1\xB9\xA2" => "\xE1\xB9\xA3", + "\xE1\xB9\xA4" => "\xE1\xB9\xA5", + "\xE1\xB9\xA6" => "\xE1\xB9\xA7", + "\xE1\xB9\xA8" => "\xE1\xB9\xA9", + "\xE1\xB9\xAA" => "\xE1\xB9\xAB", + "\xE1\xB9\xAC" => "\xE1\xB9\xAD", + "\xE1\xB9\xAE" => "\xE1\xB9\xAF", + "\xE1\xB9\xB0" => "\xE1\xB9\xB1", + "\xE1\xB9\xB2" => "\xE1\xB9\xB3", + "\xE1\xB9\xB4" => "\xE1\xB9\xB5", + "\xE1\xB9\xB6" => "\xE1\xB9\xB7", + "\xE1\xB9\xB8" => "\xE1\xB9\xB9", + "\xE1\xB9\xBA" => "\xE1\xB9\xBB", + "\xE1\xB9\xBC" => "\xE1\xB9\xBD", + "\xE1\xB9\xBE" => "\xE1\xB9\xBF", + "\xE1\xBA\x80" => "\xE1\xBA\x81", + "\xE1\xBA\x82" => "\xE1\xBA\x83", + "\xE1\xBA\x84" => "\xE1\xBA\x85", + "\xE1\xBA\x86" => "\xE1\xBA\x87", + "\xE1\xBA\x88" => "\xE1\xBA\x89", + "\xE1\xBA\x8A" => "\xE1\xBA\x8B", + "\xE1\xBA\x8C" => "\xE1\xBA\x8D", + "\xE1\xBA\x8E" => "\xE1\xBA\x8F", + "\xE1\xBA\x90" => "\xE1\xBA\x91", + "\xE1\xBA\x92" => "\xE1\xBA\x93", + "\xE1\xBA\x94" => "\xE1\xBA\x95", + "\xE1\xBA\x9B" => "\xE1\xB9\xA1", + "\xE1\xBA\x9E" => "\xC3\x9F", + "\xE1\xBA\xA0" => "\xE1\xBA\xA1", + "\xE1\xBA\xA2" => "\xE1\xBA\xA3", + "\xE1\xBA\xA4" => "\xE1\xBA\xA5", + "\xE1\xBA\xA6" => "\xE1\xBA\xA7", + "\xE1\xBA\xA8" => "\xE1\xBA\xA9", + "\xE1\xBA\xAA" => "\xE1\xBA\xAB", + "\xE1\xBA\xAC" => "\xE1\xBA\xAD", + "\xE1\xBA\xAE" => "\xE1\xBA\xAF", + "\xE1\xBA\xB0" => "\xE1\xBA\xB1", + "\xE1\xBA\xB2" => "\xE1\xBA\xB3", + "\xE1\xBA\xB4" => "\xE1\xBA\xB5", + "\xE1\xBA\xB6" => "\xE1\xBA\xB7", + "\xE1\xBA\xB8" => "\xE1\xBA\xB9", + "\xE1\xBA\xBA" => "\xE1\xBA\xBB", + "\xE1\xBA\xBC" => "\xE1\xBA\xBD", + "\xE1\xBA\xBE" => "\xE1\xBA\xBF", + "\xE1\xBB\x80" => "\xE1\xBB\x81", + "\xE1\xBB\x82" => "\xE1\xBB\x83", + "\xE1\xBB\x84" => "\xE1\xBB\x85", + "\xE1\xBB\x86" => "\xE1\xBB\x87", + "\xE1\xBB\x88" => "\xE1\xBB\x89", + "\xE1\xBB\x8A" => "\xE1\xBB\x8B", + "\xE1\xBB\x8C" => "\xE1\xBB\x8D", + "\xE1\xBB\x8E" => "\xE1\xBB\x8F", + "\xE1\xBB\x90" => "\xE1\xBB\x91", + "\xE1\xBB\x92" => "\xE1\xBB\x93", + "\xE1\xBB\x94" => "\xE1\xBB\x95", + "\xE1\xBB\x96" => "\xE1\xBB\x97", + "\xE1\xBB\x98" => "\xE1\xBB\x99", + "\xE1\xBB\x9A" => "\xE1\xBB\x9B", + "\xE1\xBB\x9C" => "\xE1\xBB\x9D", + "\xE1\xBB\x9E" => "\xE1\xBB\x9F", + "\xE1\xBB\xA0" => "\xE1\xBB\xA1", + "\xE1\xBB\xA2" => "\xE1\xBB\xA3", + "\xE1\xBB\xA4" => "\xE1\xBB\xA5", + "\xE1\xBB\xA6" => "\xE1\xBB\xA7", + "\xE1\xBB\xA8" => "\xE1\xBB\xA9", + "\xE1\xBB\xAA" => "\xE1\xBB\xAB", + "\xE1\xBB\xAC" => "\xE1\xBB\xAD", + "\xE1\xBB\xAE" => "\xE1\xBB\xAF", + "\xE1\xBB\xB0" => "\xE1\xBB\xB1", + "\xE1\xBB\xB2" => "\xE1\xBB\xB3", + "\xE1\xBB\xB4" => "\xE1\xBB\xB5", + "\xE1\xBB\xB6" => "\xE1\xBB\xB7", + "\xE1\xBB\xB8" => "\xE1\xBB\xB9", + "\xE1\xBB\xBA" => "\xE1\xBB\xBB", + "\xE1\xBB\xBC" => "\xE1\xBB\xBD", + "\xE1\xBB\xBE" => "\xE1\xBB\xBF", + "\xE1\xBC\x88" => "\xE1\xBC\x80", + "\xE1\xBC\x89" => "\xE1\xBC\x81", + "\xE1\xBC\x8A" => "\xE1\xBC\x82", + "\xE1\xBC\x8B" => "\xE1\xBC\x83", + "\xE1\xBC\x8C" => "\xE1\xBC\x84", + "\xE1\xBC\x8D" => "\xE1\xBC\x85", + "\xE1\xBC\x8E" => "\xE1\xBC\x86", + "\xE1\xBC\x8F" => "\xE1\xBC\x87", + "\xE1\xBC\x98" => "\xE1\xBC\x90", + "\xE1\xBC\x99" => "\xE1\xBC\x91", + "\xE1\xBC\x9A" => "\xE1\xBC\x92", + "\xE1\xBC\x9B" => "\xE1\xBC\x93", + "\xE1\xBC\x9C" => "\xE1\xBC\x94", + "\xE1\xBC\x9D" => "\xE1\xBC\x95", + "\xE1\xBC\xA8" => "\xE1\xBC\xA0", + "\xE1\xBC\xA9" => "\xE1\xBC\xA1", + "\xE1\xBC\xAA" => "\xE1\xBC\xA2", + "\xE1\xBC\xAB" => "\xE1\xBC\xA3", + "\xE1\xBC\xAC" => "\xE1\xBC\xA4", + "\xE1\xBC\xAD" => "\xE1\xBC\xA5", + "\xE1\xBC\xAE" => "\xE1\xBC\xA6", + "\xE1\xBC\xAF" => "\xE1\xBC\xA7", + "\xE1\xBC\xB8" => "\xE1\xBC\xB0", + "\xE1\xBC\xB9" => "\xE1\xBC\xB1", + "\xE1\xBC\xBA" => "\xE1\xBC\xB2", + "\xE1\xBC\xBB" => "\xE1\xBC\xB3", + "\xE1\xBC\xBC" => "\xE1\xBC\xB4", + "\xE1\xBC\xBD" => "\xE1\xBC\xB5", + "\xE1\xBC\xBE" => "\xE1\xBC\xB6", + "\xE1\xBC\xBF" => "\xE1\xBC\xB7", + "\xE1\xBD\x88" => "\xE1\xBD\x80", + "\xE1\xBD\x89" => "\xE1\xBD\x81", + "\xE1\xBD\x8A" => "\xE1\xBD\x82", + "\xE1\xBD\x8B" => "\xE1\xBD\x83", + "\xE1\xBD\x8C" => "\xE1\xBD\x84", + "\xE1\xBD\x8D" => "\xE1\xBD\x85", + "\xE1\xBD\x99" => "\xE1\xBD\x91", + "\xE1\xBD\x9B" => "\xE1\xBD\x93", + "\xE1\xBD\x9D" => "\xE1\xBD\x95", + "\xE1\xBD\x9F" => "\xE1\xBD\x97", + "\xE1\xBD\xA8" => "\xE1\xBD\xA0", + "\xE1\xBD\xA9" => "\xE1\xBD\xA1", + "\xE1\xBD\xAA" => "\xE1\xBD\xA2", + "\xE1\xBD\xAB" => "\xE1\xBD\xA3", + "\xE1\xBD\xAC" => "\xE1\xBD\xA4", + "\xE1\xBD\xAD" => "\xE1\xBD\xA5", + "\xE1\xBD\xAE" => "\xE1\xBD\xA6", + "\xE1\xBD\xAF" => "\xE1\xBD\xA7", + "\xE1\xBE\x88" => "\xE1\xBE\x80", + "\xE1\xBE\x89" => "\xE1\xBE\x81", + "\xE1\xBE\x8A" => "\xE1\xBE\x82", + "\xE1\xBE\x8B" => "\xE1\xBE\x83", + "\xE1\xBE\x8C" => "\xE1\xBE\x84", + "\xE1\xBE\x8D" => "\xE1\xBE\x85", + "\xE1\xBE\x8E" => "\xE1\xBE\x86", + "\xE1\xBE\x8F" => "\xE1\xBE\x87", + "\xE1\xBE\x98" => "\xE1\xBE\x90", + "\xE1\xBE\x99" => "\xE1\xBE\x91", + "\xE1\xBE\x9A" => "\xE1\xBE\x92", + "\xE1\xBE\x9B" => "\xE1\xBE\x93", + "\xE1\xBE\x9C" => "\xE1\xBE\x94", + "\xE1\xBE\x9D" => "\xE1\xBE\x95", + "\xE1\xBE\x9E" => "\xE1\xBE\x96", + "\xE1\xBE\x9F" => "\xE1\xBE\x97", + "\xE1\xBE\xA8" => "\xE1\xBE\xA0", + "\xE1\xBE\xA9" => "\xE1\xBE\xA1", + "\xE1\xBE\xAA" => "\xE1\xBE\xA2", + "\xE1\xBE\xAB" => "\xE1\xBE\xA3", + "\xE1\xBE\xAC" => "\xE1\xBE\xA4", + "\xE1\xBE\xAD" => "\xE1\xBE\xA5", + "\xE1\xBE\xAE" => "\xE1\xBE\xA6", + "\xE1\xBE\xAF" => "\xE1\xBE\xA7", + "\xE1\xBE\xB8" => "\xE1\xBE\xB0", + "\xE1\xBE\xB9" => "\xE1\xBE\xB1", + "\xE1\xBE\xBA" => "\xE1\xBD\xB0", + "\xE1\xBE\xBB" => "\xE1\xBD\xB1", + "\xE1\xBE\xBC" => "\xE1\xBE\xB3", + "\xE1\xBE\xBE" => "\xCE\xB9", + "\xE1\xBF\x88" => "\xE1\xBD\xB2", + "\xE1\xBF\x89" => "\xE1\xBD\xB3", + "\xE1\xBF\x8A" => "\xE1\xBD\xB4", + "\xE1\xBF\x8B" => "\xE1\xBD\xB5", + "\xE1\xBF\x8C" => "\xE1\xBF\x83", + "\xE1\xBF\x98" => "\xE1\xBF\x90", + "\xE1\xBF\x99" => "\xE1\xBF\x91", + "\xE1\xBF\x9A" => "\xE1\xBD\xB6", + "\xE1\xBF\x9B" => "\xE1\xBD\xB7", + "\xE1\xBF\xA8" => "\xE1\xBF\xA0", + "\xE1\xBF\xA9" => "\xE1\xBF\xA1", + "\xE1\xBF\xAA" => "\xE1\xBD\xBA", + "\xE1\xBF\xAB" => "\xE1\xBD\xBB", + "\xE1\xBF\xAC" => "\xE1\xBF\xA5", + "\xE1\xBF\xB8" => "\xE1\xBD\xB8", + "\xE1\xBF\xB9" => "\xE1\xBD\xB9", + "\xE1\xBF\xBA" => "\xE1\xBD\xBC", + "\xE1\xBF\xBB" => "\xE1\xBD\xBD", + "\xE1\xBF\xBC" => "\xE1\xBF\xB3", + "\xE2\x84\xA6" => "\xCF\x89", + "\xE2\x84\xAA" => "\x6B", + "\xE2\x84\xAB" => "\xC3\xA5", + "\xE2\x84\xB2" => "\xE2\x85\x8E", + "\xE2\x85\xA0" => "\xE2\x85\xB0", + "\xE2\x85\xA1" => "\xE2\x85\xB1", + "\xE2\x85\xA2" => "\xE2\x85\xB2", + "\xE2\x85\xA3" => "\xE2\x85\xB3", + "\xE2\x85\xA4" => "\xE2\x85\xB4", + "\xE2\x85\xA5" => "\xE2\x85\xB5", + "\xE2\x85\xA6" => "\xE2\x85\xB6", + "\xE2\x85\xA7" => "\xE2\x85\xB7", + "\xE2\x85\xA8" => "\xE2\x85\xB8", + "\xE2\x85\xA9" => "\xE2\x85\xB9", + "\xE2\x85\xAA" => "\xE2\x85\xBA", + "\xE2\x85\xAB" => "\xE2\x85\xBB", + "\xE2\x85\xAC" => "\xE2\x85\xBC", + "\xE2\x85\xAD" => "\xE2\x85\xBD", + "\xE2\x85\xAE" => "\xE2\x85\xBE", + "\xE2\x85\xAF" => "\xE2\x85\xBF", + "\xE2\x86\x83" => "\xE2\x86\x84", + "\xE2\x92\xB6" => "\xE2\x93\x90", + "\xE2\x92\xB7" => "\xE2\x93\x91", + "\xE2\x92\xB8" => "\xE2\x93\x92", + "\xE2\x92\xB9" => "\xE2\x93\x93", + "\xE2\x92\xBA" => "\xE2\x93\x94", + "\xE2\x92\xBB" => "\xE2\x93\x95", + "\xE2\x92\xBC" => "\xE2\x93\x96", + "\xE2\x92\xBD" => "\xE2\x93\x97", + "\xE2\x92\xBE" => "\xE2\x93\x98", + "\xE2\x92\xBF" => "\xE2\x93\x99", + "\xE2\x93\x80" => "\xE2\x93\x9A", + "\xE2\x93\x81" => "\xE2\x93\x9B", + "\xE2\x93\x82" => "\xE2\x93\x9C", + "\xE2\x93\x83" => "\xE2\x93\x9D", + "\xE2\x93\x84" => "\xE2\x93\x9E", + "\xE2\x93\x85" => "\xE2\x93\x9F", + "\xE2\x93\x86" => "\xE2\x93\xA0", + "\xE2\x93\x87" => "\xE2\x93\xA1", + "\xE2\x93\x88" => "\xE2\x93\xA2", + "\xE2\x93\x89" => "\xE2\x93\xA3", + "\xE2\x93\x8A" => "\xE2\x93\xA4", + "\xE2\x93\x8B" => "\xE2\x93\xA5", + "\xE2\x93\x8C" => "\xE2\x93\xA6", + "\xE2\x93\x8D" => "\xE2\x93\xA7", + "\xE2\x93\x8E" => "\xE2\x93\xA8", + "\xE2\x93\x8F" => "\xE2\x93\xA9", + "\xE2\xB0\x80" => "\xE2\xB0\xB0", + "\xE2\xB0\x81" => "\xE2\xB0\xB1", + "\xE2\xB0\x82" => "\xE2\xB0\xB2", + "\xE2\xB0\x83" => "\xE2\xB0\xB3", + "\xE2\xB0\x84" => "\xE2\xB0\xB4", + "\xE2\xB0\x85" => "\xE2\xB0\xB5", + "\xE2\xB0\x86" => "\xE2\xB0\xB6", + "\xE2\xB0\x87" => "\xE2\xB0\xB7", + "\xE2\xB0\x88" => "\xE2\xB0\xB8", + "\xE2\xB0\x89" => "\xE2\xB0\xB9", + "\xE2\xB0\x8A" => "\xE2\xB0\xBA", + "\xE2\xB0\x8B" => "\xE2\xB0\xBB", + "\xE2\xB0\x8C" => "\xE2\xB0\xBC", + "\xE2\xB0\x8D" => "\xE2\xB0\xBD", + "\xE2\xB0\x8E" => "\xE2\xB0\xBE", + "\xE2\xB0\x8F" => "\xE2\xB0\xBF", + "\xE2\xB0\x90" => "\xE2\xB1\x80", + "\xE2\xB0\x91" => "\xE2\xB1\x81", + "\xE2\xB0\x92" => "\xE2\xB1\x82", + "\xE2\xB0\x93" => "\xE2\xB1\x83", + "\xE2\xB0\x94" => "\xE2\xB1\x84", + "\xE2\xB0\x95" => "\xE2\xB1\x85", + "\xE2\xB0\x96" => "\xE2\xB1\x86", + "\xE2\xB0\x97" => "\xE2\xB1\x87", + "\xE2\xB0\x98" => "\xE2\xB1\x88", + "\xE2\xB0\x99" => "\xE2\xB1\x89", + "\xE2\xB0\x9A" => "\xE2\xB1\x8A", + "\xE2\xB0\x9B" => "\xE2\xB1\x8B", + "\xE2\xB0\x9C" => "\xE2\xB1\x8C", + "\xE2\xB0\x9D" => "\xE2\xB1\x8D", + "\xE2\xB0\x9E" => "\xE2\xB1\x8E", + "\xE2\xB0\x9F" => "\xE2\xB1\x8F", + "\xE2\xB0\xA0" => "\xE2\xB1\x90", + "\xE2\xB0\xA1" => "\xE2\xB1\x91", + "\xE2\xB0\xA2" => "\xE2\xB1\x92", + "\xE2\xB0\xA3" => "\xE2\xB1\x93", + "\xE2\xB0\xA4" => "\xE2\xB1\x94", + "\xE2\xB0\xA5" => "\xE2\xB1\x95", + "\xE2\xB0\xA6" => "\xE2\xB1\x96", + "\xE2\xB0\xA7" => "\xE2\xB1\x97", + "\xE2\xB0\xA8" => "\xE2\xB1\x98", + "\xE2\xB0\xA9" => "\xE2\xB1\x99", + "\xE2\xB0\xAA" => "\xE2\xB1\x9A", + "\xE2\xB0\xAB" => "\xE2\xB1\x9B", + "\xE2\xB0\xAC" => "\xE2\xB1\x9C", + "\xE2\xB0\xAD" => "\xE2\xB1\x9D", + "\xE2\xB0\xAE" => "\xE2\xB1\x9E", + "\xE2\xB0\xAF" => "\xE2\xB1\x9F", + "\xE2\xB1\xA0" => "\xE2\xB1\xA1", + "\xE2\xB1\xA2" => "\xC9\xAB", + "\xE2\xB1\xA3" => "\xE1\xB5\xBD", + "\xE2\xB1\xA4" => "\xC9\xBD", + "\xE2\xB1\xA7" => "\xE2\xB1\xA8", + "\xE2\xB1\xA9" => "\xE2\xB1\xAA", + "\xE2\xB1\xAB" => "\xE2\xB1\xAC", + "\xE2\xB1\xAD" => "\xC9\x91", + "\xE2\xB1\xAE" => "\xC9\xB1", + "\xE2\xB1\xAF" => "\xC9\x90", + "\xE2\xB1\xB0" => "\xC9\x92", + "\xE2\xB1\xB2" => "\xE2\xB1\xB3", + "\xE2\xB1\xB5" => "\xE2\xB1\xB6", + "\xE2\xB1\xBE" => "\xC8\xBF", + "\xE2\xB1\xBF" => "\xC9\x80", + "\xE2\xB2\x80" => "\xE2\xB2\x81", + "\xE2\xB2\x82" => "\xE2\xB2\x83", + "\xE2\xB2\x84" => "\xE2\xB2\x85", + "\xE2\xB2\x86" => "\xE2\xB2\x87", + "\xE2\xB2\x88" => "\xE2\xB2\x89", + "\xE2\xB2\x8A" => "\xE2\xB2\x8B", + "\xE2\xB2\x8C" => "\xE2\xB2\x8D", + "\xE2\xB2\x8E" => "\xE2\xB2\x8F", + "\xE2\xB2\x90" => "\xE2\xB2\x91", + "\xE2\xB2\x92" => "\xE2\xB2\x93", + "\xE2\xB2\x94" => "\xE2\xB2\x95", + "\xE2\xB2\x96" => "\xE2\xB2\x97", + "\xE2\xB2\x98" => "\xE2\xB2\x99", + "\xE2\xB2\x9A" => "\xE2\xB2\x9B", + "\xE2\xB2\x9C" => "\xE2\xB2\x9D", + "\xE2\xB2\x9E" => "\xE2\xB2\x9F", + "\xE2\xB2\xA0" => "\xE2\xB2\xA1", + "\xE2\xB2\xA2" => "\xE2\xB2\xA3", + "\xE2\xB2\xA4" => "\xE2\xB2\xA5", + "\xE2\xB2\xA6" => "\xE2\xB2\xA7", + "\xE2\xB2\xA8" => "\xE2\xB2\xA9", + "\xE2\xB2\xAA" => "\xE2\xB2\xAB", + "\xE2\xB2\xAC" => "\xE2\xB2\xAD", + "\xE2\xB2\xAE" => "\xE2\xB2\xAF", + "\xE2\xB2\xB0" => "\xE2\xB2\xB1", + "\xE2\xB2\xB2" => "\xE2\xB2\xB3", + "\xE2\xB2\xB4" => "\xE2\xB2\xB5", + "\xE2\xB2\xB6" => "\xE2\xB2\xB7", + "\xE2\xB2\xB8" => "\xE2\xB2\xB9", + "\xE2\xB2\xBA" => "\xE2\xB2\xBB", + "\xE2\xB2\xBC" => "\xE2\xB2\xBD", + "\xE2\xB2\xBE" => "\xE2\xB2\xBF", + "\xE2\xB3\x80" => "\xE2\xB3\x81", + "\xE2\xB3\x82" => "\xE2\xB3\x83", + "\xE2\xB3\x84" => "\xE2\xB3\x85", + "\xE2\xB3\x86" => "\xE2\xB3\x87", + "\xE2\xB3\x88" => "\xE2\xB3\x89", + "\xE2\xB3\x8A" => "\xE2\xB3\x8B", + "\xE2\xB3\x8C" => "\xE2\xB3\x8D", + "\xE2\xB3\x8E" => "\xE2\xB3\x8F", + "\xE2\xB3\x90" => "\xE2\xB3\x91", + "\xE2\xB3\x92" => "\xE2\xB3\x93", + "\xE2\xB3\x94" => "\xE2\xB3\x95", + "\xE2\xB3\x96" => "\xE2\xB3\x97", + "\xE2\xB3\x98" => "\xE2\xB3\x99", + "\xE2\xB3\x9A" => "\xE2\xB3\x9B", + "\xE2\xB3\x9C" => "\xE2\xB3\x9D", + "\xE2\xB3\x9E" => "\xE2\xB3\x9F", + "\xE2\xB3\xA0" => "\xE2\xB3\xA1", + "\xE2\xB3\xA2" => "\xE2\xB3\xA3", + "\xE2\xB3\xAB" => "\xE2\xB3\xAC", + "\xE2\xB3\xAD" => "\xE2\xB3\xAE", + "\xE2\xB3\xB2" => "\xE2\xB3\xB3", + "\xEA\x99\x80" => "\xEA\x99\x81", + "\xEA\x99\x82" => "\xEA\x99\x83", + "\xEA\x99\x84" => "\xEA\x99\x85", + "\xEA\x99\x86" => "\xEA\x99\x87", + "\xEA\x99\x88" => "\xEA\x99\x89", + "\xEA\x99\x8A" => "\xEA\x99\x8B", + "\xEA\x99\x8C" => "\xEA\x99\x8D", + "\xEA\x99\x8E" => "\xEA\x99\x8F", + "\xEA\x99\x90" => "\xEA\x99\x91", + "\xEA\x99\x92" => "\xEA\x99\x93", + "\xEA\x99\x94" => "\xEA\x99\x95", + "\xEA\x99\x96" => "\xEA\x99\x97", + "\xEA\x99\x98" => "\xEA\x99\x99", + "\xEA\x99\x9A" => "\xEA\x99\x9B", + "\xEA\x99\x9C" => "\xEA\x99\x9D", + "\xEA\x99\x9E" => "\xEA\x99\x9F", + "\xEA\x99\xA0" => "\xEA\x99\xA1", + "\xEA\x99\xA2" => "\xEA\x99\xA3", + "\xEA\x99\xA4" => "\xEA\x99\xA5", + "\xEA\x99\xA6" => "\xEA\x99\xA7", + "\xEA\x99\xA8" => "\xEA\x99\xA9", + "\xEA\x99\xAA" => "\xEA\x99\xAB", + "\xEA\x99\xAC" => "\xEA\x99\xAD", + "\xEA\x9A\x80" => "\xEA\x9A\x81", + "\xEA\x9A\x82" => "\xEA\x9A\x83", + "\xEA\x9A\x84" => "\xEA\x9A\x85", + "\xEA\x9A\x86" => "\xEA\x9A\x87", + "\xEA\x9A\x88" => "\xEA\x9A\x89", + "\xEA\x9A\x8A" => "\xEA\x9A\x8B", + "\xEA\x9A\x8C" => "\xEA\x9A\x8D", + "\xEA\x9A\x8E" => "\xEA\x9A\x8F", + "\xEA\x9A\x90" => "\xEA\x9A\x91", + "\xEA\x9A\x92" => "\xEA\x9A\x93", + "\xEA\x9A\x94" => "\xEA\x9A\x95", + "\xEA\x9A\x96" => "\xEA\x9A\x97", + "\xEA\x9A\x98" => "\xEA\x9A\x99", + "\xEA\x9A\x9A" => "\xEA\x9A\x9B", + "\xEA\x9C\xA2" => "\xEA\x9C\xA3", + "\xEA\x9C\xA4" => "\xEA\x9C\xA5", + "\xEA\x9C\xA6" => "\xEA\x9C\xA7", + "\xEA\x9C\xA8" => "\xEA\x9C\xA9", + "\xEA\x9C\xAA" => "\xEA\x9C\xAB", + "\xEA\x9C\xAC" => "\xEA\x9C\xAD", + "\xEA\x9C\xAE" => "\xEA\x9C\xAF", + "\xEA\x9C\xB2" => "\xEA\x9C\xB3", + "\xEA\x9C\xB4" => "\xEA\x9C\xB5", + "\xEA\x9C\xB6" => "\xEA\x9C\xB7", + "\xEA\x9C\xB8" => "\xEA\x9C\xB9", + "\xEA\x9C\xBA" => "\xEA\x9C\xBB", + "\xEA\x9C\xBC" => "\xEA\x9C\xBD", + "\xEA\x9C\xBE" => "\xEA\x9C\xBF", + "\xEA\x9D\x80" => "\xEA\x9D\x81", + "\xEA\x9D\x82" => "\xEA\x9D\x83", + "\xEA\x9D\x84" => "\xEA\x9D\x85", + "\xEA\x9D\x86" => "\xEA\x9D\x87", + "\xEA\x9D\x88" => "\xEA\x9D\x89", + "\xEA\x9D\x8A" => "\xEA\x9D\x8B", + "\xEA\x9D\x8C" => "\xEA\x9D\x8D", + "\xEA\x9D\x8E" => "\xEA\x9D\x8F", + "\xEA\x9D\x90" => "\xEA\x9D\x91", + "\xEA\x9D\x92" => "\xEA\x9D\x93", + "\xEA\x9D\x94" => "\xEA\x9D\x95", + "\xEA\x9D\x96" => "\xEA\x9D\x97", + "\xEA\x9D\x98" => "\xEA\x9D\x99", + "\xEA\x9D\x9A" => "\xEA\x9D\x9B", + "\xEA\x9D\x9C" => "\xEA\x9D\x9D", + "\xEA\x9D\x9E" => "\xEA\x9D\x9F", + "\xEA\x9D\xA0" => "\xEA\x9D\xA1", + "\xEA\x9D\xA2" => "\xEA\x9D\xA3", + "\xEA\x9D\xA4" => "\xEA\x9D\xA5", + "\xEA\x9D\xA6" => "\xEA\x9D\xA7", + "\xEA\x9D\xA8" => "\xEA\x9D\xA9", + "\xEA\x9D\xAA" => "\xEA\x9D\xAB", + "\xEA\x9D\xAC" => "\xEA\x9D\xAD", + "\xEA\x9D\xAE" => "\xEA\x9D\xAF", + "\xEA\x9D\xB9" => "\xEA\x9D\xBA", + "\xEA\x9D\xBB" => "\xEA\x9D\xBC", + "\xEA\x9D\xBD" => "\xE1\xB5\xB9", + "\xEA\x9D\xBE" => "\xEA\x9D\xBF", + "\xEA\x9E\x80" => "\xEA\x9E\x81", + "\xEA\x9E\x82" => "\xEA\x9E\x83", + "\xEA\x9E\x84" => "\xEA\x9E\x85", + "\xEA\x9E\x86" => "\xEA\x9E\x87", + "\xEA\x9E\x8B" => "\xEA\x9E\x8C", + "\xEA\x9E\x8D" => "\xC9\xA5", + "\xEA\x9E\x90" => "\xEA\x9E\x91", + "\xEA\x9E\x92" => "\xEA\x9E\x93", + "\xEA\x9E\x96" => "\xEA\x9E\x97", + "\xEA\x9E\x98" => "\xEA\x9E\x99", + "\xEA\x9E\x9A" => "\xEA\x9E\x9B", + "\xEA\x9E\x9C" => "\xEA\x9E\x9D", + "\xEA\x9E\x9E" => "\xEA\x9E\x9F", + "\xEA\x9E\xA0" => "\xEA\x9E\xA1", + "\xEA\x9E\xA2" => "\xEA\x9E\xA3", + "\xEA\x9E\xA4" => "\xEA\x9E\xA5", + "\xEA\x9E\xA6" => "\xEA\x9E\xA7", + "\xEA\x9E\xA8" => "\xEA\x9E\xA9", + "\xEA\x9E\xAA" => "\xC9\xA6", + "\xEA\x9E\xAB" => "\xC9\x9C", + "\xEA\x9E\xAC" => "\xC9\xA1", + "\xEA\x9E\xAD" => "\xC9\xAC", + "\xEA\x9E\xAE" => "\xC9\xAA", + "\xEA\x9E\xB0" => "\xCA\x9E", + "\xEA\x9E\xB1" => "\xCA\x87", + "\xEA\x9E\xB2" => "\xCA\x9D", + "\xEA\x9E\xB3" => "\xEA\xAD\x93", + "\xEA\x9E\xB4" => "\xEA\x9E\xB5", + "\xEA\x9E\xB6" => "\xEA\x9E\xB7", + "\xEA\x9E\xB8" => "\xEA\x9E\xB9", + "\xEA\x9E\xBA" => "\xEA\x9E\xBB", + "\xEA\x9E\xBC" => "\xEA\x9E\xBD", + "\xEA\x9E\xBE" => "\xEA\x9E\xBF", + "\xEA\x9F\x80" => "\xEA\x9F\x81", + "\xEA\x9F\x82" => "\xEA\x9F\x83", + "\xEA\x9F\x84" => "\xEA\x9E\x94", + "\xEA\x9F\x85" => "\xCA\x82", + "\xEA\x9F\x86" => "\xE1\xB6\x8E", + "\xEA\x9F\x87" => "\xEA\x9F\x88", + "\xEA\x9F\x89" => "\xEA\x9F\x8A", + "\xEA\x9F\x90" => "\xEA\x9F\x91", + "\xEA\x9F\x96" => "\xEA\x9F\x97", + "\xEA\x9F\x98" => "\xEA\x9F\x99", + "\xEA\x9F\xB5" => "\xEA\x9F\xB6", + "\xEA\xAD\xB0" => "\xE1\x8E\xA0", + "\xEA\xAD\xB1" => "\xE1\x8E\xA1", + "\xEA\xAD\xB2" => "\xE1\x8E\xA2", + "\xEA\xAD\xB3" => "\xE1\x8E\xA3", + "\xEA\xAD\xB4" => "\xE1\x8E\xA4", + "\xEA\xAD\xB5" => "\xE1\x8E\xA5", + "\xEA\xAD\xB6" => "\xE1\x8E\xA6", + "\xEA\xAD\xB7" => "\xE1\x8E\xA7", + "\xEA\xAD\xB8" => "\xE1\x8E\xA8", + "\xEA\xAD\xB9" => "\xE1\x8E\xA9", + "\xEA\xAD\xBA" => "\xE1\x8E\xAA", + "\xEA\xAD\xBB" => "\xE1\x8E\xAB", + "\xEA\xAD\xBC" => "\xE1\x8E\xAC", + "\xEA\xAD\xBD" => "\xE1\x8E\xAD", + "\xEA\xAD\xBE" => "\xE1\x8E\xAE", + "\xEA\xAD\xBF" => "\xE1\x8E\xAF", + "\xEA\xAE\x80" => "\xE1\x8E\xB0", + "\xEA\xAE\x81" => "\xE1\x8E\xB1", + "\xEA\xAE\x82" => "\xE1\x8E\xB2", + "\xEA\xAE\x83" => "\xE1\x8E\xB3", + "\xEA\xAE\x84" => "\xE1\x8E\xB4", + "\xEA\xAE\x85" => "\xE1\x8E\xB5", + "\xEA\xAE\x86" => "\xE1\x8E\xB6", + "\xEA\xAE\x87" => "\xE1\x8E\xB7", + "\xEA\xAE\x88" => "\xE1\x8E\xB8", + "\xEA\xAE\x89" => "\xE1\x8E\xB9", + "\xEA\xAE\x8A" => "\xE1\x8E\xBA", + "\xEA\xAE\x8B" => "\xE1\x8E\xBB", + "\xEA\xAE\x8C" => "\xE1\x8E\xBC", + "\xEA\xAE\x8D" => "\xE1\x8E\xBD", + "\xEA\xAE\x8E" => "\xE1\x8E\xBE", + "\xEA\xAE\x8F" => "\xE1\x8E\xBF", + "\xEA\xAE\x90" => "\xE1\x8F\x80", + "\xEA\xAE\x91" => "\xE1\x8F\x81", + "\xEA\xAE\x92" => "\xE1\x8F\x82", + "\xEA\xAE\x93" => "\xE1\x8F\x83", + "\xEA\xAE\x94" => "\xE1\x8F\x84", + "\xEA\xAE\x95" => "\xE1\x8F\x85", + "\xEA\xAE\x96" => "\xE1\x8F\x86", + "\xEA\xAE\x97" => "\xE1\x8F\x87", + "\xEA\xAE\x98" => "\xE1\x8F\x88", + "\xEA\xAE\x99" => "\xE1\x8F\x89", + "\xEA\xAE\x9A" => "\xE1\x8F\x8A", + "\xEA\xAE\x9B" => "\xE1\x8F\x8B", + "\xEA\xAE\x9C" => "\xE1\x8F\x8C", + "\xEA\xAE\x9D" => "\xE1\x8F\x8D", + "\xEA\xAE\x9E" => "\xE1\x8F\x8E", + "\xEA\xAE\x9F" => "\xE1\x8F\x8F", + "\xEA\xAE\xA0" => "\xE1\x8F\x90", + "\xEA\xAE\xA1" => "\xE1\x8F\x91", + "\xEA\xAE\xA2" => "\xE1\x8F\x92", + "\xEA\xAE\xA3" => "\xE1\x8F\x93", + "\xEA\xAE\xA4" => "\xE1\x8F\x94", + "\xEA\xAE\xA5" => "\xE1\x8F\x95", + "\xEA\xAE\xA6" => "\xE1\x8F\x96", + "\xEA\xAE\xA7" => "\xE1\x8F\x97", + "\xEA\xAE\xA8" => "\xE1\x8F\x98", + "\xEA\xAE\xA9" => "\xE1\x8F\x99", + "\xEA\xAE\xAA" => "\xE1\x8F\x9A", + "\xEA\xAE\xAB" => "\xE1\x8F\x9B", + "\xEA\xAE\xAC" => "\xE1\x8F\x9C", + "\xEA\xAE\xAD" => "\xE1\x8F\x9D", + "\xEA\xAE\xAE" => "\xE1\x8F\x9E", + "\xEA\xAE\xAF" => "\xE1\x8F\x9F", + "\xEA\xAE\xB0" => "\xE1\x8F\xA0", + "\xEA\xAE\xB1" => "\xE1\x8F\xA1", + "\xEA\xAE\xB2" => "\xE1\x8F\xA2", + "\xEA\xAE\xB3" => "\xE1\x8F\xA3", + "\xEA\xAE\xB4" => "\xE1\x8F\xA4", + "\xEA\xAE\xB5" => "\xE1\x8F\xA5", + "\xEA\xAE\xB6" => "\xE1\x8F\xA6", + "\xEA\xAE\xB7" => "\xE1\x8F\xA7", + "\xEA\xAE\xB8" => "\xE1\x8F\xA8", + "\xEA\xAE\xB9" => "\xE1\x8F\xA9", + "\xEA\xAE\xBA" => "\xE1\x8F\xAA", + "\xEA\xAE\xBB" => "\xE1\x8F\xAB", + "\xEA\xAE\xBC" => "\xE1\x8F\xAC", + "\xEA\xAE\xBD" => "\xE1\x8F\xAD", + "\xEA\xAE\xBE" => "\xE1\x8F\xAE", + "\xEA\xAE\xBF" => "\xE1\x8F\xAF", + "\xEF\xBC\xA1" => "\xEF\xBD\x81", + "\xEF\xBC\xA2" => "\xEF\xBD\x82", + "\xEF\xBC\xA3" => "\xEF\xBD\x83", + "\xEF\xBC\xA4" => "\xEF\xBD\x84", + "\xEF\xBC\xA5" => "\xEF\xBD\x85", + "\xEF\xBC\xA6" => "\xEF\xBD\x86", + "\xEF\xBC\xA7" => "\xEF\xBD\x87", + "\xEF\xBC\xA8" => "\xEF\xBD\x88", + "\xEF\xBC\xA9" => "\xEF\xBD\x89", + "\xEF\xBC\xAA" => "\xEF\xBD\x8A", + "\xEF\xBC\xAB" => "\xEF\xBD\x8B", + "\xEF\xBC\xAC" => "\xEF\xBD\x8C", + "\xEF\xBC\xAD" => "\xEF\xBD\x8D", + "\xEF\xBC\xAE" => "\xEF\xBD\x8E", + "\xEF\xBC\xAF" => "\xEF\xBD\x8F", + "\xEF\xBC\xB0" => "\xEF\xBD\x90", + "\xEF\xBC\xB1" => "\xEF\xBD\x91", + "\xEF\xBC\xB2" => "\xEF\xBD\x92", + "\xEF\xBC\xB3" => "\xEF\xBD\x93", + "\xEF\xBC\xB4" => "\xEF\xBD\x94", + "\xEF\xBC\xB5" => "\xEF\xBD\x95", + "\xEF\xBC\xB6" => "\xEF\xBD\x96", + "\xEF\xBC\xB7" => "\xEF\xBD\x97", + "\xEF\xBC\xB8" => "\xEF\xBD\x98", + "\xEF\xBC\xB9" => "\xEF\xBD\x99", + "\xEF\xBC\xBA" => "\xEF\xBD\x9A", + "\xF0\x90\x90\x80" => "\xF0\x90\x90\xA8", + "\xF0\x90\x90\x81" => "\xF0\x90\x90\xA9", + "\xF0\x90\x90\x82" => "\xF0\x90\x90\xAA", + "\xF0\x90\x90\x83" => "\xF0\x90\x90\xAB", + "\xF0\x90\x90\x84" => "\xF0\x90\x90\xAC", + "\xF0\x90\x90\x85" => "\xF0\x90\x90\xAD", + "\xF0\x90\x90\x86" => "\xF0\x90\x90\xAE", + "\xF0\x90\x90\x87" => "\xF0\x90\x90\xAF", + "\xF0\x90\x90\x88" => "\xF0\x90\x90\xB0", + "\xF0\x90\x90\x89" => "\xF0\x90\x90\xB1", + "\xF0\x90\x90\x8A" => "\xF0\x90\x90\xB2", + "\xF0\x90\x90\x8B" => "\xF0\x90\x90\xB3", + "\xF0\x90\x90\x8C" => "\xF0\x90\x90\xB4", + "\xF0\x90\x90\x8D" => "\xF0\x90\x90\xB5", + "\xF0\x90\x90\x8E" => "\xF0\x90\x90\xB6", + "\xF0\x90\x90\x8F" => "\xF0\x90\x90\xB7", + "\xF0\x90\x90\x90" => "\xF0\x90\x90\xB8", + "\xF0\x90\x90\x91" => "\xF0\x90\x90\xB9", + "\xF0\x90\x90\x92" => "\xF0\x90\x90\xBA", + "\xF0\x90\x90\x93" => "\xF0\x90\x90\xBB", + "\xF0\x90\x90\x94" => "\xF0\x90\x90\xBC", + "\xF0\x90\x90\x95" => "\xF0\x90\x90\xBD", + "\xF0\x90\x90\x96" => "\xF0\x90\x90\xBE", + "\xF0\x90\x90\x97" => "\xF0\x90\x90\xBF", + "\xF0\x90\x90\x98" => "\xF0\x90\x91\x80", + "\xF0\x90\x90\x99" => "\xF0\x90\x91\x81", + "\xF0\x90\x90\x9A" => "\xF0\x90\x91\x82", + "\xF0\x90\x90\x9B" => "\xF0\x90\x91\x83", + "\xF0\x90\x90\x9C" => "\xF0\x90\x91\x84", + "\xF0\x90\x90\x9D" => "\xF0\x90\x91\x85", + "\xF0\x90\x90\x9E" => "\xF0\x90\x91\x86", + "\xF0\x90\x90\x9F" => "\xF0\x90\x91\x87", + "\xF0\x90\x90\xA0" => "\xF0\x90\x91\x88", + "\xF0\x90\x90\xA1" => "\xF0\x90\x91\x89", + "\xF0\x90\x90\xA2" => "\xF0\x90\x91\x8A", + "\xF0\x90\x90\xA3" => "\xF0\x90\x91\x8B", + "\xF0\x90\x90\xA4" => "\xF0\x90\x91\x8C", + "\xF0\x90\x90\xA5" => "\xF0\x90\x91\x8D", + "\xF0\x90\x90\xA6" => "\xF0\x90\x91\x8E", + "\xF0\x90\x90\xA7" => "\xF0\x90\x91\x8F", + "\xF0\x90\x92\xB0" => "\xF0\x90\x93\x98", + "\xF0\x90\x92\xB1" => "\xF0\x90\x93\x99", + "\xF0\x90\x92\xB2" => "\xF0\x90\x93\x9A", + "\xF0\x90\x92\xB3" => "\xF0\x90\x93\x9B", + "\xF0\x90\x92\xB4" => "\xF0\x90\x93\x9C", + "\xF0\x90\x92\xB5" => "\xF0\x90\x93\x9D", + "\xF0\x90\x92\xB6" => "\xF0\x90\x93\x9E", + "\xF0\x90\x92\xB7" => "\xF0\x90\x93\x9F", + "\xF0\x90\x92\xB8" => "\xF0\x90\x93\xA0", + "\xF0\x90\x92\xB9" => "\xF0\x90\x93\xA1", + "\xF0\x90\x92\xBA" => "\xF0\x90\x93\xA2", + "\xF0\x90\x92\xBB" => "\xF0\x90\x93\xA3", + "\xF0\x90\x92\xBC" => "\xF0\x90\x93\xA4", + "\xF0\x90\x92\xBD" => "\xF0\x90\x93\xA5", + "\xF0\x90\x92\xBE" => "\xF0\x90\x93\xA6", + "\xF0\x90\x92\xBF" => "\xF0\x90\x93\xA7", + "\xF0\x90\x93\x80" => "\xF0\x90\x93\xA8", + "\xF0\x90\x93\x81" => "\xF0\x90\x93\xA9", + "\xF0\x90\x93\x82" => "\xF0\x90\x93\xAA", + "\xF0\x90\x93\x83" => "\xF0\x90\x93\xAB", + "\xF0\x90\x93\x84" => "\xF0\x90\x93\xAC", + "\xF0\x90\x93\x85" => "\xF0\x90\x93\xAD", + "\xF0\x90\x93\x86" => "\xF0\x90\x93\xAE", + "\xF0\x90\x93\x87" => "\xF0\x90\x93\xAF", + "\xF0\x90\x93\x88" => "\xF0\x90\x93\xB0", + "\xF0\x90\x93\x89" => "\xF0\x90\x93\xB1", + "\xF0\x90\x93\x8A" => "\xF0\x90\x93\xB2", + "\xF0\x90\x93\x8B" => "\xF0\x90\x93\xB3", + "\xF0\x90\x93\x8C" => "\xF0\x90\x93\xB4", + "\xF0\x90\x93\x8D" => "\xF0\x90\x93\xB5", + "\xF0\x90\x93\x8E" => "\xF0\x90\x93\xB6", + "\xF0\x90\x93\x8F" => "\xF0\x90\x93\xB7", + "\xF0\x90\x93\x90" => "\xF0\x90\x93\xB8", + "\xF0\x90\x93\x91" => "\xF0\x90\x93\xB9", + "\xF0\x90\x93\x92" => "\xF0\x90\x93\xBA", + "\xF0\x90\x93\x93" => "\xF0\x90\x93\xBB", + "\xF0\x90\x95\xB0" => "\xF0\x90\x96\x97", + "\xF0\x90\x95\xB1" => "\xF0\x90\x96\x98", + "\xF0\x90\x95\xB2" => "\xF0\x90\x96\x99", + "\xF0\x90\x95\xB3" => "\xF0\x90\x96\x9A", + "\xF0\x90\x95\xB4" => "\xF0\x90\x96\x9B", + "\xF0\x90\x95\xB5" => "\xF0\x90\x96\x9C", + "\xF0\x90\x95\xB6" => "\xF0\x90\x96\x9D", + "\xF0\x90\x95\xB7" => "\xF0\x90\x96\x9E", + "\xF0\x90\x95\xB8" => "\xF0\x90\x96\x9F", + "\xF0\x90\x95\xB9" => "\xF0\x90\x96\xA0", + "\xF0\x90\x95\xBA" => "\xF0\x90\x96\xA1", + "\xF0\x90\x95\xBC" => "\xF0\x90\x96\xA3", + "\xF0\x90\x95\xBD" => "\xF0\x90\x96\xA4", + "\xF0\x90\x95\xBE" => "\xF0\x90\x96\xA5", + "\xF0\x90\x95\xBF" => "\xF0\x90\x96\xA6", + "\xF0\x90\x96\x80" => "\xF0\x90\x96\xA7", + "\xF0\x90\x96\x81" => "\xF0\x90\x96\xA8", + "\xF0\x90\x96\x82" => "\xF0\x90\x96\xA9", + "\xF0\x90\x96\x83" => "\xF0\x90\x96\xAA", + "\xF0\x90\x96\x84" => "\xF0\x90\x96\xAB", + "\xF0\x90\x96\x85" => "\xF0\x90\x96\xAC", + "\xF0\x90\x96\x86" => "\xF0\x90\x96\xAD", + "\xF0\x90\x96\x87" => "\xF0\x90\x96\xAE", + "\xF0\x90\x96\x88" => "\xF0\x90\x96\xAF", + "\xF0\x90\x96\x89" => "\xF0\x90\x96\xB0", + "\xF0\x90\x96\x8A" => "\xF0\x90\x96\xB1", + "\xF0\x90\x96\x8C" => "\xF0\x90\x96\xB3", + "\xF0\x90\x96\x8D" => "\xF0\x90\x96\xB4", + "\xF0\x90\x96\x8E" => "\xF0\x90\x96\xB5", + "\xF0\x90\x96\x8F" => "\xF0\x90\x96\xB6", + "\xF0\x90\x96\x90" => "\xF0\x90\x96\xB7", + "\xF0\x90\x96\x91" => "\xF0\x90\x96\xB8", + "\xF0\x90\x96\x92" => "\xF0\x90\x96\xB9", + "\xF0\x90\x96\x94" => "\xF0\x90\x96\xBB", + "\xF0\x90\x96\x95" => "\xF0\x90\x96\xBC", + "\xF0\x90\xB2\x80" => "\xF0\x90\xB3\x80", + "\xF0\x90\xB2\x81" => "\xF0\x90\xB3\x81", + "\xF0\x90\xB2\x82" => "\xF0\x90\xB3\x82", + "\xF0\x90\xB2\x83" => "\xF0\x90\xB3\x83", + "\xF0\x90\xB2\x84" => "\xF0\x90\xB3\x84", + "\xF0\x90\xB2\x85" => "\xF0\x90\xB3\x85", + "\xF0\x90\xB2\x86" => "\xF0\x90\xB3\x86", + "\xF0\x90\xB2\x87" => "\xF0\x90\xB3\x87", + "\xF0\x90\xB2\x88" => "\xF0\x90\xB3\x88", + "\xF0\x90\xB2\x89" => "\xF0\x90\xB3\x89", + "\xF0\x90\xB2\x8A" => "\xF0\x90\xB3\x8A", + "\xF0\x90\xB2\x8B" => "\xF0\x90\xB3\x8B", + "\xF0\x90\xB2\x8C" => "\xF0\x90\xB3\x8C", + "\xF0\x90\xB2\x8D" => "\xF0\x90\xB3\x8D", + "\xF0\x90\xB2\x8E" => "\xF0\x90\xB3\x8E", + "\xF0\x90\xB2\x8F" => "\xF0\x90\xB3\x8F", + "\xF0\x90\xB2\x90" => "\xF0\x90\xB3\x90", + "\xF0\x90\xB2\x91" => "\xF0\x90\xB3\x91", + "\xF0\x90\xB2\x92" => "\xF0\x90\xB3\x92", + "\xF0\x90\xB2\x93" => "\xF0\x90\xB3\x93", + "\xF0\x90\xB2\x94" => "\xF0\x90\xB3\x94", + "\xF0\x90\xB2\x95" => "\xF0\x90\xB3\x95", + "\xF0\x90\xB2\x96" => "\xF0\x90\xB3\x96", + "\xF0\x90\xB2\x97" => "\xF0\x90\xB3\x97", + "\xF0\x90\xB2\x98" => "\xF0\x90\xB3\x98", + "\xF0\x90\xB2\x99" => "\xF0\x90\xB3\x99", + "\xF0\x90\xB2\x9A" => "\xF0\x90\xB3\x9A", + "\xF0\x90\xB2\x9B" => "\xF0\x90\xB3\x9B", + "\xF0\x90\xB2\x9C" => "\xF0\x90\xB3\x9C", + "\xF0\x90\xB2\x9D" => "\xF0\x90\xB3\x9D", + "\xF0\x90\xB2\x9E" => "\xF0\x90\xB3\x9E", + "\xF0\x90\xB2\x9F" => "\xF0\x90\xB3\x9F", + "\xF0\x90\xB2\xA0" => "\xF0\x90\xB3\xA0", + "\xF0\x90\xB2\xA1" => "\xF0\x90\xB3\xA1", + "\xF0\x90\xB2\xA2" => "\xF0\x90\xB3\xA2", + "\xF0\x90\xB2\xA3" => "\xF0\x90\xB3\xA3", + "\xF0\x90\xB2\xA4" => "\xF0\x90\xB3\xA4", + "\xF0\x90\xB2\xA5" => "\xF0\x90\xB3\xA5", + "\xF0\x90\xB2\xA6" => "\xF0\x90\xB3\xA6", + "\xF0\x90\xB2\xA7" => "\xF0\x90\xB3\xA7", + "\xF0\x90\xB2\xA8" => "\xF0\x90\xB3\xA8", + "\xF0\x90\xB2\xA9" => "\xF0\x90\xB3\xA9", + "\xF0\x90\xB2\xAA" => "\xF0\x90\xB3\xAA", + "\xF0\x90\xB2\xAB" => "\xF0\x90\xB3\xAB", + "\xF0\x90\xB2\xAC" => "\xF0\x90\xB3\xAC", + "\xF0\x90\xB2\xAD" => "\xF0\x90\xB3\xAD", + "\xF0\x90\xB2\xAE" => "\xF0\x90\xB3\xAE", + "\xF0\x90\xB2\xAF" => "\xF0\x90\xB3\xAF", + "\xF0\x90\xB2\xB0" => "\xF0\x90\xB3\xB0", + "\xF0\x90\xB2\xB1" => "\xF0\x90\xB3\xB1", + "\xF0\x90\xB2\xB2" => "\xF0\x90\xB3\xB2", + "\xF0\x91\xA2\xA0" => "\xF0\x91\xA3\x80", + "\xF0\x91\xA2\xA1" => "\xF0\x91\xA3\x81", + "\xF0\x91\xA2\xA2" => "\xF0\x91\xA3\x82", + "\xF0\x91\xA2\xA3" => "\xF0\x91\xA3\x83", + "\xF0\x91\xA2\xA4" => "\xF0\x91\xA3\x84", + "\xF0\x91\xA2\xA5" => "\xF0\x91\xA3\x85", + "\xF0\x91\xA2\xA6" => "\xF0\x91\xA3\x86", + "\xF0\x91\xA2\xA7" => "\xF0\x91\xA3\x87", + "\xF0\x91\xA2\xA8" => "\xF0\x91\xA3\x88", + "\xF0\x91\xA2\xA9" => "\xF0\x91\xA3\x89", + "\xF0\x91\xA2\xAA" => "\xF0\x91\xA3\x8A", + "\xF0\x91\xA2\xAB" => "\xF0\x91\xA3\x8B", + "\xF0\x91\xA2\xAC" => "\xF0\x91\xA3\x8C", + "\xF0\x91\xA2\xAD" => "\xF0\x91\xA3\x8D", + "\xF0\x91\xA2\xAE" => "\xF0\x91\xA3\x8E", + "\xF0\x91\xA2\xAF" => "\xF0\x91\xA3\x8F", + "\xF0\x91\xA2\xB0" => "\xF0\x91\xA3\x90", + "\xF0\x91\xA2\xB1" => "\xF0\x91\xA3\x91", + "\xF0\x91\xA2\xB2" => "\xF0\x91\xA3\x92", + "\xF0\x91\xA2\xB3" => "\xF0\x91\xA3\x93", + "\xF0\x91\xA2\xB4" => "\xF0\x91\xA3\x94", + "\xF0\x91\xA2\xB5" => "\xF0\x91\xA3\x95", + "\xF0\x91\xA2\xB6" => "\xF0\x91\xA3\x96", + "\xF0\x91\xA2\xB7" => "\xF0\x91\xA3\x97", + "\xF0\x91\xA2\xB8" => "\xF0\x91\xA3\x98", + "\xF0\x91\xA2\xB9" => "\xF0\x91\xA3\x99", + "\xF0\x91\xA2\xBA" => "\xF0\x91\xA3\x9A", + "\xF0\x91\xA2\xBB" => "\xF0\x91\xA3\x9B", + "\xF0\x91\xA2\xBC" => "\xF0\x91\xA3\x9C", + "\xF0\x91\xA2\xBD" => "\xF0\x91\xA3\x9D", + "\xF0\x91\xA2\xBE" => "\xF0\x91\xA3\x9E", + "\xF0\x91\xA2\xBF" => "\xF0\x91\xA3\x9F", + "\xF0\x96\xB9\x80" => "\xF0\x96\xB9\xA0", + "\xF0\x96\xB9\x81" => "\xF0\x96\xB9\xA1", + "\xF0\x96\xB9\x82" => "\xF0\x96\xB9\xA2", + "\xF0\x96\xB9\x83" => "\xF0\x96\xB9\xA3", + "\xF0\x96\xB9\x84" => "\xF0\x96\xB9\xA4", + "\xF0\x96\xB9\x85" => "\xF0\x96\xB9\xA5", + "\xF0\x96\xB9\x86" => "\xF0\x96\xB9\xA6", + "\xF0\x96\xB9\x87" => "\xF0\x96\xB9\xA7", + "\xF0\x96\xB9\x88" => "\xF0\x96\xB9\xA8", + "\xF0\x96\xB9\x89" => "\xF0\x96\xB9\xA9", + "\xF0\x96\xB9\x8A" => "\xF0\x96\xB9\xAA", + "\xF0\x96\xB9\x8B" => "\xF0\x96\xB9\xAB", + "\xF0\x96\xB9\x8C" => "\xF0\x96\xB9\xAC", + "\xF0\x96\xB9\x8D" => "\xF0\x96\xB9\xAD", + "\xF0\x96\xB9\x8E" => "\xF0\x96\xB9\xAE", + "\xF0\x96\xB9\x8F" => "\xF0\x96\xB9\xAF", + "\xF0\x96\xB9\x90" => "\xF0\x96\xB9\xB0", + "\xF0\x96\xB9\x91" => "\xF0\x96\xB9\xB1", + "\xF0\x96\xB9\x92" => "\xF0\x96\xB9\xB2", + "\xF0\x96\xB9\x93" => "\xF0\x96\xB9\xB3", + "\xF0\x96\xB9\x94" => "\xF0\x96\xB9\xB4", + "\xF0\x96\xB9\x95" => "\xF0\x96\xB9\xB5", + "\xF0\x96\xB9\x96" => "\xF0\x96\xB9\xB6", + "\xF0\x96\xB9\x97" => "\xF0\x96\xB9\xB7", + "\xF0\x96\xB9\x98" => "\xF0\x96\xB9\xB8", + "\xF0\x96\xB9\x99" => "\xF0\x96\xB9\xB9", + "\xF0\x96\xB9\x9A" => "\xF0\x96\xB9\xBA", + "\xF0\x96\xB9\x9B" => "\xF0\x96\xB9\xBB", + "\xF0\x96\xB9\x9C" => "\xF0\x96\xB9\xBC", + "\xF0\x96\xB9\x9D" => "\xF0\x96\xB9\xBD", + "\xF0\x96\xB9\x9E" => "\xF0\x96\xB9\xBE", + "\xF0\x96\xB9\x9F" => "\xF0\x96\xB9\xBF", + "\xF0\x9E\xA4\x80" => "\xF0\x9E\xA4\xA2", + "\xF0\x9E\xA4\x81" => "\xF0\x9E\xA4\xA3", + "\xF0\x9E\xA4\x82" => "\xF0\x9E\xA4\xA4", + "\xF0\x9E\xA4\x83" => "\xF0\x9E\xA4\xA5", + "\xF0\x9E\xA4\x84" => "\xF0\x9E\xA4\xA6", + "\xF0\x9E\xA4\x85" => "\xF0\x9E\xA4\xA7", + "\xF0\x9E\xA4\x86" => "\xF0\x9E\xA4\xA8", + "\xF0\x9E\xA4\x87" => "\xF0\x9E\xA4\xA9", + "\xF0\x9E\xA4\x88" => "\xF0\x9E\xA4\xAA", + "\xF0\x9E\xA4\x89" => "\xF0\x9E\xA4\xAB", + "\xF0\x9E\xA4\x8A" => "\xF0\x9E\xA4\xAC", + "\xF0\x9E\xA4\x8B" => "\xF0\x9E\xA4\xAD", + "\xF0\x9E\xA4\x8C" => "\xF0\x9E\xA4\xAE", + "\xF0\x9E\xA4\x8D" => "\xF0\x9E\xA4\xAF", + "\xF0\x9E\xA4\x8E" => "\xF0\x9E\xA4\xB0", + "\xF0\x9E\xA4\x8F" => "\xF0\x9E\xA4\xB1", + "\xF0\x9E\xA4\x90" => "\xF0\x9E\xA4\xB2", + "\xF0\x9E\xA4\x91" => "\xF0\x9E\xA4\xB3", + "\xF0\x9E\xA4\x92" => "\xF0\x9E\xA4\xB4", + "\xF0\x9E\xA4\x93" => "\xF0\x9E\xA4\xB5", + "\xF0\x9E\xA4\x94" => "\xF0\x9E\xA4\xB6", + "\xF0\x9E\xA4\x95" => "\xF0\x9E\xA4\xB7", + "\xF0\x9E\xA4\x96" => "\xF0\x9E\xA4\xB8", + "\xF0\x9E\xA4\x97" => "\xF0\x9E\xA4\xB9", + "\xF0\x9E\xA4\x98" => "\xF0\x9E\xA4\xBA", + "\xF0\x9E\xA4\x99" => "\xF0\x9E\xA4\xBB", + "\xF0\x9E\xA4\x9A" => "\xF0\x9E\xA4\xBC", + "\xF0\x9E\xA4\x9B" => "\xF0\x9E\xA4\xBD", + "\xF0\x9E\xA4\x9C" => "\xF0\x9E\xA4\xBE", + "\xF0\x9E\xA4\x9D" => "\xF0\x9E\xA4\xBF", + "\xF0\x9E\xA4\x9E" => "\xF0\x9E\xA5\x80", + "\xF0\x9E\xA4\x9F" => "\xF0\x9E\xA5\x81", + "\xF0\x9E\xA4\xA0" => "\xF0\x9E\xA5\x82", + "\xF0\x9E\xA4\xA1" => "\xF0\x9E\xA5\x83", + ); +} + +/** + * Helper function for utf8_casefold. + * + * Developers: Do not update the data in this function manually. Instead, + * run "php -f other/update_unicode_data.php" on the command line. + * + * @return array Casefolding maps. + */ +function utf8_casefold_maps() +{ + return array( + "\x41" => "\x61", + "\x42" => "\x62", + "\x43" => "\x63", + "\x44" => "\x64", + "\x45" => "\x65", + "\x46" => "\x66", + "\x47" => "\x67", + "\x48" => "\x68", + "\x49" => "\x69", + "\x4A" => "\x6A", + "\x4B" => "\x6B", + "\x4C" => "\x6C", + "\x4D" => "\x6D", + "\x4E" => "\x6E", + "\x4F" => "\x6F", + "\x50" => "\x70", + "\x51" => "\x71", + "\x52" => "\x72", + "\x53" => "\x73", + "\x54" => "\x74", + "\x55" => "\x75", + "\x56" => "\x76", + "\x57" => "\x77", + "\x58" => "\x78", + "\x59" => "\x79", + "\x5A" => "\x7A", + "\xC2\xB5" => "\xCE\xBC", + "\xC3\x80" => "\xC3\xA0", + "\xC3\x81" => "\xC3\xA1", + "\xC3\x82" => "\xC3\xA2", + "\xC3\x83" => "\xC3\xA3", + "\xC3\x84" => "\xC3\xA4", + "\xC3\x85" => "\xC3\xA5", + "\xC3\x86" => "\xC3\xA6", + "\xC3\x87" => "\xC3\xA7", + "\xC3\x88" => "\xC3\xA8", + "\xC3\x89" => "\xC3\xA9", + "\xC3\x8A" => "\xC3\xAA", + "\xC3\x8B" => "\xC3\xAB", + "\xC3\x8C" => "\xC3\xAC", + "\xC3\x8D" => "\xC3\xAD", + "\xC3\x8E" => "\xC3\xAE", + "\xC3\x8F" => "\xC3\xAF", + "\xC3\x90" => "\xC3\xB0", + "\xC3\x91" => "\xC3\xB1", + "\xC3\x92" => "\xC3\xB2", + "\xC3\x93" => "\xC3\xB3", + "\xC3\x94" => "\xC3\xB4", + "\xC3\x95" => "\xC3\xB5", + "\xC3\x96" => "\xC3\xB6", + "\xC3\x98" => "\xC3\xB8", + "\xC3\x99" => "\xC3\xB9", + "\xC3\x9A" => "\xC3\xBA", + "\xC3\x9B" => "\xC3\xBB", + "\xC3\x9C" => "\xC3\xBC", + "\xC3\x9D" => "\xC3\xBD", + "\xC3\x9E" => "\xC3\xBE", + "\xC3\x9F" => "\x73\x73", + "\xC4\x80" => "\xC4\x81", + "\xC4\x82" => "\xC4\x83", + "\xC4\x84" => "\xC4\x85", + "\xC4\x86" => "\xC4\x87", + "\xC4\x88" => "\xC4\x89", + "\xC4\x8A" => "\xC4\x8B", + "\xC4\x8C" => "\xC4\x8D", + "\xC4\x8E" => "\xC4\x8F", + "\xC4\x90" => "\xC4\x91", + "\xC4\x92" => "\xC4\x93", + "\xC4\x94" => "\xC4\x95", + "\xC4\x96" => "\xC4\x97", + "\xC4\x98" => "\xC4\x99", + "\xC4\x9A" => "\xC4\x9B", + "\xC4\x9C" => "\xC4\x9D", + "\xC4\x9E" => "\xC4\x9F", + "\xC4\xA0" => "\xC4\xA1", + "\xC4\xA2" => "\xC4\xA3", + "\xC4\xA4" => "\xC4\xA5", + "\xC4\xA6" => "\xC4\xA7", + "\xC4\xA8" => "\xC4\xA9", + "\xC4\xAA" => "\xC4\xAB", + "\xC4\xAC" => "\xC4\xAD", + "\xC4\xAE" => "\xC4\xAF", + "\xC4\xB0" => "\x69\xCC\x87", + "\xC4\xB2" => "\xC4\xB3", + "\xC4\xB4" => "\xC4\xB5", + "\xC4\xB6" => "\xC4\xB7", + "\xC4\xB9" => "\xC4\xBA", + "\xC4\xBB" => "\xC4\xBC", + "\xC4\xBD" => "\xC4\xBE", + "\xC4\xBF" => "\xC5\x80", + "\xC5\x81" => "\xC5\x82", + "\xC5\x83" => "\xC5\x84", + "\xC5\x85" => "\xC5\x86", + "\xC5\x87" => "\xC5\x88", + "\xC5\x89" => "\xCA\xBC\x6E", + "\xC5\x8A" => "\xC5\x8B", + "\xC5\x8C" => "\xC5\x8D", + "\xC5\x8E" => "\xC5\x8F", + "\xC5\x90" => "\xC5\x91", + "\xC5\x92" => "\xC5\x93", + "\xC5\x94" => "\xC5\x95", + "\xC5\x96" => "\xC5\x97", + "\xC5\x98" => "\xC5\x99", + "\xC5\x9A" => "\xC5\x9B", + "\xC5\x9C" => "\xC5\x9D", + "\xC5\x9E" => "\xC5\x9F", + "\xC5\xA0" => "\xC5\xA1", + "\xC5\xA2" => "\xC5\xA3", + "\xC5\xA4" => "\xC5\xA5", + "\xC5\xA6" => "\xC5\xA7", + "\xC5\xA8" => "\xC5\xA9", + "\xC5\xAA" => "\xC5\xAB", + "\xC5\xAC" => "\xC5\xAD", + "\xC5\xAE" => "\xC5\xAF", + "\xC5\xB0" => "\xC5\xB1", + "\xC5\xB2" => "\xC5\xB3", + "\xC5\xB4" => "\xC5\xB5", + "\xC5\xB6" => "\xC5\xB7", + "\xC5\xB8" => "\xC3\xBF", + "\xC5\xB9" => "\xC5\xBA", + "\xC5\xBB" => "\xC5\xBC", + "\xC5\xBD" => "\xC5\xBE", + "\xC5\xBF" => "\x73", + "\xC6\x81" => "\xC9\x93", + "\xC6\x82" => "\xC6\x83", + "\xC6\x84" => "\xC6\x85", + "\xC6\x86" => "\xC9\x94", + "\xC6\x87" => "\xC6\x88", + "\xC6\x89" => "\xC9\x96", + "\xC6\x8A" => "\xC9\x97", + "\xC6\x8B" => "\xC6\x8C", + "\xC6\x8E" => "\xC7\x9D", + "\xC6\x8F" => "\xC9\x99", + "\xC6\x90" => "\xC9\x9B", + "\xC6\x91" => "\xC6\x92", + "\xC6\x93" => "\xC9\xA0", + "\xC6\x94" => "\xC9\xA3", + "\xC6\x96" => "\xC9\xA9", + "\xC6\x97" => "\xC9\xA8", + "\xC6\x98" => "\xC6\x99", + "\xC6\x9C" => "\xC9\xAF", + "\xC6\x9D" => "\xC9\xB2", + "\xC6\x9F" => "\xC9\xB5", + "\xC6\xA0" => "\xC6\xA1", + "\xC6\xA2" => "\xC6\xA3", + "\xC6\xA4" => "\xC6\xA5", + "\xC6\xA6" => "\xCA\x80", + "\xC6\xA7" => "\xC6\xA8", + "\xC6\xA9" => "\xCA\x83", + "\xC6\xAC" => "\xC6\xAD", + "\xC6\xAE" => "\xCA\x88", + "\xC6\xAF" => "\xC6\xB0", + "\xC6\xB1" => "\xCA\x8A", + "\xC6\xB2" => "\xCA\x8B", + "\xC6\xB3" => "\xC6\xB4", + "\xC6\xB5" => "\xC6\xB6", + "\xC6\xB7" => "\xCA\x92", + "\xC6\xB8" => "\xC6\xB9", + "\xC6\xBC" => "\xC6\xBD", + "\xC7\x84" => "\xC7\x86", + "\xC7\x85" => "\xC7\x86", + "\xC7\x87" => "\xC7\x89", + "\xC7\x88" => "\xC7\x89", + "\xC7\x8A" => "\xC7\x8C", + "\xC7\x8B" => "\xC7\x8C", + "\xC7\x8D" => "\xC7\x8E", + "\xC7\x8F" => "\xC7\x90", + "\xC7\x91" => "\xC7\x92", + "\xC7\x93" => "\xC7\x94", + "\xC7\x95" => "\xC7\x96", + "\xC7\x97" => "\xC7\x98", + "\xC7\x99" => "\xC7\x9A", + "\xC7\x9B" => "\xC7\x9C", + "\xC7\x9E" => "\xC7\x9F", + "\xC7\xA0" => "\xC7\xA1", + "\xC7\xA2" => "\xC7\xA3", + "\xC7\xA4" => "\xC7\xA5", + "\xC7\xA6" => "\xC7\xA7", + "\xC7\xA8" => "\xC7\xA9", + "\xC7\xAA" => "\xC7\xAB", + "\xC7\xAC" => "\xC7\xAD", + "\xC7\xAE" => "\xC7\xAF", + "\xC7\xB0" => "\x6A\xCC\x8C", + "\xC7\xB1" => "\xC7\xB3", + "\xC7\xB2" => "\xC7\xB3", + "\xC7\xB4" => "\xC7\xB5", + "\xC7\xB6" => "\xC6\x95", + "\xC7\xB7" => "\xC6\xBF", + "\xC7\xB8" => "\xC7\xB9", + "\xC7\xBA" => "\xC7\xBB", + "\xC7\xBC" => "\xC7\xBD", + "\xC7\xBE" => "\xC7\xBF", + "\xC8\x80" => "\xC8\x81", + "\xC8\x82" => "\xC8\x83", + "\xC8\x84" => "\xC8\x85", + "\xC8\x86" => "\xC8\x87", + "\xC8\x88" => "\xC8\x89", + "\xC8\x8A" => "\xC8\x8B", + "\xC8\x8C" => "\xC8\x8D", + "\xC8\x8E" => "\xC8\x8F", + "\xC8\x90" => "\xC8\x91", + "\xC8\x92" => "\xC8\x93", + "\xC8\x94" => "\xC8\x95", + "\xC8\x96" => "\xC8\x97", + "\xC8\x98" => "\xC8\x99", + "\xC8\x9A" => "\xC8\x9B", + "\xC8\x9C" => "\xC8\x9D", + "\xC8\x9E" => "\xC8\x9F", + "\xC8\xA0" => "\xC6\x9E", + "\xC8\xA2" => "\xC8\xA3", + "\xC8\xA4" => "\xC8\xA5", + "\xC8\xA6" => "\xC8\xA7", + "\xC8\xA8" => "\xC8\xA9", + "\xC8\xAA" => "\xC8\xAB", + "\xC8\xAC" => "\xC8\xAD", + "\xC8\xAE" => "\xC8\xAF", + "\xC8\xB0" => "\xC8\xB1", + "\xC8\xB2" => "\xC8\xB3", + "\xC8\xBA" => "\xE2\xB1\xA5", + "\xC8\xBB" => "\xC8\xBC", + "\xC8\xBD" => "\xC6\x9A", + "\xC8\xBE" => "\xE2\xB1\xA6", + "\xC9\x81" => "\xC9\x82", + "\xC9\x83" => "\xC6\x80", + "\xC9\x84" => "\xCA\x89", + "\xC9\x85" => "\xCA\x8C", + "\xC9\x86" => "\xC9\x87", + "\xC9\x88" => "\xC9\x89", + "\xC9\x8A" => "\xC9\x8B", + "\xC9\x8C" => "\xC9\x8D", + "\xC9\x8E" => "\xC9\x8F", + "\xCD\x85" => "\xCE\xB9", + "\xCD\xB0" => "\xCD\xB1", + "\xCD\xB2" => "\xCD\xB3", + "\xCD\xB6" => "\xCD\xB7", + "\xCD\xBF" => "\xCF\xB3", + "\xCE\x86" => "\xCE\xAC", + "\xCE\x88" => "\xCE\xAD", + "\xCE\x89" => "\xCE\xAE", + "\xCE\x8A" => "\xCE\xAF", + "\xCE\x8C" => "\xCF\x8C", + "\xCE\x8E" => "\xCF\x8D", + "\xCE\x8F" => "\xCF\x8E", + "\xCE\x90" => "\xCE\xB9\xCC\x88\xCC\x81", + "\xCE\x91" => "\xCE\xB1", + "\xCE\x92" => "\xCE\xB2", + "\xCE\x93" => "\xCE\xB3", + "\xCE\x94" => "\xCE\xB4", + "\xCE\x95" => "\xCE\xB5", + "\xCE\x96" => "\xCE\xB6", + "\xCE\x97" => "\xCE\xB7", + "\xCE\x98" => "\xCE\xB8", + "\xCE\x99" => "\xCE\xB9", + "\xCE\x9A" => "\xCE\xBA", + "\xCE\x9B" => "\xCE\xBB", + "\xCE\x9C" => "\xCE\xBC", + "\xCE\x9D" => "\xCE\xBD", + "\xCE\x9E" => "\xCE\xBE", + "\xCE\x9F" => "\xCE\xBF", + "\xCE\xA0" => "\xCF\x80", + "\xCE\xA1" => "\xCF\x81", + "\xCE\xA3" => "\xCF\x83", + "\xCE\xA4" => "\xCF\x84", + "\xCE\xA5" => "\xCF\x85", + "\xCE\xA6" => "\xCF\x86", + "\xCE\xA7" => "\xCF\x87", + "\xCE\xA8" => "\xCF\x88", + "\xCE\xA9" => "\xCF\x89", + "\xCE\xAA" => "\xCF\x8A", + "\xCE\xAB" => "\xCF\x8B", + "\xCE\xB0" => "\xCF\x85\xCC\x88\xCC\x81", + "\xCF\x82" => "\xCF\x83", + "\xCF\x8F" => "\xCF\x97", + "\xCF\x90" => "\xCE\xB2", + "\xCF\x91" => "\xCE\xB8", + "\xCF\x95" => "\xCF\x86", + "\xCF\x96" => "\xCF\x80", + "\xCF\x98" => "\xCF\x99", + "\xCF\x9A" => "\xCF\x9B", + "\xCF\x9C" => "\xCF\x9D", + "\xCF\x9E" => "\xCF\x9F", + "\xCF\xA0" => "\xCF\xA1", + "\xCF\xA2" => "\xCF\xA3", + "\xCF\xA4" => "\xCF\xA5", + "\xCF\xA6" => "\xCF\xA7", + "\xCF\xA8" => "\xCF\xA9", + "\xCF\xAA" => "\xCF\xAB", + "\xCF\xAC" => "\xCF\xAD", + "\xCF\xAE" => "\xCF\xAF", + "\xCF\xB0" => "\xCE\xBA", + "\xCF\xB1" => "\xCF\x81", + "\xCF\xB4" => "\xCE\xB8", + "\xCF\xB5" => "\xCE\xB5", + "\xCF\xB7" => "\xCF\xB8", + "\xCF\xB9" => "\xCF\xB2", + "\xCF\xBA" => "\xCF\xBB", + "\xCF\xBD" => "\xCD\xBB", + "\xCF\xBE" => "\xCD\xBC", + "\xCF\xBF" => "\xCD\xBD", + "\xD0\x80" => "\xD1\x90", + "\xD0\x81" => "\xD1\x91", + "\xD0\x82" => "\xD1\x92", + "\xD0\x83" => "\xD1\x93", + "\xD0\x84" => "\xD1\x94", + "\xD0\x85" => "\xD1\x95", + "\xD0\x86" => "\xD1\x96", + "\xD0\x87" => "\xD1\x97", + "\xD0\x88" => "\xD1\x98", + "\xD0\x89" => "\xD1\x99", + "\xD0\x8A" => "\xD1\x9A", + "\xD0\x8B" => "\xD1\x9B", + "\xD0\x8C" => "\xD1\x9C", + "\xD0\x8D" => "\xD1\x9D", + "\xD0\x8E" => "\xD1\x9E", + "\xD0\x8F" => "\xD1\x9F", + "\xD0\x90" => "\xD0\xB0", + "\xD0\x91" => "\xD0\xB1", + "\xD0\x92" => "\xD0\xB2", + "\xD0\x93" => "\xD0\xB3", + "\xD0\x94" => "\xD0\xB4", + "\xD0\x95" => "\xD0\xB5", + "\xD0\x96" => "\xD0\xB6", + "\xD0\x97" => "\xD0\xB7", + "\xD0\x98" => "\xD0\xB8", + "\xD0\x99" => "\xD0\xB9", + "\xD0\x9A" => "\xD0\xBA", + "\xD0\x9B" => "\xD0\xBB", + "\xD0\x9C" => "\xD0\xBC", + "\xD0\x9D" => "\xD0\xBD", + "\xD0\x9E" => "\xD0\xBE", + "\xD0\x9F" => "\xD0\xBF", + "\xD0\xA0" => "\xD1\x80", + "\xD0\xA1" => "\xD1\x81", + "\xD0\xA2" => "\xD1\x82", + "\xD0\xA3" => "\xD1\x83", + "\xD0\xA4" => "\xD1\x84", + "\xD0\xA5" => "\xD1\x85", + "\xD0\xA6" => "\xD1\x86", + "\xD0\xA7" => "\xD1\x87", + "\xD0\xA8" => "\xD1\x88", + "\xD0\xA9" => "\xD1\x89", + "\xD0\xAA" => "\xD1\x8A", + "\xD0\xAB" => "\xD1\x8B", + "\xD0\xAC" => "\xD1\x8C", + "\xD0\xAD" => "\xD1\x8D", + "\xD0\xAE" => "\xD1\x8E", + "\xD0\xAF" => "\xD1\x8F", + "\xD1\xA0" => "\xD1\xA1", + "\xD1\xA2" => "\xD1\xA3", + "\xD1\xA4" => "\xD1\xA5", + "\xD1\xA6" => "\xD1\xA7", + "\xD1\xA8" => "\xD1\xA9", + "\xD1\xAA" => "\xD1\xAB", + "\xD1\xAC" => "\xD1\xAD", + "\xD1\xAE" => "\xD1\xAF", + "\xD1\xB0" => "\xD1\xB1", + "\xD1\xB2" => "\xD1\xB3", + "\xD1\xB4" => "\xD1\xB5", + "\xD1\xB6" => "\xD1\xB7", + "\xD1\xB8" => "\xD1\xB9", + "\xD1\xBA" => "\xD1\xBB", + "\xD1\xBC" => "\xD1\xBD", + "\xD1\xBE" => "\xD1\xBF", + "\xD2\x80" => "\xD2\x81", + "\xD2\x8A" => "\xD2\x8B", + "\xD2\x8C" => "\xD2\x8D", + "\xD2\x8E" => "\xD2\x8F", + "\xD2\x90" => "\xD2\x91", + "\xD2\x92" => "\xD2\x93", + "\xD2\x94" => "\xD2\x95", + "\xD2\x96" => "\xD2\x97", + "\xD2\x98" => "\xD2\x99", + "\xD2\x9A" => "\xD2\x9B", + "\xD2\x9C" => "\xD2\x9D", + "\xD2\x9E" => "\xD2\x9F", + "\xD2\xA0" => "\xD2\xA1", + "\xD2\xA2" => "\xD2\xA3", + "\xD2\xA4" => "\xD2\xA5", + "\xD2\xA6" => "\xD2\xA7", + "\xD2\xA8" => "\xD2\xA9", + "\xD2\xAA" => "\xD2\xAB", + "\xD2\xAC" => "\xD2\xAD", + "\xD2\xAE" => "\xD2\xAF", + "\xD2\xB0" => "\xD2\xB1", + "\xD2\xB2" => "\xD2\xB3", + "\xD2\xB4" => "\xD2\xB5", + "\xD2\xB6" => "\xD2\xB7", + "\xD2\xB8" => "\xD2\xB9", + "\xD2\xBA" => "\xD2\xBB", + "\xD2\xBC" => "\xD2\xBD", + "\xD2\xBE" => "\xD2\xBF", + "\xD3\x80" => "\xD3\x8F", + "\xD3\x81" => "\xD3\x82", + "\xD3\x83" => "\xD3\x84", + "\xD3\x85" => "\xD3\x86", + "\xD3\x87" => "\xD3\x88", + "\xD3\x89" => "\xD3\x8A", + "\xD3\x8B" => "\xD3\x8C", + "\xD3\x8D" => "\xD3\x8E", + "\xD3\x90" => "\xD3\x91", + "\xD3\x92" => "\xD3\x93", + "\xD3\x94" => "\xD3\x95", + "\xD3\x96" => "\xD3\x97", + "\xD3\x98" => "\xD3\x99", + "\xD3\x9A" => "\xD3\x9B", + "\xD3\x9C" => "\xD3\x9D", + "\xD3\x9E" => "\xD3\x9F", + "\xD3\xA0" => "\xD3\xA1", + "\xD3\xA2" => "\xD3\xA3", + "\xD3\xA4" => "\xD3\xA5", + "\xD3\xA6" => "\xD3\xA7", + "\xD3\xA8" => "\xD3\xA9", + "\xD3\xAA" => "\xD3\xAB", + "\xD3\xAC" => "\xD3\xAD", + "\xD3\xAE" => "\xD3\xAF", + "\xD3\xB0" => "\xD3\xB1", + "\xD3\xB2" => "\xD3\xB3", + "\xD3\xB4" => "\xD3\xB5", + "\xD3\xB6" => "\xD3\xB7", + "\xD3\xB8" => "\xD3\xB9", + "\xD3\xBA" => "\xD3\xBB", + "\xD3\xBC" => "\xD3\xBD", + "\xD3\xBE" => "\xD3\xBF", + "\xD4\x80" => "\xD4\x81", + "\xD4\x82" => "\xD4\x83", + "\xD4\x84" => "\xD4\x85", + "\xD4\x86" => "\xD4\x87", + "\xD4\x88" => "\xD4\x89", + "\xD4\x8A" => "\xD4\x8B", + "\xD4\x8C" => "\xD4\x8D", + "\xD4\x8E" => "\xD4\x8F", + "\xD4\x90" => "\xD4\x91", + "\xD4\x92" => "\xD4\x93", + "\xD4\x94" => "\xD4\x95", + "\xD4\x96" => "\xD4\x97", + "\xD4\x98" => "\xD4\x99", + "\xD4\x9A" => "\xD4\x9B", + "\xD4\x9C" => "\xD4\x9D", + "\xD4\x9E" => "\xD4\x9F", + "\xD4\xA0" => "\xD4\xA1", + "\xD4\xA2" => "\xD4\xA3", + "\xD4\xA4" => "\xD4\xA5", + "\xD4\xA6" => "\xD4\xA7", + "\xD4\xA8" => "\xD4\xA9", + "\xD4\xAA" => "\xD4\xAB", + "\xD4\xAC" => "\xD4\xAD", + "\xD4\xAE" => "\xD4\xAF", + "\xD4\xB1" => "\xD5\xA1", + "\xD4\xB2" => "\xD5\xA2", + "\xD4\xB3" => "\xD5\xA3", + "\xD4\xB4" => "\xD5\xA4", + "\xD4\xB5" => "\xD5\xA5", + "\xD4\xB6" => "\xD5\xA6", + "\xD4\xB7" => "\xD5\xA7", + "\xD4\xB8" => "\xD5\xA8", + "\xD4\xB9" => "\xD5\xA9", + "\xD4\xBA" => "\xD5\xAA", + "\xD4\xBB" => "\xD5\xAB", + "\xD4\xBC" => "\xD5\xAC", + "\xD4\xBD" => "\xD5\xAD", + "\xD4\xBE" => "\xD5\xAE", + "\xD4\xBF" => "\xD5\xAF", + "\xD5\x80" => "\xD5\xB0", + "\xD5\x81" => "\xD5\xB1", + "\xD5\x82" => "\xD5\xB2", + "\xD5\x83" => "\xD5\xB3", + "\xD5\x84" => "\xD5\xB4", + "\xD5\x85" => "\xD5\xB5", + "\xD5\x86" => "\xD5\xB6", + "\xD5\x87" => "\xD5\xB7", + "\xD5\x88" => "\xD5\xB8", + "\xD5\x89" => "\xD5\xB9", + "\xD5\x8A" => "\xD5\xBA", + "\xD5\x8B" => "\xD5\xBB", + "\xD5\x8C" => "\xD5\xBC", + "\xD5\x8D" => "\xD5\xBD", + "\xD5\x8E" => "\xD5\xBE", + "\xD5\x8F" => "\xD5\xBF", + "\xD5\x90" => "\xD6\x80", + "\xD5\x91" => "\xD6\x81", + "\xD5\x92" => "\xD6\x82", + "\xD5\x93" => "\xD6\x83", + "\xD5\x94" => "\xD6\x84", + "\xD5\x95" => "\xD6\x85", + "\xD5\x96" => "\xD6\x86", + "\xD6\x87" => "\xD5\xA5\xD6\x82", + "\xE1\x82\xA0" => "\xE2\xB4\x80", + "\xE1\x82\xA1" => "\xE2\xB4\x81", + "\xE1\x82\xA2" => "\xE2\xB4\x82", + "\xE1\x82\xA3" => "\xE2\xB4\x83", + "\xE1\x82\xA4" => "\xE2\xB4\x84", + "\xE1\x82\xA5" => "\xE2\xB4\x85", + "\xE1\x82\xA6" => "\xE2\xB4\x86", + "\xE1\x82\xA7" => "\xE2\xB4\x87", + "\xE1\x82\xA8" => "\xE2\xB4\x88", + "\xE1\x82\xA9" => "\xE2\xB4\x89", + "\xE1\x82\xAA" => "\xE2\xB4\x8A", + "\xE1\x82\xAB" => "\xE2\xB4\x8B", + "\xE1\x82\xAC" => "\xE2\xB4\x8C", + "\xE1\x82\xAD" => "\xE2\xB4\x8D", + "\xE1\x82\xAE" => "\xE2\xB4\x8E", + "\xE1\x82\xAF" => "\xE2\xB4\x8F", + "\xE1\x82\xB0" => "\xE2\xB4\x90", + "\xE1\x82\xB1" => "\xE2\xB4\x91", + "\xE1\x82\xB2" => "\xE2\xB4\x92", + "\xE1\x82\xB3" => "\xE2\xB4\x93", + "\xE1\x82\xB4" => "\xE2\xB4\x94", + "\xE1\x82\xB5" => "\xE2\xB4\x95", + "\xE1\x82\xB6" => "\xE2\xB4\x96", + "\xE1\x82\xB7" => "\xE2\xB4\x97", + "\xE1\x82\xB8" => "\xE2\xB4\x98", + "\xE1\x82\xB9" => "\xE2\xB4\x99", + "\xE1\x82\xBA" => "\xE2\xB4\x9A", + "\xE1\x82\xBB" => "\xE2\xB4\x9B", + "\xE1\x82\xBC" => "\xE2\xB4\x9C", + "\xE1\x82\xBD" => "\xE2\xB4\x9D", + "\xE1\x82\xBE" => "\xE2\xB4\x9E", + "\xE1\x82\xBF" => "\xE2\xB4\x9F", + "\xE1\x83\x80" => "\xE2\xB4\xA0", + "\xE1\x83\x81" => "\xE2\xB4\xA1", + "\xE1\x83\x82" => "\xE2\xB4\xA2", + "\xE1\x83\x83" => "\xE2\xB4\xA3", + "\xE1\x83\x84" => "\xE2\xB4\xA4", + "\xE1\x83\x85" => "\xE2\xB4\xA5", + "\xE1\x83\x87" => "\xE2\xB4\xA7", + "\xE1\x83\x8D" => "\xE2\xB4\xAD", + "\xE1\x8F\xB8" => "\xE1\x8F\xB0", + "\xE1\x8F\xB9" => "\xE1\x8F\xB1", + "\xE1\x8F\xBA" => "\xE1\x8F\xB2", + "\xE1\x8F\xBB" => "\xE1\x8F\xB3", + "\xE1\x8F\xBC" => "\xE1\x8F\xB4", + "\xE1\x8F\xBD" => "\xE1\x8F\xB5", + "\xE1\xB2\x80" => "\xD0\xB2", + "\xE1\xB2\x81" => "\xD0\xB4", + "\xE1\xB2\x82" => "\xD0\xBE", + "\xE1\xB2\x83" => "\xD1\x81", + "\xE1\xB2\x84" => "\xD1\x82", + "\xE1\xB2\x85" => "\xD1\x82", + "\xE1\xB2\x86" => "\xD1\x8A", + "\xE1\xB2\x87" => "\xD1\xA3", + "\xE1\xB2\x88" => "\xEA\x99\x8B", + "\xE1\xB2\x90" => "\xE1\x83\x90", + "\xE1\xB2\x91" => "\xE1\x83\x91", + "\xE1\xB2\x92" => "\xE1\x83\x92", + "\xE1\xB2\x93" => "\xE1\x83\x93", + "\xE1\xB2\x94" => "\xE1\x83\x94", + "\xE1\xB2\x95" => "\xE1\x83\x95", + "\xE1\xB2\x96" => "\xE1\x83\x96", + "\xE1\xB2\x97" => "\xE1\x83\x97", + "\xE1\xB2\x98" => "\xE1\x83\x98", + "\xE1\xB2\x99" => "\xE1\x83\x99", + "\xE1\xB2\x9A" => "\xE1\x83\x9A", + "\xE1\xB2\x9B" => "\xE1\x83\x9B", + "\xE1\xB2\x9C" => "\xE1\x83\x9C", + "\xE1\xB2\x9D" => "\xE1\x83\x9D", + "\xE1\xB2\x9E" => "\xE1\x83\x9E", + "\xE1\xB2\x9F" => "\xE1\x83\x9F", + "\xE1\xB2\xA0" => "\xE1\x83\xA0", + "\xE1\xB2\xA1" => "\xE1\x83\xA1", + "\xE1\xB2\xA2" => "\xE1\x83\xA2", + "\xE1\xB2\xA3" => "\xE1\x83\xA3", + "\xE1\xB2\xA4" => "\xE1\x83\xA4", + "\xE1\xB2\xA5" => "\xE1\x83\xA5", + "\xE1\xB2\xA6" => "\xE1\x83\xA6", + "\xE1\xB2\xA7" => "\xE1\x83\xA7", + "\xE1\xB2\xA8" => "\xE1\x83\xA8", + "\xE1\xB2\xA9" => "\xE1\x83\xA9", + "\xE1\xB2\xAA" => "\xE1\x83\xAA", + "\xE1\xB2\xAB" => "\xE1\x83\xAB", + "\xE1\xB2\xAC" => "\xE1\x83\xAC", + "\xE1\xB2\xAD" => "\xE1\x83\xAD", + "\xE1\xB2\xAE" => "\xE1\x83\xAE", + "\xE1\xB2\xAF" => "\xE1\x83\xAF", + "\xE1\xB2\xB0" => "\xE1\x83\xB0", + "\xE1\xB2\xB1" => "\xE1\x83\xB1", + "\xE1\xB2\xB2" => "\xE1\x83\xB2", + "\xE1\xB2\xB3" => "\xE1\x83\xB3", + "\xE1\xB2\xB4" => "\xE1\x83\xB4", + "\xE1\xB2\xB5" => "\xE1\x83\xB5", + "\xE1\xB2\xB6" => "\xE1\x83\xB6", + "\xE1\xB2\xB7" => "\xE1\x83\xB7", + "\xE1\xB2\xB8" => "\xE1\x83\xB8", + "\xE1\xB2\xB9" => "\xE1\x83\xB9", + "\xE1\xB2\xBA" => "\xE1\x83\xBA", + "\xE1\xB2\xBD" => "\xE1\x83\xBD", + "\xE1\xB2\xBE" => "\xE1\x83\xBE", + "\xE1\xB2\xBF" => "\xE1\x83\xBF", + "\xE1\xB8\x80" => "\xE1\xB8\x81", + "\xE1\xB8\x82" => "\xE1\xB8\x83", + "\xE1\xB8\x84" => "\xE1\xB8\x85", + "\xE1\xB8\x86" => "\xE1\xB8\x87", + "\xE1\xB8\x88" => "\xE1\xB8\x89", + "\xE1\xB8\x8A" => "\xE1\xB8\x8B", + "\xE1\xB8\x8C" => "\xE1\xB8\x8D", + "\xE1\xB8\x8E" => "\xE1\xB8\x8F", + "\xE1\xB8\x90" => "\xE1\xB8\x91", + "\xE1\xB8\x92" => "\xE1\xB8\x93", + "\xE1\xB8\x94" => "\xE1\xB8\x95", + "\xE1\xB8\x96" => "\xE1\xB8\x97", + "\xE1\xB8\x98" => "\xE1\xB8\x99", + "\xE1\xB8\x9A" => "\xE1\xB8\x9B", + "\xE1\xB8\x9C" => "\xE1\xB8\x9D", + "\xE1\xB8\x9E" => "\xE1\xB8\x9F", + "\xE1\xB8\xA0" => "\xE1\xB8\xA1", + "\xE1\xB8\xA2" => "\xE1\xB8\xA3", + "\xE1\xB8\xA4" => "\xE1\xB8\xA5", + "\xE1\xB8\xA6" => "\xE1\xB8\xA7", + "\xE1\xB8\xA8" => "\xE1\xB8\xA9", + "\xE1\xB8\xAA" => "\xE1\xB8\xAB", + "\xE1\xB8\xAC" => "\xE1\xB8\xAD", + "\xE1\xB8\xAE" => "\xE1\xB8\xAF", + "\xE1\xB8\xB0" => "\xE1\xB8\xB1", + "\xE1\xB8\xB2" => "\xE1\xB8\xB3", + "\xE1\xB8\xB4" => "\xE1\xB8\xB5", + "\xE1\xB8\xB6" => "\xE1\xB8\xB7", + "\xE1\xB8\xB8" => "\xE1\xB8\xB9", + "\xE1\xB8\xBA" => "\xE1\xB8\xBB", + "\xE1\xB8\xBC" => "\xE1\xB8\xBD", + "\xE1\xB8\xBE" => "\xE1\xB8\xBF", + "\xE1\xB9\x80" => "\xE1\xB9\x81", + "\xE1\xB9\x82" => "\xE1\xB9\x83", + "\xE1\xB9\x84" => "\xE1\xB9\x85", + "\xE1\xB9\x86" => "\xE1\xB9\x87", + "\xE1\xB9\x88" => "\xE1\xB9\x89", + "\xE1\xB9\x8A" => "\xE1\xB9\x8B", + "\xE1\xB9\x8C" => "\xE1\xB9\x8D", + "\xE1\xB9\x8E" => "\xE1\xB9\x8F", + "\xE1\xB9\x90" => "\xE1\xB9\x91", + "\xE1\xB9\x92" => "\xE1\xB9\x93", + "\xE1\xB9\x94" => "\xE1\xB9\x95", + "\xE1\xB9\x96" => "\xE1\xB9\x97", + "\xE1\xB9\x98" => "\xE1\xB9\x99", + "\xE1\xB9\x9A" => "\xE1\xB9\x9B", + "\xE1\xB9\x9C" => "\xE1\xB9\x9D", + "\xE1\xB9\x9E" => "\xE1\xB9\x9F", + "\xE1\xB9\xA0" => "\xE1\xB9\xA1", + "\xE1\xB9\xA2" => "\xE1\xB9\xA3", + "\xE1\xB9\xA4" => "\xE1\xB9\xA5", + "\xE1\xB9\xA6" => "\xE1\xB9\xA7", + "\xE1\xB9\xA8" => "\xE1\xB9\xA9", + "\xE1\xB9\xAA" => "\xE1\xB9\xAB", + "\xE1\xB9\xAC" => "\xE1\xB9\xAD", + "\xE1\xB9\xAE" => "\xE1\xB9\xAF", + "\xE1\xB9\xB0" => "\xE1\xB9\xB1", + "\xE1\xB9\xB2" => "\xE1\xB9\xB3", + "\xE1\xB9\xB4" => "\xE1\xB9\xB5", + "\xE1\xB9\xB6" => "\xE1\xB9\xB7", + "\xE1\xB9\xB8" => "\xE1\xB9\xB9", + "\xE1\xB9\xBA" => "\xE1\xB9\xBB", + "\xE1\xB9\xBC" => "\xE1\xB9\xBD", + "\xE1\xB9\xBE" => "\xE1\xB9\xBF", + "\xE1\xBA\x80" => "\xE1\xBA\x81", + "\xE1\xBA\x82" => "\xE1\xBA\x83", + "\xE1\xBA\x84" => "\xE1\xBA\x85", + "\xE1\xBA\x86" => "\xE1\xBA\x87", + "\xE1\xBA\x88" => "\xE1\xBA\x89", + "\xE1\xBA\x8A" => "\xE1\xBA\x8B", + "\xE1\xBA\x8C" => "\xE1\xBA\x8D", + "\xE1\xBA\x8E" => "\xE1\xBA\x8F", + "\xE1\xBA\x90" => "\xE1\xBA\x91", + "\xE1\xBA\x92" => "\xE1\xBA\x93", + "\xE1\xBA\x94" => "\xE1\xBA\x95", + "\xE1\xBA\x96" => "\x68\xCC\xB1", + "\xE1\xBA\x97" => "\x74\xCC\x88", + "\xE1\xBA\x98" => "\x77\xCC\x8A", + "\xE1\xBA\x99" => "\x79\xCC\x8A", + "\xE1\xBA\x9A" => "\x61\xCA\xBE", + "\xE1\xBA\x9B" => "\xE1\xB9\xA1", + "\xE1\xBA\x9E" => "\x73\x73", + "\xE1\xBA\xA0" => "\xE1\xBA\xA1", + "\xE1\xBA\xA2" => "\xE1\xBA\xA3", + "\xE1\xBA\xA4" => "\xE1\xBA\xA5", + "\xE1\xBA\xA6" => "\xE1\xBA\xA7", + "\xE1\xBA\xA8" => "\xE1\xBA\xA9", + "\xE1\xBA\xAA" => "\xE1\xBA\xAB", + "\xE1\xBA\xAC" => "\xE1\xBA\xAD", + "\xE1\xBA\xAE" => "\xE1\xBA\xAF", + "\xE1\xBA\xB0" => "\xE1\xBA\xB1", + "\xE1\xBA\xB2" => "\xE1\xBA\xB3", + "\xE1\xBA\xB4" => "\xE1\xBA\xB5", + "\xE1\xBA\xB6" => "\xE1\xBA\xB7", + "\xE1\xBA\xB8" => "\xE1\xBA\xB9", + "\xE1\xBA\xBA" => "\xE1\xBA\xBB", + "\xE1\xBA\xBC" => "\xE1\xBA\xBD", + "\xE1\xBA\xBE" => "\xE1\xBA\xBF", + "\xE1\xBB\x80" => "\xE1\xBB\x81", + "\xE1\xBB\x82" => "\xE1\xBB\x83", + "\xE1\xBB\x84" => "\xE1\xBB\x85", + "\xE1\xBB\x86" => "\xE1\xBB\x87", + "\xE1\xBB\x88" => "\xE1\xBB\x89", + "\xE1\xBB\x8A" => "\xE1\xBB\x8B", + "\xE1\xBB\x8C" => "\xE1\xBB\x8D", + "\xE1\xBB\x8E" => "\xE1\xBB\x8F", + "\xE1\xBB\x90" => "\xE1\xBB\x91", + "\xE1\xBB\x92" => "\xE1\xBB\x93", + "\xE1\xBB\x94" => "\xE1\xBB\x95", + "\xE1\xBB\x96" => "\xE1\xBB\x97", + "\xE1\xBB\x98" => "\xE1\xBB\x99", + "\xE1\xBB\x9A" => "\xE1\xBB\x9B", + "\xE1\xBB\x9C" => "\xE1\xBB\x9D", + "\xE1\xBB\x9E" => "\xE1\xBB\x9F", + "\xE1\xBB\xA0" => "\xE1\xBB\xA1", + "\xE1\xBB\xA2" => "\xE1\xBB\xA3", + "\xE1\xBB\xA4" => "\xE1\xBB\xA5", + "\xE1\xBB\xA6" => "\xE1\xBB\xA7", + "\xE1\xBB\xA8" => "\xE1\xBB\xA9", + "\xE1\xBB\xAA" => "\xE1\xBB\xAB", + "\xE1\xBB\xAC" => "\xE1\xBB\xAD", + "\xE1\xBB\xAE" => "\xE1\xBB\xAF", + "\xE1\xBB\xB0" => "\xE1\xBB\xB1", + "\xE1\xBB\xB2" => "\xE1\xBB\xB3", + "\xE1\xBB\xB4" => "\xE1\xBB\xB5", + "\xE1\xBB\xB6" => "\xE1\xBB\xB7", + "\xE1\xBB\xB8" => "\xE1\xBB\xB9", + "\xE1\xBB\xBA" => "\xE1\xBB\xBB", + "\xE1\xBB\xBC" => "\xE1\xBB\xBD", + "\xE1\xBB\xBE" => "\xE1\xBB\xBF", + "\xE1\xBC\x88" => "\xE1\xBC\x80", + "\xE1\xBC\x89" => "\xE1\xBC\x81", + "\xE1\xBC\x8A" => "\xE1\xBC\x82", + "\xE1\xBC\x8B" => "\xE1\xBC\x83", + "\xE1\xBC\x8C" => "\xE1\xBC\x84", + "\xE1\xBC\x8D" => "\xE1\xBC\x85", + "\xE1\xBC\x8E" => "\xE1\xBC\x86", + "\xE1\xBC\x8F" => "\xE1\xBC\x87", + "\xE1\xBC\x98" => "\xE1\xBC\x90", + "\xE1\xBC\x99" => "\xE1\xBC\x91", + "\xE1\xBC\x9A" => "\xE1\xBC\x92", + "\xE1\xBC\x9B" => "\xE1\xBC\x93", + "\xE1\xBC\x9C" => "\xE1\xBC\x94", + "\xE1\xBC\x9D" => "\xE1\xBC\x95", + "\xE1\xBC\xA8" => "\xE1\xBC\xA0", + "\xE1\xBC\xA9" => "\xE1\xBC\xA1", + "\xE1\xBC\xAA" => "\xE1\xBC\xA2", + "\xE1\xBC\xAB" => "\xE1\xBC\xA3", + "\xE1\xBC\xAC" => "\xE1\xBC\xA4", + "\xE1\xBC\xAD" => "\xE1\xBC\xA5", + "\xE1\xBC\xAE" => "\xE1\xBC\xA6", + "\xE1\xBC\xAF" => "\xE1\xBC\xA7", + "\xE1\xBC\xB8" => "\xE1\xBC\xB0", + "\xE1\xBC\xB9" => "\xE1\xBC\xB1", + "\xE1\xBC\xBA" => "\xE1\xBC\xB2", + "\xE1\xBC\xBB" => "\xE1\xBC\xB3", + "\xE1\xBC\xBC" => "\xE1\xBC\xB4", + "\xE1\xBC\xBD" => "\xE1\xBC\xB5", + "\xE1\xBC\xBE" => "\xE1\xBC\xB6", + "\xE1\xBC\xBF" => "\xE1\xBC\xB7", + "\xE1\xBD\x88" => "\xE1\xBD\x80", + "\xE1\xBD\x89" => "\xE1\xBD\x81", + "\xE1\xBD\x8A" => "\xE1\xBD\x82", + "\xE1\xBD\x8B" => "\xE1\xBD\x83", + "\xE1\xBD\x8C" => "\xE1\xBD\x84", + "\xE1\xBD\x8D" => "\xE1\xBD\x85", + "\xE1\xBD\x90" => "\xCF\x85\xCC\x93", + "\xE1\xBD\x92" => "\xCF\x85\xCC\x93\xCC\x80", + "\xE1\xBD\x94" => "\xCF\x85\xCC\x93\xCC\x81", + "\xE1\xBD\x96" => "\xCF\x85\xCC\x93\xCD\x82", + "\xE1\xBD\x99" => "\xE1\xBD\x91", + "\xE1\xBD\x9B" => "\xE1\xBD\x93", + "\xE1\xBD\x9D" => "\xE1\xBD\x95", + "\xE1\xBD\x9F" => "\xE1\xBD\x97", + "\xE1\xBD\xA8" => "\xE1\xBD\xA0", + "\xE1\xBD\xA9" => "\xE1\xBD\xA1", + "\xE1\xBD\xAA" => "\xE1\xBD\xA2", + "\xE1\xBD\xAB" => "\xE1\xBD\xA3", + "\xE1\xBD\xAC" => "\xE1\xBD\xA4", + "\xE1\xBD\xAD" => "\xE1\xBD\xA5", + "\xE1\xBD\xAE" => "\xE1\xBD\xA6", + "\xE1\xBD\xAF" => "\xE1\xBD\xA7", + "\xE1\xBE\x80" => "\xE1\xBC\x80\xCE\xB9", + "\xE1\xBE\x81" => "\xE1\xBC\x81\xCE\xB9", + "\xE1\xBE\x82" => "\xE1\xBC\x82\xCE\xB9", + "\xE1\xBE\x83" => "\xE1\xBC\x83\xCE\xB9", + "\xE1\xBE\x84" => "\xE1\xBC\x84\xCE\xB9", + "\xE1\xBE\x85" => "\xE1\xBC\x85\xCE\xB9", + "\xE1\xBE\x86" => "\xE1\xBC\x86\xCE\xB9", + "\xE1\xBE\x87" => "\xE1\xBC\x87\xCE\xB9", + "\xE1\xBE\x88" => "\xE1\xBC\x80\xCE\xB9", + "\xE1\xBE\x89" => "\xE1\xBC\x81\xCE\xB9", + "\xE1\xBE\x8A" => "\xE1\xBC\x82\xCE\xB9", + "\xE1\xBE\x8B" => "\xE1\xBC\x83\xCE\xB9", + "\xE1\xBE\x8C" => "\xE1\xBC\x84\xCE\xB9", + "\xE1\xBE\x8D" => "\xE1\xBC\x85\xCE\xB9", + "\xE1\xBE\x8E" => "\xE1\xBC\x86\xCE\xB9", + "\xE1\xBE\x8F" => "\xE1\xBC\x87\xCE\xB9", + "\xE1\xBE\x90" => "\xE1\xBC\xA0\xCE\xB9", + "\xE1\xBE\x91" => "\xE1\xBC\xA1\xCE\xB9", + "\xE1\xBE\x92" => "\xE1\xBC\xA2\xCE\xB9", + "\xE1\xBE\x93" => "\xE1\xBC\xA3\xCE\xB9", + "\xE1\xBE\x94" => "\xE1\xBC\xA4\xCE\xB9", + "\xE1\xBE\x95" => "\xE1\xBC\xA5\xCE\xB9", + "\xE1\xBE\x96" => "\xE1\xBC\xA6\xCE\xB9", + "\xE1\xBE\x97" => "\xE1\xBC\xA7\xCE\xB9", + "\xE1\xBE\x98" => "\xE1\xBC\xA0\xCE\xB9", + "\xE1\xBE\x99" => "\xE1\xBC\xA1\xCE\xB9", + "\xE1\xBE\x9A" => "\xE1\xBC\xA2\xCE\xB9", + "\xE1\xBE\x9B" => "\xE1\xBC\xA3\xCE\xB9", + "\xE1\xBE\x9C" => "\xE1\xBC\xA4\xCE\xB9", + "\xE1\xBE\x9D" => "\xE1\xBC\xA5\xCE\xB9", + "\xE1\xBE\x9E" => "\xE1\xBC\xA6\xCE\xB9", + "\xE1\xBE\x9F" => "\xE1\xBC\xA7\xCE\xB9", + "\xE1\xBE\xA0" => "\xE1\xBD\xA0\xCE\xB9", + "\xE1\xBE\xA1" => "\xE1\xBD\xA1\xCE\xB9", + "\xE1\xBE\xA2" => "\xE1\xBD\xA2\xCE\xB9", + "\xE1\xBE\xA3" => "\xE1\xBD\xA3\xCE\xB9", + "\xE1\xBE\xA4" => "\xE1\xBD\xA4\xCE\xB9", + "\xE1\xBE\xA5" => "\xE1\xBD\xA5\xCE\xB9", + "\xE1\xBE\xA6" => "\xE1\xBD\xA6\xCE\xB9", + "\xE1\xBE\xA7" => "\xE1\xBD\xA7\xCE\xB9", + "\xE1\xBE\xA8" => "\xE1\xBD\xA0\xCE\xB9", + "\xE1\xBE\xA9" => "\xE1\xBD\xA1\xCE\xB9", + "\xE1\xBE\xAA" => "\xE1\xBD\xA2\xCE\xB9", + "\xE1\xBE\xAB" => "\xE1\xBD\xA3\xCE\xB9", + "\xE1\xBE\xAC" => "\xE1\xBD\xA4\xCE\xB9", + "\xE1\xBE\xAD" => "\xE1\xBD\xA5\xCE\xB9", + "\xE1\xBE\xAE" => "\xE1\xBD\xA6\xCE\xB9", + "\xE1\xBE\xAF" => "\xE1\xBD\xA7\xCE\xB9", + "\xE1\xBE\xB2" => "\xE1\xBD\xB0\xCE\xB9", + "\xE1\xBE\xB3" => "\xCE\xB1\xCE\xB9", + "\xE1\xBE\xB4" => "\xCE\xAC\xCE\xB9", + "\xE1\xBE\xB6" => "\xCE\xB1\xCD\x82", + "\xE1\xBE\xB7" => "\xCE\xB1\xCD\x82\xCE\xB9", + "\xE1\xBE\xB8" => "\xE1\xBE\xB0", + "\xE1\xBE\xB9" => "\xE1\xBE\xB1", + "\xE1\xBE\xBA" => "\xE1\xBD\xB0", + "\xE1\xBE\xBB" => "\xE1\xBD\xB1", + "\xE1\xBE\xBC" => "\xCE\xB1\xCE\xB9", + "\xE1\xBE\xBE" => "\xCE\xB9", + "\xE1\xBF\x82" => "\xE1\xBD\xB4\xCE\xB9", + "\xE1\xBF\x83" => "\xCE\xB7\xCE\xB9", + "\xE1\xBF\x84" => "\xCE\xAE\xCE\xB9", + "\xE1\xBF\x86" => "\xCE\xB7\xCD\x82", + "\xE1\xBF\x87" => "\xCE\xB7\xCD\x82\xCE\xB9", + "\xE1\xBF\x88" => "\xE1\xBD\xB2", + "\xE1\xBF\x89" => "\xE1\xBD\xB3", + "\xE1\xBF\x8A" => "\xE1\xBD\xB4", + "\xE1\xBF\x8B" => "\xE1\xBD\xB5", + "\xE1\xBF\x8C" => "\xCE\xB7\xCE\xB9", + "\xE1\xBF\x92" => "\xCE\xB9\xCC\x88\xCC\x80", + "\xE1\xBF\x93" => "\xCE\xB9\xCC\x88\xCC\x81", + "\xE1\xBF\x96" => "\xCE\xB9\xCD\x82", + "\xE1\xBF\x97" => "\xCE\xB9\xCC\x88\xCD\x82", + "\xE1\xBF\x98" => "\xE1\xBF\x90", + "\xE1\xBF\x99" => "\xE1\xBF\x91", + "\xE1\xBF\x9A" => "\xE1\xBD\xB6", + "\xE1\xBF\x9B" => "\xE1\xBD\xB7", + "\xE1\xBF\xA2" => "\xCF\x85\xCC\x88\xCC\x80", + "\xE1\xBF\xA3" => "\xCF\x85\xCC\x88\xCC\x81", + "\xE1\xBF\xA4" => "\xCF\x81\xCC\x93", + "\xE1\xBF\xA6" => "\xCF\x85\xCD\x82", + "\xE1\xBF\xA7" => "\xCF\x85\xCC\x88\xCD\x82", + "\xE1\xBF\xA8" => "\xE1\xBF\xA0", + "\xE1\xBF\xA9" => "\xE1\xBF\xA1", + "\xE1\xBF\xAA" => "\xE1\xBD\xBA", + "\xE1\xBF\xAB" => "\xE1\xBD\xBB", + "\xE1\xBF\xAC" => "\xE1\xBF\xA5", + "\xE1\xBF\xB2" => "\xE1\xBD\xBC\xCE\xB9", + "\xE1\xBF\xB3" => "\xCF\x89\xCE\xB9", + "\xE1\xBF\xB4" => "\xCF\x8E\xCE\xB9", + "\xE1\xBF\xB6" => "\xCF\x89\xCD\x82", + "\xE1\xBF\xB7" => "\xCF\x89\xCD\x82\xCE\xB9", + "\xE1\xBF\xB8" => "\xE1\xBD\xB8", + "\xE1\xBF\xB9" => "\xE1\xBD\xB9", + "\xE1\xBF\xBA" => "\xE1\xBD\xBC", + "\xE1\xBF\xBB" => "\xE1\xBD\xBD", + "\xE1\xBF\xBC" => "\xCF\x89\xCE\xB9", + "\xE2\x84\xA6" => "\xCF\x89", + "\xE2\x84\xAA" => "\x6B", + "\xE2\x84\xAB" => "\xC3\xA5", + "\xE2\x84\xB2" => "\xE2\x85\x8E", + "\xE2\x85\xA0" => "\xE2\x85\xB0", + "\xE2\x85\xA1" => "\xE2\x85\xB1", + "\xE2\x85\xA2" => "\xE2\x85\xB2", + "\xE2\x85\xA3" => "\xE2\x85\xB3", + "\xE2\x85\xA4" => "\xE2\x85\xB4", + "\xE2\x85\xA5" => "\xE2\x85\xB5", + "\xE2\x85\xA6" => "\xE2\x85\xB6", + "\xE2\x85\xA7" => "\xE2\x85\xB7", + "\xE2\x85\xA8" => "\xE2\x85\xB8", + "\xE2\x85\xA9" => "\xE2\x85\xB9", + "\xE2\x85\xAA" => "\xE2\x85\xBA", + "\xE2\x85\xAB" => "\xE2\x85\xBB", + "\xE2\x85\xAC" => "\xE2\x85\xBC", + "\xE2\x85\xAD" => "\xE2\x85\xBD", + "\xE2\x85\xAE" => "\xE2\x85\xBE", + "\xE2\x85\xAF" => "\xE2\x85\xBF", + "\xE2\x86\x83" => "\xE2\x86\x84", + "\xE2\x92\xB6" => "\xE2\x93\x90", + "\xE2\x92\xB7" => "\xE2\x93\x91", + "\xE2\x92\xB8" => "\xE2\x93\x92", + "\xE2\x92\xB9" => "\xE2\x93\x93", + "\xE2\x92\xBA" => "\xE2\x93\x94", + "\xE2\x92\xBB" => "\xE2\x93\x95", + "\xE2\x92\xBC" => "\xE2\x93\x96", + "\xE2\x92\xBD" => "\xE2\x93\x97", + "\xE2\x92\xBE" => "\xE2\x93\x98", + "\xE2\x92\xBF" => "\xE2\x93\x99", + "\xE2\x93\x80" => "\xE2\x93\x9A", + "\xE2\x93\x81" => "\xE2\x93\x9B", + "\xE2\x93\x82" => "\xE2\x93\x9C", + "\xE2\x93\x83" => "\xE2\x93\x9D", + "\xE2\x93\x84" => "\xE2\x93\x9E", + "\xE2\x93\x85" => "\xE2\x93\x9F", + "\xE2\x93\x86" => "\xE2\x93\xA0", + "\xE2\x93\x87" => "\xE2\x93\xA1", + "\xE2\x93\x88" => "\xE2\x93\xA2", + "\xE2\x93\x89" => "\xE2\x93\xA3", + "\xE2\x93\x8A" => "\xE2\x93\xA4", + "\xE2\x93\x8B" => "\xE2\x93\xA5", + "\xE2\x93\x8C" => "\xE2\x93\xA6", + "\xE2\x93\x8D" => "\xE2\x93\xA7", + "\xE2\x93\x8E" => "\xE2\x93\xA8", + "\xE2\x93\x8F" => "\xE2\x93\xA9", + "\xE2\xB0\x80" => "\xE2\xB0\xB0", + "\xE2\xB0\x81" => "\xE2\xB0\xB1", + "\xE2\xB0\x82" => "\xE2\xB0\xB2", + "\xE2\xB0\x83" => "\xE2\xB0\xB3", + "\xE2\xB0\x84" => "\xE2\xB0\xB4", + "\xE2\xB0\x85" => "\xE2\xB0\xB5", + "\xE2\xB0\x86" => "\xE2\xB0\xB6", + "\xE2\xB0\x87" => "\xE2\xB0\xB7", + "\xE2\xB0\x88" => "\xE2\xB0\xB8", + "\xE2\xB0\x89" => "\xE2\xB0\xB9", + "\xE2\xB0\x8A" => "\xE2\xB0\xBA", + "\xE2\xB0\x8B" => "\xE2\xB0\xBB", + "\xE2\xB0\x8C" => "\xE2\xB0\xBC", + "\xE2\xB0\x8D" => "\xE2\xB0\xBD", + "\xE2\xB0\x8E" => "\xE2\xB0\xBE", + "\xE2\xB0\x8F" => "\xE2\xB0\xBF", + "\xE2\xB0\x90" => "\xE2\xB1\x80", + "\xE2\xB0\x91" => "\xE2\xB1\x81", + "\xE2\xB0\x92" => "\xE2\xB1\x82", + "\xE2\xB0\x93" => "\xE2\xB1\x83", + "\xE2\xB0\x94" => "\xE2\xB1\x84", + "\xE2\xB0\x95" => "\xE2\xB1\x85", + "\xE2\xB0\x96" => "\xE2\xB1\x86", + "\xE2\xB0\x97" => "\xE2\xB1\x87", + "\xE2\xB0\x98" => "\xE2\xB1\x88", + "\xE2\xB0\x99" => "\xE2\xB1\x89", + "\xE2\xB0\x9A" => "\xE2\xB1\x8A", + "\xE2\xB0\x9B" => "\xE2\xB1\x8B", + "\xE2\xB0\x9C" => "\xE2\xB1\x8C", + "\xE2\xB0\x9D" => "\xE2\xB1\x8D", + "\xE2\xB0\x9E" => "\xE2\xB1\x8E", + "\xE2\xB0\x9F" => "\xE2\xB1\x8F", + "\xE2\xB0\xA0" => "\xE2\xB1\x90", + "\xE2\xB0\xA1" => "\xE2\xB1\x91", + "\xE2\xB0\xA2" => "\xE2\xB1\x92", + "\xE2\xB0\xA3" => "\xE2\xB1\x93", + "\xE2\xB0\xA4" => "\xE2\xB1\x94", + "\xE2\xB0\xA5" => "\xE2\xB1\x95", + "\xE2\xB0\xA6" => "\xE2\xB1\x96", + "\xE2\xB0\xA7" => "\xE2\xB1\x97", + "\xE2\xB0\xA8" => "\xE2\xB1\x98", + "\xE2\xB0\xA9" => "\xE2\xB1\x99", + "\xE2\xB0\xAA" => "\xE2\xB1\x9A", + "\xE2\xB0\xAB" => "\xE2\xB1\x9B", + "\xE2\xB0\xAC" => "\xE2\xB1\x9C", + "\xE2\xB0\xAD" => "\xE2\xB1\x9D", + "\xE2\xB0\xAE" => "\xE2\xB1\x9E", + "\xE2\xB0\xAF" => "\xE2\xB1\x9F", + "\xE2\xB1\xA0" => "\xE2\xB1\xA1", + "\xE2\xB1\xA2" => "\xC9\xAB", + "\xE2\xB1\xA3" => "\xE1\xB5\xBD", + "\xE2\xB1\xA4" => "\xC9\xBD", + "\xE2\xB1\xA7" => "\xE2\xB1\xA8", + "\xE2\xB1\xA9" => "\xE2\xB1\xAA", + "\xE2\xB1\xAB" => "\xE2\xB1\xAC", + "\xE2\xB1\xAD" => "\xC9\x91", + "\xE2\xB1\xAE" => "\xC9\xB1", + "\xE2\xB1\xAF" => "\xC9\x90", + "\xE2\xB1\xB0" => "\xC9\x92", + "\xE2\xB1\xB2" => "\xE2\xB1\xB3", + "\xE2\xB1\xB5" => "\xE2\xB1\xB6", + "\xE2\xB1\xBE" => "\xC8\xBF", + "\xE2\xB1\xBF" => "\xC9\x80", + "\xE2\xB2\x80" => "\xE2\xB2\x81", + "\xE2\xB2\x82" => "\xE2\xB2\x83", + "\xE2\xB2\x84" => "\xE2\xB2\x85", + "\xE2\xB2\x86" => "\xE2\xB2\x87", + "\xE2\xB2\x88" => "\xE2\xB2\x89", + "\xE2\xB2\x8A" => "\xE2\xB2\x8B", + "\xE2\xB2\x8C" => "\xE2\xB2\x8D", + "\xE2\xB2\x8E" => "\xE2\xB2\x8F", + "\xE2\xB2\x90" => "\xE2\xB2\x91", + "\xE2\xB2\x92" => "\xE2\xB2\x93", + "\xE2\xB2\x94" => "\xE2\xB2\x95", + "\xE2\xB2\x96" => "\xE2\xB2\x97", + "\xE2\xB2\x98" => "\xE2\xB2\x99", + "\xE2\xB2\x9A" => "\xE2\xB2\x9B", + "\xE2\xB2\x9C" => "\xE2\xB2\x9D", + "\xE2\xB2\x9E" => "\xE2\xB2\x9F", + "\xE2\xB2\xA0" => "\xE2\xB2\xA1", + "\xE2\xB2\xA2" => "\xE2\xB2\xA3", + "\xE2\xB2\xA4" => "\xE2\xB2\xA5", + "\xE2\xB2\xA6" => "\xE2\xB2\xA7", + "\xE2\xB2\xA8" => "\xE2\xB2\xA9", + "\xE2\xB2\xAA" => "\xE2\xB2\xAB", + "\xE2\xB2\xAC" => "\xE2\xB2\xAD", + "\xE2\xB2\xAE" => "\xE2\xB2\xAF", + "\xE2\xB2\xB0" => "\xE2\xB2\xB1", + "\xE2\xB2\xB2" => "\xE2\xB2\xB3", + "\xE2\xB2\xB4" => "\xE2\xB2\xB5", + "\xE2\xB2\xB6" => "\xE2\xB2\xB7", + "\xE2\xB2\xB8" => "\xE2\xB2\xB9", + "\xE2\xB2\xBA" => "\xE2\xB2\xBB", + "\xE2\xB2\xBC" => "\xE2\xB2\xBD", + "\xE2\xB2\xBE" => "\xE2\xB2\xBF", + "\xE2\xB3\x80" => "\xE2\xB3\x81", + "\xE2\xB3\x82" => "\xE2\xB3\x83", + "\xE2\xB3\x84" => "\xE2\xB3\x85", + "\xE2\xB3\x86" => "\xE2\xB3\x87", + "\xE2\xB3\x88" => "\xE2\xB3\x89", + "\xE2\xB3\x8A" => "\xE2\xB3\x8B", + "\xE2\xB3\x8C" => "\xE2\xB3\x8D", + "\xE2\xB3\x8E" => "\xE2\xB3\x8F", + "\xE2\xB3\x90" => "\xE2\xB3\x91", + "\xE2\xB3\x92" => "\xE2\xB3\x93", + "\xE2\xB3\x94" => "\xE2\xB3\x95", + "\xE2\xB3\x96" => "\xE2\xB3\x97", + "\xE2\xB3\x98" => "\xE2\xB3\x99", + "\xE2\xB3\x9A" => "\xE2\xB3\x9B", + "\xE2\xB3\x9C" => "\xE2\xB3\x9D", + "\xE2\xB3\x9E" => "\xE2\xB3\x9F", + "\xE2\xB3\xA0" => "\xE2\xB3\xA1", + "\xE2\xB3\xA2" => "\xE2\xB3\xA3", + "\xE2\xB3\xAB" => "\xE2\xB3\xAC", + "\xE2\xB3\xAD" => "\xE2\xB3\xAE", + "\xE2\xB3\xB2" => "\xE2\xB3\xB3", + "\xEA\x99\x80" => "\xEA\x99\x81", + "\xEA\x99\x82" => "\xEA\x99\x83", + "\xEA\x99\x84" => "\xEA\x99\x85", + "\xEA\x99\x86" => "\xEA\x99\x87", + "\xEA\x99\x88" => "\xEA\x99\x89", + "\xEA\x99\x8A" => "\xEA\x99\x8B", + "\xEA\x99\x8C" => "\xEA\x99\x8D", + "\xEA\x99\x8E" => "\xEA\x99\x8F", + "\xEA\x99\x90" => "\xEA\x99\x91", + "\xEA\x99\x92" => "\xEA\x99\x93", + "\xEA\x99\x94" => "\xEA\x99\x95", + "\xEA\x99\x96" => "\xEA\x99\x97", + "\xEA\x99\x98" => "\xEA\x99\x99", + "\xEA\x99\x9A" => "\xEA\x99\x9B", + "\xEA\x99\x9C" => "\xEA\x99\x9D", + "\xEA\x99\x9E" => "\xEA\x99\x9F", + "\xEA\x99\xA0" => "\xEA\x99\xA1", + "\xEA\x99\xA2" => "\xEA\x99\xA3", + "\xEA\x99\xA4" => "\xEA\x99\xA5", + "\xEA\x99\xA6" => "\xEA\x99\xA7", + "\xEA\x99\xA8" => "\xEA\x99\xA9", + "\xEA\x99\xAA" => "\xEA\x99\xAB", + "\xEA\x99\xAC" => "\xEA\x99\xAD", + "\xEA\x9A\x80" => "\xEA\x9A\x81", + "\xEA\x9A\x82" => "\xEA\x9A\x83", + "\xEA\x9A\x84" => "\xEA\x9A\x85", + "\xEA\x9A\x86" => "\xEA\x9A\x87", + "\xEA\x9A\x88" => "\xEA\x9A\x89", + "\xEA\x9A\x8A" => "\xEA\x9A\x8B", + "\xEA\x9A\x8C" => "\xEA\x9A\x8D", + "\xEA\x9A\x8E" => "\xEA\x9A\x8F", + "\xEA\x9A\x90" => "\xEA\x9A\x91", + "\xEA\x9A\x92" => "\xEA\x9A\x93", + "\xEA\x9A\x94" => "\xEA\x9A\x95", + "\xEA\x9A\x96" => "\xEA\x9A\x97", + "\xEA\x9A\x98" => "\xEA\x9A\x99", + "\xEA\x9A\x9A" => "\xEA\x9A\x9B", + "\xEA\x9C\xA2" => "\xEA\x9C\xA3", + "\xEA\x9C\xA4" => "\xEA\x9C\xA5", + "\xEA\x9C\xA6" => "\xEA\x9C\xA7", + "\xEA\x9C\xA8" => "\xEA\x9C\xA9", + "\xEA\x9C\xAA" => "\xEA\x9C\xAB", + "\xEA\x9C\xAC" => "\xEA\x9C\xAD", + "\xEA\x9C\xAE" => "\xEA\x9C\xAF", + "\xEA\x9C\xB2" => "\xEA\x9C\xB3", + "\xEA\x9C\xB4" => "\xEA\x9C\xB5", + "\xEA\x9C\xB6" => "\xEA\x9C\xB7", + "\xEA\x9C\xB8" => "\xEA\x9C\xB9", + "\xEA\x9C\xBA" => "\xEA\x9C\xBB", + "\xEA\x9C\xBC" => "\xEA\x9C\xBD", + "\xEA\x9C\xBE" => "\xEA\x9C\xBF", + "\xEA\x9D\x80" => "\xEA\x9D\x81", + "\xEA\x9D\x82" => "\xEA\x9D\x83", + "\xEA\x9D\x84" => "\xEA\x9D\x85", + "\xEA\x9D\x86" => "\xEA\x9D\x87", + "\xEA\x9D\x88" => "\xEA\x9D\x89", + "\xEA\x9D\x8A" => "\xEA\x9D\x8B", + "\xEA\x9D\x8C" => "\xEA\x9D\x8D", + "\xEA\x9D\x8E" => "\xEA\x9D\x8F", + "\xEA\x9D\x90" => "\xEA\x9D\x91", + "\xEA\x9D\x92" => "\xEA\x9D\x93", + "\xEA\x9D\x94" => "\xEA\x9D\x95", + "\xEA\x9D\x96" => "\xEA\x9D\x97", + "\xEA\x9D\x98" => "\xEA\x9D\x99", + "\xEA\x9D\x9A" => "\xEA\x9D\x9B", + "\xEA\x9D\x9C" => "\xEA\x9D\x9D", + "\xEA\x9D\x9E" => "\xEA\x9D\x9F", + "\xEA\x9D\xA0" => "\xEA\x9D\xA1", + "\xEA\x9D\xA2" => "\xEA\x9D\xA3", + "\xEA\x9D\xA4" => "\xEA\x9D\xA5", + "\xEA\x9D\xA6" => "\xEA\x9D\xA7", + "\xEA\x9D\xA8" => "\xEA\x9D\xA9", + "\xEA\x9D\xAA" => "\xEA\x9D\xAB", + "\xEA\x9D\xAC" => "\xEA\x9D\xAD", + "\xEA\x9D\xAE" => "\xEA\x9D\xAF", + "\xEA\x9D\xB9" => "\xEA\x9D\xBA", + "\xEA\x9D\xBB" => "\xEA\x9D\xBC", + "\xEA\x9D\xBD" => "\xE1\xB5\xB9", + "\xEA\x9D\xBE" => "\xEA\x9D\xBF", + "\xEA\x9E\x80" => "\xEA\x9E\x81", + "\xEA\x9E\x82" => "\xEA\x9E\x83", + "\xEA\x9E\x84" => "\xEA\x9E\x85", + "\xEA\x9E\x86" => "\xEA\x9E\x87", + "\xEA\x9E\x8B" => "\xEA\x9E\x8C", + "\xEA\x9E\x8D" => "\xC9\xA5", + "\xEA\x9E\x90" => "\xEA\x9E\x91", + "\xEA\x9E\x92" => "\xEA\x9E\x93", + "\xEA\x9E\x96" => "\xEA\x9E\x97", + "\xEA\x9E\x98" => "\xEA\x9E\x99", + "\xEA\x9E\x9A" => "\xEA\x9E\x9B", + "\xEA\x9E\x9C" => "\xEA\x9E\x9D", + "\xEA\x9E\x9E" => "\xEA\x9E\x9F", + "\xEA\x9E\xA0" => "\xEA\x9E\xA1", + "\xEA\x9E\xA2" => "\xEA\x9E\xA3", + "\xEA\x9E\xA4" => "\xEA\x9E\xA5", + "\xEA\x9E\xA6" => "\xEA\x9E\xA7", + "\xEA\x9E\xA8" => "\xEA\x9E\xA9", + "\xEA\x9E\xAA" => "\xC9\xA6", + "\xEA\x9E\xAB" => "\xC9\x9C", + "\xEA\x9E\xAC" => "\xC9\xA1", + "\xEA\x9E\xAD" => "\xC9\xAC", + "\xEA\x9E\xAE" => "\xC9\xAA", + "\xEA\x9E\xB0" => "\xCA\x9E", + "\xEA\x9E\xB1" => "\xCA\x87", + "\xEA\x9E\xB2" => "\xCA\x9D", + "\xEA\x9E\xB3" => "\xEA\xAD\x93", + "\xEA\x9E\xB4" => "\xEA\x9E\xB5", + "\xEA\x9E\xB6" => "\xEA\x9E\xB7", + "\xEA\x9E\xB8" => "\xEA\x9E\xB9", + "\xEA\x9E\xBA" => "\xEA\x9E\xBB", + "\xEA\x9E\xBC" => "\xEA\x9E\xBD", + "\xEA\x9E\xBE" => "\xEA\x9E\xBF", + "\xEA\x9F\x80" => "\xEA\x9F\x81", + "\xEA\x9F\x82" => "\xEA\x9F\x83", + "\xEA\x9F\x84" => "\xEA\x9E\x94", + "\xEA\x9F\x85" => "\xCA\x82", + "\xEA\x9F\x86" => "\xE1\xB6\x8E", + "\xEA\x9F\x87" => "\xEA\x9F\x88", + "\xEA\x9F\x89" => "\xEA\x9F\x8A", + "\xEA\x9F\x90" => "\xEA\x9F\x91", + "\xEA\x9F\x96" => "\xEA\x9F\x97", + "\xEA\x9F\x98" => "\xEA\x9F\x99", + "\xEA\x9F\xB5" => "\xEA\x9F\xB6", + "\xEA\xAD\xB0" => "\xE1\x8E\xA0", + "\xEA\xAD\xB1" => "\xE1\x8E\xA1", + "\xEA\xAD\xB2" => "\xE1\x8E\xA2", + "\xEA\xAD\xB3" => "\xE1\x8E\xA3", + "\xEA\xAD\xB4" => "\xE1\x8E\xA4", + "\xEA\xAD\xB5" => "\xE1\x8E\xA5", + "\xEA\xAD\xB6" => "\xE1\x8E\xA6", + "\xEA\xAD\xB7" => "\xE1\x8E\xA7", + "\xEA\xAD\xB8" => "\xE1\x8E\xA8", + "\xEA\xAD\xB9" => "\xE1\x8E\xA9", + "\xEA\xAD\xBA" => "\xE1\x8E\xAA", + "\xEA\xAD\xBB" => "\xE1\x8E\xAB", + "\xEA\xAD\xBC" => "\xE1\x8E\xAC", + "\xEA\xAD\xBD" => "\xE1\x8E\xAD", + "\xEA\xAD\xBE" => "\xE1\x8E\xAE", + "\xEA\xAD\xBF" => "\xE1\x8E\xAF", + "\xEA\xAE\x80" => "\xE1\x8E\xB0", + "\xEA\xAE\x81" => "\xE1\x8E\xB1", + "\xEA\xAE\x82" => "\xE1\x8E\xB2", + "\xEA\xAE\x83" => "\xE1\x8E\xB3", + "\xEA\xAE\x84" => "\xE1\x8E\xB4", + "\xEA\xAE\x85" => "\xE1\x8E\xB5", + "\xEA\xAE\x86" => "\xE1\x8E\xB6", + "\xEA\xAE\x87" => "\xE1\x8E\xB7", + "\xEA\xAE\x88" => "\xE1\x8E\xB8", + "\xEA\xAE\x89" => "\xE1\x8E\xB9", + "\xEA\xAE\x8A" => "\xE1\x8E\xBA", + "\xEA\xAE\x8B" => "\xE1\x8E\xBB", + "\xEA\xAE\x8C" => "\xE1\x8E\xBC", + "\xEA\xAE\x8D" => "\xE1\x8E\xBD", + "\xEA\xAE\x8E" => "\xE1\x8E\xBE", + "\xEA\xAE\x8F" => "\xE1\x8E\xBF", + "\xEA\xAE\x90" => "\xE1\x8F\x80", + "\xEA\xAE\x91" => "\xE1\x8F\x81", + "\xEA\xAE\x92" => "\xE1\x8F\x82", + "\xEA\xAE\x93" => "\xE1\x8F\x83", + "\xEA\xAE\x94" => "\xE1\x8F\x84", + "\xEA\xAE\x95" => "\xE1\x8F\x85", + "\xEA\xAE\x96" => "\xE1\x8F\x86", + "\xEA\xAE\x97" => "\xE1\x8F\x87", + "\xEA\xAE\x98" => "\xE1\x8F\x88", + "\xEA\xAE\x99" => "\xE1\x8F\x89", + "\xEA\xAE\x9A" => "\xE1\x8F\x8A", + "\xEA\xAE\x9B" => "\xE1\x8F\x8B", + "\xEA\xAE\x9C" => "\xE1\x8F\x8C", + "\xEA\xAE\x9D" => "\xE1\x8F\x8D", + "\xEA\xAE\x9E" => "\xE1\x8F\x8E", + "\xEA\xAE\x9F" => "\xE1\x8F\x8F", + "\xEA\xAE\xA0" => "\xE1\x8F\x90", + "\xEA\xAE\xA1" => "\xE1\x8F\x91", + "\xEA\xAE\xA2" => "\xE1\x8F\x92", + "\xEA\xAE\xA3" => "\xE1\x8F\x93", + "\xEA\xAE\xA4" => "\xE1\x8F\x94", + "\xEA\xAE\xA5" => "\xE1\x8F\x95", + "\xEA\xAE\xA6" => "\xE1\x8F\x96", + "\xEA\xAE\xA7" => "\xE1\x8F\x97", + "\xEA\xAE\xA8" => "\xE1\x8F\x98", + "\xEA\xAE\xA9" => "\xE1\x8F\x99", + "\xEA\xAE\xAA" => "\xE1\x8F\x9A", + "\xEA\xAE\xAB" => "\xE1\x8F\x9B", + "\xEA\xAE\xAC" => "\xE1\x8F\x9C", + "\xEA\xAE\xAD" => "\xE1\x8F\x9D", + "\xEA\xAE\xAE" => "\xE1\x8F\x9E", + "\xEA\xAE\xAF" => "\xE1\x8F\x9F", + "\xEA\xAE\xB0" => "\xE1\x8F\xA0", + "\xEA\xAE\xB1" => "\xE1\x8F\xA1", + "\xEA\xAE\xB2" => "\xE1\x8F\xA2", + "\xEA\xAE\xB3" => "\xE1\x8F\xA3", + "\xEA\xAE\xB4" => "\xE1\x8F\xA4", + "\xEA\xAE\xB5" => "\xE1\x8F\xA5", + "\xEA\xAE\xB6" => "\xE1\x8F\xA6", + "\xEA\xAE\xB7" => "\xE1\x8F\xA7", + "\xEA\xAE\xB8" => "\xE1\x8F\xA8", + "\xEA\xAE\xB9" => "\xE1\x8F\xA9", + "\xEA\xAE\xBA" => "\xE1\x8F\xAA", + "\xEA\xAE\xBB" => "\xE1\x8F\xAB", + "\xEA\xAE\xBC" => "\xE1\x8F\xAC", + "\xEA\xAE\xBD" => "\xE1\x8F\xAD", + "\xEA\xAE\xBE" => "\xE1\x8F\xAE", + "\xEA\xAE\xBF" => "\xE1\x8F\xAF", + "\xEF\xAC\x80" => "\x66\x66", + "\xEF\xAC\x81" => "\x66\x69", + "\xEF\xAC\x82" => "\x66\x6C", + "\xEF\xAC\x83" => "\x66\x66\x69", + "\xEF\xAC\x84" => "\x66\x66\x6C", + "\xEF\xAC\x85" => "\x73\x74", + "\xEF\xAC\x86" => "\x73\x74", + "\xEF\xAC\x93" => "\xD5\xB4\xD5\xB6", + "\xEF\xAC\x94" => "\xD5\xB4\xD5\xA5", + "\xEF\xAC\x95" => "\xD5\xB4\xD5\xAB", + "\xEF\xAC\x96" => "\xD5\xBE\xD5\xB6", + "\xEF\xAC\x97" => "\xD5\xB4\xD5\xAD", + "\xEF\xBC\xA1" => "\xEF\xBD\x81", + "\xEF\xBC\xA2" => "\xEF\xBD\x82", + "\xEF\xBC\xA3" => "\xEF\xBD\x83", + "\xEF\xBC\xA4" => "\xEF\xBD\x84", + "\xEF\xBC\xA5" => "\xEF\xBD\x85", + "\xEF\xBC\xA6" => "\xEF\xBD\x86", + "\xEF\xBC\xA7" => "\xEF\xBD\x87", + "\xEF\xBC\xA8" => "\xEF\xBD\x88", + "\xEF\xBC\xA9" => "\xEF\xBD\x89", + "\xEF\xBC\xAA" => "\xEF\xBD\x8A", + "\xEF\xBC\xAB" => "\xEF\xBD\x8B", + "\xEF\xBC\xAC" => "\xEF\xBD\x8C", + "\xEF\xBC\xAD" => "\xEF\xBD\x8D", + "\xEF\xBC\xAE" => "\xEF\xBD\x8E", + "\xEF\xBC\xAF" => "\xEF\xBD\x8F", + "\xEF\xBC\xB0" => "\xEF\xBD\x90", + "\xEF\xBC\xB1" => "\xEF\xBD\x91", + "\xEF\xBC\xB2" => "\xEF\xBD\x92", + "\xEF\xBC\xB3" => "\xEF\xBD\x93", + "\xEF\xBC\xB4" => "\xEF\xBD\x94", + "\xEF\xBC\xB5" => "\xEF\xBD\x95", + "\xEF\xBC\xB6" => "\xEF\xBD\x96", + "\xEF\xBC\xB7" => "\xEF\xBD\x97", + "\xEF\xBC\xB8" => "\xEF\xBD\x98", + "\xEF\xBC\xB9" => "\xEF\xBD\x99", + "\xEF\xBC\xBA" => "\xEF\xBD\x9A", + "\xF0\x90\x90\x80" => "\xF0\x90\x90\xA8", + "\xF0\x90\x90\x81" => "\xF0\x90\x90\xA9", + "\xF0\x90\x90\x82" => "\xF0\x90\x90\xAA", + "\xF0\x90\x90\x83" => "\xF0\x90\x90\xAB", + "\xF0\x90\x90\x84" => "\xF0\x90\x90\xAC", + "\xF0\x90\x90\x85" => "\xF0\x90\x90\xAD", + "\xF0\x90\x90\x86" => "\xF0\x90\x90\xAE", + "\xF0\x90\x90\x87" => "\xF0\x90\x90\xAF", + "\xF0\x90\x90\x88" => "\xF0\x90\x90\xB0", + "\xF0\x90\x90\x89" => "\xF0\x90\x90\xB1", + "\xF0\x90\x90\x8A" => "\xF0\x90\x90\xB2", + "\xF0\x90\x90\x8B" => "\xF0\x90\x90\xB3", + "\xF0\x90\x90\x8C" => "\xF0\x90\x90\xB4", + "\xF0\x90\x90\x8D" => "\xF0\x90\x90\xB5", + "\xF0\x90\x90\x8E" => "\xF0\x90\x90\xB6", + "\xF0\x90\x90\x8F" => "\xF0\x90\x90\xB7", + "\xF0\x90\x90\x90" => "\xF0\x90\x90\xB8", + "\xF0\x90\x90\x91" => "\xF0\x90\x90\xB9", + "\xF0\x90\x90\x92" => "\xF0\x90\x90\xBA", + "\xF0\x90\x90\x93" => "\xF0\x90\x90\xBB", + "\xF0\x90\x90\x94" => "\xF0\x90\x90\xBC", + "\xF0\x90\x90\x95" => "\xF0\x90\x90\xBD", + "\xF0\x90\x90\x96" => "\xF0\x90\x90\xBE", + "\xF0\x90\x90\x97" => "\xF0\x90\x90\xBF", + "\xF0\x90\x90\x98" => "\xF0\x90\x91\x80", + "\xF0\x90\x90\x99" => "\xF0\x90\x91\x81", + "\xF0\x90\x90\x9A" => "\xF0\x90\x91\x82", + "\xF0\x90\x90\x9B" => "\xF0\x90\x91\x83", + "\xF0\x90\x90\x9C" => "\xF0\x90\x91\x84", + "\xF0\x90\x90\x9D" => "\xF0\x90\x91\x85", + "\xF0\x90\x90\x9E" => "\xF0\x90\x91\x86", + "\xF0\x90\x90\x9F" => "\xF0\x90\x91\x87", + "\xF0\x90\x90\xA0" => "\xF0\x90\x91\x88", + "\xF0\x90\x90\xA1" => "\xF0\x90\x91\x89", + "\xF0\x90\x90\xA2" => "\xF0\x90\x91\x8A", + "\xF0\x90\x90\xA3" => "\xF0\x90\x91\x8B", + "\xF0\x90\x90\xA4" => "\xF0\x90\x91\x8C", + "\xF0\x90\x90\xA5" => "\xF0\x90\x91\x8D", + "\xF0\x90\x90\xA6" => "\xF0\x90\x91\x8E", + "\xF0\x90\x90\xA7" => "\xF0\x90\x91\x8F", + "\xF0\x90\x92\xB0" => "\xF0\x90\x93\x98", + "\xF0\x90\x92\xB1" => "\xF0\x90\x93\x99", + "\xF0\x90\x92\xB2" => "\xF0\x90\x93\x9A", + "\xF0\x90\x92\xB3" => "\xF0\x90\x93\x9B", + "\xF0\x90\x92\xB4" => "\xF0\x90\x93\x9C", + "\xF0\x90\x92\xB5" => "\xF0\x90\x93\x9D", + "\xF0\x90\x92\xB6" => "\xF0\x90\x93\x9E", + "\xF0\x90\x92\xB7" => "\xF0\x90\x93\x9F", + "\xF0\x90\x92\xB8" => "\xF0\x90\x93\xA0", + "\xF0\x90\x92\xB9" => "\xF0\x90\x93\xA1", + "\xF0\x90\x92\xBA" => "\xF0\x90\x93\xA2", + "\xF0\x90\x92\xBB" => "\xF0\x90\x93\xA3", + "\xF0\x90\x92\xBC" => "\xF0\x90\x93\xA4", + "\xF0\x90\x92\xBD" => "\xF0\x90\x93\xA5", + "\xF0\x90\x92\xBE" => "\xF0\x90\x93\xA6", + "\xF0\x90\x92\xBF" => "\xF0\x90\x93\xA7", + "\xF0\x90\x93\x80" => "\xF0\x90\x93\xA8", + "\xF0\x90\x93\x81" => "\xF0\x90\x93\xA9", + "\xF0\x90\x93\x82" => "\xF0\x90\x93\xAA", + "\xF0\x90\x93\x83" => "\xF0\x90\x93\xAB", + "\xF0\x90\x93\x84" => "\xF0\x90\x93\xAC", + "\xF0\x90\x93\x85" => "\xF0\x90\x93\xAD", + "\xF0\x90\x93\x86" => "\xF0\x90\x93\xAE", + "\xF0\x90\x93\x87" => "\xF0\x90\x93\xAF", + "\xF0\x90\x93\x88" => "\xF0\x90\x93\xB0", + "\xF0\x90\x93\x89" => "\xF0\x90\x93\xB1", + "\xF0\x90\x93\x8A" => "\xF0\x90\x93\xB2", + "\xF0\x90\x93\x8B" => "\xF0\x90\x93\xB3", + "\xF0\x90\x93\x8C" => "\xF0\x90\x93\xB4", + "\xF0\x90\x93\x8D" => "\xF0\x90\x93\xB5", + "\xF0\x90\x93\x8E" => "\xF0\x90\x93\xB6", + "\xF0\x90\x93\x8F" => "\xF0\x90\x93\xB7", + "\xF0\x90\x93\x90" => "\xF0\x90\x93\xB8", + "\xF0\x90\x93\x91" => "\xF0\x90\x93\xB9", + "\xF0\x90\x93\x92" => "\xF0\x90\x93\xBA", + "\xF0\x90\x93\x93" => "\xF0\x90\x93\xBB", + "\xF0\x90\x95\xB0" => "\xF0\x90\x96\x97", + "\xF0\x90\x95\xB1" => "\xF0\x90\x96\x98", + "\xF0\x90\x95\xB2" => "\xF0\x90\x96\x99", + "\xF0\x90\x95\xB3" => "\xF0\x90\x96\x9A", + "\xF0\x90\x95\xB4" => "\xF0\x90\x96\x9B", + "\xF0\x90\x95\xB5" => "\xF0\x90\x96\x9C", + "\xF0\x90\x95\xB6" => "\xF0\x90\x96\x9D", + "\xF0\x90\x95\xB7" => "\xF0\x90\x96\x9E", + "\xF0\x90\x95\xB8" => "\xF0\x90\x96\x9F", + "\xF0\x90\x95\xB9" => "\xF0\x90\x96\xA0", + "\xF0\x90\x95\xBA" => "\xF0\x90\x96\xA1", + "\xF0\x90\x95\xBC" => "\xF0\x90\x96\xA3", + "\xF0\x90\x95\xBD" => "\xF0\x90\x96\xA4", + "\xF0\x90\x95\xBE" => "\xF0\x90\x96\xA5", + "\xF0\x90\x95\xBF" => "\xF0\x90\x96\xA6", + "\xF0\x90\x96\x80" => "\xF0\x90\x96\xA7", + "\xF0\x90\x96\x81" => "\xF0\x90\x96\xA8", + "\xF0\x90\x96\x82" => "\xF0\x90\x96\xA9", + "\xF0\x90\x96\x83" => "\xF0\x90\x96\xAA", + "\xF0\x90\x96\x84" => "\xF0\x90\x96\xAB", + "\xF0\x90\x96\x85" => "\xF0\x90\x96\xAC", + "\xF0\x90\x96\x86" => "\xF0\x90\x96\xAD", + "\xF0\x90\x96\x87" => "\xF0\x90\x96\xAE", + "\xF0\x90\x96\x88" => "\xF0\x90\x96\xAF", + "\xF0\x90\x96\x89" => "\xF0\x90\x96\xB0", + "\xF0\x90\x96\x8A" => "\xF0\x90\x96\xB1", + "\xF0\x90\x96\x8C" => "\xF0\x90\x96\xB3", + "\xF0\x90\x96\x8D" => "\xF0\x90\x96\xB4", + "\xF0\x90\x96\x8E" => "\xF0\x90\x96\xB5", + "\xF0\x90\x96\x8F" => "\xF0\x90\x96\xB6", + "\xF0\x90\x96\x90" => "\xF0\x90\x96\xB7", + "\xF0\x90\x96\x91" => "\xF0\x90\x96\xB8", + "\xF0\x90\x96\x92" => "\xF0\x90\x96\xB9", + "\xF0\x90\x96\x94" => "\xF0\x90\x96\xBB", + "\xF0\x90\x96\x95" => "\xF0\x90\x96\xBC", + "\xF0\x90\xB2\x80" => "\xF0\x90\xB3\x80", + "\xF0\x90\xB2\x81" => "\xF0\x90\xB3\x81", + "\xF0\x90\xB2\x82" => "\xF0\x90\xB3\x82", + "\xF0\x90\xB2\x83" => "\xF0\x90\xB3\x83", + "\xF0\x90\xB2\x84" => "\xF0\x90\xB3\x84", + "\xF0\x90\xB2\x85" => "\xF0\x90\xB3\x85", + "\xF0\x90\xB2\x86" => "\xF0\x90\xB3\x86", + "\xF0\x90\xB2\x87" => "\xF0\x90\xB3\x87", + "\xF0\x90\xB2\x88" => "\xF0\x90\xB3\x88", + "\xF0\x90\xB2\x89" => "\xF0\x90\xB3\x89", + "\xF0\x90\xB2\x8A" => "\xF0\x90\xB3\x8A", + "\xF0\x90\xB2\x8B" => "\xF0\x90\xB3\x8B", + "\xF0\x90\xB2\x8C" => "\xF0\x90\xB3\x8C", + "\xF0\x90\xB2\x8D" => "\xF0\x90\xB3\x8D", + "\xF0\x90\xB2\x8E" => "\xF0\x90\xB3\x8E", + "\xF0\x90\xB2\x8F" => "\xF0\x90\xB3\x8F", + "\xF0\x90\xB2\x90" => "\xF0\x90\xB3\x90", + "\xF0\x90\xB2\x91" => "\xF0\x90\xB3\x91", + "\xF0\x90\xB2\x92" => "\xF0\x90\xB3\x92", + "\xF0\x90\xB2\x93" => "\xF0\x90\xB3\x93", + "\xF0\x90\xB2\x94" => "\xF0\x90\xB3\x94", + "\xF0\x90\xB2\x95" => "\xF0\x90\xB3\x95", + "\xF0\x90\xB2\x96" => "\xF0\x90\xB3\x96", + "\xF0\x90\xB2\x97" => "\xF0\x90\xB3\x97", + "\xF0\x90\xB2\x98" => "\xF0\x90\xB3\x98", + "\xF0\x90\xB2\x99" => "\xF0\x90\xB3\x99", + "\xF0\x90\xB2\x9A" => "\xF0\x90\xB3\x9A", + "\xF0\x90\xB2\x9B" => "\xF0\x90\xB3\x9B", + "\xF0\x90\xB2\x9C" => "\xF0\x90\xB3\x9C", + "\xF0\x90\xB2\x9D" => "\xF0\x90\xB3\x9D", + "\xF0\x90\xB2\x9E" => "\xF0\x90\xB3\x9E", + "\xF0\x90\xB2\x9F" => "\xF0\x90\xB3\x9F", + "\xF0\x90\xB2\xA0" => "\xF0\x90\xB3\xA0", + "\xF0\x90\xB2\xA1" => "\xF0\x90\xB3\xA1", + "\xF0\x90\xB2\xA2" => "\xF0\x90\xB3\xA2", + "\xF0\x90\xB2\xA3" => "\xF0\x90\xB3\xA3", + "\xF0\x90\xB2\xA4" => "\xF0\x90\xB3\xA4", + "\xF0\x90\xB2\xA5" => "\xF0\x90\xB3\xA5", + "\xF0\x90\xB2\xA6" => "\xF0\x90\xB3\xA6", + "\xF0\x90\xB2\xA7" => "\xF0\x90\xB3\xA7", + "\xF0\x90\xB2\xA8" => "\xF0\x90\xB3\xA8", + "\xF0\x90\xB2\xA9" => "\xF0\x90\xB3\xA9", + "\xF0\x90\xB2\xAA" => "\xF0\x90\xB3\xAA", + "\xF0\x90\xB2\xAB" => "\xF0\x90\xB3\xAB", + "\xF0\x90\xB2\xAC" => "\xF0\x90\xB3\xAC", + "\xF0\x90\xB2\xAD" => "\xF0\x90\xB3\xAD", + "\xF0\x90\xB2\xAE" => "\xF0\x90\xB3\xAE", + "\xF0\x90\xB2\xAF" => "\xF0\x90\xB3\xAF", + "\xF0\x90\xB2\xB0" => "\xF0\x90\xB3\xB0", + "\xF0\x90\xB2\xB1" => "\xF0\x90\xB3\xB1", + "\xF0\x90\xB2\xB2" => "\xF0\x90\xB3\xB2", + "\xF0\x91\xA2\xA0" => "\xF0\x91\xA3\x80", + "\xF0\x91\xA2\xA1" => "\xF0\x91\xA3\x81", + "\xF0\x91\xA2\xA2" => "\xF0\x91\xA3\x82", + "\xF0\x91\xA2\xA3" => "\xF0\x91\xA3\x83", + "\xF0\x91\xA2\xA4" => "\xF0\x91\xA3\x84", + "\xF0\x91\xA2\xA5" => "\xF0\x91\xA3\x85", + "\xF0\x91\xA2\xA6" => "\xF0\x91\xA3\x86", + "\xF0\x91\xA2\xA7" => "\xF0\x91\xA3\x87", + "\xF0\x91\xA2\xA8" => "\xF0\x91\xA3\x88", + "\xF0\x91\xA2\xA9" => "\xF0\x91\xA3\x89", + "\xF0\x91\xA2\xAA" => "\xF0\x91\xA3\x8A", + "\xF0\x91\xA2\xAB" => "\xF0\x91\xA3\x8B", + "\xF0\x91\xA2\xAC" => "\xF0\x91\xA3\x8C", + "\xF0\x91\xA2\xAD" => "\xF0\x91\xA3\x8D", + "\xF0\x91\xA2\xAE" => "\xF0\x91\xA3\x8E", + "\xF0\x91\xA2\xAF" => "\xF0\x91\xA3\x8F", + "\xF0\x91\xA2\xB0" => "\xF0\x91\xA3\x90", + "\xF0\x91\xA2\xB1" => "\xF0\x91\xA3\x91", + "\xF0\x91\xA2\xB2" => "\xF0\x91\xA3\x92", + "\xF0\x91\xA2\xB3" => "\xF0\x91\xA3\x93", + "\xF0\x91\xA2\xB4" => "\xF0\x91\xA3\x94", + "\xF0\x91\xA2\xB5" => "\xF0\x91\xA3\x95", + "\xF0\x91\xA2\xB6" => "\xF0\x91\xA3\x96", + "\xF0\x91\xA2\xB7" => "\xF0\x91\xA3\x97", + "\xF0\x91\xA2\xB8" => "\xF0\x91\xA3\x98", + "\xF0\x91\xA2\xB9" => "\xF0\x91\xA3\x99", + "\xF0\x91\xA2\xBA" => "\xF0\x91\xA3\x9A", + "\xF0\x91\xA2\xBB" => "\xF0\x91\xA3\x9B", + "\xF0\x91\xA2\xBC" => "\xF0\x91\xA3\x9C", + "\xF0\x91\xA2\xBD" => "\xF0\x91\xA3\x9D", + "\xF0\x91\xA2\xBE" => "\xF0\x91\xA3\x9E", + "\xF0\x91\xA2\xBF" => "\xF0\x91\xA3\x9F", + "\xF0\x96\xB9\x80" => "\xF0\x96\xB9\xA0", + "\xF0\x96\xB9\x81" => "\xF0\x96\xB9\xA1", + "\xF0\x96\xB9\x82" => "\xF0\x96\xB9\xA2", + "\xF0\x96\xB9\x83" => "\xF0\x96\xB9\xA3", + "\xF0\x96\xB9\x84" => "\xF0\x96\xB9\xA4", + "\xF0\x96\xB9\x85" => "\xF0\x96\xB9\xA5", + "\xF0\x96\xB9\x86" => "\xF0\x96\xB9\xA6", + "\xF0\x96\xB9\x87" => "\xF0\x96\xB9\xA7", + "\xF0\x96\xB9\x88" => "\xF0\x96\xB9\xA8", + "\xF0\x96\xB9\x89" => "\xF0\x96\xB9\xA9", + "\xF0\x96\xB9\x8A" => "\xF0\x96\xB9\xAA", + "\xF0\x96\xB9\x8B" => "\xF0\x96\xB9\xAB", + "\xF0\x96\xB9\x8C" => "\xF0\x96\xB9\xAC", + "\xF0\x96\xB9\x8D" => "\xF0\x96\xB9\xAD", + "\xF0\x96\xB9\x8E" => "\xF0\x96\xB9\xAE", + "\xF0\x96\xB9\x8F" => "\xF0\x96\xB9\xAF", + "\xF0\x96\xB9\x90" => "\xF0\x96\xB9\xB0", + "\xF0\x96\xB9\x91" => "\xF0\x96\xB9\xB1", + "\xF0\x96\xB9\x92" => "\xF0\x96\xB9\xB2", + "\xF0\x96\xB9\x93" => "\xF0\x96\xB9\xB3", + "\xF0\x96\xB9\x94" => "\xF0\x96\xB9\xB4", + "\xF0\x96\xB9\x95" => "\xF0\x96\xB9\xB5", + "\xF0\x96\xB9\x96" => "\xF0\x96\xB9\xB6", + "\xF0\x96\xB9\x97" => "\xF0\x96\xB9\xB7", + "\xF0\x96\xB9\x98" => "\xF0\x96\xB9\xB8", + "\xF0\x96\xB9\x99" => "\xF0\x96\xB9\xB9", + "\xF0\x96\xB9\x9A" => "\xF0\x96\xB9\xBA", + "\xF0\x96\xB9\x9B" => "\xF0\x96\xB9\xBB", + "\xF0\x96\xB9\x9C" => "\xF0\x96\xB9\xBC", + "\xF0\x96\xB9\x9D" => "\xF0\x96\xB9\xBD", + "\xF0\x96\xB9\x9E" => "\xF0\x96\xB9\xBE", + "\xF0\x96\xB9\x9F" => "\xF0\x96\xB9\xBF", + "\xF0\x9E\xA4\x80" => "\xF0\x9E\xA4\xA2", + "\xF0\x9E\xA4\x81" => "\xF0\x9E\xA4\xA3", + "\xF0\x9E\xA4\x82" => "\xF0\x9E\xA4\xA4", + "\xF0\x9E\xA4\x83" => "\xF0\x9E\xA4\xA5", + "\xF0\x9E\xA4\x84" => "\xF0\x9E\xA4\xA6", + "\xF0\x9E\xA4\x85" => "\xF0\x9E\xA4\xA7", + "\xF0\x9E\xA4\x86" => "\xF0\x9E\xA4\xA8", + "\xF0\x9E\xA4\x87" => "\xF0\x9E\xA4\xA9", + "\xF0\x9E\xA4\x88" => "\xF0\x9E\xA4\xAA", + "\xF0\x9E\xA4\x89" => "\xF0\x9E\xA4\xAB", + "\xF0\x9E\xA4\x8A" => "\xF0\x9E\xA4\xAC", + "\xF0\x9E\xA4\x8B" => "\xF0\x9E\xA4\xAD", + "\xF0\x9E\xA4\x8C" => "\xF0\x9E\xA4\xAE", + "\xF0\x9E\xA4\x8D" => "\xF0\x9E\xA4\xAF", + "\xF0\x9E\xA4\x8E" => "\xF0\x9E\xA4\xB0", + "\xF0\x9E\xA4\x8F" => "\xF0\x9E\xA4\xB1", + "\xF0\x9E\xA4\x90" => "\xF0\x9E\xA4\xB2", + "\xF0\x9E\xA4\x91" => "\xF0\x9E\xA4\xB3", + "\xF0\x9E\xA4\x92" => "\xF0\x9E\xA4\xB4", + "\xF0\x9E\xA4\x93" => "\xF0\x9E\xA4\xB5", + "\xF0\x9E\xA4\x94" => "\xF0\x9E\xA4\xB6", + "\xF0\x9E\xA4\x95" => "\xF0\x9E\xA4\xB7", + "\xF0\x9E\xA4\x96" => "\xF0\x9E\xA4\xB8", + "\xF0\x9E\xA4\x97" => "\xF0\x9E\xA4\xB9", + "\xF0\x9E\xA4\x98" => "\xF0\x9E\xA4\xBA", + "\xF0\x9E\xA4\x99" => "\xF0\x9E\xA4\xBB", + "\xF0\x9E\xA4\x9A" => "\xF0\x9E\xA4\xBC", + "\xF0\x9E\xA4\x9B" => "\xF0\x9E\xA4\xBD", + "\xF0\x9E\xA4\x9C" => "\xF0\x9E\xA4\xBE", + "\xF0\x9E\xA4\x9D" => "\xF0\x9E\xA4\xBF", + "\xF0\x9E\xA4\x9E" => "\xF0\x9E\xA5\x80", + "\xF0\x9E\xA4\x9F" => "\xF0\x9E\xA5\x81", + "\xF0\x9E\xA4\xA0" => "\xF0\x9E\xA5\x82", + "\xF0\x9E\xA4\xA1" => "\xF0\x9E\xA5\x83", + ); +} + +?> \ No newline at end of file diff --git a/Sources/Unicode/CaseLower.php b/Sources/Unicode/CaseLower.php new file mode 100644 index 0000000..6d22c9f --- /dev/null +++ b/Sources/Unicode/CaseLower.php @@ -0,0 +1,2986 @@ + "\x61", + "\x42" => "\x62", + "\x43" => "\x63", + "\x44" => "\x64", + "\x45" => "\x65", + "\x46" => "\x66", + "\x47" => "\x67", + "\x48" => "\x68", + "\x49" => "\x69", + "\x4A" => "\x6A", + "\x4B" => "\x6B", + "\x4C" => "\x6C", + "\x4D" => "\x6D", + "\x4E" => "\x6E", + "\x4F" => "\x6F", + "\x50" => "\x70", + "\x51" => "\x71", + "\x52" => "\x72", + "\x53" => "\x73", + "\x54" => "\x74", + "\x55" => "\x75", + "\x56" => "\x76", + "\x57" => "\x77", + "\x58" => "\x78", + "\x59" => "\x79", + "\x5A" => "\x7A", + "\xC3\x80" => "\xC3\xA0", + "\xC3\x81" => "\xC3\xA1", + "\xC3\x82" => "\xC3\xA2", + "\xC3\x83" => "\xC3\xA3", + "\xC3\x84" => "\xC3\xA4", + "\xC3\x85" => "\xC3\xA5", + "\xC3\x86" => "\xC3\xA6", + "\xC3\x87" => "\xC3\xA7", + "\xC3\x88" => "\xC3\xA8", + "\xC3\x89" => "\xC3\xA9", + "\xC3\x8A" => "\xC3\xAA", + "\xC3\x8B" => "\xC3\xAB", + "\xC3\x8C" => "\xC3\xAC", + "\xC3\x8D" => "\xC3\xAD", + "\xC3\x8E" => "\xC3\xAE", + "\xC3\x8F" => "\xC3\xAF", + "\xC3\x90" => "\xC3\xB0", + "\xC3\x91" => "\xC3\xB1", + "\xC3\x92" => "\xC3\xB2", + "\xC3\x93" => "\xC3\xB3", + "\xC3\x94" => "\xC3\xB4", + "\xC3\x95" => "\xC3\xB5", + "\xC3\x96" => "\xC3\xB6", + "\xC3\x98" => "\xC3\xB8", + "\xC3\x99" => "\xC3\xB9", + "\xC3\x9A" => "\xC3\xBA", + "\xC3\x9B" => "\xC3\xBB", + "\xC3\x9C" => "\xC3\xBC", + "\xC3\x9D" => "\xC3\xBD", + "\xC3\x9E" => "\xC3\xBE", + "\xC4\x80" => "\xC4\x81", + "\xC4\x82" => "\xC4\x83", + "\xC4\x84" => "\xC4\x85", + "\xC4\x86" => "\xC4\x87", + "\xC4\x88" => "\xC4\x89", + "\xC4\x8A" => "\xC4\x8B", + "\xC4\x8C" => "\xC4\x8D", + "\xC4\x8E" => "\xC4\x8F", + "\xC4\x90" => "\xC4\x91", + "\xC4\x92" => "\xC4\x93", + "\xC4\x94" => "\xC4\x95", + "\xC4\x96" => "\xC4\x97", + "\xC4\x98" => "\xC4\x99", + "\xC4\x9A" => "\xC4\x9B", + "\xC4\x9C" => "\xC4\x9D", + "\xC4\x9E" => "\xC4\x9F", + "\xC4\xA0" => "\xC4\xA1", + "\xC4\xA2" => "\xC4\xA3", + "\xC4\xA4" => "\xC4\xA5", + "\xC4\xA6" => "\xC4\xA7", + "\xC4\xA8" => "\xC4\xA9", + "\xC4\xAA" => "\xC4\xAB", + "\xC4\xAC" => "\xC4\xAD", + "\xC4\xAE" => "\xC4\xAF", + "\xC4\xB0" => "\x69", + "\xC4\xB2" => "\xC4\xB3", + "\xC4\xB4" => "\xC4\xB5", + "\xC4\xB6" => "\xC4\xB7", + "\xC4\xB9" => "\xC4\xBA", + "\xC4\xBB" => "\xC4\xBC", + "\xC4\xBD" => "\xC4\xBE", + "\xC4\xBF" => "\xC5\x80", + "\xC5\x81" => "\xC5\x82", + "\xC5\x83" => "\xC5\x84", + "\xC5\x85" => "\xC5\x86", + "\xC5\x87" => "\xC5\x88", + "\xC5\x8A" => "\xC5\x8B", + "\xC5\x8C" => "\xC5\x8D", + "\xC5\x8E" => "\xC5\x8F", + "\xC5\x90" => "\xC5\x91", + "\xC5\x92" => "\xC5\x93", + "\xC5\x94" => "\xC5\x95", + "\xC5\x96" => "\xC5\x97", + "\xC5\x98" => "\xC5\x99", + "\xC5\x9A" => "\xC5\x9B", + "\xC5\x9C" => "\xC5\x9D", + "\xC5\x9E" => "\xC5\x9F", + "\xC5\xA0" => "\xC5\xA1", + "\xC5\xA2" => "\xC5\xA3", + "\xC5\xA4" => "\xC5\xA5", + "\xC5\xA6" => "\xC5\xA7", + "\xC5\xA8" => "\xC5\xA9", + "\xC5\xAA" => "\xC5\xAB", + "\xC5\xAC" => "\xC5\xAD", + "\xC5\xAE" => "\xC5\xAF", + "\xC5\xB0" => "\xC5\xB1", + "\xC5\xB2" => "\xC5\xB3", + "\xC5\xB4" => "\xC5\xB5", + "\xC5\xB6" => "\xC5\xB7", + "\xC5\xB8" => "\xC3\xBF", + "\xC5\xB9" => "\xC5\xBA", + "\xC5\xBB" => "\xC5\xBC", + "\xC5\xBD" => "\xC5\xBE", + "\xC6\x81" => "\xC9\x93", + "\xC6\x82" => "\xC6\x83", + "\xC6\x84" => "\xC6\x85", + "\xC6\x86" => "\xC9\x94", + "\xC6\x87" => "\xC6\x88", + "\xC6\x89" => "\xC9\x96", + "\xC6\x8A" => "\xC9\x97", + "\xC6\x8B" => "\xC6\x8C", + "\xC6\x8E" => "\xC7\x9D", + "\xC6\x8F" => "\xC9\x99", + "\xC6\x90" => "\xC9\x9B", + "\xC6\x91" => "\xC6\x92", + "\xC6\x93" => "\xC9\xA0", + "\xC6\x94" => "\xC9\xA3", + "\xC6\x96" => "\xC9\xA9", + "\xC6\x97" => "\xC9\xA8", + "\xC6\x98" => "\xC6\x99", + "\xC6\x9C" => "\xC9\xAF", + "\xC6\x9D" => "\xC9\xB2", + "\xC6\x9F" => "\xC9\xB5", + "\xC6\xA0" => "\xC6\xA1", + "\xC6\xA2" => "\xC6\xA3", + "\xC6\xA4" => "\xC6\xA5", + "\xC6\xA6" => "\xCA\x80", + "\xC6\xA7" => "\xC6\xA8", + "\xC6\xA9" => "\xCA\x83", + "\xC6\xAC" => "\xC6\xAD", + "\xC6\xAE" => "\xCA\x88", + "\xC6\xAF" => "\xC6\xB0", + "\xC6\xB1" => "\xCA\x8A", + "\xC6\xB2" => "\xCA\x8B", + "\xC6\xB3" => "\xC6\xB4", + "\xC6\xB5" => "\xC6\xB6", + "\xC6\xB7" => "\xCA\x92", + "\xC6\xB8" => "\xC6\xB9", + "\xC6\xBC" => "\xC6\xBD", + "\xC7\x84" => "\xC7\x86", + "\xC7\x85" => "\xC7\x86", + "\xC7\x87" => "\xC7\x89", + "\xC7\x88" => "\xC7\x89", + "\xC7\x8A" => "\xC7\x8C", + "\xC7\x8B" => "\xC7\x8C", + "\xC7\x8D" => "\xC7\x8E", + "\xC7\x8F" => "\xC7\x90", + "\xC7\x91" => "\xC7\x92", + "\xC7\x93" => "\xC7\x94", + "\xC7\x95" => "\xC7\x96", + "\xC7\x97" => "\xC7\x98", + "\xC7\x99" => "\xC7\x9A", + "\xC7\x9B" => "\xC7\x9C", + "\xC7\x9E" => "\xC7\x9F", + "\xC7\xA0" => "\xC7\xA1", + "\xC7\xA2" => "\xC7\xA3", + "\xC7\xA4" => "\xC7\xA5", + "\xC7\xA6" => "\xC7\xA7", + "\xC7\xA8" => "\xC7\xA9", + "\xC7\xAA" => "\xC7\xAB", + "\xC7\xAC" => "\xC7\xAD", + "\xC7\xAE" => "\xC7\xAF", + "\xC7\xB1" => "\xC7\xB3", + "\xC7\xB2" => "\xC7\xB3", + "\xC7\xB4" => "\xC7\xB5", + "\xC7\xB6" => "\xC6\x95", + "\xC7\xB7" => "\xC6\xBF", + "\xC7\xB8" => "\xC7\xB9", + "\xC7\xBA" => "\xC7\xBB", + "\xC7\xBC" => "\xC7\xBD", + "\xC7\xBE" => "\xC7\xBF", + "\xC8\x80" => "\xC8\x81", + "\xC8\x82" => "\xC8\x83", + "\xC8\x84" => "\xC8\x85", + "\xC8\x86" => "\xC8\x87", + "\xC8\x88" => "\xC8\x89", + "\xC8\x8A" => "\xC8\x8B", + "\xC8\x8C" => "\xC8\x8D", + "\xC8\x8E" => "\xC8\x8F", + "\xC8\x90" => "\xC8\x91", + "\xC8\x92" => "\xC8\x93", + "\xC8\x94" => "\xC8\x95", + "\xC8\x96" => "\xC8\x97", + "\xC8\x98" => "\xC8\x99", + "\xC8\x9A" => "\xC8\x9B", + "\xC8\x9C" => "\xC8\x9D", + "\xC8\x9E" => "\xC8\x9F", + "\xC8\xA0" => "\xC6\x9E", + "\xC8\xA2" => "\xC8\xA3", + "\xC8\xA4" => "\xC8\xA5", + "\xC8\xA6" => "\xC8\xA7", + "\xC8\xA8" => "\xC8\xA9", + "\xC8\xAA" => "\xC8\xAB", + "\xC8\xAC" => "\xC8\xAD", + "\xC8\xAE" => "\xC8\xAF", + "\xC8\xB0" => "\xC8\xB1", + "\xC8\xB2" => "\xC8\xB3", + "\xC8\xBA" => "\xE2\xB1\xA5", + "\xC8\xBB" => "\xC8\xBC", + "\xC8\xBD" => "\xC6\x9A", + "\xC8\xBE" => "\xE2\xB1\xA6", + "\xC9\x81" => "\xC9\x82", + "\xC9\x83" => "\xC6\x80", + "\xC9\x84" => "\xCA\x89", + "\xC9\x85" => "\xCA\x8C", + "\xC9\x86" => "\xC9\x87", + "\xC9\x88" => "\xC9\x89", + "\xC9\x8A" => "\xC9\x8B", + "\xC9\x8C" => "\xC9\x8D", + "\xC9\x8E" => "\xC9\x8F", + "\xCD\xB0" => "\xCD\xB1", + "\xCD\xB2" => "\xCD\xB3", + "\xCD\xB6" => "\xCD\xB7", + "\xCD\xBF" => "\xCF\xB3", + "\xCE\x86" => "\xCE\xAC", + "\xCE\x88" => "\xCE\xAD", + "\xCE\x89" => "\xCE\xAE", + "\xCE\x8A" => "\xCE\xAF", + "\xCE\x8C" => "\xCF\x8C", + "\xCE\x8E" => "\xCF\x8D", + "\xCE\x8F" => "\xCF\x8E", + "\xCE\x91" => "\xCE\xB1", + "\xCE\x92" => "\xCE\xB2", + "\xCE\x93" => "\xCE\xB3", + "\xCE\x94" => "\xCE\xB4", + "\xCE\x95" => "\xCE\xB5", + "\xCE\x96" => "\xCE\xB6", + "\xCE\x97" => "\xCE\xB7", + "\xCE\x98" => "\xCE\xB8", + "\xCE\x99" => "\xCE\xB9", + "\xCE\x9A" => "\xCE\xBA", + "\xCE\x9B" => "\xCE\xBB", + "\xCE\x9C" => "\xCE\xBC", + "\xCE\x9D" => "\xCE\xBD", + "\xCE\x9E" => "\xCE\xBE", + "\xCE\x9F" => "\xCE\xBF", + "\xCE\xA0" => "\xCF\x80", + "\xCE\xA1" => "\xCF\x81", + "\xCE\xA3" => "\xCF\x83", + "\xCE\xA4" => "\xCF\x84", + "\xCE\xA5" => "\xCF\x85", + "\xCE\xA6" => "\xCF\x86", + "\xCE\xA7" => "\xCF\x87", + "\xCE\xA8" => "\xCF\x88", + "\xCE\xA9" => "\xCF\x89", + "\xCE\xAA" => "\xCF\x8A", + "\xCE\xAB" => "\xCF\x8B", + "\xCF\x8F" => "\xCF\x97", + "\xCF\x98" => "\xCF\x99", + "\xCF\x9A" => "\xCF\x9B", + "\xCF\x9C" => "\xCF\x9D", + "\xCF\x9E" => "\xCF\x9F", + "\xCF\xA0" => "\xCF\xA1", + "\xCF\xA2" => "\xCF\xA3", + "\xCF\xA4" => "\xCF\xA5", + "\xCF\xA6" => "\xCF\xA7", + "\xCF\xA8" => "\xCF\xA9", + "\xCF\xAA" => "\xCF\xAB", + "\xCF\xAC" => "\xCF\xAD", + "\xCF\xAE" => "\xCF\xAF", + "\xCF\xB4" => "\xCE\xB8", + "\xCF\xB7" => "\xCF\xB8", + "\xCF\xB9" => "\xCF\xB2", + "\xCF\xBA" => "\xCF\xBB", + "\xCF\xBD" => "\xCD\xBB", + "\xCF\xBE" => "\xCD\xBC", + "\xCF\xBF" => "\xCD\xBD", + "\xD0\x80" => "\xD1\x90", + "\xD0\x81" => "\xD1\x91", + "\xD0\x82" => "\xD1\x92", + "\xD0\x83" => "\xD1\x93", + "\xD0\x84" => "\xD1\x94", + "\xD0\x85" => "\xD1\x95", + "\xD0\x86" => "\xD1\x96", + "\xD0\x87" => "\xD1\x97", + "\xD0\x88" => "\xD1\x98", + "\xD0\x89" => "\xD1\x99", + "\xD0\x8A" => "\xD1\x9A", + "\xD0\x8B" => "\xD1\x9B", + "\xD0\x8C" => "\xD1\x9C", + "\xD0\x8D" => "\xD1\x9D", + "\xD0\x8E" => "\xD1\x9E", + "\xD0\x8F" => "\xD1\x9F", + "\xD0\x90" => "\xD0\xB0", + "\xD0\x91" => "\xD0\xB1", + "\xD0\x92" => "\xD0\xB2", + "\xD0\x93" => "\xD0\xB3", + "\xD0\x94" => "\xD0\xB4", + "\xD0\x95" => "\xD0\xB5", + "\xD0\x96" => "\xD0\xB6", + "\xD0\x97" => "\xD0\xB7", + "\xD0\x98" => "\xD0\xB8", + "\xD0\x99" => "\xD0\xB9", + "\xD0\x9A" => "\xD0\xBA", + "\xD0\x9B" => "\xD0\xBB", + "\xD0\x9C" => "\xD0\xBC", + "\xD0\x9D" => "\xD0\xBD", + "\xD0\x9E" => "\xD0\xBE", + "\xD0\x9F" => "\xD0\xBF", + "\xD0\xA0" => "\xD1\x80", + "\xD0\xA1" => "\xD1\x81", + "\xD0\xA2" => "\xD1\x82", + "\xD0\xA3" => "\xD1\x83", + "\xD0\xA4" => "\xD1\x84", + "\xD0\xA5" => "\xD1\x85", + "\xD0\xA6" => "\xD1\x86", + "\xD0\xA7" => "\xD1\x87", + "\xD0\xA8" => "\xD1\x88", + "\xD0\xA9" => "\xD1\x89", + "\xD0\xAA" => "\xD1\x8A", + "\xD0\xAB" => "\xD1\x8B", + "\xD0\xAC" => "\xD1\x8C", + "\xD0\xAD" => "\xD1\x8D", + "\xD0\xAE" => "\xD1\x8E", + "\xD0\xAF" => "\xD1\x8F", + "\xD1\xA0" => "\xD1\xA1", + "\xD1\xA2" => "\xD1\xA3", + "\xD1\xA4" => "\xD1\xA5", + "\xD1\xA6" => "\xD1\xA7", + "\xD1\xA8" => "\xD1\xA9", + "\xD1\xAA" => "\xD1\xAB", + "\xD1\xAC" => "\xD1\xAD", + "\xD1\xAE" => "\xD1\xAF", + "\xD1\xB0" => "\xD1\xB1", + "\xD1\xB2" => "\xD1\xB3", + "\xD1\xB4" => "\xD1\xB5", + "\xD1\xB6" => "\xD1\xB7", + "\xD1\xB8" => "\xD1\xB9", + "\xD1\xBA" => "\xD1\xBB", + "\xD1\xBC" => "\xD1\xBD", + "\xD1\xBE" => "\xD1\xBF", + "\xD2\x80" => "\xD2\x81", + "\xD2\x8A" => "\xD2\x8B", + "\xD2\x8C" => "\xD2\x8D", + "\xD2\x8E" => "\xD2\x8F", + "\xD2\x90" => "\xD2\x91", + "\xD2\x92" => "\xD2\x93", + "\xD2\x94" => "\xD2\x95", + "\xD2\x96" => "\xD2\x97", + "\xD2\x98" => "\xD2\x99", + "\xD2\x9A" => "\xD2\x9B", + "\xD2\x9C" => "\xD2\x9D", + "\xD2\x9E" => "\xD2\x9F", + "\xD2\xA0" => "\xD2\xA1", + "\xD2\xA2" => "\xD2\xA3", + "\xD2\xA4" => "\xD2\xA5", + "\xD2\xA6" => "\xD2\xA7", + "\xD2\xA8" => "\xD2\xA9", + "\xD2\xAA" => "\xD2\xAB", + "\xD2\xAC" => "\xD2\xAD", + "\xD2\xAE" => "\xD2\xAF", + "\xD2\xB0" => "\xD2\xB1", + "\xD2\xB2" => "\xD2\xB3", + "\xD2\xB4" => "\xD2\xB5", + "\xD2\xB6" => "\xD2\xB7", + "\xD2\xB8" => "\xD2\xB9", + "\xD2\xBA" => "\xD2\xBB", + "\xD2\xBC" => "\xD2\xBD", + "\xD2\xBE" => "\xD2\xBF", + "\xD3\x80" => "\xD3\x8F", + "\xD3\x81" => "\xD3\x82", + "\xD3\x83" => "\xD3\x84", + "\xD3\x85" => "\xD3\x86", + "\xD3\x87" => "\xD3\x88", + "\xD3\x89" => "\xD3\x8A", + "\xD3\x8B" => "\xD3\x8C", + "\xD3\x8D" => "\xD3\x8E", + "\xD3\x90" => "\xD3\x91", + "\xD3\x92" => "\xD3\x93", + "\xD3\x94" => "\xD3\x95", + "\xD3\x96" => "\xD3\x97", + "\xD3\x98" => "\xD3\x99", + "\xD3\x9A" => "\xD3\x9B", + "\xD3\x9C" => "\xD3\x9D", + "\xD3\x9E" => "\xD3\x9F", + "\xD3\xA0" => "\xD3\xA1", + "\xD3\xA2" => "\xD3\xA3", + "\xD3\xA4" => "\xD3\xA5", + "\xD3\xA6" => "\xD3\xA7", + "\xD3\xA8" => "\xD3\xA9", + "\xD3\xAA" => "\xD3\xAB", + "\xD3\xAC" => "\xD3\xAD", + "\xD3\xAE" => "\xD3\xAF", + "\xD3\xB0" => "\xD3\xB1", + "\xD3\xB2" => "\xD3\xB3", + "\xD3\xB4" => "\xD3\xB5", + "\xD3\xB6" => "\xD3\xB7", + "\xD3\xB8" => "\xD3\xB9", + "\xD3\xBA" => "\xD3\xBB", + "\xD3\xBC" => "\xD3\xBD", + "\xD3\xBE" => "\xD3\xBF", + "\xD4\x80" => "\xD4\x81", + "\xD4\x82" => "\xD4\x83", + "\xD4\x84" => "\xD4\x85", + "\xD4\x86" => "\xD4\x87", + "\xD4\x88" => "\xD4\x89", + "\xD4\x8A" => "\xD4\x8B", + "\xD4\x8C" => "\xD4\x8D", + "\xD4\x8E" => "\xD4\x8F", + "\xD4\x90" => "\xD4\x91", + "\xD4\x92" => "\xD4\x93", + "\xD4\x94" => "\xD4\x95", + "\xD4\x96" => "\xD4\x97", + "\xD4\x98" => "\xD4\x99", + "\xD4\x9A" => "\xD4\x9B", + "\xD4\x9C" => "\xD4\x9D", + "\xD4\x9E" => "\xD4\x9F", + "\xD4\xA0" => "\xD4\xA1", + "\xD4\xA2" => "\xD4\xA3", + "\xD4\xA4" => "\xD4\xA5", + "\xD4\xA6" => "\xD4\xA7", + "\xD4\xA8" => "\xD4\xA9", + "\xD4\xAA" => "\xD4\xAB", + "\xD4\xAC" => "\xD4\xAD", + "\xD4\xAE" => "\xD4\xAF", + "\xD4\xB1" => "\xD5\xA1", + "\xD4\xB2" => "\xD5\xA2", + "\xD4\xB3" => "\xD5\xA3", + "\xD4\xB4" => "\xD5\xA4", + "\xD4\xB5" => "\xD5\xA5", + "\xD4\xB6" => "\xD5\xA6", + "\xD4\xB7" => "\xD5\xA7", + "\xD4\xB8" => "\xD5\xA8", + "\xD4\xB9" => "\xD5\xA9", + "\xD4\xBA" => "\xD5\xAA", + "\xD4\xBB" => "\xD5\xAB", + "\xD4\xBC" => "\xD5\xAC", + "\xD4\xBD" => "\xD5\xAD", + "\xD4\xBE" => "\xD5\xAE", + "\xD4\xBF" => "\xD5\xAF", + "\xD5\x80" => "\xD5\xB0", + "\xD5\x81" => "\xD5\xB1", + "\xD5\x82" => "\xD5\xB2", + "\xD5\x83" => "\xD5\xB3", + "\xD5\x84" => "\xD5\xB4", + "\xD5\x85" => "\xD5\xB5", + "\xD5\x86" => "\xD5\xB6", + "\xD5\x87" => "\xD5\xB7", + "\xD5\x88" => "\xD5\xB8", + "\xD5\x89" => "\xD5\xB9", + "\xD5\x8A" => "\xD5\xBA", + "\xD5\x8B" => "\xD5\xBB", + "\xD5\x8C" => "\xD5\xBC", + "\xD5\x8D" => "\xD5\xBD", + "\xD5\x8E" => "\xD5\xBE", + "\xD5\x8F" => "\xD5\xBF", + "\xD5\x90" => "\xD6\x80", + "\xD5\x91" => "\xD6\x81", + "\xD5\x92" => "\xD6\x82", + "\xD5\x93" => "\xD6\x83", + "\xD5\x94" => "\xD6\x84", + "\xD5\x95" => "\xD6\x85", + "\xD5\x96" => "\xD6\x86", + "\xE1\x82\xA0" => "\xE2\xB4\x80", + "\xE1\x82\xA1" => "\xE2\xB4\x81", + "\xE1\x82\xA2" => "\xE2\xB4\x82", + "\xE1\x82\xA3" => "\xE2\xB4\x83", + "\xE1\x82\xA4" => "\xE2\xB4\x84", + "\xE1\x82\xA5" => "\xE2\xB4\x85", + "\xE1\x82\xA6" => "\xE2\xB4\x86", + "\xE1\x82\xA7" => "\xE2\xB4\x87", + "\xE1\x82\xA8" => "\xE2\xB4\x88", + "\xE1\x82\xA9" => "\xE2\xB4\x89", + "\xE1\x82\xAA" => "\xE2\xB4\x8A", + "\xE1\x82\xAB" => "\xE2\xB4\x8B", + "\xE1\x82\xAC" => "\xE2\xB4\x8C", + "\xE1\x82\xAD" => "\xE2\xB4\x8D", + "\xE1\x82\xAE" => "\xE2\xB4\x8E", + "\xE1\x82\xAF" => "\xE2\xB4\x8F", + "\xE1\x82\xB0" => "\xE2\xB4\x90", + "\xE1\x82\xB1" => "\xE2\xB4\x91", + "\xE1\x82\xB2" => "\xE2\xB4\x92", + "\xE1\x82\xB3" => "\xE2\xB4\x93", + "\xE1\x82\xB4" => "\xE2\xB4\x94", + "\xE1\x82\xB5" => "\xE2\xB4\x95", + "\xE1\x82\xB6" => "\xE2\xB4\x96", + "\xE1\x82\xB7" => "\xE2\xB4\x97", + "\xE1\x82\xB8" => "\xE2\xB4\x98", + "\xE1\x82\xB9" => "\xE2\xB4\x99", + "\xE1\x82\xBA" => "\xE2\xB4\x9A", + "\xE1\x82\xBB" => "\xE2\xB4\x9B", + "\xE1\x82\xBC" => "\xE2\xB4\x9C", + "\xE1\x82\xBD" => "\xE2\xB4\x9D", + "\xE1\x82\xBE" => "\xE2\xB4\x9E", + "\xE1\x82\xBF" => "\xE2\xB4\x9F", + "\xE1\x83\x80" => "\xE2\xB4\xA0", + "\xE1\x83\x81" => "\xE2\xB4\xA1", + "\xE1\x83\x82" => "\xE2\xB4\xA2", + "\xE1\x83\x83" => "\xE2\xB4\xA3", + "\xE1\x83\x84" => "\xE2\xB4\xA4", + "\xE1\x83\x85" => "\xE2\xB4\xA5", + "\xE1\x83\x87" => "\xE2\xB4\xA7", + "\xE1\x83\x8D" => "\xE2\xB4\xAD", + "\xE1\x8E\xA0" => "\xEA\xAD\xB0", + "\xE1\x8E\xA1" => "\xEA\xAD\xB1", + "\xE1\x8E\xA2" => "\xEA\xAD\xB2", + "\xE1\x8E\xA3" => "\xEA\xAD\xB3", + "\xE1\x8E\xA4" => "\xEA\xAD\xB4", + "\xE1\x8E\xA5" => "\xEA\xAD\xB5", + "\xE1\x8E\xA6" => "\xEA\xAD\xB6", + "\xE1\x8E\xA7" => "\xEA\xAD\xB7", + "\xE1\x8E\xA8" => "\xEA\xAD\xB8", + "\xE1\x8E\xA9" => "\xEA\xAD\xB9", + "\xE1\x8E\xAA" => "\xEA\xAD\xBA", + "\xE1\x8E\xAB" => "\xEA\xAD\xBB", + "\xE1\x8E\xAC" => "\xEA\xAD\xBC", + "\xE1\x8E\xAD" => "\xEA\xAD\xBD", + "\xE1\x8E\xAE" => "\xEA\xAD\xBE", + "\xE1\x8E\xAF" => "\xEA\xAD\xBF", + "\xE1\x8E\xB0" => "\xEA\xAE\x80", + "\xE1\x8E\xB1" => "\xEA\xAE\x81", + "\xE1\x8E\xB2" => "\xEA\xAE\x82", + "\xE1\x8E\xB3" => "\xEA\xAE\x83", + "\xE1\x8E\xB4" => "\xEA\xAE\x84", + "\xE1\x8E\xB5" => "\xEA\xAE\x85", + "\xE1\x8E\xB6" => "\xEA\xAE\x86", + "\xE1\x8E\xB7" => "\xEA\xAE\x87", + "\xE1\x8E\xB8" => "\xEA\xAE\x88", + "\xE1\x8E\xB9" => "\xEA\xAE\x89", + "\xE1\x8E\xBA" => "\xEA\xAE\x8A", + "\xE1\x8E\xBB" => "\xEA\xAE\x8B", + "\xE1\x8E\xBC" => "\xEA\xAE\x8C", + "\xE1\x8E\xBD" => "\xEA\xAE\x8D", + "\xE1\x8E\xBE" => "\xEA\xAE\x8E", + "\xE1\x8E\xBF" => "\xEA\xAE\x8F", + "\xE1\x8F\x80" => "\xEA\xAE\x90", + "\xE1\x8F\x81" => "\xEA\xAE\x91", + "\xE1\x8F\x82" => "\xEA\xAE\x92", + "\xE1\x8F\x83" => "\xEA\xAE\x93", + "\xE1\x8F\x84" => "\xEA\xAE\x94", + "\xE1\x8F\x85" => "\xEA\xAE\x95", + "\xE1\x8F\x86" => "\xEA\xAE\x96", + "\xE1\x8F\x87" => "\xEA\xAE\x97", + "\xE1\x8F\x88" => "\xEA\xAE\x98", + "\xE1\x8F\x89" => "\xEA\xAE\x99", + "\xE1\x8F\x8A" => "\xEA\xAE\x9A", + "\xE1\x8F\x8B" => "\xEA\xAE\x9B", + "\xE1\x8F\x8C" => "\xEA\xAE\x9C", + "\xE1\x8F\x8D" => "\xEA\xAE\x9D", + "\xE1\x8F\x8E" => "\xEA\xAE\x9E", + "\xE1\x8F\x8F" => "\xEA\xAE\x9F", + "\xE1\x8F\x90" => "\xEA\xAE\xA0", + "\xE1\x8F\x91" => "\xEA\xAE\xA1", + "\xE1\x8F\x92" => "\xEA\xAE\xA2", + "\xE1\x8F\x93" => "\xEA\xAE\xA3", + "\xE1\x8F\x94" => "\xEA\xAE\xA4", + "\xE1\x8F\x95" => "\xEA\xAE\xA5", + "\xE1\x8F\x96" => "\xEA\xAE\xA6", + "\xE1\x8F\x97" => "\xEA\xAE\xA7", + "\xE1\x8F\x98" => "\xEA\xAE\xA8", + "\xE1\x8F\x99" => "\xEA\xAE\xA9", + "\xE1\x8F\x9A" => "\xEA\xAE\xAA", + "\xE1\x8F\x9B" => "\xEA\xAE\xAB", + "\xE1\x8F\x9C" => "\xEA\xAE\xAC", + "\xE1\x8F\x9D" => "\xEA\xAE\xAD", + "\xE1\x8F\x9E" => "\xEA\xAE\xAE", + "\xE1\x8F\x9F" => "\xEA\xAE\xAF", + "\xE1\x8F\xA0" => "\xEA\xAE\xB0", + "\xE1\x8F\xA1" => "\xEA\xAE\xB1", + "\xE1\x8F\xA2" => "\xEA\xAE\xB2", + "\xE1\x8F\xA3" => "\xEA\xAE\xB3", + "\xE1\x8F\xA4" => "\xEA\xAE\xB4", + "\xE1\x8F\xA5" => "\xEA\xAE\xB5", + "\xE1\x8F\xA6" => "\xEA\xAE\xB6", + "\xE1\x8F\xA7" => "\xEA\xAE\xB7", + "\xE1\x8F\xA8" => "\xEA\xAE\xB8", + "\xE1\x8F\xA9" => "\xEA\xAE\xB9", + "\xE1\x8F\xAA" => "\xEA\xAE\xBA", + "\xE1\x8F\xAB" => "\xEA\xAE\xBB", + "\xE1\x8F\xAC" => "\xEA\xAE\xBC", + "\xE1\x8F\xAD" => "\xEA\xAE\xBD", + "\xE1\x8F\xAE" => "\xEA\xAE\xBE", + "\xE1\x8F\xAF" => "\xEA\xAE\xBF", + "\xE1\x8F\xB0" => "\xE1\x8F\xB8", + "\xE1\x8F\xB1" => "\xE1\x8F\xB9", + "\xE1\x8F\xB2" => "\xE1\x8F\xBA", + "\xE1\x8F\xB3" => "\xE1\x8F\xBB", + "\xE1\x8F\xB4" => "\xE1\x8F\xBC", + "\xE1\x8F\xB5" => "\xE1\x8F\xBD", + "\xE1\xB2\x90" => "\xE1\x83\x90", + "\xE1\xB2\x91" => "\xE1\x83\x91", + "\xE1\xB2\x92" => "\xE1\x83\x92", + "\xE1\xB2\x93" => "\xE1\x83\x93", + "\xE1\xB2\x94" => "\xE1\x83\x94", + "\xE1\xB2\x95" => "\xE1\x83\x95", + "\xE1\xB2\x96" => "\xE1\x83\x96", + "\xE1\xB2\x97" => "\xE1\x83\x97", + "\xE1\xB2\x98" => "\xE1\x83\x98", + "\xE1\xB2\x99" => "\xE1\x83\x99", + "\xE1\xB2\x9A" => "\xE1\x83\x9A", + "\xE1\xB2\x9B" => "\xE1\x83\x9B", + "\xE1\xB2\x9C" => "\xE1\x83\x9C", + "\xE1\xB2\x9D" => "\xE1\x83\x9D", + "\xE1\xB2\x9E" => "\xE1\x83\x9E", + "\xE1\xB2\x9F" => "\xE1\x83\x9F", + "\xE1\xB2\xA0" => "\xE1\x83\xA0", + "\xE1\xB2\xA1" => "\xE1\x83\xA1", + "\xE1\xB2\xA2" => "\xE1\x83\xA2", + "\xE1\xB2\xA3" => "\xE1\x83\xA3", + "\xE1\xB2\xA4" => "\xE1\x83\xA4", + "\xE1\xB2\xA5" => "\xE1\x83\xA5", + "\xE1\xB2\xA6" => "\xE1\x83\xA6", + "\xE1\xB2\xA7" => "\xE1\x83\xA7", + "\xE1\xB2\xA8" => "\xE1\x83\xA8", + "\xE1\xB2\xA9" => "\xE1\x83\xA9", + "\xE1\xB2\xAA" => "\xE1\x83\xAA", + "\xE1\xB2\xAB" => "\xE1\x83\xAB", + "\xE1\xB2\xAC" => "\xE1\x83\xAC", + "\xE1\xB2\xAD" => "\xE1\x83\xAD", + "\xE1\xB2\xAE" => "\xE1\x83\xAE", + "\xE1\xB2\xAF" => "\xE1\x83\xAF", + "\xE1\xB2\xB0" => "\xE1\x83\xB0", + "\xE1\xB2\xB1" => "\xE1\x83\xB1", + "\xE1\xB2\xB2" => "\xE1\x83\xB2", + "\xE1\xB2\xB3" => "\xE1\x83\xB3", + "\xE1\xB2\xB4" => "\xE1\x83\xB4", + "\xE1\xB2\xB5" => "\xE1\x83\xB5", + "\xE1\xB2\xB6" => "\xE1\x83\xB6", + "\xE1\xB2\xB7" => "\xE1\x83\xB7", + "\xE1\xB2\xB8" => "\xE1\x83\xB8", + "\xE1\xB2\xB9" => "\xE1\x83\xB9", + "\xE1\xB2\xBA" => "\xE1\x83\xBA", + "\xE1\xB2\xBD" => "\xE1\x83\xBD", + "\xE1\xB2\xBE" => "\xE1\x83\xBE", + "\xE1\xB2\xBF" => "\xE1\x83\xBF", + "\xE1\xB8\x80" => "\xE1\xB8\x81", + "\xE1\xB8\x82" => "\xE1\xB8\x83", + "\xE1\xB8\x84" => "\xE1\xB8\x85", + "\xE1\xB8\x86" => "\xE1\xB8\x87", + "\xE1\xB8\x88" => "\xE1\xB8\x89", + "\xE1\xB8\x8A" => "\xE1\xB8\x8B", + "\xE1\xB8\x8C" => "\xE1\xB8\x8D", + "\xE1\xB8\x8E" => "\xE1\xB8\x8F", + "\xE1\xB8\x90" => "\xE1\xB8\x91", + "\xE1\xB8\x92" => "\xE1\xB8\x93", + "\xE1\xB8\x94" => "\xE1\xB8\x95", + "\xE1\xB8\x96" => "\xE1\xB8\x97", + "\xE1\xB8\x98" => "\xE1\xB8\x99", + "\xE1\xB8\x9A" => "\xE1\xB8\x9B", + "\xE1\xB8\x9C" => "\xE1\xB8\x9D", + "\xE1\xB8\x9E" => "\xE1\xB8\x9F", + "\xE1\xB8\xA0" => "\xE1\xB8\xA1", + "\xE1\xB8\xA2" => "\xE1\xB8\xA3", + "\xE1\xB8\xA4" => "\xE1\xB8\xA5", + "\xE1\xB8\xA6" => "\xE1\xB8\xA7", + "\xE1\xB8\xA8" => "\xE1\xB8\xA9", + "\xE1\xB8\xAA" => "\xE1\xB8\xAB", + "\xE1\xB8\xAC" => "\xE1\xB8\xAD", + "\xE1\xB8\xAE" => "\xE1\xB8\xAF", + "\xE1\xB8\xB0" => "\xE1\xB8\xB1", + "\xE1\xB8\xB2" => "\xE1\xB8\xB3", + "\xE1\xB8\xB4" => "\xE1\xB8\xB5", + "\xE1\xB8\xB6" => "\xE1\xB8\xB7", + "\xE1\xB8\xB8" => "\xE1\xB8\xB9", + "\xE1\xB8\xBA" => "\xE1\xB8\xBB", + "\xE1\xB8\xBC" => "\xE1\xB8\xBD", + "\xE1\xB8\xBE" => "\xE1\xB8\xBF", + "\xE1\xB9\x80" => "\xE1\xB9\x81", + "\xE1\xB9\x82" => "\xE1\xB9\x83", + "\xE1\xB9\x84" => "\xE1\xB9\x85", + "\xE1\xB9\x86" => "\xE1\xB9\x87", + "\xE1\xB9\x88" => "\xE1\xB9\x89", + "\xE1\xB9\x8A" => "\xE1\xB9\x8B", + "\xE1\xB9\x8C" => "\xE1\xB9\x8D", + "\xE1\xB9\x8E" => "\xE1\xB9\x8F", + "\xE1\xB9\x90" => "\xE1\xB9\x91", + "\xE1\xB9\x92" => "\xE1\xB9\x93", + "\xE1\xB9\x94" => "\xE1\xB9\x95", + "\xE1\xB9\x96" => "\xE1\xB9\x97", + "\xE1\xB9\x98" => "\xE1\xB9\x99", + "\xE1\xB9\x9A" => "\xE1\xB9\x9B", + "\xE1\xB9\x9C" => "\xE1\xB9\x9D", + "\xE1\xB9\x9E" => "\xE1\xB9\x9F", + "\xE1\xB9\xA0" => "\xE1\xB9\xA1", + "\xE1\xB9\xA2" => "\xE1\xB9\xA3", + "\xE1\xB9\xA4" => "\xE1\xB9\xA5", + "\xE1\xB9\xA6" => "\xE1\xB9\xA7", + "\xE1\xB9\xA8" => "\xE1\xB9\xA9", + "\xE1\xB9\xAA" => "\xE1\xB9\xAB", + "\xE1\xB9\xAC" => "\xE1\xB9\xAD", + "\xE1\xB9\xAE" => "\xE1\xB9\xAF", + "\xE1\xB9\xB0" => "\xE1\xB9\xB1", + "\xE1\xB9\xB2" => "\xE1\xB9\xB3", + "\xE1\xB9\xB4" => "\xE1\xB9\xB5", + "\xE1\xB9\xB6" => "\xE1\xB9\xB7", + "\xE1\xB9\xB8" => "\xE1\xB9\xB9", + "\xE1\xB9\xBA" => "\xE1\xB9\xBB", + "\xE1\xB9\xBC" => "\xE1\xB9\xBD", + "\xE1\xB9\xBE" => "\xE1\xB9\xBF", + "\xE1\xBA\x80" => "\xE1\xBA\x81", + "\xE1\xBA\x82" => "\xE1\xBA\x83", + "\xE1\xBA\x84" => "\xE1\xBA\x85", + "\xE1\xBA\x86" => "\xE1\xBA\x87", + "\xE1\xBA\x88" => "\xE1\xBA\x89", + "\xE1\xBA\x8A" => "\xE1\xBA\x8B", + "\xE1\xBA\x8C" => "\xE1\xBA\x8D", + "\xE1\xBA\x8E" => "\xE1\xBA\x8F", + "\xE1\xBA\x90" => "\xE1\xBA\x91", + "\xE1\xBA\x92" => "\xE1\xBA\x93", + "\xE1\xBA\x94" => "\xE1\xBA\x95", + "\xE1\xBA\x9E" => "\xC3\x9F", + "\xE1\xBA\xA0" => "\xE1\xBA\xA1", + "\xE1\xBA\xA2" => "\xE1\xBA\xA3", + "\xE1\xBA\xA4" => "\xE1\xBA\xA5", + "\xE1\xBA\xA6" => "\xE1\xBA\xA7", + "\xE1\xBA\xA8" => "\xE1\xBA\xA9", + "\xE1\xBA\xAA" => "\xE1\xBA\xAB", + "\xE1\xBA\xAC" => "\xE1\xBA\xAD", + "\xE1\xBA\xAE" => "\xE1\xBA\xAF", + "\xE1\xBA\xB0" => "\xE1\xBA\xB1", + "\xE1\xBA\xB2" => "\xE1\xBA\xB3", + "\xE1\xBA\xB4" => "\xE1\xBA\xB5", + "\xE1\xBA\xB6" => "\xE1\xBA\xB7", + "\xE1\xBA\xB8" => "\xE1\xBA\xB9", + "\xE1\xBA\xBA" => "\xE1\xBA\xBB", + "\xE1\xBA\xBC" => "\xE1\xBA\xBD", + "\xE1\xBA\xBE" => "\xE1\xBA\xBF", + "\xE1\xBB\x80" => "\xE1\xBB\x81", + "\xE1\xBB\x82" => "\xE1\xBB\x83", + "\xE1\xBB\x84" => "\xE1\xBB\x85", + "\xE1\xBB\x86" => "\xE1\xBB\x87", + "\xE1\xBB\x88" => "\xE1\xBB\x89", + "\xE1\xBB\x8A" => "\xE1\xBB\x8B", + "\xE1\xBB\x8C" => "\xE1\xBB\x8D", + "\xE1\xBB\x8E" => "\xE1\xBB\x8F", + "\xE1\xBB\x90" => "\xE1\xBB\x91", + "\xE1\xBB\x92" => "\xE1\xBB\x93", + "\xE1\xBB\x94" => "\xE1\xBB\x95", + "\xE1\xBB\x96" => "\xE1\xBB\x97", + "\xE1\xBB\x98" => "\xE1\xBB\x99", + "\xE1\xBB\x9A" => "\xE1\xBB\x9B", + "\xE1\xBB\x9C" => "\xE1\xBB\x9D", + "\xE1\xBB\x9E" => "\xE1\xBB\x9F", + "\xE1\xBB\xA0" => "\xE1\xBB\xA1", + "\xE1\xBB\xA2" => "\xE1\xBB\xA3", + "\xE1\xBB\xA4" => "\xE1\xBB\xA5", + "\xE1\xBB\xA6" => "\xE1\xBB\xA7", + "\xE1\xBB\xA8" => "\xE1\xBB\xA9", + "\xE1\xBB\xAA" => "\xE1\xBB\xAB", + "\xE1\xBB\xAC" => "\xE1\xBB\xAD", + "\xE1\xBB\xAE" => "\xE1\xBB\xAF", + "\xE1\xBB\xB0" => "\xE1\xBB\xB1", + "\xE1\xBB\xB2" => "\xE1\xBB\xB3", + "\xE1\xBB\xB4" => "\xE1\xBB\xB5", + "\xE1\xBB\xB6" => "\xE1\xBB\xB7", + "\xE1\xBB\xB8" => "\xE1\xBB\xB9", + "\xE1\xBB\xBA" => "\xE1\xBB\xBB", + "\xE1\xBB\xBC" => "\xE1\xBB\xBD", + "\xE1\xBB\xBE" => "\xE1\xBB\xBF", + "\xE1\xBC\x88" => "\xE1\xBC\x80", + "\xE1\xBC\x89" => "\xE1\xBC\x81", + "\xE1\xBC\x8A" => "\xE1\xBC\x82", + "\xE1\xBC\x8B" => "\xE1\xBC\x83", + "\xE1\xBC\x8C" => "\xE1\xBC\x84", + "\xE1\xBC\x8D" => "\xE1\xBC\x85", + "\xE1\xBC\x8E" => "\xE1\xBC\x86", + "\xE1\xBC\x8F" => "\xE1\xBC\x87", + "\xE1\xBC\x98" => "\xE1\xBC\x90", + "\xE1\xBC\x99" => "\xE1\xBC\x91", + "\xE1\xBC\x9A" => "\xE1\xBC\x92", + "\xE1\xBC\x9B" => "\xE1\xBC\x93", + "\xE1\xBC\x9C" => "\xE1\xBC\x94", + "\xE1\xBC\x9D" => "\xE1\xBC\x95", + "\xE1\xBC\xA8" => "\xE1\xBC\xA0", + "\xE1\xBC\xA9" => "\xE1\xBC\xA1", + "\xE1\xBC\xAA" => "\xE1\xBC\xA2", + "\xE1\xBC\xAB" => "\xE1\xBC\xA3", + "\xE1\xBC\xAC" => "\xE1\xBC\xA4", + "\xE1\xBC\xAD" => "\xE1\xBC\xA5", + "\xE1\xBC\xAE" => "\xE1\xBC\xA6", + "\xE1\xBC\xAF" => "\xE1\xBC\xA7", + "\xE1\xBC\xB8" => "\xE1\xBC\xB0", + "\xE1\xBC\xB9" => "\xE1\xBC\xB1", + "\xE1\xBC\xBA" => "\xE1\xBC\xB2", + "\xE1\xBC\xBB" => "\xE1\xBC\xB3", + "\xE1\xBC\xBC" => "\xE1\xBC\xB4", + "\xE1\xBC\xBD" => "\xE1\xBC\xB5", + "\xE1\xBC\xBE" => "\xE1\xBC\xB6", + "\xE1\xBC\xBF" => "\xE1\xBC\xB7", + "\xE1\xBD\x88" => "\xE1\xBD\x80", + "\xE1\xBD\x89" => "\xE1\xBD\x81", + "\xE1\xBD\x8A" => "\xE1\xBD\x82", + "\xE1\xBD\x8B" => "\xE1\xBD\x83", + "\xE1\xBD\x8C" => "\xE1\xBD\x84", + "\xE1\xBD\x8D" => "\xE1\xBD\x85", + "\xE1\xBD\x99" => "\xE1\xBD\x91", + "\xE1\xBD\x9B" => "\xE1\xBD\x93", + "\xE1\xBD\x9D" => "\xE1\xBD\x95", + "\xE1\xBD\x9F" => "\xE1\xBD\x97", + "\xE1\xBD\xA8" => "\xE1\xBD\xA0", + "\xE1\xBD\xA9" => "\xE1\xBD\xA1", + "\xE1\xBD\xAA" => "\xE1\xBD\xA2", + "\xE1\xBD\xAB" => "\xE1\xBD\xA3", + "\xE1\xBD\xAC" => "\xE1\xBD\xA4", + "\xE1\xBD\xAD" => "\xE1\xBD\xA5", + "\xE1\xBD\xAE" => "\xE1\xBD\xA6", + "\xE1\xBD\xAF" => "\xE1\xBD\xA7", + "\xE1\xBE\x88" => "\xE1\xBE\x80", + "\xE1\xBE\x89" => "\xE1\xBE\x81", + "\xE1\xBE\x8A" => "\xE1\xBE\x82", + "\xE1\xBE\x8B" => "\xE1\xBE\x83", + "\xE1\xBE\x8C" => "\xE1\xBE\x84", + "\xE1\xBE\x8D" => "\xE1\xBE\x85", + "\xE1\xBE\x8E" => "\xE1\xBE\x86", + "\xE1\xBE\x8F" => "\xE1\xBE\x87", + "\xE1\xBE\x98" => "\xE1\xBE\x90", + "\xE1\xBE\x99" => "\xE1\xBE\x91", + "\xE1\xBE\x9A" => "\xE1\xBE\x92", + "\xE1\xBE\x9B" => "\xE1\xBE\x93", + "\xE1\xBE\x9C" => "\xE1\xBE\x94", + "\xE1\xBE\x9D" => "\xE1\xBE\x95", + "\xE1\xBE\x9E" => "\xE1\xBE\x96", + "\xE1\xBE\x9F" => "\xE1\xBE\x97", + "\xE1\xBE\xA8" => "\xE1\xBE\xA0", + "\xE1\xBE\xA9" => "\xE1\xBE\xA1", + "\xE1\xBE\xAA" => "\xE1\xBE\xA2", + "\xE1\xBE\xAB" => "\xE1\xBE\xA3", + "\xE1\xBE\xAC" => "\xE1\xBE\xA4", + "\xE1\xBE\xAD" => "\xE1\xBE\xA5", + "\xE1\xBE\xAE" => "\xE1\xBE\xA6", + "\xE1\xBE\xAF" => "\xE1\xBE\xA7", + "\xE1\xBE\xB8" => "\xE1\xBE\xB0", + "\xE1\xBE\xB9" => "\xE1\xBE\xB1", + "\xE1\xBE\xBA" => "\xE1\xBD\xB0", + "\xE1\xBE\xBB" => "\xE1\xBD\xB1", + "\xE1\xBE\xBC" => "\xE1\xBE\xB3", + "\xE1\xBF\x88" => "\xE1\xBD\xB2", + "\xE1\xBF\x89" => "\xE1\xBD\xB3", + "\xE1\xBF\x8A" => "\xE1\xBD\xB4", + "\xE1\xBF\x8B" => "\xE1\xBD\xB5", + "\xE1\xBF\x8C" => "\xE1\xBF\x83", + "\xE1\xBF\x98" => "\xE1\xBF\x90", + "\xE1\xBF\x99" => "\xE1\xBF\x91", + "\xE1\xBF\x9A" => "\xE1\xBD\xB6", + "\xE1\xBF\x9B" => "\xE1\xBD\xB7", + "\xE1\xBF\xA8" => "\xE1\xBF\xA0", + "\xE1\xBF\xA9" => "\xE1\xBF\xA1", + "\xE1\xBF\xAA" => "\xE1\xBD\xBA", + "\xE1\xBF\xAB" => "\xE1\xBD\xBB", + "\xE1\xBF\xAC" => "\xE1\xBF\xA5", + "\xE1\xBF\xB8" => "\xE1\xBD\xB8", + "\xE1\xBF\xB9" => "\xE1\xBD\xB9", + "\xE1\xBF\xBA" => "\xE1\xBD\xBC", + "\xE1\xBF\xBB" => "\xE1\xBD\xBD", + "\xE1\xBF\xBC" => "\xE1\xBF\xB3", + "\xE2\x84\xA6" => "\xCF\x89", + "\xE2\x84\xAA" => "\x6B", + "\xE2\x84\xAB" => "\xC3\xA5", + "\xE2\x84\xB2" => "\xE2\x85\x8E", + "\xE2\x85\xA0" => "\xE2\x85\xB0", + "\xE2\x85\xA1" => "\xE2\x85\xB1", + "\xE2\x85\xA2" => "\xE2\x85\xB2", + "\xE2\x85\xA3" => "\xE2\x85\xB3", + "\xE2\x85\xA4" => "\xE2\x85\xB4", + "\xE2\x85\xA5" => "\xE2\x85\xB5", + "\xE2\x85\xA6" => "\xE2\x85\xB6", + "\xE2\x85\xA7" => "\xE2\x85\xB7", + "\xE2\x85\xA8" => "\xE2\x85\xB8", + "\xE2\x85\xA9" => "\xE2\x85\xB9", + "\xE2\x85\xAA" => "\xE2\x85\xBA", + "\xE2\x85\xAB" => "\xE2\x85\xBB", + "\xE2\x85\xAC" => "\xE2\x85\xBC", + "\xE2\x85\xAD" => "\xE2\x85\xBD", + "\xE2\x85\xAE" => "\xE2\x85\xBE", + "\xE2\x85\xAF" => "\xE2\x85\xBF", + "\xE2\x86\x83" => "\xE2\x86\x84", + "\xE2\x92\xB6" => "\xE2\x93\x90", + "\xE2\x92\xB7" => "\xE2\x93\x91", + "\xE2\x92\xB8" => "\xE2\x93\x92", + "\xE2\x92\xB9" => "\xE2\x93\x93", + "\xE2\x92\xBA" => "\xE2\x93\x94", + "\xE2\x92\xBB" => "\xE2\x93\x95", + "\xE2\x92\xBC" => "\xE2\x93\x96", + "\xE2\x92\xBD" => "\xE2\x93\x97", + "\xE2\x92\xBE" => "\xE2\x93\x98", + "\xE2\x92\xBF" => "\xE2\x93\x99", + "\xE2\x93\x80" => "\xE2\x93\x9A", + "\xE2\x93\x81" => "\xE2\x93\x9B", + "\xE2\x93\x82" => "\xE2\x93\x9C", + "\xE2\x93\x83" => "\xE2\x93\x9D", + "\xE2\x93\x84" => "\xE2\x93\x9E", + "\xE2\x93\x85" => "\xE2\x93\x9F", + "\xE2\x93\x86" => "\xE2\x93\xA0", + "\xE2\x93\x87" => "\xE2\x93\xA1", + "\xE2\x93\x88" => "\xE2\x93\xA2", + "\xE2\x93\x89" => "\xE2\x93\xA3", + "\xE2\x93\x8A" => "\xE2\x93\xA4", + "\xE2\x93\x8B" => "\xE2\x93\xA5", + "\xE2\x93\x8C" => "\xE2\x93\xA6", + "\xE2\x93\x8D" => "\xE2\x93\xA7", + "\xE2\x93\x8E" => "\xE2\x93\xA8", + "\xE2\x93\x8F" => "\xE2\x93\xA9", + "\xE2\xB0\x80" => "\xE2\xB0\xB0", + "\xE2\xB0\x81" => "\xE2\xB0\xB1", + "\xE2\xB0\x82" => "\xE2\xB0\xB2", + "\xE2\xB0\x83" => "\xE2\xB0\xB3", + "\xE2\xB0\x84" => "\xE2\xB0\xB4", + "\xE2\xB0\x85" => "\xE2\xB0\xB5", + "\xE2\xB0\x86" => "\xE2\xB0\xB6", + "\xE2\xB0\x87" => "\xE2\xB0\xB7", + "\xE2\xB0\x88" => "\xE2\xB0\xB8", + "\xE2\xB0\x89" => "\xE2\xB0\xB9", + "\xE2\xB0\x8A" => "\xE2\xB0\xBA", + "\xE2\xB0\x8B" => "\xE2\xB0\xBB", + "\xE2\xB0\x8C" => "\xE2\xB0\xBC", + "\xE2\xB0\x8D" => "\xE2\xB0\xBD", + "\xE2\xB0\x8E" => "\xE2\xB0\xBE", + "\xE2\xB0\x8F" => "\xE2\xB0\xBF", + "\xE2\xB0\x90" => "\xE2\xB1\x80", + "\xE2\xB0\x91" => "\xE2\xB1\x81", + "\xE2\xB0\x92" => "\xE2\xB1\x82", + "\xE2\xB0\x93" => "\xE2\xB1\x83", + "\xE2\xB0\x94" => "\xE2\xB1\x84", + "\xE2\xB0\x95" => "\xE2\xB1\x85", + "\xE2\xB0\x96" => "\xE2\xB1\x86", + "\xE2\xB0\x97" => "\xE2\xB1\x87", + "\xE2\xB0\x98" => "\xE2\xB1\x88", + "\xE2\xB0\x99" => "\xE2\xB1\x89", + "\xE2\xB0\x9A" => "\xE2\xB1\x8A", + "\xE2\xB0\x9B" => "\xE2\xB1\x8B", + "\xE2\xB0\x9C" => "\xE2\xB1\x8C", + "\xE2\xB0\x9D" => "\xE2\xB1\x8D", + "\xE2\xB0\x9E" => "\xE2\xB1\x8E", + "\xE2\xB0\x9F" => "\xE2\xB1\x8F", + "\xE2\xB0\xA0" => "\xE2\xB1\x90", + "\xE2\xB0\xA1" => "\xE2\xB1\x91", + "\xE2\xB0\xA2" => "\xE2\xB1\x92", + "\xE2\xB0\xA3" => "\xE2\xB1\x93", + "\xE2\xB0\xA4" => "\xE2\xB1\x94", + "\xE2\xB0\xA5" => "\xE2\xB1\x95", + "\xE2\xB0\xA6" => "\xE2\xB1\x96", + "\xE2\xB0\xA7" => "\xE2\xB1\x97", + "\xE2\xB0\xA8" => "\xE2\xB1\x98", + "\xE2\xB0\xA9" => "\xE2\xB1\x99", + "\xE2\xB0\xAA" => "\xE2\xB1\x9A", + "\xE2\xB0\xAB" => "\xE2\xB1\x9B", + "\xE2\xB0\xAC" => "\xE2\xB1\x9C", + "\xE2\xB0\xAD" => "\xE2\xB1\x9D", + "\xE2\xB0\xAE" => "\xE2\xB1\x9E", + "\xE2\xB0\xAF" => "\xE2\xB1\x9F", + "\xE2\xB1\xA0" => "\xE2\xB1\xA1", + "\xE2\xB1\xA2" => "\xC9\xAB", + "\xE2\xB1\xA3" => "\xE1\xB5\xBD", + "\xE2\xB1\xA4" => "\xC9\xBD", + "\xE2\xB1\xA7" => "\xE2\xB1\xA8", + "\xE2\xB1\xA9" => "\xE2\xB1\xAA", + "\xE2\xB1\xAB" => "\xE2\xB1\xAC", + "\xE2\xB1\xAD" => "\xC9\x91", + "\xE2\xB1\xAE" => "\xC9\xB1", + "\xE2\xB1\xAF" => "\xC9\x90", + "\xE2\xB1\xB0" => "\xC9\x92", + "\xE2\xB1\xB2" => "\xE2\xB1\xB3", + "\xE2\xB1\xB5" => "\xE2\xB1\xB6", + "\xE2\xB1\xBE" => "\xC8\xBF", + "\xE2\xB1\xBF" => "\xC9\x80", + "\xE2\xB2\x80" => "\xE2\xB2\x81", + "\xE2\xB2\x82" => "\xE2\xB2\x83", + "\xE2\xB2\x84" => "\xE2\xB2\x85", + "\xE2\xB2\x86" => "\xE2\xB2\x87", + "\xE2\xB2\x88" => "\xE2\xB2\x89", + "\xE2\xB2\x8A" => "\xE2\xB2\x8B", + "\xE2\xB2\x8C" => "\xE2\xB2\x8D", + "\xE2\xB2\x8E" => "\xE2\xB2\x8F", + "\xE2\xB2\x90" => "\xE2\xB2\x91", + "\xE2\xB2\x92" => "\xE2\xB2\x93", + "\xE2\xB2\x94" => "\xE2\xB2\x95", + "\xE2\xB2\x96" => "\xE2\xB2\x97", + "\xE2\xB2\x98" => "\xE2\xB2\x99", + "\xE2\xB2\x9A" => "\xE2\xB2\x9B", + "\xE2\xB2\x9C" => "\xE2\xB2\x9D", + "\xE2\xB2\x9E" => "\xE2\xB2\x9F", + "\xE2\xB2\xA0" => "\xE2\xB2\xA1", + "\xE2\xB2\xA2" => "\xE2\xB2\xA3", + "\xE2\xB2\xA4" => "\xE2\xB2\xA5", + "\xE2\xB2\xA6" => "\xE2\xB2\xA7", + "\xE2\xB2\xA8" => "\xE2\xB2\xA9", + "\xE2\xB2\xAA" => "\xE2\xB2\xAB", + "\xE2\xB2\xAC" => "\xE2\xB2\xAD", + "\xE2\xB2\xAE" => "\xE2\xB2\xAF", + "\xE2\xB2\xB0" => "\xE2\xB2\xB1", + "\xE2\xB2\xB2" => "\xE2\xB2\xB3", + "\xE2\xB2\xB4" => "\xE2\xB2\xB5", + "\xE2\xB2\xB6" => "\xE2\xB2\xB7", + "\xE2\xB2\xB8" => "\xE2\xB2\xB9", + "\xE2\xB2\xBA" => "\xE2\xB2\xBB", + "\xE2\xB2\xBC" => "\xE2\xB2\xBD", + "\xE2\xB2\xBE" => "\xE2\xB2\xBF", + "\xE2\xB3\x80" => "\xE2\xB3\x81", + "\xE2\xB3\x82" => "\xE2\xB3\x83", + "\xE2\xB3\x84" => "\xE2\xB3\x85", + "\xE2\xB3\x86" => "\xE2\xB3\x87", + "\xE2\xB3\x88" => "\xE2\xB3\x89", + "\xE2\xB3\x8A" => "\xE2\xB3\x8B", + "\xE2\xB3\x8C" => "\xE2\xB3\x8D", + "\xE2\xB3\x8E" => "\xE2\xB3\x8F", + "\xE2\xB3\x90" => "\xE2\xB3\x91", + "\xE2\xB3\x92" => "\xE2\xB3\x93", + "\xE2\xB3\x94" => "\xE2\xB3\x95", + "\xE2\xB3\x96" => "\xE2\xB3\x97", + "\xE2\xB3\x98" => "\xE2\xB3\x99", + "\xE2\xB3\x9A" => "\xE2\xB3\x9B", + "\xE2\xB3\x9C" => "\xE2\xB3\x9D", + "\xE2\xB3\x9E" => "\xE2\xB3\x9F", + "\xE2\xB3\xA0" => "\xE2\xB3\xA1", + "\xE2\xB3\xA2" => "\xE2\xB3\xA3", + "\xE2\xB3\xAB" => "\xE2\xB3\xAC", + "\xE2\xB3\xAD" => "\xE2\xB3\xAE", + "\xE2\xB3\xB2" => "\xE2\xB3\xB3", + "\xEA\x99\x80" => "\xEA\x99\x81", + "\xEA\x99\x82" => "\xEA\x99\x83", + "\xEA\x99\x84" => "\xEA\x99\x85", + "\xEA\x99\x86" => "\xEA\x99\x87", + "\xEA\x99\x88" => "\xEA\x99\x89", + "\xEA\x99\x8A" => "\xEA\x99\x8B", + "\xEA\x99\x8C" => "\xEA\x99\x8D", + "\xEA\x99\x8E" => "\xEA\x99\x8F", + "\xEA\x99\x90" => "\xEA\x99\x91", + "\xEA\x99\x92" => "\xEA\x99\x93", + "\xEA\x99\x94" => "\xEA\x99\x95", + "\xEA\x99\x96" => "\xEA\x99\x97", + "\xEA\x99\x98" => "\xEA\x99\x99", + "\xEA\x99\x9A" => "\xEA\x99\x9B", + "\xEA\x99\x9C" => "\xEA\x99\x9D", + "\xEA\x99\x9E" => "\xEA\x99\x9F", + "\xEA\x99\xA0" => "\xEA\x99\xA1", + "\xEA\x99\xA2" => "\xEA\x99\xA3", + "\xEA\x99\xA4" => "\xEA\x99\xA5", + "\xEA\x99\xA6" => "\xEA\x99\xA7", + "\xEA\x99\xA8" => "\xEA\x99\xA9", + "\xEA\x99\xAA" => "\xEA\x99\xAB", + "\xEA\x99\xAC" => "\xEA\x99\xAD", + "\xEA\x9A\x80" => "\xEA\x9A\x81", + "\xEA\x9A\x82" => "\xEA\x9A\x83", + "\xEA\x9A\x84" => "\xEA\x9A\x85", + "\xEA\x9A\x86" => "\xEA\x9A\x87", + "\xEA\x9A\x88" => "\xEA\x9A\x89", + "\xEA\x9A\x8A" => "\xEA\x9A\x8B", + "\xEA\x9A\x8C" => "\xEA\x9A\x8D", + "\xEA\x9A\x8E" => "\xEA\x9A\x8F", + "\xEA\x9A\x90" => "\xEA\x9A\x91", + "\xEA\x9A\x92" => "\xEA\x9A\x93", + "\xEA\x9A\x94" => "\xEA\x9A\x95", + "\xEA\x9A\x96" => "\xEA\x9A\x97", + "\xEA\x9A\x98" => "\xEA\x9A\x99", + "\xEA\x9A\x9A" => "\xEA\x9A\x9B", + "\xEA\x9C\xA2" => "\xEA\x9C\xA3", + "\xEA\x9C\xA4" => "\xEA\x9C\xA5", + "\xEA\x9C\xA6" => "\xEA\x9C\xA7", + "\xEA\x9C\xA8" => "\xEA\x9C\xA9", + "\xEA\x9C\xAA" => "\xEA\x9C\xAB", + "\xEA\x9C\xAC" => "\xEA\x9C\xAD", + "\xEA\x9C\xAE" => "\xEA\x9C\xAF", + "\xEA\x9C\xB2" => "\xEA\x9C\xB3", + "\xEA\x9C\xB4" => "\xEA\x9C\xB5", + "\xEA\x9C\xB6" => "\xEA\x9C\xB7", + "\xEA\x9C\xB8" => "\xEA\x9C\xB9", + "\xEA\x9C\xBA" => "\xEA\x9C\xBB", + "\xEA\x9C\xBC" => "\xEA\x9C\xBD", + "\xEA\x9C\xBE" => "\xEA\x9C\xBF", + "\xEA\x9D\x80" => "\xEA\x9D\x81", + "\xEA\x9D\x82" => "\xEA\x9D\x83", + "\xEA\x9D\x84" => "\xEA\x9D\x85", + "\xEA\x9D\x86" => "\xEA\x9D\x87", + "\xEA\x9D\x88" => "\xEA\x9D\x89", + "\xEA\x9D\x8A" => "\xEA\x9D\x8B", + "\xEA\x9D\x8C" => "\xEA\x9D\x8D", + "\xEA\x9D\x8E" => "\xEA\x9D\x8F", + "\xEA\x9D\x90" => "\xEA\x9D\x91", + "\xEA\x9D\x92" => "\xEA\x9D\x93", + "\xEA\x9D\x94" => "\xEA\x9D\x95", + "\xEA\x9D\x96" => "\xEA\x9D\x97", + "\xEA\x9D\x98" => "\xEA\x9D\x99", + "\xEA\x9D\x9A" => "\xEA\x9D\x9B", + "\xEA\x9D\x9C" => "\xEA\x9D\x9D", + "\xEA\x9D\x9E" => "\xEA\x9D\x9F", + "\xEA\x9D\xA0" => "\xEA\x9D\xA1", + "\xEA\x9D\xA2" => "\xEA\x9D\xA3", + "\xEA\x9D\xA4" => "\xEA\x9D\xA5", + "\xEA\x9D\xA6" => "\xEA\x9D\xA7", + "\xEA\x9D\xA8" => "\xEA\x9D\xA9", + "\xEA\x9D\xAA" => "\xEA\x9D\xAB", + "\xEA\x9D\xAC" => "\xEA\x9D\xAD", + "\xEA\x9D\xAE" => "\xEA\x9D\xAF", + "\xEA\x9D\xB9" => "\xEA\x9D\xBA", + "\xEA\x9D\xBB" => "\xEA\x9D\xBC", + "\xEA\x9D\xBD" => "\xE1\xB5\xB9", + "\xEA\x9D\xBE" => "\xEA\x9D\xBF", + "\xEA\x9E\x80" => "\xEA\x9E\x81", + "\xEA\x9E\x82" => "\xEA\x9E\x83", + "\xEA\x9E\x84" => "\xEA\x9E\x85", + "\xEA\x9E\x86" => "\xEA\x9E\x87", + "\xEA\x9E\x8B" => "\xEA\x9E\x8C", + "\xEA\x9E\x8D" => "\xC9\xA5", + "\xEA\x9E\x90" => "\xEA\x9E\x91", + "\xEA\x9E\x92" => "\xEA\x9E\x93", + "\xEA\x9E\x96" => "\xEA\x9E\x97", + "\xEA\x9E\x98" => "\xEA\x9E\x99", + "\xEA\x9E\x9A" => "\xEA\x9E\x9B", + "\xEA\x9E\x9C" => "\xEA\x9E\x9D", + "\xEA\x9E\x9E" => "\xEA\x9E\x9F", + "\xEA\x9E\xA0" => "\xEA\x9E\xA1", + "\xEA\x9E\xA2" => "\xEA\x9E\xA3", + "\xEA\x9E\xA4" => "\xEA\x9E\xA5", + "\xEA\x9E\xA6" => "\xEA\x9E\xA7", + "\xEA\x9E\xA8" => "\xEA\x9E\xA9", + "\xEA\x9E\xAA" => "\xC9\xA6", + "\xEA\x9E\xAB" => "\xC9\x9C", + "\xEA\x9E\xAC" => "\xC9\xA1", + "\xEA\x9E\xAD" => "\xC9\xAC", + "\xEA\x9E\xAE" => "\xC9\xAA", + "\xEA\x9E\xB0" => "\xCA\x9E", + "\xEA\x9E\xB1" => "\xCA\x87", + "\xEA\x9E\xB2" => "\xCA\x9D", + "\xEA\x9E\xB3" => "\xEA\xAD\x93", + "\xEA\x9E\xB4" => "\xEA\x9E\xB5", + "\xEA\x9E\xB6" => "\xEA\x9E\xB7", + "\xEA\x9E\xB8" => "\xEA\x9E\xB9", + "\xEA\x9E\xBA" => "\xEA\x9E\xBB", + "\xEA\x9E\xBC" => "\xEA\x9E\xBD", + "\xEA\x9E\xBE" => "\xEA\x9E\xBF", + "\xEA\x9F\x80" => "\xEA\x9F\x81", + "\xEA\x9F\x82" => "\xEA\x9F\x83", + "\xEA\x9F\x84" => "\xEA\x9E\x94", + "\xEA\x9F\x85" => "\xCA\x82", + "\xEA\x9F\x86" => "\xE1\xB6\x8E", + "\xEA\x9F\x87" => "\xEA\x9F\x88", + "\xEA\x9F\x89" => "\xEA\x9F\x8A", + "\xEA\x9F\x90" => "\xEA\x9F\x91", + "\xEA\x9F\x96" => "\xEA\x9F\x97", + "\xEA\x9F\x98" => "\xEA\x9F\x99", + "\xEA\x9F\xB5" => "\xEA\x9F\xB6", + "\xEF\xBC\xA1" => "\xEF\xBD\x81", + "\xEF\xBC\xA2" => "\xEF\xBD\x82", + "\xEF\xBC\xA3" => "\xEF\xBD\x83", + "\xEF\xBC\xA4" => "\xEF\xBD\x84", + "\xEF\xBC\xA5" => "\xEF\xBD\x85", + "\xEF\xBC\xA6" => "\xEF\xBD\x86", + "\xEF\xBC\xA7" => "\xEF\xBD\x87", + "\xEF\xBC\xA8" => "\xEF\xBD\x88", + "\xEF\xBC\xA9" => "\xEF\xBD\x89", + "\xEF\xBC\xAA" => "\xEF\xBD\x8A", + "\xEF\xBC\xAB" => "\xEF\xBD\x8B", + "\xEF\xBC\xAC" => "\xEF\xBD\x8C", + "\xEF\xBC\xAD" => "\xEF\xBD\x8D", + "\xEF\xBC\xAE" => "\xEF\xBD\x8E", + "\xEF\xBC\xAF" => "\xEF\xBD\x8F", + "\xEF\xBC\xB0" => "\xEF\xBD\x90", + "\xEF\xBC\xB1" => "\xEF\xBD\x91", + "\xEF\xBC\xB2" => "\xEF\xBD\x92", + "\xEF\xBC\xB3" => "\xEF\xBD\x93", + "\xEF\xBC\xB4" => "\xEF\xBD\x94", + "\xEF\xBC\xB5" => "\xEF\xBD\x95", + "\xEF\xBC\xB6" => "\xEF\xBD\x96", + "\xEF\xBC\xB7" => "\xEF\xBD\x97", + "\xEF\xBC\xB8" => "\xEF\xBD\x98", + "\xEF\xBC\xB9" => "\xEF\xBD\x99", + "\xEF\xBC\xBA" => "\xEF\xBD\x9A", + "\xF0\x90\x90\x80" => "\xF0\x90\x90\xA8", + "\xF0\x90\x90\x81" => "\xF0\x90\x90\xA9", + "\xF0\x90\x90\x82" => "\xF0\x90\x90\xAA", + "\xF0\x90\x90\x83" => "\xF0\x90\x90\xAB", + "\xF0\x90\x90\x84" => "\xF0\x90\x90\xAC", + "\xF0\x90\x90\x85" => "\xF0\x90\x90\xAD", + "\xF0\x90\x90\x86" => "\xF0\x90\x90\xAE", + "\xF0\x90\x90\x87" => "\xF0\x90\x90\xAF", + "\xF0\x90\x90\x88" => "\xF0\x90\x90\xB0", + "\xF0\x90\x90\x89" => "\xF0\x90\x90\xB1", + "\xF0\x90\x90\x8A" => "\xF0\x90\x90\xB2", + "\xF0\x90\x90\x8B" => "\xF0\x90\x90\xB3", + "\xF0\x90\x90\x8C" => "\xF0\x90\x90\xB4", + "\xF0\x90\x90\x8D" => "\xF0\x90\x90\xB5", + "\xF0\x90\x90\x8E" => "\xF0\x90\x90\xB6", + "\xF0\x90\x90\x8F" => "\xF0\x90\x90\xB7", + "\xF0\x90\x90\x90" => "\xF0\x90\x90\xB8", + "\xF0\x90\x90\x91" => "\xF0\x90\x90\xB9", + "\xF0\x90\x90\x92" => "\xF0\x90\x90\xBA", + "\xF0\x90\x90\x93" => "\xF0\x90\x90\xBB", + "\xF0\x90\x90\x94" => "\xF0\x90\x90\xBC", + "\xF0\x90\x90\x95" => "\xF0\x90\x90\xBD", + "\xF0\x90\x90\x96" => "\xF0\x90\x90\xBE", + "\xF0\x90\x90\x97" => "\xF0\x90\x90\xBF", + "\xF0\x90\x90\x98" => "\xF0\x90\x91\x80", + "\xF0\x90\x90\x99" => "\xF0\x90\x91\x81", + "\xF0\x90\x90\x9A" => "\xF0\x90\x91\x82", + "\xF0\x90\x90\x9B" => "\xF0\x90\x91\x83", + "\xF0\x90\x90\x9C" => "\xF0\x90\x91\x84", + "\xF0\x90\x90\x9D" => "\xF0\x90\x91\x85", + "\xF0\x90\x90\x9E" => "\xF0\x90\x91\x86", + "\xF0\x90\x90\x9F" => "\xF0\x90\x91\x87", + "\xF0\x90\x90\xA0" => "\xF0\x90\x91\x88", + "\xF0\x90\x90\xA1" => "\xF0\x90\x91\x89", + "\xF0\x90\x90\xA2" => "\xF0\x90\x91\x8A", + "\xF0\x90\x90\xA3" => "\xF0\x90\x91\x8B", + "\xF0\x90\x90\xA4" => "\xF0\x90\x91\x8C", + "\xF0\x90\x90\xA5" => "\xF0\x90\x91\x8D", + "\xF0\x90\x90\xA6" => "\xF0\x90\x91\x8E", + "\xF0\x90\x90\xA7" => "\xF0\x90\x91\x8F", + "\xF0\x90\x92\xB0" => "\xF0\x90\x93\x98", + "\xF0\x90\x92\xB1" => "\xF0\x90\x93\x99", + "\xF0\x90\x92\xB2" => "\xF0\x90\x93\x9A", + "\xF0\x90\x92\xB3" => "\xF0\x90\x93\x9B", + "\xF0\x90\x92\xB4" => "\xF0\x90\x93\x9C", + "\xF0\x90\x92\xB5" => "\xF0\x90\x93\x9D", + "\xF0\x90\x92\xB6" => "\xF0\x90\x93\x9E", + "\xF0\x90\x92\xB7" => "\xF0\x90\x93\x9F", + "\xF0\x90\x92\xB8" => "\xF0\x90\x93\xA0", + "\xF0\x90\x92\xB9" => "\xF0\x90\x93\xA1", + "\xF0\x90\x92\xBA" => "\xF0\x90\x93\xA2", + "\xF0\x90\x92\xBB" => "\xF0\x90\x93\xA3", + "\xF0\x90\x92\xBC" => "\xF0\x90\x93\xA4", + "\xF0\x90\x92\xBD" => "\xF0\x90\x93\xA5", + "\xF0\x90\x92\xBE" => "\xF0\x90\x93\xA6", + "\xF0\x90\x92\xBF" => "\xF0\x90\x93\xA7", + "\xF0\x90\x93\x80" => "\xF0\x90\x93\xA8", + "\xF0\x90\x93\x81" => "\xF0\x90\x93\xA9", + "\xF0\x90\x93\x82" => "\xF0\x90\x93\xAA", + "\xF0\x90\x93\x83" => "\xF0\x90\x93\xAB", + "\xF0\x90\x93\x84" => "\xF0\x90\x93\xAC", + "\xF0\x90\x93\x85" => "\xF0\x90\x93\xAD", + "\xF0\x90\x93\x86" => "\xF0\x90\x93\xAE", + "\xF0\x90\x93\x87" => "\xF0\x90\x93\xAF", + "\xF0\x90\x93\x88" => "\xF0\x90\x93\xB0", + "\xF0\x90\x93\x89" => "\xF0\x90\x93\xB1", + "\xF0\x90\x93\x8A" => "\xF0\x90\x93\xB2", + "\xF0\x90\x93\x8B" => "\xF0\x90\x93\xB3", + "\xF0\x90\x93\x8C" => "\xF0\x90\x93\xB4", + "\xF0\x90\x93\x8D" => "\xF0\x90\x93\xB5", + "\xF0\x90\x93\x8E" => "\xF0\x90\x93\xB6", + "\xF0\x90\x93\x8F" => "\xF0\x90\x93\xB7", + "\xF0\x90\x93\x90" => "\xF0\x90\x93\xB8", + "\xF0\x90\x93\x91" => "\xF0\x90\x93\xB9", + "\xF0\x90\x93\x92" => "\xF0\x90\x93\xBA", + "\xF0\x90\x93\x93" => "\xF0\x90\x93\xBB", + "\xF0\x90\x95\xB0" => "\xF0\x90\x96\x97", + "\xF0\x90\x95\xB1" => "\xF0\x90\x96\x98", + "\xF0\x90\x95\xB2" => "\xF0\x90\x96\x99", + "\xF0\x90\x95\xB3" => "\xF0\x90\x96\x9A", + "\xF0\x90\x95\xB4" => "\xF0\x90\x96\x9B", + "\xF0\x90\x95\xB5" => "\xF0\x90\x96\x9C", + "\xF0\x90\x95\xB6" => "\xF0\x90\x96\x9D", + "\xF0\x90\x95\xB7" => "\xF0\x90\x96\x9E", + "\xF0\x90\x95\xB8" => "\xF0\x90\x96\x9F", + "\xF0\x90\x95\xB9" => "\xF0\x90\x96\xA0", + "\xF0\x90\x95\xBA" => "\xF0\x90\x96\xA1", + "\xF0\x90\x95\xBC" => "\xF0\x90\x96\xA3", + "\xF0\x90\x95\xBD" => "\xF0\x90\x96\xA4", + "\xF0\x90\x95\xBE" => "\xF0\x90\x96\xA5", + "\xF0\x90\x95\xBF" => "\xF0\x90\x96\xA6", + "\xF0\x90\x96\x80" => "\xF0\x90\x96\xA7", + "\xF0\x90\x96\x81" => "\xF0\x90\x96\xA8", + "\xF0\x90\x96\x82" => "\xF0\x90\x96\xA9", + "\xF0\x90\x96\x83" => "\xF0\x90\x96\xAA", + "\xF0\x90\x96\x84" => "\xF0\x90\x96\xAB", + "\xF0\x90\x96\x85" => "\xF0\x90\x96\xAC", + "\xF0\x90\x96\x86" => "\xF0\x90\x96\xAD", + "\xF0\x90\x96\x87" => "\xF0\x90\x96\xAE", + "\xF0\x90\x96\x88" => "\xF0\x90\x96\xAF", + "\xF0\x90\x96\x89" => "\xF0\x90\x96\xB0", + "\xF0\x90\x96\x8A" => "\xF0\x90\x96\xB1", + "\xF0\x90\x96\x8C" => "\xF0\x90\x96\xB3", + "\xF0\x90\x96\x8D" => "\xF0\x90\x96\xB4", + "\xF0\x90\x96\x8E" => "\xF0\x90\x96\xB5", + "\xF0\x90\x96\x8F" => "\xF0\x90\x96\xB6", + "\xF0\x90\x96\x90" => "\xF0\x90\x96\xB7", + "\xF0\x90\x96\x91" => "\xF0\x90\x96\xB8", + "\xF0\x90\x96\x92" => "\xF0\x90\x96\xB9", + "\xF0\x90\x96\x94" => "\xF0\x90\x96\xBB", + "\xF0\x90\x96\x95" => "\xF0\x90\x96\xBC", + "\xF0\x90\xB2\x80" => "\xF0\x90\xB3\x80", + "\xF0\x90\xB2\x81" => "\xF0\x90\xB3\x81", + "\xF0\x90\xB2\x82" => "\xF0\x90\xB3\x82", + "\xF0\x90\xB2\x83" => "\xF0\x90\xB3\x83", + "\xF0\x90\xB2\x84" => "\xF0\x90\xB3\x84", + "\xF0\x90\xB2\x85" => "\xF0\x90\xB3\x85", + "\xF0\x90\xB2\x86" => "\xF0\x90\xB3\x86", + "\xF0\x90\xB2\x87" => "\xF0\x90\xB3\x87", + "\xF0\x90\xB2\x88" => "\xF0\x90\xB3\x88", + "\xF0\x90\xB2\x89" => "\xF0\x90\xB3\x89", + "\xF0\x90\xB2\x8A" => "\xF0\x90\xB3\x8A", + "\xF0\x90\xB2\x8B" => "\xF0\x90\xB3\x8B", + "\xF0\x90\xB2\x8C" => "\xF0\x90\xB3\x8C", + "\xF0\x90\xB2\x8D" => "\xF0\x90\xB3\x8D", + "\xF0\x90\xB2\x8E" => "\xF0\x90\xB3\x8E", + "\xF0\x90\xB2\x8F" => "\xF0\x90\xB3\x8F", + "\xF0\x90\xB2\x90" => "\xF0\x90\xB3\x90", + "\xF0\x90\xB2\x91" => "\xF0\x90\xB3\x91", + "\xF0\x90\xB2\x92" => "\xF0\x90\xB3\x92", + "\xF0\x90\xB2\x93" => "\xF0\x90\xB3\x93", + "\xF0\x90\xB2\x94" => "\xF0\x90\xB3\x94", + "\xF0\x90\xB2\x95" => "\xF0\x90\xB3\x95", + "\xF0\x90\xB2\x96" => "\xF0\x90\xB3\x96", + "\xF0\x90\xB2\x97" => "\xF0\x90\xB3\x97", + "\xF0\x90\xB2\x98" => "\xF0\x90\xB3\x98", + "\xF0\x90\xB2\x99" => "\xF0\x90\xB3\x99", + "\xF0\x90\xB2\x9A" => "\xF0\x90\xB3\x9A", + "\xF0\x90\xB2\x9B" => "\xF0\x90\xB3\x9B", + "\xF0\x90\xB2\x9C" => "\xF0\x90\xB3\x9C", + "\xF0\x90\xB2\x9D" => "\xF0\x90\xB3\x9D", + "\xF0\x90\xB2\x9E" => "\xF0\x90\xB3\x9E", + "\xF0\x90\xB2\x9F" => "\xF0\x90\xB3\x9F", + "\xF0\x90\xB2\xA0" => "\xF0\x90\xB3\xA0", + "\xF0\x90\xB2\xA1" => "\xF0\x90\xB3\xA1", + "\xF0\x90\xB2\xA2" => "\xF0\x90\xB3\xA2", + "\xF0\x90\xB2\xA3" => "\xF0\x90\xB3\xA3", + "\xF0\x90\xB2\xA4" => "\xF0\x90\xB3\xA4", + "\xF0\x90\xB2\xA5" => "\xF0\x90\xB3\xA5", + "\xF0\x90\xB2\xA6" => "\xF0\x90\xB3\xA6", + "\xF0\x90\xB2\xA7" => "\xF0\x90\xB3\xA7", + "\xF0\x90\xB2\xA8" => "\xF0\x90\xB3\xA8", + "\xF0\x90\xB2\xA9" => "\xF0\x90\xB3\xA9", + "\xF0\x90\xB2\xAA" => "\xF0\x90\xB3\xAA", + "\xF0\x90\xB2\xAB" => "\xF0\x90\xB3\xAB", + "\xF0\x90\xB2\xAC" => "\xF0\x90\xB3\xAC", + "\xF0\x90\xB2\xAD" => "\xF0\x90\xB3\xAD", + "\xF0\x90\xB2\xAE" => "\xF0\x90\xB3\xAE", + "\xF0\x90\xB2\xAF" => "\xF0\x90\xB3\xAF", + "\xF0\x90\xB2\xB0" => "\xF0\x90\xB3\xB0", + "\xF0\x90\xB2\xB1" => "\xF0\x90\xB3\xB1", + "\xF0\x90\xB2\xB2" => "\xF0\x90\xB3\xB2", + "\xF0\x91\xA2\xA0" => "\xF0\x91\xA3\x80", + "\xF0\x91\xA2\xA1" => "\xF0\x91\xA3\x81", + "\xF0\x91\xA2\xA2" => "\xF0\x91\xA3\x82", + "\xF0\x91\xA2\xA3" => "\xF0\x91\xA3\x83", + "\xF0\x91\xA2\xA4" => "\xF0\x91\xA3\x84", + "\xF0\x91\xA2\xA5" => "\xF0\x91\xA3\x85", + "\xF0\x91\xA2\xA6" => "\xF0\x91\xA3\x86", + "\xF0\x91\xA2\xA7" => "\xF0\x91\xA3\x87", + "\xF0\x91\xA2\xA8" => "\xF0\x91\xA3\x88", + "\xF0\x91\xA2\xA9" => "\xF0\x91\xA3\x89", + "\xF0\x91\xA2\xAA" => "\xF0\x91\xA3\x8A", + "\xF0\x91\xA2\xAB" => "\xF0\x91\xA3\x8B", + "\xF0\x91\xA2\xAC" => "\xF0\x91\xA3\x8C", + "\xF0\x91\xA2\xAD" => "\xF0\x91\xA3\x8D", + "\xF0\x91\xA2\xAE" => "\xF0\x91\xA3\x8E", + "\xF0\x91\xA2\xAF" => "\xF0\x91\xA3\x8F", + "\xF0\x91\xA2\xB0" => "\xF0\x91\xA3\x90", + "\xF0\x91\xA2\xB1" => "\xF0\x91\xA3\x91", + "\xF0\x91\xA2\xB2" => "\xF0\x91\xA3\x92", + "\xF0\x91\xA2\xB3" => "\xF0\x91\xA3\x93", + "\xF0\x91\xA2\xB4" => "\xF0\x91\xA3\x94", + "\xF0\x91\xA2\xB5" => "\xF0\x91\xA3\x95", + "\xF0\x91\xA2\xB6" => "\xF0\x91\xA3\x96", + "\xF0\x91\xA2\xB7" => "\xF0\x91\xA3\x97", + "\xF0\x91\xA2\xB8" => "\xF0\x91\xA3\x98", + "\xF0\x91\xA2\xB9" => "\xF0\x91\xA3\x99", + "\xF0\x91\xA2\xBA" => "\xF0\x91\xA3\x9A", + "\xF0\x91\xA2\xBB" => "\xF0\x91\xA3\x9B", + "\xF0\x91\xA2\xBC" => "\xF0\x91\xA3\x9C", + "\xF0\x91\xA2\xBD" => "\xF0\x91\xA3\x9D", + "\xF0\x91\xA2\xBE" => "\xF0\x91\xA3\x9E", + "\xF0\x91\xA2\xBF" => "\xF0\x91\xA3\x9F", + "\xF0\x96\xB9\x80" => "\xF0\x96\xB9\xA0", + "\xF0\x96\xB9\x81" => "\xF0\x96\xB9\xA1", + "\xF0\x96\xB9\x82" => "\xF0\x96\xB9\xA2", + "\xF0\x96\xB9\x83" => "\xF0\x96\xB9\xA3", + "\xF0\x96\xB9\x84" => "\xF0\x96\xB9\xA4", + "\xF0\x96\xB9\x85" => "\xF0\x96\xB9\xA5", + "\xF0\x96\xB9\x86" => "\xF0\x96\xB9\xA6", + "\xF0\x96\xB9\x87" => "\xF0\x96\xB9\xA7", + "\xF0\x96\xB9\x88" => "\xF0\x96\xB9\xA8", + "\xF0\x96\xB9\x89" => "\xF0\x96\xB9\xA9", + "\xF0\x96\xB9\x8A" => "\xF0\x96\xB9\xAA", + "\xF0\x96\xB9\x8B" => "\xF0\x96\xB9\xAB", + "\xF0\x96\xB9\x8C" => "\xF0\x96\xB9\xAC", + "\xF0\x96\xB9\x8D" => "\xF0\x96\xB9\xAD", + "\xF0\x96\xB9\x8E" => "\xF0\x96\xB9\xAE", + "\xF0\x96\xB9\x8F" => "\xF0\x96\xB9\xAF", + "\xF0\x96\xB9\x90" => "\xF0\x96\xB9\xB0", + "\xF0\x96\xB9\x91" => "\xF0\x96\xB9\xB1", + "\xF0\x96\xB9\x92" => "\xF0\x96\xB9\xB2", + "\xF0\x96\xB9\x93" => "\xF0\x96\xB9\xB3", + "\xF0\x96\xB9\x94" => "\xF0\x96\xB9\xB4", + "\xF0\x96\xB9\x95" => "\xF0\x96\xB9\xB5", + "\xF0\x96\xB9\x96" => "\xF0\x96\xB9\xB6", + "\xF0\x96\xB9\x97" => "\xF0\x96\xB9\xB7", + "\xF0\x96\xB9\x98" => "\xF0\x96\xB9\xB8", + "\xF0\x96\xB9\x99" => "\xF0\x96\xB9\xB9", + "\xF0\x96\xB9\x9A" => "\xF0\x96\xB9\xBA", + "\xF0\x96\xB9\x9B" => "\xF0\x96\xB9\xBB", + "\xF0\x96\xB9\x9C" => "\xF0\x96\xB9\xBC", + "\xF0\x96\xB9\x9D" => "\xF0\x96\xB9\xBD", + "\xF0\x96\xB9\x9E" => "\xF0\x96\xB9\xBE", + "\xF0\x96\xB9\x9F" => "\xF0\x96\xB9\xBF", + "\xF0\x9E\xA4\x80" => "\xF0\x9E\xA4\xA2", + "\xF0\x9E\xA4\x81" => "\xF0\x9E\xA4\xA3", + "\xF0\x9E\xA4\x82" => "\xF0\x9E\xA4\xA4", + "\xF0\x9E\xA4\x83" => "\xF0\x9E\xA4\xA5", + "\xF0\x9E\xA4\x84" => "\xF0\x9E\xA4\xA6", + "\xF0\x9E\xA4\x85" => "\xF0\x9E\xA4\xA7", + "\xF0\x9E\xA4\x86" => "\xF0\x9E\xA4\xA8", + "\xF0\x9E\xA4\x87" => "\xF0\x9E\xA4\xA9", + "\xF0\x9E\xA4\x88" => "\xF0\x9E\xA4\xAA", + "\xF0\x9E\xA4\x89" => "\xF0\x9E\xA4\xAB", + "\xF0\x9E\xA4\x8A" => "\xF0\x9E\xA4\xAC", + "\xF0\x9E\xA4\x8B" => "\xF0\x9E\xA4\xAD", + "\xF0\x9E\xA4\x8C" => "\xF0\x9E\xA4\xAE", + "\xF0\x9E\xA4\x8D" => "\xF0\x9E\xA4\xAF", + "\xF0\x9E\xA4\x8E" => "\xF0\x9E\xA4\xB0", + "\xF0\x9E\xA4\x8F" => "\xF0\x9E\xA4\xB1", + "\xF0\x9E\xA4\x90" => "\xF0\x9E\xA4\xB2", + "\xF0\x9E\xA4\x91" => "\xF0\x9E\xA4\xB3", + "\xF0\x9E\xA4\x92" => "\xF0\x9E\xA4\xB4", + "\xF0\x9E\xA4\x93" => "\xF0\x9E\xA4\xB5", + "\xF0\x9E\xA4\x94" => "\xF0\x9E\xA4\xB6", + "\xF0\x9E\xA4\x95" => "\xF0\x9E\xA4\xB7", + "\xF0\x9E\xA4\x96" => "\xF0\x9E\xA4\xB8", + "\xF0\x9E\xA4\x97" => "\xF0\x9E\xA4\xB9", + "\xF0\x9E\xA4\x98" => "\xF0\x9E\xA4\xBA", + "\xF0\x9E\xA4\x99" => "\xF0\x9E\xA4\xBB", + "\xF0\x9E\xA4\x9A" => "\xF0\x9E\xA4\xBC", + "\xF0\x9E\xA4\x9B" => "\xF0\x9E\xA4\xBD", + "\xF0\x9E\xA4\x9C" => "\xF0\x9E\xA4\xBE", + "\xF0\x9E\xA4\x9D" => "\xF0\x9E\xA4\xBF", + "\xF0\x9E\xA4\x9E" => "\xF0\x9E\xA5\x80", + "\xF0\x9E\xA4\x9F" => "\xF0\x9E\xA5\x81", + "\xF0\x9E\xA4\xA0" => "\xF0\x9E\xA5\x82", + "\xF0\x9E\xA4\xA1" => "\xF0\x9E\xA5\x83", + ); +} + +/** + * Helper function for utf8_strtolower. + * + * Developers: Do not update the data in this function manually. Instead, + * run "php -f other/update_unicode_data.php" on the command line. + * + * @return array Uppercase to lowercase maps. + */ +function utf8_strtolower_maps() +{ + return array( + "\x41" => "\x61", + "\x42" => "\x62", + "\x43" => "\x63", + "\x44" => "\x64", + "\x45" => "\x65", + "\x46" => "\x66", + "\x47" => "\x67", + "\x48" => "\x68", + "\x49" => "\x69", + "\x4A" => "\x6A", + "\x4B" => "\x6B", + "\x4C" => "\x6C", + "\x4D" => "\x6D", + "\x4E" => "\x6E", + "\x4F" => "\x6F", + "\x50" => "\x70", + "\x51" => "\x71", + "\x52" => "\x72", + "\x53" => "\x73", + "\x54" => "\x74", + "\x55" => "\x75", + "\x56" => "\x76", + "\x57" => "\x77", + "\x58" => "\x78", + "\x59" => "\x79", + "\x5A" => "\x7A", + "\xC3\x80" => "\xC3\xA0", + "\xC3\x81" => "\xC3\xA1", + "\xC3\x82" => "\xC3\xA2", + "\xC3\x83" => "\xC3\xA3", + "\xC3\x84" => "\xC3\xA4", + "\xC3\x85" => "\xC3\xA5", + "\xC3\x86" => "\xC3\xA6", + "\xC3\x87" => "\xC3\xA7", + "\xC3\x88" => "\xC3\xA8", + "\xC3\x89" => "\xC3\xA9", + "\xC3\x8A" => "\xC3\xAA", + "\xC3\x8B" => "\xC3\xAB", + "\xC3\x8C" => "\xC3\xAC", + "\xC3\x8D" => "\xC3\xAD", + "\xC3\x8E" => "\xC3\xAE", + "\xC3\x8F" => "\xC3\xAF", + "\xC3\x90" => "\xC3\xB0", + "\xC3\x91" => "\xC3\xB1", + "\xC3\x92" => "\xC3\xB2", + "\xC3\x93" => "\xC3\xB3", + "\xC3\x94" => "\xC3\xB4", + "\xC3\x95" => "\xC3\xB5", + "\xC3\x96" => "\xC3\xB6", + "\xC3\x98" => "\xC3\xB8", + "\xC3\x99" => "\xC3\xB9", + "\xC3\x9A" => "\xC3\xBA", + "\xC3\x9B" => "\xC3\xBB", + "\xC3\x9C" => "\xC3\xBC", + "\xC3\x9D" => "\xC3\xBD", + "\xC3\x9E" => "\xC3\xBE", + "\xC3\x9F" => "\xC3\x9F", + "\xC4\x80" => "\xC4\x81", + "\xC4\x82" => "\xC4\x83", + "\xC4\x84" => "\xC4\x85", + "\xC4\x86" => "\xC4\x87", + "\xC4\x88" => "\xC4\x89", + "\xC4\x8A" => "\xC4\x8B", + "\xC4\x8C" => "\xC4\x8D", + "\xC4\x8E" => "\xC4\x8F", + "\xC4\x90" => "\xC4\x91", + "\xC4\x92" => "\xC4\x93", + "\xC4\x94" => "\xC4\x95", + "\xC4\x96" => "\xC4\x97", + "\xC4\x98" => "\xC4\x99", + "\xC4\x9A" => "\xC4\x9B", + "\xC4\x9C" => "\xC4\x9D", + "\xC4\x9E" => "\xC4\x9F", + "\xC4\xA0" => "\xC4\xA1", + "\xC4\xA2" => "\xC4\xA3", + "\xC4\xA4" => "\xC4\xA5", + "\xC4\xA6" => "\xC4\xA7", + "\xC4\xA8" => "\xC4\xA9", + "\xC4\xAA" => "\xC4\xAB", + "\xC4\xAC" => "\xC4\xAD", + "\xC4\xAE" => "\xC4\xAF", + "\xC4\xB0" => "\x69\xCC\x87", + "\xC4\xB2" => "\xC4\xB3", + "\xC4\xB4" => "\xC4\xB5", + "\xC4\xB6" => "\xC4\xB7", + "\xC4\xB9" => "\xC4\xBA", + "\xC4\xBB" => "\xC4\xBC", + "\xC4\xBD" => "\xC4\xBE", + "\xC4\xBF" => "\xC5\x80", + "\xC5\x81" => "\xC5\x82", + "\xC5\x83" => "\xC5\x84", + "\xC5\x85" => "\xC5\x86", + "\xC5\x87" => "\xC5\x88", + "\xC5\x89" => "\xC5\x89", + "\xC5\x8A" => "\xC5\x8B", + "\xC5\x8C" => "\xC5\x8D", + "\xC5\x8E" => "\xC5\x8F", + "\xC5\x90" => "\xC5\x91", + "\xC5\x92" => "\xC5\x93", + "\xC5\x94" => "\xC5\x95", + "\xC5\x96" => "\xC5\x97", + "\xC5\x98" => "\xC5\x99", + "\xC5\x9A" => "\xC5\x9B", + "\xC5\x9C" => "\xC5\x9D", + "\xC5\x9E" => "\xC5\x9F", + "\xC5\xA0" => "\xC5\xA1", + "\xC5\xA2" => "\xC5\xA3", + "\xC5\xA4" => "\xC5\xA5", + "\xC5\xA6" => "\xC5\xA7", + "\xC5\xA8" => "\xC5\xA9", + "\xC5\xAA" => "\xC5\xAB", + "\xC5\xAC" => "\xC5\xAD", + "\xC5\xAE" => "\xC5\xAF", + "\xC5\xB0" => "\xC5\xB1", + "\xC5\xB2" => "\xC5\xB3", + "\xC5\xB4" => "\xC5\xB5", + "\xC5\xB6" => "\xC5\xB7", + "\xC5\xB8" => "\xC3\xBF", + "\xC5\xB9" => "\xC5\xBA", + "\xC5\xBB" => "\xC5\xBC", + "\xC5\xBD" => "\xC5\xBE", + "\xC6\x81" => "\xC9\x93", + "\xC6\x82" => "\xC6\x83", + "\xC6\x84" => "\xC6\x85", + "\xC6\x86" => "\xC9\x94", + "\xC6\x87" => "\xC6\x88", + "\xC6\x89" => "\xC9\x96", + "\xC6\x8A" => "\xC9\x97", + "\xC6\x8B" => "\xC6\x8C", + "\xC6\x8E" => "\xC7\x9D", + "\xC6\x8F" => "\xC9\x99", + "\xC6\x90" => "\xC9\x9B", + "\xC6\x91" => "\xC6\x92", + "\xC6\x93" => "\xC9\xA0", + "\xC6\x94" => "\xC9\xA3", + "\xC6\x96" => "\xC9\xA9", + "\xC6\x97" => "\xC9\xA8", + "\xC6\x98" => "\xC6\x99", + "\xC6\x9C" => "\xC9\xAF", + "\xC6\x9D" => "\xC9\xB2", + "\xC6\x9F" => "\xC9\xB5", + "\xC6\xA0" => "\xC6\xA1", + "\xC6\xA2" => "\xC6\xA3", + "\xC6\xA4" => "\xC6\xA5", + "\xC6\xA6" => "\xCA\x80", + "\xC6\xA7" => "\xC6\xA8", + "\xC6\xA9" => "\xCA\x83", + "\xC6\xAC" => "\xC6\xAD", + "\xC6\xAE" => "\xCA\x88", + "\xC6\xAF" => "\xC6\xB0", + "\xC6\xB1" => "\xCA\x8A", + "\xC6\xB2" => "\xCA\x8B", + "\xC6\xB3" => "\xC6\xB4", + "\xC6\xB5" => "\xC6\xB6", + "\xC6\xB7" => "\xCA\x92", + "\xC6\xB8" => "\xC6\xB9", + "\xC6\xBC" => "\xC6\xBD", + "\xC7\x84" => "\xC7\x86", + "\xC7\x85" => "\xC7\x86", + "\xC7\x87" => "\xC7\x89", + "\xC7\x88" => "\xC7\x89", + "\xC7\x8A" => "\xC7\x8C", + "\xC7\x8B" => "\xC7\x8C", + "\xC7\x8D" => "\xC7\x8E", + "\xC7\x8F" => "\xC7\x90", + "\xC7\x91" => "\xC7\x92", + "\xC7\x93" => "\xC7\x94", + "\xC7\x95" => "\xC7\x96", + "\xC7\x97" => "\xC7\x98", + "\xC7\x99" => "\xC7\x9A", + "\xC7\x9B" => "\xC7\x9C", + "\xC7\x9E" => "\xC7\x9F", + "\xC7\xA0" => "\xC7\xA1", + "\xC7\xA2" => "\xC7\xA3", + "\xC7\xA4" => "\xC7\xA5", + "\xC7\xA6" => "\xC7\xA7", + "\xC7\xA8" => "\xC7\xA9", + "\xC7\xAA" => "\xC7\xAB", + "\xC7\xAC" => "\xC7\xAD", + "\xC7\xAE" => "\xC7\xAF", + "\xC7\xB0" => "\xC7\xB0", + "\xC7\xB1" => "\xC7\xB3", + "\xC7\xB2" => "\xC7\xB3", + "\xC7\xB4" => "\xC7\xB5", + "\xC7\xB6" => "\xC6\x95", + "\xC7\xB7" => "\xC6\xBF", + "\xC7\xB8" => "\xC7\xB9", + "\xC7\xBA" => "\xC7\xBB", + "\xC7\xBC" => "\xC7\xBD", + "\xC7\xBE" => "\xC7\xBF", + "\xC8\x80" => "\xC8\x81", + "\xC8\x82" => "\xC8\x83", + "\xC8\x84" => "\xC8\x85", + "\xC8\x86" => "\xC8\x87", + "\xC8\x88" => "\xC8\x89", + "\xC8\x8A" => "\xC8\x8B", + "\xC8\x8C" => "\xC8\x8D", + "\xC8\x8E" => "\xC8\x8F", + "\xC8\x90" => "\xC8\x91", + "\xC8\x92" => "\xC8\x93", + "\xC8\x94" => "\xC8\x95", + "\xC8\x96" => "\xC8\x97", + "\xC8\x98" => "\xC8\x99", + "\xC8\x9A" => "\xC8\x9B", + "\xC8\x9C" => "\xC8\x9D", + "\xC8\x9E" => "\xC8\x9F", + "\xC8\xA0" => "\xC6\x9E", + "\xC8\xA2" => "\xC8\xA3", + "\xC8\xA4" => "\xC8\xA5", + "\xC8\xA6" => "\xC8\xA7", + "\xC8\xA8" => "\xC8\xA9", + "\xC8\xAA" => "\xC8\xAB", + "\xC8\xAC" => "\xC8\xAD", + "\xC8\xAE" => "\xC8\xAF", + "\xC8\xB0" => "\xC8\xB1", + "\xC8\xB2" => "\xC8\xB3", + "\xC8\xBA" => "\xE2\xB1\xA5", + "\xC8\xBB" => "\xC8\xBC", + "\xC8\xBD" => "\xC6\x9A", + "\xC8\xBE" => "\xE2\xB1\xA6", + "\xC9\x81" => "\xC9\x82", + "\xC9\x83" => "\xC6\x80", + "\xC9\x84" => "\xCA\x89", + "\xC9\x85" => "\xCA\x8C", + "\xC9\x86" => "\xC9\x87", + "\xC9\x88" => "\xC9\x89", + "\xC9\x8A" => "\xC9\x8B", + "\xC9\x8C" => "\xC9\x8D", + "\xC9\x8E" => "\xC9\x8F", + "\xCD\xB0" => "\xCD\xB1", + "\xCD\xB2" => "\xCD\xB3", + "\xCD\xB6" => "\xCD\xB7", + "\xCD\xBF" => "\xCF\xB3", + "\xCE\x86" => "\xCE\xAC", + "\xCE\x88" => "\xCE\xAD", + "\xCE\x89" => "\xCE\xAE", + "\xCE\x8A" => "\xCE\xAF", + "\xCE\x8C" => "\xCF\x8C", + "\xCE\x8E" => "\xCF\x8D", + "\xCE\x8F" => "\xCF\x8E", + "\xCE\x90" => "\xCE\x90", + "\xCE\x91" => "\xCE\xB1", + "\xCE\x92" => "\xCE\xB2", + "\xCE\x93" => "\xCE\xB3", + "\xCE\x94" => "\xCE\xB4", + "\xCE\x95" => "\xCE\xB5", + "\xCE\x96" => "\xCE\xB6", + "\xCE\x97" => "\xCE\xB7", + "\xCE\x98" => "\xCE\xB8", + "\xCE\x99" => "\xCE\xB9", + "\xCE\x9A" => "\xCE\xBA", + "\xCE\x9B" => "\xCE\xBB", + "\xCE\x9C" => "\xCE\xBC", + "\xCE\x9D" => "\xCE\xBD", + "\xCE\x9E" => "\xCE\xBE", + "\xCE\x9F" => "\xCE\xBF", + "\xCE\xA0" => "\xCF\x80", + "\xCE\xA1" => "\xCF\x81", + "\xCE\xA3" => "\xCF\x83", + "\xCE\xA4" => "\xCF\x84", + "\xCE\xA5" => "\xCF\x85", + "\xCE\xA6" => "\xCF\x86", + "\xCE\xA7" => "\xCF\x87", + "\xCE\xA8" => "\xCF\x88", + "\xCE\xA9" => "\xCF\x89", + "\xCE\xAA" => "\xCF\x8A", + "\xCE\xAB" => "\xCF\x8B", + "\xCE\xB0" => "\xCE\xB0", + "\xCF\x8F" => "\xCF\x97", + "\xCF\x98" => "\xCF\x99", + "\xCF\x9A" => "\xCF\x9B", + "\xCF\x9C" => "\xCF\x9D", + "\xCF\x9E" => "\xCF\x9F", + "\xCF\xA0" => "\xCF\xA1", + "\xCF\xA2" => "\xCF\xA3", + "\xCF\xA4" => "\xCF\xA5", + "\xCF\xA6" => "\xCF\xA7", + "\xCF\xA8" => "\xCF\xA9", + "\xCF\xAA" => "\xCF\xAB", + "\xCF\xAC" => "\xCF\xAD", + "\xCF\xAE" => "\xCF\xAF", + "\xCF\xB4" => "\xCE\xB8", + "\xCF\xB7" => "\xCF\xB8", + "\xCF\xB9" => "\xCF\xB2", + "\xCF\xBA" => "\xCF\xBB", + "\xCF\xBD" => "\xCD\xBB", + "\xCF\xBE" => "\xCD\xBC", + "\xCF\xBF" => "\xCD\xBD", + "\xD0\x80" => "\xD1\x90", + "\xD0\x81" => "\xD1\x91", + "\xD0\x82" => "\xD1\x92", + "\xD0\x83" => "\xD1\x93", + "\xD0\x84" => "\xD1\x94", + "\xD0\x85" => "\xD1\x95", + "\xD0\x86" => "\xD1\x96", + "\xD0\x87" => "\xD1\x97", + "\xD0\x88" => "\xD1\x98", + "\xD0\x89" => "\xD1\x99", + "\xD0\x8A" => "\xD1\x9A", + "\xD0\x8B" => "\xD1\x9B", + "\xD0\x8C" => "\xD1\x9C", + "\xD0\x8D" => "\xD1\x9D", + "\xD0\x8E" => "\xD1\x9E", + "\xD0\x8F" => "\xD1\x9F", + "\xD0\x90" => "\xD0\xB0", + "\xD0\x91" => "\xD0\xB1", + "\xD0\x92" => "\xD0\xB2", + "\xD0\x93" => "\xD0\xB3", + "\xD0\x94" => "\xD0\xB4", + "\xD0\x95" => "\xD0\xB5", + "\xD0\x96" => "\xD0\xB6", + "\xD0\x97" => "\xD0\xB7", + "\xD0\x98" => "\xD0\xB8", + "\xD0\x99" => "\xD0\xB9", + "\xD0\x9A" => "\xD0\xBA", + "\xD0\x9B" => "\xD0\xBB", + "\xD0\x9C" => "\xD0\xBC", + "\xD0\x9D" => "\xD0\xBD", + "\xD0\x9E" => "\xD0\xBE", + "\xD0\x9F" => "\xD0\xBF", + "\xD0\xA0" => "\xD1\x80", + "\xD0\xA1" => "\xD1\x81", + "\xD0\xA2" => "\xD1\x82", + "\xD0\xA3" => "\xD1\x83", + "\xD0\xA4" => "\xD1\x84", + "\xD0\xA5" => "\xD1\x85", + "\xD0\xA6" => "\xD1\x86", + "\xD0\xA7" => "\xD1\x87", + "\xD0\xA8" => "\xD1\x88", + "\xD0\xA9" => "\xD1\x89", + "\xD0\xAA" => "\xD1\x8A", + "\xD0\xAB" => "\xD1\x8B", + "\xD0\xAC" => "\xD1\x8C", + "\xD0\xAD" => "\xD1\x8D", + "\xD0\xAE" => "\xD1\x8E", + "\xD0\xAF" => "\xD1\x8F", + "\xD1\xA0" => "\xD1\xA1", + "\xD1\xA2" => "\xD1\xA3", + "\xD1\xA4" => "\xD1\xA5", + "\xD1\xA6" => "\xD1\xA7", + "\xD1\xA8" => "\xD1\xA9", + "\xD1\xAA" => "\xD1\xAB", + "\xD1\xAC" => "\xD1\xAD", + "\xD1\xAE" => "\xD1\xAF", + "\xD1\xB0" => "\xD1\xB1", + "\xD1\xB2" => "\xD1\xB3", + "\xD1\xB4" => "\xD1\xB5", + "\xD1\xB6" => "\xD1\xB7", + "\xD1\xB8" => "\xD1\xB9", + "\xD1\xBA" => "\xD1\xBB", + "\xD1\xBC" => "\xD1\xBD", + "\xD1\xBE" => "\xD1\xBF", + "\xD2\x80" => "\xD2\x81", + "\xD2\x8A" => "\xD2\x8B", + "\xD2\x8C" => "\xD2\x8D", + "\xD2\x8E" => "\xD2\x8F", + "\xD2\x90" => "\xD2\x91", + "\xD2\x92" => "\xD2\x93", + "\xD2\x94" => "\xD2\x95", + "\xD2\x96" => "\xD2\x97", + "\xD2\x98" => "\xD2\x99", + "\xD2\x9A" => "\xD2\x9B", + "\xD2\x9C" => "\xD2\x9D", + "\xD2\x9E" => "\xD2\x9F", + "\xD2\xA0" => "\xD2\xA1", + "\xD2\xA2" => "\xD2\xA3", + "\xD2\xA4" => "\xD2\xA5", + "\xD2\xA6" => "\xD2\xA7", + "\xD2\xA8" => "\xD2\xA9", + "\xD2\xAA" => "\xD2\xAB", + "\xD2\xAC" => "\xD2\xAD", + "\xD2\xAE" => "\xD2\xAF", + "\xD2\xB0" => "\xD2\xB1", + "\xD2\xB2" => "\xD2\xB3", + "\xD2\xB4" => "\xD2\xB5", + "\xD2\xB6" => "\xD2\xB7", + "\xD2\xB8" => "\xD2\xB9", + "\xD2\xBA" => "\xD2\xBB", + "\xD2\xBC" => "\xD2\xBD", + "\xD2\xBE" => "\xD2\xBF", + "\xD3\x80" => "\xD3\x8F", + "\xD3\x81" => "\xD3\x82", + "\xD3\x83" => "\xD3\x84", + "\xD3\x85" => "\xD3\x86", + "\xD3\x87" => "\xD3\x88", + "\xD3\x89" => "\xD3\x8A", + "\xD3\x8B" => "\xD3\x8C", + "\xD3\x8D" => "\xD3\x8E", + "\xD3\x90" => "\xD3\x91", + "\xD3\x92" => "\xD3\x93", + "\xD3\x94" => "\xD3\x95", + "\xD3\x96" => "\xD3\x97", + "\xD3\x98" => "\xD3\x99", + "\xD3\x9A" => "\xD3\x9B", + "\xD3\x9C" => "\xD3\x9D", + "\xD3\x9E" => "\xD3\x9F", + "\xD3\xA0" => "\xD3\xA1", + "\xD3\xA2" => "\xD3\xA3", + "\xD3\xA4" => "\xD3\xA5", + "\xD3\xA6" => "\xD3\xA7", + "\xD3\xA8" => "\xD3\xA9", + "\xD3\xAA" => "\xD3\xAB", + "\xD3\xAC" => "\xD3\xAD", + "\xD3\xAE" => "\xD3\xAF", + "\xD3\xB0" => "\xD3\xB1", + "\xD3\xB2" => "\xD3\xB3", + "\xD3\xB4" => "\xD3\xB5", + "\xD3\xB6" => "\xD3\xB7", + "\xD3\xB8" => "\xD3\xB9", + "\xD3\xBA" => "\xD3\xBB", + "\xD3\xBC" => "\xD3\xBD", + "\xD3\xBE" => "\xD3\xBF", + "\xD4\x80" => "\xD4\x81", + "\xD4\x82" => "\xD4\x83", + "\xD4\x84" => "\xD4\x85", + "\xD4\x86" => "\xD4\x87", + "\xD4\x88" => "\xD4\x89", + "\xD4\x8A" => "\xD4\x8B", + "\xD4\x8C" => "\xD4\x8D", + "\xD4\x8E" => "\xD4\x8F", + "\xD4\x90" => "\xD4\x91", + "\xD4\x92" => "\xD4\x93", + "\xD4\x94" => "\xD4\x95", + "\xD4\x96" => "\xD4\x97", + "\xD4\x98" => "\xD4\x99", + "\xD4\x9A" => "\xD4\x9B", + "\xD4\x9C" => "\xD4\x9D", + "\xD4\x9E" => "\xD4\x9F", + "\xD4\xA0" => "\xD4\xA1", + "\xD4\xA2" => "\xD4\xA3", + "\xD4\xA4" => "\xD4\xA5", + "\xD4\xA6" => "\xD4\xA7", + "\xD4\xA8" => "\xD4\xA9", + "\xD4\xAA" => "\xD4\xAB", + "\xD4\xAC" => "\xD4\xAD", + "\xD4\xAE" => "\xD4\xAF", + "\xD4\xB1" => "\xD5\xA1", + "\xD4\xB2" => "\xD5\xA2", + "\xD4\xB3" => "\xD5\xA3", + "\xD4\xB4" => "\xD5\xA4", + "\xD4\xB5" => "\xD5\xA5", + "\xD4\xB6" => "\xD5\xA6", + "\xD4\xB7" => "\xD5\xA7", + "\xD4\xB8" => "\xD5\xA8", + "\xD4\xB9" => "\xD5\xA9", + "\xD4\xBA" => "\xD5\xAA", + "\xD4\xBB" => "\xD5\xAB", + "\xD4\xBC" => "\xD5\xAC", + "\xD4\xBD" => "\xD5\xAD", + "\xD4\xBE" => "\xD5\xAE", + "\xD4\xBF" => "\xD5\xAF", + "\xD5\x80" => "\xD5\xB0", + "\xD5\x81" => "\xD5\xB1", + "\xD5\x82" => "\xD5\xB2", + "\xD5\x83" => "\xD5\xB3", + "\xD5\x84" => "\xD5\xB4", + "\xD5\x85" => "\xD5\xB5", + "\xD5\x86" => "\xD5\xB6", + "\xD5\x87" => "\xD5\xB7", + "\xD5\x88" => "\xD5\xB8", + "\xD5\x89" => "\xD5\xB9", + "\xD5\x8A" => "\xD5\xBA", + "\xD5\x8B" => "\xD5\xBB", + "\xD5\x8C" => "\xD5\xBC", + "\xD5\x8D" => "\xD5\xBD", + "\xD5\x8E" => "\xD5\xBE", + "\xD5\x8F" => "\xD5\xBF", + "\xD5\x90" => "\xD6\x80", + "\xD5\x91" => "\xD6\x81", + "\xD5\x92" => "\xD6\x82", + "\xD5\x93" => "\xD6\x83", + "\xD5\x94" => "\xD6\x84", + "\xD5\x95" => "\xD6\x85", + "\xD5\x96" => "\xD6\x86", + "\xD6\x87" => "\xD6\x87", + "\xF0\x90\x90\x80" => "\xF0\x90\x90\xA8", + "\xF0\x90\x90\x81" => "\xF0\x90\x90\xA9", + "\xF0\x90\x90\x82" => "\xF0\x90\x90\xAA", + "\xF0\x90\x90\x83" => "\xF0\x90\x90\xAB", + "\xF0\x90\x90\x84" => "\xF0\x90\x90\xAC", + "\xF0\x90\x90\x85" => "\xF0\x90\x90\xAD", + "\xF0\x90\x90\x86" => "\xF0\x90\x90\xAE", + "\xF0\x90\x90\x87" => "\xF0\x90\x90\xAF", + "\xF0\x90\x90\x88" => "\xF0\x90\x90\xB0", + "\xF0\x90\x90\x89" => "\xF0\x90\x90\xB1", + "\xF0\x90\x90\x8A" => "\xF0\x90\x90\xB2", + "\xF0\x90\x90\x8B" => "\xF0\x90\x90\xB3", + "\xF0\x90\x90\x8C" => "\xF0\x90\x90\xB4", + "\xF0\x90\x90\x8D" => "\xF0\x90\x90\xB5", + "\xF0\x90\x90\x8E" => "\xF0\x90\x90\xB6", + "\xF0\x90\x90\x8F" => "\xF0\x90\x90\xB7", + "\xF0\x90\x90\x90" => "\xF0\x90\x90\xB8", + "\xF0\x90\x90\x91" => "\xF0\x90\x90\xB9", + "\xF0\x90\x90\x92" => "\xF0\x90\x90\xBA", + "\xF0\x90\x90\x93" => "\xF0\x90\x90\xBB", + "\xF0\x90\x90\x94" => "\xF0\x90\x90\xBC", + "\xF0\x90\x90\x95" => "\xF0\x90\x90\xBD", + "\xF0\x90\x90\x96" => "\xF0\x90\x90\xBE", + "\xF0\x90\x90\x97" => "\xF0\x90\x90\xBF", + "\xF0\x90\x90\x98" => "\xF0\x90\x91\x80", + "\xF0\x90\x90\x99" => "\xF0\x90\x91\x81", + "\xF0\x90\x90\x9A" => "\xF0\x90\x91\x82", + "\xF0\x90\x90\x9B" => "\xF0\x90\x91\x83", + "\xF0\x90\x90\x9C" => "\xF0\x90\x91\x84", + "\xF0\x90\x90\x9D" => "\xF0\x90\x91\x85", + "\xF0\x90\x90\x9E" => "\xF0\x90\x91\x86", + "\xF0\x90\x90\x9F" => "\xF0\x90\x91\x87", + "\xF0\x90\x90\xA0" => "\xF0\x90\x91\x88", + "\xF0\x90\x90\xA1" => "\xF0\x90\x91\x89", + "\xF0\x90\x90\xA2" => "\xF0\x90\x91\x8A", + "\xF0\x90\x90\xA3" => "\xF0\x90\x91\x8B", + "\xF0\x90\x90\xA4" => "\xF0\x90\x91\x8C", + "\xF0\x90\x90\xA5" => "\xF0\x90\x91\x8D", + "\xF0\x90\x90\xA6" => "\xF0\x90\x91\x8E", + "\xF0\x90\x90\xA7" => "\xF0\x90\x91\x8F", + "\xF0\x90\x92\xB0" => "\xF0\x90\x93\x98", + "\xF0\x90\x92\xB1" => "\xF0\x90\x93\x99", + "\xF0\x90\x92\xB2" => "\xF0\x90\x93\x9A", + "\xF0\x90\x92\xB3" => "\xF0\x90\x93\x9B", + "\xF0\x90\x92\xB4" => "\xF0\x90\x93\x9C", + "\xF0\x90\x92\xB5" => "\xF0\x90\x93\x9D", + "\xF0\x90\x92\xB6" => "\xF0\x90\x93\x9E", + "\xF0\x90\x92\xB7" => "\xF0\x90\x93\x9F", + "\xF0\x90\x92\xB8" => "\xF0\x90\x93\xA0", + "\xF0\x90\x92\xB9" => "\xF0\x90\x93\xA1", + "\xF0\x90\x92\xBA" => "\xF0\x90\x93\xA2", + "\xF0\x90\x92\xBB" => "\xF0\x90\x93\xA3", + "\xF0\x90\x92\xBC" => "\xF0\x90\x93\xA4", + "\xF0\x90\x92\xBD" => "\xF0\x90\x93\xA5", + "\xF0\x90\x92\xBE" => "\xF0\x90\x93\xA6", + "\xF0\x90\x92\xBF" => "\xF0\x90\x93\xA7", + "\xF0\x90\x93\x80" => "\xF0\x90\x93\xA8", + "\xF0\x90\x93\x81" => "\xF0\x90\x93\xA9", + "\xF0\x90\x93\x82" => "\xF0\x90\x93\xAA", + "\xF0\x90\x93\x83" => "\xF0\x90\x93\xAB", + "\xF0\x90\x93\x84" => "\xF0\x90\x93\xAC", + "\xF0\x90\x93\x85" => "\xF0\x90\x93\xAD", + "\xF0\x90\x93\x86" => "\xF0\x90\x93\xAE", + "\xF0\x90\x93\x87" => "\xF0\x90\x93\xAF", + "\xF0\x90\x93\x88" => "\xF0\x90\x93\xB0", + "\xF0\x90\x93\x89" => "\xF0\x90\x93\xB1", + "\xF0\x90\x93\x8A" => "\xF0\x90\x93\xB2", + "\xF0\x90\x93\x8B" => "\xF0\x90\x93\xB3", + "\xF0\x90\x93\x8C" => "\xF0\x90\x93\xB4", + "\xF0\x90\x93\x8D" => "\xF0\x90\x93\xB5", + "\xF0\x90\x93\x8E" => "\xF0\x90\x93\xB6", + "\xF0\x90\x93\x8F" => "\xF0\x90\x93\xB7", + "\xF0\x90\x93\x90" => "\xF0\x90\x93\xB8", + "\xF0\x90\x93\x91" => "\xF0\x90\x93\xB9", + "\xF0\x90\x93\x92" => "\xF0\x90\x93\xBA", + "\xF0\x90\x93\x93" => "\xF0\x90\x93\xBB", + "\xF0\x90\x95\xB0" => "\xF0\x90\x96\x97", + "\xF0\x90\x95\xB1" => "\xF0\x90\x96\x98", + "\xF0\x90\x95\xB2" => "\xF0\x90\x96\x99", + "\xF0\x90\x95\xB3" => "\xF0\x90\x96\x9A", + "\xF0\x90\x95\xB4" => "\xF0\x90\x96\x9B", + "\xF0\x90\x95\xB5" => "\xF0\x90\x96\x9C", + "\xF0\x90\x95\xB6" => "\xF0\x90\x96\x9D", + "\xF0\x90\x95\xB7" => "\xF0\x90\x96\x9E", + "\xF0\x90\x95\xB8" => "\xF0\x90\x96\x9F", + "\xF0\x90\x95\xB9" => "\xF0\x90\x96\xA0", + "\xF0\x90\x95\xBA" => "\xF0\x90\x96\xA1", + "\xF0\x90\x95\xBC" => "\xF0\x90\x96\xA3", + "\xF0\x90\x95\xBD" => "\xF0\x90\x96\xA4", + "\xF0\x90\x95\xBE" => "\xF0\x90\x96\xA5", + "\xF0\x90\x95\xBF" => "\xF0\x90\x96\xA6", + "\xF0\x90\x96\x80" => "\xF0\x90\x96\xA7", + "\xF0\x90\x96\x81" => "\xF0\x90\x96\xA8", + "\xF0\x90\x96\x82" => "\xF0\x90\x96\xA9", + "\xF0\x90\x96\x83" => "\xF0\x90\x96\xAA", + "\xF0\x90\x96\x84" => "\xF0\x90\x96\xAB", + "\xF0\x90\x96\x85" => "\xF0\x90\x96\xAC", + "\xF0\x90\x96\x86" => "\xF0\x90\x96\xAD", + "\xF0\x90\x96\x87" => "\xF0\x90\x96\xAE", + "\xF0\x90\x96\x88" => "\xF0\x90\x96\xAF", + "\xF0\x90\x96\x89" => "\xF0\x90\x96\xB0", + "\xF0\x90\x96\x8A" => "\xF0\x90\x96\xB1", + "\xF0\x90\x96\x8C" => "\xF0\x90\x96\xB3", + "\xF0\x90\x96\x8D" => "\xF0\x90\x96\xB4", + "\xF0\x90\x96\x8E" => "\xF0\x90\x96\xB5", + "\xF0\x90\x96\x8F" => "\xF0\x90\x96\xB6", + "\xF0\x90\x96\x90" => "\xF0\x90\x96\xB7", + "\xF0\x90\x96\x91" => "\xF0\x90\x96\xB8", + "\xF0\x90\x96\x92" => "\xF0\x90\x96\xB9", + "\xF0\x90\x96\x94" => "\xF0\x90\x96\xBB", + "\xF0\x90\x96\x95" => "\xF0\x90\x96\xBC", + "\xE1\x82\xA0" => "\xE2\xB4\x80", + "\xE1\x82\xA1" => "\xE2\xB4\x81", + "\xE1\x82\xA2" => "\xE2\xB4\x82", + "\xE1\x82\xA3" => "\xE2\xB4\x83", + "\xE1\x82\xA4" => "\xE2\xB4\x84", + "\xE1\x82\xA5" => "\xE2\xB4\x85", + "\xE1\x82\xA6" => "\xE2\xB4\x86", + "\xE1\x82\xA7" => "\xE2\xB4\x87", + "\xE1\x82\xA8" => "\xE2\xB4\x88", + "\xE1\x82\xA9" => "\xE2\xB4\x89", + "\xE1\x82\xAA" => "\xE2\xB4\x8A", + "\xE1\x82\xAB" => "\xE2\xB4\x8B", + "\xE1\x82\xAC" => "\xE2\xB4\x8C", + "\xE1\x82\xAD" => "\xE2\xB4\x8D", + "\xE1\x82\xAE" => "\xE2\xB4\x8E", + "\xE1\x82\xAF" => "\xE2\xB4\x8F", + "\xE1\x82\xB0" => "\xE2\xB4\x90", + "\xE1\x82\xB1" => "\xE2\xB4\x91", + "\xE1\x82\xB2" => "\xE2\xB4\x92", + "\xE1\x82\xB3" => "\xE2\xB4\x93", + "\xE1\x82\xB4" => "\xE2\xB4\x94", + "\xE1\x82\xB5" => "\xE2\xB4\x95", + "\xE1\x82\xB6" => "\xE2\xB4\x96", + "\xE1\x82\xB7" => "\xE2\xB4\x97", + "\xE1\x82\xB8" => "\xE2\xB4\x98", + "\xE1\x82\xB9" => "\xE2\xB4\x99", + "\xE1\x82\xBA" => "\xE2\xB4\x9A", + "\xE1\x82\xBB" => "\xE2\xB4\x9B", + "\xE1\x82\xBC" => "\xE2\xB4\x9C", + "\xE1\x82\xBD" => "\xE2\xB4\x9D", + "\xE1\x82\xBE" => "\xE2\xB4\x9E", + "\xE1\x82\xBF" => "\xE2\xB4\x9F", + "\xE1\x83\x80" => "\xE2\xB4\xA0", + "\xE1\x83\x81" => "\xE2\xB4\xA1", + "\xE1\x83\x82" => "\xE2\xB4\xA2", + "\xE1\x83\x83" => "\xE2\xB4\xA3", + "\xE1\x83\x84" => "\xE2\xB4\xA4", + "\xE1\x83\x85" => "\xE2\xB4\xA5", + "\xE1\x83\x87" => "\xE2\xB4\xA7", + "\xF0\x90\xB2\x80" => "\xF0\x90\xB3\x80", + "\xF0\x90\xB2\x81" => "\xF0\x90\xB3\x81", + "\xF0\x90\xB2\x82" => "\xF0\x90\xB3\x82", + "\xF0\x90\xB2\x83" => "\xF0\x90\xB3\x83", + "\xF0\x90\xB2\x84" => "\xF0\x90\xB3\x84", + "\xF0\x90\xB2\x85" => "\xF0\x90\xB3\x85", + "\xF0\x90\xB2\x86" => "\xF0\x90\xB3\x86", + "\xF0\x90\xB2\x87" => "\xF0\x90\xB3\x87", + "\xF0\x90\xB2\x88" => "\xF0\x90\xB3\x88", + "\xF0\x90\xB2\x89" => "\xF0\x90\xB3\x89", + "\xF0\x90\xB2\x8A" => "\xF0\x90\xB3\x8A", + "\xF0\x90\xB2\x8B" => "\xF0\x90\xB3\x8B", + "\xF0\x90\xB2\x8C" => "\xF0\x90\xB3\x8C", + "\xF0\x90\xB2\x8D" => "\xF0\x90\xB3\x8D", + "\xF0\x90\xB2\x8E" => "\xF0\x90\xB3\x8E", + "\xF0\x90\xB2\x8F" => "\xF0\x90\xB3\x8F", + "\xF0\x90\xB2\x90" => "\xF0\x90\xB3\x90", + "\xF0\x90\xB2\x91" => "\xF0\x90\xB3\x91", + "\xF0\x90\xB2\x92" => "\xF0\x90\xB3\x92", + "\xF0\x90\xB2\x93" => "\xF0\x90\xB3\x93", + "\xF0\x90\xB2\x94" => "\xF0\x90\xB3\x94", + "\xF0\x90\xB2\x95" => "\xF0\x90\xB3\x95", + "\xF0\x90\xB2\x96" => "\xF0\x90\xB3\x96", + "\xF0\x90\xB2\x97" => "\xF0\x90\xB3\x97", + "\xF0\x90\xB2\x98" => "\xF0\x90\xB3\x98", + "\xF0\x90\xB2\x99" => "\xF0\x90\xB3\x99", + "\xF0\x90\xB2\x9A" => "\xF0\x90\xB3\x9A", + "\xF0\x90\xB2\x9B" => "\xF0\x90\xB3\x9B", + "\xF0\x90\xB2\x9C" => "\xF0\x90\xB3\x9C", + "\xF0\x90\xB2\x9D" => "\xF0\x90\xB3\x9D", + "\xF0\x90\xB2\x9E" => "\xF0\x90\xB3\x9E", + "\xF0\x90\xB2\x9F" => "\xF0\x90\xB3\x9F", + "\xF0\x90\xB2\xA0" => "\xF0\x90\xB3\xA0", + "\xF0\x90\xB2\xA1" => "\xF0\x90\xB3\xA1", + "\xF0\x90\xB2\xA2" => "\xF0\x90\xB3\xA2", + "\xF0\x90\xB2\xA3" => "\xF0\x90\xB3\xA3", + "\xF0\x90\xB2\xA4" => "\xF0\x90\xB3\xA4", + "\xF0\x90\xB2\xA5" => "\xF0\x90\xB3\xA5", + "\xF0\x90\xB2\xA6" => "\xF0\x90\xB3\xA6", + "\xF0\x90\xB2\xA7" => "\xF0\x90\xB3\xA7", + "\xF0\x90\xB2\xA8" => "\xF0\x90\xB3\xA8", + "\xF0\x90\xB2\xA9" => "\xF0\x90\xB3\xA9", + "\xF0\x90\xB2\xAA" => "\xF0\x90\xB3\xAA", + "\xF0\x90\xB2\xAB" => "\xF0\x90\xB3\xAB", + "\xF0\x90\xB2\xAC" => "\xF0\x90\xB3\xAC", + "\xF0\x90\xB2\xAD" => "\xF0\x90\xB3\xAD", + "\xF0\x90\xB2\xAE" => "\xF0\x90\xB3\xAE", + "\xF0\x90\xB2\xAF" => "\xF0\x90\xB3\xAF", + "\xF0\x90\xB2\xB0" => "\xF0\x90\xB3\xB0", + "\xF0\x90\xB2\xB1" => "\xF0\x90\xB3\xB1", + "\xF0\x90\xB2\xB2" => "\xF0\x90\xB3\xB2", + "\xE1\x83\x8D" => "\xE2\xB4\xAD", + "\xF0\x91\xA2\xA0" => "\xF0\x91\xA3\x80", + "\xF0\x91\xA2\xA1" => "\xF0\x91\xA3\x81", + "\xF0\x91\xA2\xA2" => "\xF0\x91\xA3\x82", + "\xF0\x91\xA2\xA3" => "\xF0\x91\xA3\x83", + "\xF0\x91\xA2\xA4" => "\xF0\x91\xA3\x84", + "\xF0\x91\xA2\xA5" => "\xF0\x91\xA3\x85", + "\xF0\x91\xA2\xA6" => "\xF0\x91\xA3\x86", + "\xF0\x91\xA2\xA7" => "\xF0\x91\xA3\x87", + "\xF0\x91\xA2\xA8" => "\xF0\x91\xA3\x88", + "\xF0\x91\xA2\xA9" => "\xF0\x91\xA3\x89", + "\xF0\x91\xA2\xAA" => "\xF0\x91\xA3\x8A", + "\xF0\x91\xA2\xAB" => "\xF0\x91\xA3\x8B", + "\xF0\x91\xA2\xAC" => "\xF0\x91\xA3\x8C", + "\xF0\x91\xA2\xAD" => "\xF0\x91\xA3\x8D", + "\xF0\x91\xA2\xAE" => "\xF0\x91\xA3\x8E", + "\xF0\x91\xA2\xAF" => "\xF0\x91\xA3\x8F", + "\xF0\x91\xA2\xB0" => "\xF0\x91\xA3\x90", + "\xF0\x91\xA2\xB1" => "\xF0\x91\xA3\x91", + "\xF0\x91\xA2\xB2" => "\xF0\x91\xA3\x92", + "\xF0\x91\xA2\xB3" => "\xF0\x91\xA3\x93", + "\xF0\x91\xA2\xB4" => "\xF0\x91\xA3\x94", + "\xF0\x91\xA2\xB5" => "\xF0\x91\xA3\x95", + "\xF0\x91\xA2\xB6" => "\xF0\x91\xA3\x96", + "\xF0\x91\xA2\xB7" => "\xF0\x91\xA3\x97", + "\xF0\x91\xA2\xB8" => "\xF0\x91\xA3\x98", + "\xF0\x91\xA2\xB9" => "\xF0\x91\xA3\x99", + "\xF0\x91\xA2\xBA" => "\xF0\x91\xA3\x9A", + "\xF0\x91\xA2\xBB" => "\xF0\x91\xA3\x9B", + "\xF0\x91\xA2\xBC" => "\xF0\x91\xA3\x9C", + "\xF0\x91\xA2\xBD" => "\xF0\x91\xA3\x9D", + "\xF0\x91\xA2\xBE" => "\xF0\x91\xA3\x9E", + "\xF0\x91\xA2\xBF" => "\xF0\x91\xA3\x9F", + "\xE1\x8E\xA0" => "\xEA\xAD\xB0", + "\xE1\x8E\xA1" => "\xEA\xAD\xB1", + "\xE1\x8E\xA2" => "\xEA\xAD\xB2", + "\xE1\x8E\xA3" => "\xEA\xAD\xB3", + "\xE1\x8E\xA4" => "\xEA\xAD\xB4", + "\xE1\x8E\xA5" => "\xEA\xAD\xB5", + "\xE1\x8E\xA6" => "\xEA\xAD\xB6", + "\xE1\x8E\xA7" => "\xEA\xAD\xB7", + "\xE1\x8E\xA8" => "\xEA\xAD\xB8", + "\xE1\x8E\xA9" => "\xEA\xAD\xB9", + "\xE1\x8E\xAA" => "\xEA\xAD\xBA", + "\xE1\x8E\xAB" => "\xEA\xAD\xBB", + "\xE1\x8E\xAC" => "\xEA\xAD\xBC", + "\xE1\x8E\xAD" => "\xEA\xAD\xBD", + "\xE1\x8E\xAE" => "\xEA\xAD\xBE", + "\xE1\x8E\xAF" => "\xEA\xAD\xBF", + "\xE1\x8E\xB0" => "\xEA\xAE\x80", + "\xE1\x8E\xB1" => "\xEA\xAE\x81", + "\xE1\x8E\xB2" => "\xEA\xAE\x82", + "\xE1\x8E\xB3" => "\xEA\xAE\x83", + "\xE1\x8E\xB4" => "\xEA\xAE\x84", + "\xE1\x8E\xB5" => "\xEA\xAE\x85", + "\xE1\x8E\xB6" => "\xEA\xAE\x86", + "\xE1\x8E\xB7" => "\xEA\xAE\x87", + "\xE1\x8E\xB8" => "\xEA\xAE\x88", + "\xE1\x8E\xB9" => "\xEA\xAE\x89", + "\xE1\x8E\xBA" => "\xEA\xAE\x8A", + "\xE1\x8E\xBB" => "\xEA\xAE\x8B", + "\xE1\x8E\xBC" => "\xEA\xAE\x8C", + "\xE1\x8E\xBD" => "\xEA\xAE\x8D", + "\xE1\x8E\xBE" => "\xEA\xAE\x8E", + "\xE1\x8E\xBF" => "\xEA\xAE\x8F", + "\xE1\x8F\x80" => "\xEA\xAE\x90", + "\xE1\x8F\x81" => "\xEA\xAE\x91", + "\xE1\x8F\x82" => "\xEA\xAE\x92", + "\xE1\x8F\x83" => "\xEA\xAE\x93", + "\xE1\x8F\x84" => "\xEA\xAE\x94", + "\xE1\x8F\x85" => "\xEA\xAE\x95", + "\xE1\x8F\x86" => "\xEA\xAE\x96", + "\xE1\x8F\x87" => "\xEA\xAE\x97", + "\xE1\x8F\x88" => "\xEA\xAE\x98", + "\xE1\x8F\x89" => "\xEA\xAE\x99", + "\xE1\x8F\x8A" => "\xEA\xAE\x9A", + "\xE1\x8F\x8B" => "\xEA\xAE\x9B", + "\xE1\x8F\x8C" => "\xEA\xAE\x9C", + "\xE1\x8F\x8D" => "\xEA\xAE\x9D", + "\xE1\x8F\x8E" => "\xEA\xAE\x9E", + "\xE1\x8F\x8F" => "\xEA\xAE\x9F", + "\xE1\x8F\x90" => "\xEA\xAE\xA0", + "\xE1\x8F\x91" => "\xEA\xAE\xA1", + "\xE1\x8F\x92" => "\xEA\xAE\xA2", + "\xE1\x8F\x93" => "\xEA\xAE\xA3", + "\xE1\x8F\x94" => "\xEA\xAE\xA4", + "\xE1\x8F\x95" => "\xEA\xAE\xA5", + "\xE1\x8F\x96" => "\xEA\xAE\xA6", + "\xE1\x8F\x97" => "\xEA\xAE\xA7", + "\xE1\x8F\x98" => "\xEA\xAE\xA8", + "\xE1\x8F\x99" => "\xEA\xAE\xA9", + "\xE1\x8F\x9A" => "\xEA\xAE\xAA", + "\xE1\x8F\x9B" => "\xEA\xAE\xAB", + "\xE1\x8F\x9C" => "\xEA\xAE\xAC", + "\xE1\x8F\x9D" => "\xEA\xAE\xAD", + "\xE1\x8F\x9E" => "\xEA\xAE\xAE", + "\xE1\x8F\x9F" => "\xEA\xAE\xAF", + "\xE1\x8F\xA0" => "\xEA\xAE\xB0", + "\xE1\x8F\xA1" => "\xEA\xAE\xB1", + "\xE1\x8F\xA2" => "\xEA\xAE\xB2", + "\xE1\x8F\xA3" => "\xEA\xAE\xB3", + "\xE1\x8F\xA4" => "\xEA\xAE\xB4", + "\xE1\x8F\xA5" => "\xEA\xAE\xB5", + "\xE1\x8F\xA6" => "\xEA\xAE\xB6", + "\xE1\x8F\xA7" => "\xEA\xAE\xB7", + "\xE1\x8F\xA8" => "\xEA\xAE\xB8", + "\xE1\x8F\xA9" => "\xEA\xAE\xB9", + "\xE1\x8F\xAA" => "\xEA\xAE\xBA", + "\xE1\x8F\xAB" => "\xEA\xAE\xBB", + "\xE1\x8F\xAC" => "\xEA\xAE\xBC", + "\xE1\x8F\xAD" => "\xEA\xAE\xBD", + "\xE1\x8F\xAE" => "\xEA\xAE\xBE", + "\xE1\x8F\xAF" => "\xEA\xAE\xBF", + "\xE1\x8F\xB0" => "\xE1\x8F\xB8", + "\xE1\x8F\xB1" => "\xE1\x8F\xB9", + "\xE1\x8F\xB2" => "\xE1\x8F\xBA", + "\xE1\x8F\xB3" => "\xE1\x8F\xBB", + "\xE1\x8F\xB4" => "\xE1\x8F\xBC", + "\xE1\x8F\xB5" => "\xE1\x8F\xBD", + "\xF0\x96\xB9\x80" => "\xF0\x96\xB9\xA0", + "\xF0\x96\xB9\x81" => "\xF0\x96\xB9\xA1", + "\xF0\x96\xB9\x82" => "\xF0\x96\xB9\xA2", + "\xF0\x96\xB9\x83" => "\xF0\x96\xB9\xA3", + "\xF0\x96\xB9\x84" => "\xF0\x96\xB9\xA4", + "\xF0\x96\xB9\x85" => "\xF0\x96\xB9\xA5", + "\xF0\x96\xB9\x86" => "\xF0\x96\xB9\xA6", + "\xF0\x96\xB9\x87" => "\xF0\x96\xB9\xA7", + "\xF0\x96\xB9\x88" => "\xF0\x96\xB9\xA8", + "\xF0\x96\xB9\x89" => "\xF0\x96\xB9\xA9", + "\xF0\x96\xB9\x8A" => "\xF0\x96\xB9\xAA", + "\xF0\x96\xB9\x8B" => "\xF0\x96\xB9\xAB", + "\xF0\x96\xB9\x8C" => "\xF0\x96\xB9\xAC", + "\xF0\x96\xB9\x8D" => "\xF0\x96\xB9\xAD", + "\xF0\x96\xB9\x8E" => "\xF0\x96\xB9\xAE", + "\xF0\x96\xB9\x8F" => "\xF0\x96\xB9\xAF", + "\xF0\x96\xB9\x90" => "\xF0\x96\xB9\xB0", + "\xF0\x96\xB9\x91" => "\xF0\x96\xB9\xB1", + "\xF0\x96\xB9\x92" => "\xF0\x96\xB9\xB2", + "\xF0\x96\xB9\x93" => "\xF0\x96\xB9\xB3", + "\xF0\x96\xB9\x94" => "\xF0\x96\xB9\xB4", + "\xF0\x96\xB9\x95" => "\xF0\x96\xB9\xB5", + "\xF0\x96\xB9\x96" => "\xF0\x96\xB9\xB6", + "\xF0\x96\xB9\x97" => "\xF0\x96\xB9\xB7", + "\xF0\x96\xB9\x98" => "\xF0\x96\xB9\xB8", + "\xF0\x96\xB9\x99" => "\xF0\x96\xB9\xB9", + "\xF0\x96\xB9\x9A" => "\xF0\x96\xB9\xBA", + "\xF0\x96\xB9\x9B" => "\xF0\x96\xB9\xBB", + "\xF0\x96\xB9\x9C" => "\xF0\x96\xB9\xBC", + "\xF0\x96\xB9\x9D" => "\xF0\x96\xB9\xBD", + "\xF0\x96\xB9\x9E" => "\xF0\x96\xB9\xBE", + "\xF0\x96\xB9\x9F" => "\xF0\x96\xB9\xBF", + "\xE1\xB2\x90" => "\xE1\x83\x90", + "\xE1\xB2\x91" => "\xE1\x83\x91", + "\xE1\xB2\x92" => "\xE1\x83\x92", + "\xE1\xB2\x93" => "\xE1\x83\x93", + "\xE1\xB2\x94" => "\xE1\x83\x94", + "\xE1\xB2\x95" => "\xE1\x83\x95", + "\xE1\xB2\x96" => "\xE1\x83\x96", + "\xE1\xB2\x97" => "\xE1\x83\x97", + "\xE1\xB2\x98" => "\xE1\x83\x98", + "\xE1\xB2\x99" => "\xE1\x83\x99", + "\xE1\xB2\x9A" => "\xE1\x83\x9A", + "\xE1\xB2\x9B" => "\xE1\x83\x9B", + "\xE1\xB2\x9C" => "\xE1\x83\x9C", + "\xE1\xB2\x9D" => "\xE1\x83\x9D", + "\xE1\xB2\x9E" => "\xE1\x83\x9E", + "\xE1\xB2\x9F" => "\xE1\x83\x9F", + "\xE1\xB2\xA0" => "\xE1\x83\xA0", + "\xE1\xB2\xA1" => "\xE1\x83\xA1", + "\xE1\xB2\xA2" => "\xE1\x83\xA2", + "\xE1\xB2\xA3" => "\xE1\x83\xA3", + "\xE1\xB2\xA4" => "\xE1\x83\xA4", + "\xE1\xB2\xA5" => "\xE1\x83\xA5", + "\xE1\xB2\xA6" => "\xE1\x83\xA6", + "\xE1\xB2\xA7" => "\xE1\x83\xA7", + "\xE1\xB2\xA8" => "\xE1\x83\xA8", + "\xE1\xB2\xA9" => "\xE1\x83\xA9", + "\xE1\xB2\xAA" => "\xE1\x83\xAA", + "\xE1\xB2\xAB" => "\xE1\x83\xAB", + "\xE1\xB2\xAC" => "\xE1\x83\xAC", + "\xE1\xB2\xAD" => "\xE1\x83\xAD", + "\xE1\xB2\xAE" => "\xE1\x83\xAE", + "\xE1\xB2\xAF" => "\xE1\x83\xAF", + "\xE1\xB2\xB0" => "\xE1\x83\xB0", + "\xE1\xB2\xB1" => "\xE1\x83\xB1", + "\xE1\xB2\xB2" => "\xE1\x83\xB2", + "\xE1\xB2\xB3" => "\xE1\x83\xB3", + "\xE1\xB2\xB4" => "\xE1\x83\xB4", + "\xE1\xB2\xB5" => "\xE1\x83\xB5", + "\xE1\xB2\xB6" => "\xE1\x83\xB6", + "\xE1\xB2\xB7" => "\xE1\x83\xB7", + "\xE1\xB2\xB8" => "\xE1\x83\xB8", + "\xE1\xB2\xB9" => "\xE1\x83\xB9", + "\xE1\xB2\xBA" => "\xE1\x83\xBA", + "\xE1\xB2\xBD" => "\xE1\x83\xBD", + "\xE1\xB2\xBE" => "\xE1\x83\xBE", + "\xE1\xB2\xBF" => "\xE1\x83\xBF", + "\xE1\xB8\x80" => "\xE1\xB8\x81", + "\xE1\xB8\x82" => "\xE1\xB8\x83", + "\xE1\xB8\x84" => "\xE1\xB8\x85", + "\xE1\xB8\x86" => "\xE1\xB8\x87", + "\xE1\xB8\x88" => "\xE1\xB8\x89", + "\xE1\xB8\x8A" => "\xE1\xB8\x8B", + "\xE1\xB8\x8C" => "\xE1\xB8\x8D", + "\xE1\xB8\x8E" => "\xE1\xB8\x8F", + "\xE1\xB8\x90" => "\xE1\xB8\x91", + "\xE1\xB8\x92" => "\xE1\xB8\x93", + "\xE1\xB8\x94" => "\xE1\xB8\x95", + "\xE1\xB8\x96" => "\xE1\xB8\x97", + "\xE1\xB8\x98" => "\xE1\xB8\x99", + "\xE1\xB8\x9A" => "\xE1\xB8\x9B", + "\xE1\xB8\x9C" => "\xE1\xB8\x9D", + "\xE1\xB8\x9E" => "\xE1\xB8\x9F", + "\xE1\xB8\xA0" => "\xE1\xB8\xA1", + "\xE1\xB8\xA2" => "\xE1\xB8\xA3", + "\xE1\xB8\xA4" => "\xE1\xB8\xA5", + "\xE1\xB8\xA6" => "\xE1\xB8\xA7", + "\xE1\xB8\xA8" => "\xE1\xB8\xA9", + "\xE1\xB8\xAA" => "\xE1\xB8\xAB", + "\xE1\xB8\xAC" => "\xE1\xB8\xAD", + "\xE1\xB8\xAE" => "\xE1\xB8\xAF", + "\xE1\xB8\xB0" => "\xE1\xB8\xB1", + "\xE1\xB8\xB2" => "\xE1\xB8\xB3", + "\xE1\xB8\xB4" => "\xE1\xB8\xB5", + "\xE1\xB8\xB6" => "\xE1\xB8\xB7", + "\xE1\xB8\xB8" => "\xE1\xB8\xB9", + "\xE1\xB8\xBA" => "\xE1\xB8\xBB", + "\xE1\xB8\xBC" => "\xE1\xB8\xBD", + "\xE1\xB8\xBE" => "\xE1\xB8\xBF", + "\xE1\xB9\x80" => "\xE1\xB9\x81", + "\xE1\xB9\x82" => "\xE1\xB9\x83", + "\xE1\xB9\x84" => "\xE1\xB9\x85", + "\xE1\xB9\x86" => "\xE1\xB9\x87", + "\xE1\xB9\x88" => "\xE1\xB9\x89", + "\xE1\xB9\x8A" => "\xE1\xB9\x8B", + "\xE1\xB9\x8C" => "\xE1\xB9\x8D", + "\xE1\xB9\x8E" => "\xE1\xB9\x8F", + "\xE1\xB9\x90" => "\xE1\xB9\x91", + "\xE1\xB9\x92" => "\xE1\xB9\x93", + "\xE1\xB9\x94" => "\xE1\xB9\x95", + "\xE1\xB9\x96" => "\xE1\xB9\x97", + "\xE1\xB9\x98" => "\xE1\xB9\x99", + "\xE1\xB9\x9A" => "\xE1\xB9\x9B", + "\xE1\xB9\x9C" => "\xE1\xB9\x9D", + "\xE1\xB9\x9E" => "\xE1\xB9\x9F", + "\xE1\xB9\xA0" => "\xE1\xB9\xA1", + "\xE1\xB9\xA2" => "\xE1\xB9\xA3", + "\xE1\xB9\xA4" => "\xE1\xB9\xA5", + "\xE1\xB9\xA6" => "\xE1\xB9\xA7", + "\xE1\xB9\xA8" => "\xE1\xB9\xA9", + "\xE1\xB9\xAA" => "\xE1\xB9\xAB", + "\xE1\xB9\xAC" => "\xE1\xB9\xAD", + "\xE1\xB9\xAE" => "\xE1\xB9\xAF", + "\xE1\xB9\xB0" => "\xE1\xB9\xB1", + "\xE1\xB9\xB2" => "\xE1\xB9\xB3", + "\xE1\xB9\xB4" => "\xE1\xB9\xB5", + "\xE1\xB9\xB6" => "\xE1\xB9\xB7", + "\xE1\xB9\xB8" => "\xE1\xB9\xB9", + "\xE1\xB9\xBA" => "\xE1\xB9\xBB", + "\xE1\xB9\xBC" => "\xE1\xB9\xBD", + "\xE1\xB9\xBE" => "\xE1\xB9\xBF", + "\xE1\xBA\x80" => "\xE1\xBA\x81", + "\xE1\xBA\x82" => "\xE1\xBA\x83", + "\xE1\xBA\x84" => "\xE1\xBA\x85", + "\xE1\xBA\x86" => "\xE1\xBA\x87", + "\xE1\xBA\x88" => "\xE1\xBA\x89", + "\xE1\xBA\x8A" => "\xE1\xBA\x8B", + "\xE1\xBA\x8C" => "\xE1\xBA\x8D", + "\xE1\xBA\x8E" => "\xE1\xBA\x8F", + "\xF0\x9E\xA4\x80" => "\xF0\x9E\xA4\xA2", + "\xF0\x9E\xA4\x81" => "\xF0\x9E\xA4\xA3", + "\xF0\x9E\xA4\x82" => "\xF0\x9E\xA4\xA4", + "\xF0\x9E\xA4\x83" => "\xF0\x9E\xA4\xA5", + "\xF0\x9E\xA4\x84" => "\xF0\x9E\xA4\xA6", + "\xF0\x9E\xA4\x85" => "\xF0\x9E\xA4\xA7", + "\xF0\x9E\xA4\x86" => "\xF0\x9E\xA4\xA8", + "\xF0\x9E\xA4\x87" => "\xF0\x9E\xA4\xA9", + "\xF0\x9E\xA4\x88" => "\xF0\x9E\xA4\xAA", + "\xF0\x9E\xA4\x89" => "\xF0\x9E\xA4\xAB", + "\xE1\xBA\x90" => "\xE1\xBA\x91", + "\xF0\x9E\xA4\x8A" => "\xF0\x9E\xA4\xAC", + "\xF0\x9E\xA4\x8B" => "\xF0\x9E\xA4\xAD", + "\xF0\x9E\xA4\x8C" => "\xF0\x9E\xA4\xAE", + "\xF0\x9E\xA4\x8D" => "\xF0\x9E\xA4\xAF", + "\xF0\x9E\xA4\x8E" => "\xF0\x9E\xA4\xB0", + "\xF0\x9E\xA4\x8F" => "\xF0\x9E\xA4\xB1", + "\xF0\x9E\xA4\x90" => "\xF0\x9E\xA4\xB2", + "\xF0\x9E\xA4\x91" => "\xF0\x9E\xA4\xB3", + "\xF0\x9E\xA4\x92" => "\xF0\x9E\xA4\xB4", + "\xF0\x9E\xA4\x93" => "\xF0\x9E\xA4\xB5", + "\xF0\x9E\xA4\x94" => "\xF0\x9E\xA4\xB6", + "\xF0\x9E\xA4\x95" => "\xF0\x9E\xA4\xB7", + "\xF0\x9E\xA4\x96" => "\xF0\x9E\xA4\xB8", + "\xF0\x9E\xA4\x97" => "\xF0\x9E\xA4\xB9", + "\xF0\x9E\xA4\x98" => "\xF0\x9E\xA4\xBA", + "\xF0\x9E\xA4\x99" => "\xF0\x9E\xA4\xBB", + "\xF0\x9E\xA4\x9A" => "\xF0\x9E\xA4\xBC", + "\xF0\x9E\xA4\x9B" => "\xF0\x9E\xA4\xBD", + "\xF0\x9E\xA4\x9C" => "\xF0\x9E\xA4\xBE", + "\xF0\x9E\xA4\x9D" => "\xF0\x9E\xA4\xBF", + "\xF0\x9E\xA4\x9E" => "\xF0\x9E\xA5\x80", + "\xF0\x9E\xA4\x9F" => "\xF0\x9E\xA5\x81", + "\xF0\x9E\xA4\xA0" => "\xF0\x9E\xA5\x82", + "\xF0\x9E\xA4\xA1" => "\xF0\x9E\xA5\x83", + "\xE1\xBA\x92" => "\xE1\xBA\x93", + "\xE1\xBA\x94" => "\xE1\xBA\x95", + "\xE1\xBA\x96" => "\xE1\xBA\x96", + "\xE1\xBA\x97" => "\xE1\xBA\x97", + "\xE1\xBA\x98" => "\xE1\xBA\x98", + "\xE1\xBA\x99" => "\xE1\xBA\x99", + "\xE1\xBA\x9A" => "\xE1\xBA\x9A", + "\xE1\xBA\x9E" => "\xC3\x9F", + "\xE1\xBA\xA0" => "\xE1\xBA\xA1", + "\xE1\xBA\xA2" => "\xE1\xBA\xA3", + "\xE1\xBA\xA4" => "\xE1\xBA\xA5", + "\xE1\xBA\xA6" => "\xE1\xBA\xA7", + "\xE1\xBA\xA8" => "\xE1\xBA\xA9", + "\xE1\xBA\xAA" => "\xE1\xBA\xAB", + "\xE1\xBA\xAC" => "\xE1\xBA\xAD", + "\xE1\xBA\xAE" => "\xE1\xBA\xAF", + "\xE1\xBA\xB0" => "\xE1\xBA\xB1", + "\xE1\xBA\xB2" => "\xE1\xBA\xB3", + "\xE1\xBA\xB4" => "\xE1\xBA\xB5", + "\xE1\xBA\xB6" => "\xE1\xBA\xB7", + "\xE1\xBA\xB8" => "\xE1\xBA\xB9", + "\xE1\xBA\xBA" => "\xE1\xBA\xBB", + "\xE1\xBA\xBC" => "\xE1\xBA\xBD", + "\xE1\xBA\xBE" => "\xE1\xBA\xBF", + "\xE1\xBB\x80" => "\xE1\xBB\x81", + "\xE1\xBB\x82" => "\xE1\xBB\x83", + "\xE1\xBB\x84" => "\xE1\xBB\x85", + "\xE1\xBB\x86" => "\xE1\xBB\x87", + "\xE1\xBB\x88" => "\xE1\xBB\x89", + "\xE1\xBB\x8A" => "\xE1\xBB\x8B", + "\xE1\xBB\x8C" => "\xE1\xBB\x8D", + "\xE1\xBB\x8E" => "\xE1\xBB\x8F", + "\xE1\xBB\x90" => "\xE1\xBB\x91", + "\xE1\xBB\x92" => "\xE1\xBB\x93", + "\xE1\xBB\x94" => "\xE1\xBB\x95", + "\xE1\xBB\x96" => "\xE1\xBB\x97", + "\xE1\xBB\x98" => "\xE1\xBB\x99", + "\xE1\xBB\x9A" => "\xE1\xBB\x9B", + "\xE1\xBB\x9C" => "\xE1\xBB\x9D", + "\xE1\xBB\x9E" => "\xE1\xBB\x9F", + "\xE1\xBB\xA0" => "\xE1\xBB\xA1", + "\xE1\xBB\xA2" => "\xE1\xBB\xA3", + "\xE1\xBB\xA4" => "\xE1\xBB\xA5", + "\xE1\xBB\xA6" => "\xE1\xBB\xA7", + "\xE1\xBB\xA8" => "\xE1\xBB\xA9", + "\xE1\xBB\xAA" => "\xE1\xBB\xAB", + "\xE1\xBB\xAC" => "\xE1\xBB\xAD", + "\xE1\xBB\xAE" => "\xE1\xBB\xAF", + "\xE1\xBB\xB0" => "\xE1\xBB\xB1", + "\xE1\xBB\xB2" => "\xE1\xBB\xB3", + "\xE1\xBB\xB4" => "\xE1\xBB\xB5", + "\xE1\xBB\xB6" => "\xE1\xBB\xB7", + "\xE1\xBB\xB8" => "\xE1\xBB\xB9", + "\xE1\xBB\xBA" => "\xE1\xBB\xBB", + "\xE1\xBB\xBC" => "\xE1\xBB\xBD", + "\xE1\xBB\xBE" => "\xE1\xBB\xBF", + "\xE1\xBC\x88" => "\xE1\xBC\x80", + "\xE1\xBC\x89" => "\xE1\xBC\x81", + "\xE1\xBC\x8A" => "\xE1\xBC\x82", + "\xE1\xBC\x8B" => "\xE1\xBC\x83", + "\xE1\xBC\x8C" => "\xE1\xBC\x84", + "\xE1\xBC\x8D" => "\xE1\xBC\x85", + "\xE1\xBC\x8E" => "\xE1\xBC\x86", + "\xE1\xBC\x8F" => "\xE1\xBC\x87", + "\xE1\xBC\x98" => "\xE1\xBC\x90", + "\xE1\xBC\x99" => "\xE1\xBC\x91", + "\xE1\xBC\x9A" => "\xE1\xBC\x92", + "\xE1\xBC\x9B" => "\xE1\xBC\x93", + "\xE1\xBC\x9C" => "\xE1\xBC\x94", + "\xE1\xBC\x9D" => "\xE1\xBC\x95", + "\xE1\xBC\xA8" => "\xE1\xBC\xA0", + "\xE1\xBC\xA9" => "\xE1\xBC\xA1", + "\xE1\xBC\xAA" => "\xE1\xBC\xA2", + "\xE1\xBC\xAB" => "\xE1\xBC\xA3", + "\xE1\xBC\xAC" => "\xE1\xBC\xA4", + "\xE1\xBC\xAD" => "\xE1\xBC\xA5", + "\xE1\xBC\xAE" => "\xE1\xBC\xA6", + "\xE1\xBC\xAF" => "\xE1\xBC\xA7", + "\xE1\xBC\xB8" => "\xE1\xBC\xB0", + "\xE1\xBC\xB9" => "\xE1\xBC\xB1", + "\xE1\xBC\xBA" => "\xE1\xBC\xB2", + "\xE1\xBC\xBB" => "\xE1\xBC\xB3", + "\xE1\xBC\xBC" => "\xE1\xBC\xB4", + "\xE1\xBC\xBD" => "\xE1\xBC\xB5", + "\xE1\xBC\xBE" => "\xE1\xBC\xB6", + "\xE1\xBC\xBF" => "\xE1\xBC\xB7", + "\xE1\xBD\x88" => "\xE1\xBD\x80", + "\xE1\xBD\x89" => "\xE1\xBD\x81", + "\xE1\xBD\x8A" => "\xE1\xBD\x82", + "\xE1\xBD\x8B" => "\xE1\xBD\x83", + "\xE1\xBD\x8C" => "\xE1\xBD\x84", + "\xE1\xBD\x8D" => "\xE1\xBD\x85", + "\xE1\xBD\x90" => "\xE1\xBD\x90", + "\xE1\xBD\x92" => "\xE1\xBD\x92", + "\xE1\xBD\x94" => "\xE1\xBD\x94", + "\xE1\xBD\x96" => "\xE1\xBD\x96", + "\xE1\xBD\x99" => "\xE1\xBD\x91", + "\xE1\xBD\x9B" => "\xE1\xBD\x93", + "\xE1\xBD\x9D" => "\xE1\xBD\x95", + "\xE1\xBD\x9F" => "\xE1\xBD\x97", + "\xE1\xBD\xA8" => "\xE1\xBD\xA0", + "\xE1\xBD\xA9" => "\xE1\xBD\xA1", + "\xE1\xBD\xAA" => "\xE1\xBD\xA2", + "\xE1\xBD\xAB" => "\xE1\xBD\xA3", + "\xE1\xBD\xAC" => "\xE1\xBD\xA4", + "\xE1\xBD\xAD" => "\xE1\xBD\xA5", + "\xE1\xBD\xAE" => "\xE1\xBD\xA6", + "\xE1\xBD\xAF" => "\xE1\xBD\xA7", + "\xE1\xBE\x80" => "\xE1\xBE\x80", + "\xE1\xBE\x81" => "\xE1\xBE\x81", + "\xE1\xBE\x82" => "\xE1\xBE\x82", + "\xE1\xBE\x83" => "\xE1\xBE\x83", + "\xE1\xBE\x84" => "\xE1\xBE\x84", + "\xE1\xBE\x85" => "\xE1\xBE\x85", + "\xE1\xBE\x86" => "\xE1\xBE\x86", + "\xE1\xBE\x87" => "\xE1\xBE\x87", + "\xE1\xBE\x88" => "\xE1\xBE\x80", + "\xE1\xBE\x89" => "\xE1\xBE\x81", + "\xE1\xBE\x8A" => "\xE1\xBE\x82", + "\xE1\xBE\x8B" => "\xE1\xBE\x83", + "\xE1\xBE\x8C" => "\xE1\xBE\x84", + "\xE1\xBE\x8D" => "\xE1\xBE\x85", + "\xE1\xBE\x8E" => "\xE1\xBE\x86", + "\xE1\xBE\x8F" => "\xE1\xBE\x87", + "\xE1\xBE\x90" => "\xE1\xBE\x90", + "\xE1\xBE\x91" => "\xE1\xBE\x91", + "\xE1\xBE\x92" => "\xE1\xBE\x92", + "\xE1\xBE\x93" => "\xE1\xBE\x93", + "\xE1\xBE\x94" => "\xE1\xBE\x94", + "\xE1\xBE\x95" => "\xE1\xBE\x95", + "\xE1\xBE\x96" => "\xE1\xBE\x96", + "\xE1\xBE\x97" => "\xE1\xBE\x97", + "\xE1\xBE\x98" => "\xE1\xBE\x90", + "\xE1\xBE\x99" => "\xE1\xBE\x91", + "\xE1\xBE\x9A" => "\xE1\xBE\x92", + "\xE1\xBE\x9B" => "\xE1\xBE\x93", + "\xE1\xBE\x9C" => "\xE1\xBE\x94", + "\xE1\xBE\x9D" => "\xE1\xBE\x95", + "\xE1\xBE\x9E" => "\xE1\xBE\x96", + "\xE1\xBE\x9F" => "\xE1\xBE\x97", + "\xE1\xBE\xA0" => "\xE1\xBE\xA0", + "\xE1\xBE\xA1" => "\xE1\xBE\xA1", + "\xE1\xBE\xA2" => "\xE1\xBE\xA2", + "\xE1\xBE\xA3" => "\xE1\xBE\xA3", + "\xE1\xBE\xA4" => "\xE1\xBE\xA4", + "\xE1\xBE\xA5" => "\xE1\xBE\xA5", + "\xE1\xBE\xA6" => "\xE1\xBE\xA6", + "\xE1\xBE\xA7" => "\xE1\xBE\xA7", + "\xE1\xBE\xA8" => "\xE1\xBE\xA0", + "\xE1\xBE\xA9" => "\xE1\xBE\xA1", + "\xE1\xBE\xAA" => "\xE1\xBE\xA2", + "\xE1\xBE\xAB" => "\xE1\xBE\xA3", + "\xE1\xBE\xAC" => "\xE1\xBE\xA4", + "\xE1\xBE\xAD" => "\xE1\xBE\xA5", + "\xE1\xBE\xAE" => "\xE1\xBE\xA6", + "\xE1\xBE\xAF" => "\xE1\xBE\xA7", + "\xE1\xBE\xB2" => "\xE1\xBE\xB2", + "\xE1\xBE\xB3" => "\xE1\xBE\xB3", + "\xE1\xBE\xB4" => "\xE1\xBE\xB4", + "\xE1\xBE\xB6" => "\xE1\xBE\xB6", + "\xE1\xBE\xB7" => "\xE1\xBE\xB7", + "\xE1\xBE\xB8" => "\xE1\xBE\xB0", + "\xE1\xBE\xB9" => "\xE1\xBE\xB1", + "\xE1\xBE\xBA" => "\xE1\xBD\xB0", + "\xE1\xBE\xBB" => "\xE1\xBD\xB1", + "\xE1\xBE\xBC" => "\xE1\xBE\xB3", + "\xE1\xBF\x82" => "\xE1\xBF\x82", + "\xE1\xBF\x83" => "\xE1\xBF\x83", + "\xE1\xBF\x84" => "\xE1\xBF\x84", + "\xE1\xBF\x86" => "\xE1\xBF\x86", + "\xE1\xBF\x87" => "\xE1\xBF\x87", + "\xE1\xBF\x88" => "\xE1\xBD\xB2", + "\xE1\xBF\x89" => "\xE1\xBD\xB3", + "\xE1\xBF\x8A" => "\xE1\xBD\xB4", + "\xE1\xBF\x8B" => "\xE1\xBD\xB5", + "\xE1\xBF\x8C" => "\xE1\xBF\x83", + "\xE1\xBF\x92" => "\xE1\xBF\x92", + "\xE1\xBF\x93" => "\xE1\xBF\x93", + "\xE1\xBF\x96" => "\xE1\xBF\x96", + "\xE1\xBF\x97" => "\xE1\xBF\x97", + "\xE1\xBF\x98" => "\xE1\xBF\x90", + "\xE1\xBF\x99" => "\xE1\xBF\x91", + "\xE1\xBF\x9A" => "\xE1\xBD\xB6", + "\xE1\xBF\x9B" => "\xE1\xBD\xB7", + "\xE1\xBF\xA2" => "\xE1\xBF\xA2", + "\xE1\xBF\xA3" => "\xE1\xBF\xA3", + "\xE1\xBF\xA4" => "\xE1\xBF\xA4", + "\xE1\xBF\xA6" => "\xE1\xBF\xA6", + "\xE1\xBF\xA7" => "\xE1\xBF\xA7", + "\xE1\xBF\xA8" => "\xE1\xBF\xA0", + "\xE1\xBF\xA9" => "\xE1\xBF\xA1", + "\xE1\xBF\xAA" => "\xE1\xBD\xBA", + "\xE1\xBF\xAB" => "\xE1\xBD\xBB", + "\xE1\xBF\xAC" => "\xE1\xBF\xA5", + "\xE1\xBF\xB2" => "\xE1\xBF\xB2", + "\xE1\xBF\xB3" => "\xE1\xBF\xB3", + "\xE1\xBF\xB4" => "\xE1\xBF\xB4", + "\xE1\xBF\xB6" => "\xE1\xBF\xB6", + "\xE1\xBF\xB7" => "\xE1\xBF\xB7", + "\xE1\xBF\xB8" => "\xE1\xBD\xB8", + "\xE1\xBF\xB9" => "\xE1\xBD\xB9", + "\xE1\xBF\xBA" => "\xE1\xBD\xBC", + "\xE1\xBF\xBB" => "\xE1\xBD\xBD", + "\xE1\xBF\xBC" => "\xE1\xBF\xB3", + "\xE2\x84\xA6" => "\xCF\x89", + "\xE2\x84\xAA" => "\x6B", + "\xE2\x84\xAB" => "\xC3\xA5", + "\xE2\x84\xB2" => "\xE2\x85\x8E", + "\xE2\x85\xA0" => "\xE2\x85\xB0", + "\xE2\x85\xA1" => "\xE2\x85\xB1", + "\xE2\x85\xA2" => "\xE2\x85\xB2", + "\xE2\x85\xA3" => "\xE2\x85\xB3", + "\xE2\x85\xA4" => "\xE2\x85\xB4", + "\xE2\x85\xA5" => "\xE2\x85\xB5", + "\xE2\x85\xA6" => "\xE2\x85\xB6", + "\xE2\x85\xA7" => "\xE2\x85\xB7", + "\xE2\x85\xA8" => "\xE2\x85\xB8", + "\xE2\x85\xA9" => "\xE2\x85\xB9", + "\xE2\x85\xAA" => "\xE2\x85\xBA", + "\xE2\x85\xAB" => "\xE2\x85\xBB", + "\xE2\x85\xAC" => "\xE2\x85\xBC", + "\xE2\x85\xAD" => "\xE2\x85\xBD", + "\xE2\x85\xAE" => "\xE2\x85\xBE", + "\xE2\x85\xAF" => "\xE2\x85\xBF", + "\xE2\x86\x83" => "\xE2\x86\x84", + "\xE2\x92\xB6" => "\xE2\x93\x90", + "\xE2\x92\xB7" => "\xE2\x93\x91", + "\xE2\x92\xB8" => "\xE2\x93\x92", + "\xE2\x92\xB9" => "\xE2\x93\x93", + "\xE2\x92\xBA" => "\xE2\x93\x94", + "\xE2\x92\xBB" => "\xE2\x93\x95", + "\xE2\x92\xBC" => "\xE2\x93\x96", + "\xE2\x92\xBD" => "\xE2\x93\x97", + "\xE2\x92\xBE" => "\xE2\x93\x98", + "\xE2\x92\xBF" => "\xE2\x93\x99", + "\xE2\x93\x80" => "\xE2\x93\x9A", + "\xE2\x93\x81" => "\xE2\x93\x9B", + "\xE2\x93\x82" => "\xE2\x93\x9C", + "\xE2\x93\x83" => "\xE2\x93\x9D", + "\xE2\x93\x84" => "\xE2\x93\x9E", + "\xE2\x93\x85" => "\xE2\x93\x9F", + "\xE2\x93\x86" => "\xE2\x93\xA0", + "\xE2\x93\x87" => "\xE2\x93\xA1", + "\xE2\x93\x88" => "\xE2\x93\xA2", + "\xE2\x93\x89" => "\xE2\x93\xA3", + "\xE2\x93\x8A" => "\xE2\x93\xA4", + "\xE2\x93\x8B" => "\xE2\x93\xA5", + "\xE2\x93\x8C" => "\xE2\x93\xA6", + "\xE2\x93\x8D" => "\xE2\x93\xA7", + "\xE2\x93\x8E" => "\xE2\x93\xA8", + "\xE2\x93\x8F" => "\xE2\x93\xA9", + "\xE2\xB0\x80" => "\xE2\xB0\xB0", + "\xE2\xB0\x81" => "\xE2\xB0\xB1", + "\xE2\xB0\x82" => "\xE2\xB0\xB2", + "\xE2\xB0\x83" => "\xE2\xB0\xB3", + "\xE2\xB0\x84" => "\xE2\xB0\xB4", + "\xE2\xB0\x85" => "\xE2\xB0\xB5", + "\xE2\xB0\x86" => "\xE2\xB0\xB6", + "\xE2\xB0\x87" => "\xE2\xB0\xB7", + "\xE2\xB0\x88" => "\xE2\xB0\xB8", + "\xE2\xB0\x89" => "\xE2\xB0\xB9", + "\xE2\xB0\x8A" => "\xE2\xB0\xBA", + "\xE2\xB0\x8B" => "\xE2\xB0\xBB", + "\xE2\xB0\x8C" => "\xE2\xB0\xBC", + "\xE2\xB0\x8D" => "\xE2\xB0\xBD", + "\xE2\xB0\x8E" => "\xE2\xB0\xBE", + "\xE2\xB0\x8F" => "\xE2\xB0\xBF", + "\xE2\xB0\x90" => "\xE2\xB1\x80", + "\xE2\xB0\x91" => "\xE2\xB1\x81", + "\xE2\xB0\x92" => "\xE2\xB1\x82", + "\xE2\xB0\x93" => "\xE2\xB1\x83", + "\xE2\xB0\x94" => "\xE2\xB1\x84", + "\xE2\xB0\x95" => "\xE2\xB1\x85", + "\xE2\xB0\x96" => "\xE2\xB1\x86", + "\xE2\xB0\x97" => "\xE2\xB1\x87", + "\xE2\xB0\x98" => "\xE2\xB1\x88", + "\xE2\xB0\x99" => "\xE2\xB1\x89", + "\xE2\xB0\x9A" => "\xE2\xB1\x8A", + "\xE2\xB0\x9B" => "\xE2\xB1\x8B", + "\xE2\xB0\x9C" => "\xE2\xB1\x8C", + "\xE2\xB0\x9D" => "\xE2\xB1\x8D", + "\xE2\xB0\x9E" => "\xE2\xB1\x8E", + "\xE2\xB0\x9F" => "\xE2\xB1\x8F", + "\xE2\xB0\xA0" => "\xE2\xB1\x90", + "\xE2\xB0\xA1" => "\xE2\xB1\x91", + "\xE2\xB0\xA2" => "\xE2\xB1\x92", + "\xE2\xB0\xA3" => "\xE2\xB1\x93", + "\xE2\xB0\xA4" => "\xE2\xB1\x94", + "\xE2\xB0\xA5" => "\xE2\xB1\x95", + "\xE2\xB0\xA6" => "\xE2\xB1\x96", + "\xE2\xB0\xA7" => "\xE2\xB1\x97", + "\xE2\xB0\xA8" => "\xE2\xB1\x98", + "\xE2\xB0\xA9" => "\xE2\xB1\x99", + "\xE2\xB0\xAA" => "\xE2\xB1\x9A", + "\xE2\xB0\xAB" => "\xE2\xB1\x9B", + "\xE2\xB0\xAC" => "\xE2\xB1\x9C", + "\xE2\xB0\xAD" => "\xE2\xB1\x9D", + "\xE2\xB0\xAE" => "\xE2\xB1\x9E", + "\xE2\xB0\xAF" => "\xE2\xB1\x9F", + "\xE2\xB1\xA0" => "\xE2\xB1\xA1", + "\xE2\xB1\xA2" => "\xC9\xAB", + "\xE2\xB1\xA3" => "\xE1\xB5\xBD", + "\xE2\xB1\xA4" => "\xC9\xBD", + "\xE2\xB1\xA7" => "\xE2\xB1\xA8", + "\xE2\xB1\xA9" => "\xE2\xB1\xAA", + "\xE2\xB1\xAB" => "\xE2\xB1\xAC", + "\xE2\xB1\xAD" => "\xC9\x91", + "\xE2\xB1\xAE" => "\xC9\xB1", + "\xE2\xB1\xAF" => "\xC9\x90", + "\xE2\xB1\xB0" => "\xC9\x92", + "\xE2\xB1\xB2" => "\xE2\xB1\xB3", + "\xE2\xB1\xB5" => "\xE2\xB1\xB6", + "\xE2\xB1\xBE" => "\xC8\xBF", + "\xE2\xB1\xBF" => "\xC9\x80", + "\xE2\xB2\x80" => "\xE2\xB2\x81", + "\xE2\xB2\x82" => "\xE2\xB2\x83", + "\xE2\xB2\x84" => "\xE2\xB2\x85", + "\xE2\xB2\x86" => "\xE2\xB2\x87", + "\xE2\xB2\x88" => "\xE2\xB2\x89", + "\xE2\xB2\x8A" => "\xE2\xB2\x8B", + "\xE2\xB2\x8C" => "\xE2\xB2\x8D", + "\xE2\xB2\x8E" => "\xE2\xB2\x8F", + "\xE2\xB2\x90" => "\xE2\xB2\x91", + "\xE2\xB2\x92" => "\xE2\xB2\x93", + "\xE2\xB2\x94" => "\xE2\xB2\x95", + "\xE2\xB2\x96" => "\xE2\xB2\x97", + "\xE2\xB2\x98" => "\xE2\xB2\x99", + "\xE2\xB2\x9A" => "\xE2\xB2\x9B", + "\xE2\xB2\x9C" => "\xE2\xB2\x9D", + "\xE2\xB2\x9E" => "\xE2\xB2\x9F", + "\xE2\xB2\xA0" => "\xE2\xB2\xA1", + "\xE2\xB2\xA2" => "\xE2\xB2\xA3", + "\xE2\xB2\xA4" => "\xE2\xB2\xA5", + "\xE2\xB2\xA6" => "\xE2\xB2\xA7", + "\xE2\xB2\xA8" => "\xE2\xB2\xA9", + "\xE2\xB2\xAA" => "\xE2\xB2\xAB", + "\xE2\xB2\xAC" => "\xE2\xB2\xAD", + "\xE2\xB2\xAE" => "\xE2\xB2\xAF", + "\xE2\xB2\xB0" => "\xE2\xB2\xB1", + "\xE2\xB2\xB2" => "\xE2\xB2\xB3", + "\xE2\xB2\xB4" => "\xE2\xB2\xB5", + "\xE2\xB2\xB6" => "\xE2\xB2\xB7", + "\xE2\xB2\xB8" => "\xE2\xB2\xB9", + "\xE2\xB2\xBA" => "\xE2\xB2\xBB", + "\xE2\xB2\xBC" => "\xE2\xB2\xBD", + "\xE2\xB2\xBE" => "\xE2\xB2\xBF", + "\xE2\xB3\x80" => "\xE2\xB3\x81", + "\xE2\xB3\x82" => "\xE2\xB3\x83", + "\xE2\xB3\x84" => "\xE2\xB3\x85", + "\xE2\xB3\x86" => "\xE2\xB3\x87", + "\xE2\xB3\x88" => "\xE2\xB3\x89", + "\xE2\xB3\x8A" => "\xE2\xB3\x8B", + "\xE2\xB3\x8C" => "\xE2\xB3\x8D", + "\xE2\xB3\x8E" => "\xE2\xB3\x8F", + "\xE2\xB3\x90" => "\xE2\xB3\x91", + "\xE2\xB3\x92" => "\xE2\xB3\x93", + "\xE2\xB3\x94" => "\xE2\xB3\x95", + "\xE2\xB3\x96" => "\xE2\xB3\x97", + "\xE2\xB3\x98" => "\xE2\xB3\x99", + "\xE2\xB3\x9A" => "\xE2\xB3\x9B", + "\xE2\xB3\x9C" => "\xE2\xB3\x9D", + "\xE2\xB3\x9E" => "\xE2\xB3\x9F", + "\xE2\xB3\xA0" => "\xE2\xB3\xA1", + "\xE2\xB3\xA2" => "\xE2\xB3\xA3", + "\xE2\xB3\xAB" => "\xE2\xB3\xAC", + "\xE2\xB3\xAD" => "\xE2\xB3\xAE", + "\xE2\xB3\xB2" => "\xE2\xB3\xB3", + "\xEA\x99\x80" => "\xEA\x99\x81", + "\xEA\x99\x82" => "\xEA\x99\x83", + "\xEA\x99\x84" => "\xEA\x99\x85", + "\xEA\x99\x86" => "\xEA\x99\x87", + "\xEA\x99\x88" => "\xEA\x99\x89", + "\xEA\x99\x8A" => "\xEA\x99\x8B", + "\xEA\x99\x8C" => "\xEA\x99\x8D", + "\xEA\x99\x8E" => "\xEA\x99\x8F", + "\xEA\x99\x90" => "\xEA\x99\x91", + "\xEA\x99\x92" => "\xEA\x99\x93", + "\xEA\x99\x94" => "\xEA\x99\x95", + "\xEA\x99\x96" => "\xEA\x99\x97", + "\xEA\x99\x98" => "\xEA\x99\x99", + "\xEA\x99\x9A" => "\xEA\x99\x9B", + "\xEA\x99\x9C" => "\xEA\x99\x9D", + "\xEA\x99\x9E" => "\xEA\x99\x9F", + "\xEA\x99\xA0" => "\xEA\x99\xA1", + "\xEA\x99\xA2" => "\xEA\x99\xA3", + "\xEA\x99\xA4" => "\xEA\x99\xA5", + "\xEA\x99\xA6" => "\xEA\x99\xA7", + "\xEA\x99\xA8" => "\xEA\x99\xA9", + "\xEA\x99\xAA" => "\xEA\x99\xAB", + "\xEA\x99\xAC" => "\xEA\x99\xAD", + "\xEA\x9A\x80" => "\xEA\x9A\x81", + "\xEA\x9A\x82" => "\xEA\x9A\x83", + "\xEA\x9A\x84" => "\xEA\x9A\x85", + "\xEA\x9A\x86" => "\xEA\x9A\x87", + "\xEA\x9A\x88" => "\xEA\x9A\x89", + "\xEA\x9A\x8A" => "\xEA\x9A\x8B", + "\xEA\x9A\x8C" => "\xEA\x9A\x8D", + "\xEA\x9A\x8E" => "\xEA\x9A\x8F", + "\xEA\x9A\x90" => "\xEA\x9A\x91", + "\xEA\x9A\x92" => "\xEA\x9A\x93", + "\xEA\x9A\x94" => "\xEA\x9A\x95", + "\xEA\x9A\x96" => "\xEA\x9A\x97", + "\xEA\x9A\x98" => "\xEA\x9A\x99", + "\xEA\x9A\x9A" => "\xEA\x9A\x9B", + "\xEA\x9C\xA2" => "\xEA\x9C\xA3", + "\xEA\x9C\xA4" => "\xEA\x9C\xA5", + "\xEA\x9C\xA6" => "\xEA\x9C\xA7", + "\xEA\x9C\xA8" => "\xEA\x9C\xA9", + "\xEA\x9C\xAA" => "\xEA\x9C\xAB", + "\xEA\x9C\xAC" => "\xEA\x9C\xAD", + "\xEA\x9C\xAE" => "\xEA\x9C\xAF", + "\xEA\x9C\xB2" => "\xEA\x9C\xB3", + "\xEA\x9C\xB4" => "\xEA\x9C\xB5", + "\xEA\x9C\xB6" => "\xEA\x9C\xB7", + "\xEA\x9C\xB8" => "\xEA\x9C\xB9", + "\xEA\x9C\xBA" => "\xEA\x9C\xBB", + "\xEA\x9C\xBC" => "\xEA\x9C\xBD", + "\xEA\x9C\xBE" => "\xEA\x9C\xBF", + "\xEA\x9D\x80" => "\xEA\x9D\x81", + "\xEA\x9D\x82" => "\xEA\x9D\x83", + "\xEA\x9D\x84" => "\xEA\x9D\x85", + "\xEA\x9D\x86" => "\xEA\x9D\x87", + "\xEA\x9D\x88" => "\xEA\x9D\x89", + "\xEA\x9D\x8A" => "\xEA\x9D\x8B", + "\xEA\x9D\x8C" => "\xEA\x9D\x8D", + "\xEA\x9D\x8E" => "\xEA\x9D\x8F", + "\xEA\x9D\x90" => "\xEA\x9D\x91", + "\xEA\x9D\x92" => "\xEA\x9D\x93", + "\xEA\x9D\x94" => "\xEA\x9D\x95", + "\xEA\x9D\x96" => "\xEA\x9D\x97", + "\xEA\x9D\x98" => "\xEA\x9D\x99", + "\xEA\x9D\x9A" => "\xEA\x9D\x9B", + "\xEA\x9D\x9C" => "\xEA\x9D\x9D", + "\xEA\x9D\x9E" => "\xEA\x9D\x9F", + "\xEA\x9D\xA0" => "\xEA\x9D\xA1", + "\xEA\x9D\xA2" => "\xEA\x9D\xA3", + "\xEA\x9D\xA4" => "\xEA\x9D\xA5", + "\xEA\x9D\xA6" => "\xEA\x9D\xA7", + "\xEA\x9D\xA8" => "\xEA\x9D\xA9", + "\xEA\x9D\xAA" => "\xEA\x9D\xAB", + "\xEA\x9D\xAC" => "\xEA\x9D\xAD", + "\xEA\x9D\xAE" => "\xEA\x9D\xAF", + "\xEA\x9D\xB9" => "\xEA\x9D\xBA", + "\xEA\x9D\xBB" => "\xEA\x9D\xBC", + "\xEA\x9D\xBD" => "\xE1\xB5\xB9", + "\xEA\x9D\xBE" => "\xEA\x9D\xBF", + "\xEA\x9E\x80" => "\xEA\x9E\x81", + "\xEA\x9E\x82" => "\xEA\x9E\x83", + "\xEA\x9E\x84" => "\xEA\x9E\x85", + "\xEA\x9E\x86" => "\xEA\x9E\x87", + "\xEA\x9E\x8B" => "\xEA\x9E\x8C", + "\xEA\x9E\x8D" => "\xC9\xA5", + "\xEA\x9E\x90" => "\xEA\x9E\x91", + "\xEA\x9E\x92" => "\xEA\x9E\x93", + "\xEA\x9E\x96" => "\xEA\x9E\x97", + "\xEA\x9E\x98" => "\xEA\x9E\x99", + "\xEA\x9E\x9A" => "\xEA\x9E\x9B", + "\xEA\x9E\x9C" => "\xEA\x9E\x9D", + "\xEA\x9E\x9E" => "\xEA\x9E\x9F", + "\xEA\x9E\xA0" => "\xEA\x9E\xA1", + "\xEA\x9E\xA2" => "\xEA\x9E\xA3", + "\xEA\x9E\xA4" => "\xEA\x9E\xA5", + "\xEA\x9E\xA6" => "\xEA\x9E\xA7", + "\xEA\x9E\xA8" => "\xEA\x9E\xA9", + "\xEA\x9E\xAA" => "\xC9\xA6", + "\xEA\x9E\xAB" => "\xC9\x9C", + "\xEA\x9E\xAC" => "\xC9\xA1", + "\xEA\x9E\xAD" => "\xC9\xAC", + "\xEA\x9E\xAE" => "\xC9\xAA", + "\xEA\x9E\xB0" => "\xCA\x9E", + "\xEA\x9E\xB1" => "\xCA\x87", + "\xEA\x9E\xB2" => "\xCA\x9D", + "\xEA\x9E\xB3" => "\xEA\xAD\x93", + "\xEA\x9E\xB4" => "\xEA\x9E\xB5", + "\xEA\x9E\xB6" => "\xEA\x9E\xB7", + "\xEA\x9E\xB8" => "\xEA\x9E\xB9", + "\xEA\x9E\xBA" => "\xEA\x9E\xBB", + "\xEA\x9E\xBC" => "\xEA\x9E\xBD", + "\xEA\x9E\xBE" => "\xEA\x9E\xBF", + "\xEA\x9F\x80" => "\xEA\x9F\x81", + "\xEA\x9F\x82" => "\xEA\x9F\x83", + "\xEA\x9F\x84" => "\xEA\x9E\x94", + "\xEA\x9F\x85" => "\xCA\x82", + "\xEA\x9F\x86" => "\xE1\xB6\x8E", + "\xEA\x9F\x87" => "\xEA\x9F\x88", + "\xEA\x9F\x89" => "\xEA\x9F\x8A", + "\xEA\x9F\x90" => "\xEA\x9F\x91", + "\xEA\x9F\x96" => "\xEA\x9F\x97", + "\xEA\x9F\x98" => "\xEA\x9F\x99", + "\xEA\x9F\xB5" => "\xEA\x9F\xB6", + "\xEF\xAC\x80" => "\xEF\xAC\x80", + "\xEF\xAC\x81" => "\xEF\xAC\x81", + "\xEF\xAC\x82" => "\xEF\xAC\x82", + "\xEF\xAC\x83" => "\xEF\xAC\x83", + "\xEF\xAC\x84" => "\xEF\xAC\x84", + "\xEF\xAC\x85" => "\xEF\xAC\x85", + "\xEF\xAC\x86" => "\xEF\xAC\x86", + "\xEF\xAC\x93" => "\xEF\xAC\x93", + "\xEF\xAC\x94" => "\xEF\xAC\x94", + "\xEF\xAC\x95" => "\xEF\xAC\x95", + "\xEF\xAC\x96" => "\xEF\xAC\x96", + "\xEF\xAC\x97" => "\xEF\xAC\x97", + "\xEF\xBC\xA1" => "\xEF\xBD\x81", + "\xEF\xBC\xA2" => "\xEF\xBD\x82", + "\xEF\xBC\xA3" => "\xEF\xBD\x83", + "\xEF\xBC\xA4" => "\xEF\xBD\x84", + "\xEF\xBC\xA5" => "\xEF\xBD\x85", + "\xEF\xBC\xA6" => "\xEF\xBD\x86", + "\xEF\xBC\xA7" => "\xEF\xBD\x87", + "\xEF\xBC\xA8" => "\xEF\xBD\x88", + "\xEF\xBC\xA9" => "\xEF\xBD\x89", + "\xEF\xBC\xAA" => "\xEF\xBD\x8A", + "\xEF\xBC\xAB" => "\xEF\xBD\x8B", + "\xEF\xBC\xAC" => "\xEF\xBD\x8C", + "\xEF\xBC\xAD" => "\xEF\xBD\x8D", + "\xEF\xBC\xAE" => "\xEF\xBD\x8E", + "\xEF\xBC\xAF" => "\xEF\xBD\x8F", + "\xEF\xBC\xB0" => "\xEF\xBD\x90", + "\xEF\xBC\xB1" => "\xEF\xBD\x91", + "\xEF\xBC\xB2" => "\xEF\xBD\x92", + "\xEF\xBC\xB3" => "\xEF\xBD\x93", + "\xEF\xBC\xB4" => "\xEF\xBD\x94", + "\xEF\xBC\xB5" => "\xEF\xBD\x95", + "\xEF\xBC\xB6" => "\xEF\xBD\x96", + "\xEF\xBC\xB7" => "\xEF\xBD\x97", + "\xEF\xBC\xB8" => "\xEF\xBD\x98", + "\xEF\xBC\xB9" => "\xEF\xBD\x99", + "\xEF\xBC\xBA" => "\xEF\xBD\x9A", + ); +} + +?> \ No newline at end of file diff --git a/Sources/Unicode/CaseTitle.php b/Sources/Unicode/CaseTitle.php new file mode 100644 index 0000000..492159d --- /dev/null +++ b/Sources/Unicode/CaseTitle.php @@ -0,0 +1,238 @@ + "\xC7\x85", + "\xC7\x85" => "\xC7\x85", + "\xC7\x86" => "\xC7\x85", + "\xC7\x87" => "\xC7\x88", + "\xC7\x88" => "\xC7\x88", + "\xC7\x89" => "\xC7\x88", + "\xC7\x8A" => "\xC7\x8B", + "\xC7\x8B" => "\xC7\x8B", + "\xC7\x8C" => "\xC7\x8B", + "\xC7\xB1" => "\xC7\xB2", + "\xC7\xB2" => "\xC7\xB2", + "\xC7\xB3" => "\xC7\xB2", + "\xE1\x83\x90" => "\xE1\x83\x90", + "\xE1\x83\x91" => "\xE1\x83\x91", + "\xE1\x83\x92" => "\xE1\x83\x92", + "\xE1\x83\x93" => "\xE1\x83\x93", + "\xE1\x83\x94" => "\xE1\x83\x94", + "\xE1\x83\x95" => "\xE1\x83\x95", + "\xE1\x83\x96" => "\xE1\x83\x96", + "\xE1\x83\x97" => "\xE1\x83\x97", + "\xE1\x83\x98" => "\xE1\x83\x98", + "\xE1\x83\x99" => "\xE1\x83\x99", + "\xE1\x83\x9A" => "\xE1\x83\x9A", + "\xE1\x83\x9B" => "\xE1\x83\x9B", + "\xE1\x83\x9C" => "\xE1\x83\x9C", + "\xE1\x83\x9D" => "\xE1\x83\x9D", + "\xE1\x83\x9E" => "\xE1\x83\x9E", + "\xE1\x83\x9F" => "\xE1\x83\x9F", + "\xE1\x83\xA0" => "\xE1\x83\xA0", + "\xE1\x83\xA1" => "\xE1\x83\xA1", + "\xE1\x83\xA2" => "\xE1\x83\xA2", + "\xE1\x83\xA3" => "\xE1\x83\xA3", + "\xE1\x83\xA4" => "\xE1\x83\xA4", + "\xE1\x83\xA5" => "\xE1\x83\xA5", + "\xE1\x83\xA6" => "\xE1\x83\xA6", + "\xE1\x83\xA7" => "\xE1\x83\xA7", + "\xE1\x83\xA8" => "\xE1\x83\xA8", + "\xE1\x83\xA9" => "\xE1\x83\xA9", + "\xE1\x83\xAA" => "\xE1\x83\xAA", + "\xE1\x83\xAB" => "\xE1\x83\xAB", + "\xE1\x83\xAC" => "\xE1\x83\xAC", + "\xE1\x83\xAD" => "\xE1\x83\xAD", + "\xE1\x83\xAE" => "\xE1\x83\xAE", + "\xE1\x83\xAF" => "\xE1\x83\xAF", + "\xE1\x83\xB0" => "\xE1\x83\xB0", + "\xE1\x83\xB1" => "\xE1\x83\xB1", + "\xE1\x83\xB2" => "\xE1\x83\xB2", + "\xE1\x83\xB3" => "\xE1\x83\xB3", + "\xE1\x83\xB4" => "\xE1\x83\xB4", + "\xE1\x83\xB5" => "\xE1\x83\xB5", + "\xE1\x83\xB6" => "\xE1\x83\xB6", + "\xE1\x83\xB7" => "\xE1\x83\xB7", + "\xE1\x83\xB8" => "\xE1\x83\xB8", + "\xE1\x83\xB9" => "\xE1\x83\xB9", + "\xE1\x83\xBA" => "\xE1\x83\xBA", + "\xE1\x83\xBD" => "\xE1\x83\xBD", + "\xE1\x83\xBE" => "\xE1\x83\xBE", + "\xE1\x83\xBF" => "\xE1\x83\xBF", + ); +} + +/** + * Helper function for utf8_convert_case. + * + * Developers: Do not update the data in this function manually. Instead, + * run "php -f other/update_unicode_data.php" on the command line. + * + * @return array Full title case maps. + */ +function utf8_titlecase_maps() +{ + return array( + "\xC3\x9F" => "\x53\x73", + "\xC7\x84" => "\xC7\x85", + "\xC7\x85" => "\xC7\x85", + "\xC7\x86" => "\xC7\x85", + "\xC7\x87" => "\xC7\x88", + "\xC7\x88" => "\xC7\x88", + "\xC7\x89" => "\xC7\x88", + "\xC7\x8A" => "\xC7\x8B", + "\xC7\x8B" => "\xC7\x8B", + "\xC7\x8C" => "\xC7\x8B", + "\xC7\xB1" => "\xC7\xB2", + "\xC7\xB2" => "\xC7\xB2", + "\xC7\xB3" => "\xC7\xB2", + "\xD6\x87" => "\xD4\xB5\xD6\x82", + "\xE1\x83\x90" => "\xE1\x83\x90", + "\xE1\x83\x91" => "\xE1\x83\x91", + "\xE1\x83\x92" => "\xE1\x83\x92", + "\xE1\x83\x93" => "\xE1\x83\x93", + "\xE1\x83\x94" => "\xE1\x83\x94", + "\xE1\x83\x95" => "\xE1\x83\x95", + "\xE1\x83\x96" => "\xE1\x83\x96", + "\xE1\x83\x97" => "\xE1\x83\x97", + "\xE1\x83\x98" => "\xE1\x83\x98", + "\xE1\x83\x99" => "\xE1\x83\x99", + "\xE1\x83\x9A" => "\xE1\x83\x9A", + "\xE1\x83\x9B" => "\xE1\x83\x9B", + "\xE1\x83\x9C" => "\xE1\x83\x9C", + "\xE1\x83\x9D" => "\xE1\x83\x9D", + "\xE1\x83\x9E" => "\xE1\x83\x9E", + "\xE1\x83\x9F" => "\xE1\x83\x9F", + "\xE1\x83\xA0" => "\xE1\x83\xA0", + "\xE1\x83\xA1" => "\xE1\x83\xA1", + "\xE1\x83\xA2" => "\xE1\x83\xA2", + "\xE1\x83\xA3" => "\xE1\x83\xA3", + "\xE1\x83\xA4" => "\xE1\x83\xA4", + "\xE1\x83\xA5" => "\xE1\x83\xA5", + "\xE1\x83\xA6" => "\xE1\x83\xA6", + "\xE1\x83\xA7" => "\xE1\x83\xA7", + "\xE1\x83\xA8" => "\xE1\x83\xA8", + "\xE1\x83\xA9" => "\xE1\x83\xA9", + "\xE1\x83\xAA" => "\xE1\x83\xAA", + "\xE1\x83\xAB" => "\xE1\x83\xAB", + "\xE1\x83\xAC" => "\xE1\x83\xAC", + "\xE1\x83\xAD" => "\xE1\x83\xAD", + "\xE1\x83\xAE" => "\xE1\x83\xAE", + "\xE1\x83\xAF" => "\xE1\x83\xAF", + "\xE1\x83\xB0" => "\xE1\x83\xB0", + "\xE1\x83\xB1" => "\xE1\x83\xB1", + "\xE1\x83\xB2" => "\xE1\x83\xB2", + "\xE1\x83\xB3" => "\xE1\x83\xB3", + "\xE1\x83\xB4" => "\xE1\x83\xB4", + "\xE1\x83\xB5" => "\xE1\x83\xB5", + "\xE1\x83\xB6" => "\xE1\x83\xB6", + "\xE1\x83\xB7" => "\xE1\x83\xB7", + "\xE1\x83\xB8" => "\xE1\x83\xB8", + "\xE1\x83\xB9" => "\xE1\x83\xB9", + "\xE1\x83\xBA" => "\xE1\x83\xBA", + "\xE1\x83\xBD" => "\xE1\x83\xBD", + "\xE1\x83\xBE" => "\xE1\x83\xBE", + "\xE1\x83\xBF" => "\xE1\x83\xBF", + "\xE1\xBE\x80" => "\xE1\xBE\x88", + "\xE1\xBE\x81" => "\xE1\xBE\x89", + "\xE1\xBE\x82" => "\xE1\xBE\x8A", + "\xE1\xBE\x83" => "\xE1\xBE\x8B", + "\xE1\xBE\x84" => "\xE1\xBE\x8C", + "\xE1\xBE\x85" => "\xE1\xBE\x8D", + "\xE1\xBE\x86" => "\xE1\xBE\x8E", + "\xE1\xBE\x87" => "\xE1\xBE\x8F", + "\xE1\xBE\x88" => "\xE1\xBE\x88", + "\xE1\xBE\x89" => "\xE1\xBE\x89", + "\xE1\xBE\x8A" => "\xE1\xBE\x8A", + "\xE1\xBE\x8B" => "\xE1\xBE\x8B", + "\xE1\xBE\x8C" => "\xE1\xBE\x8C", + "\xE1\xBE\x8D" => "\xE1\xBE\x8D", + "\xE1\xBE\x8E" => "\xE1\xBE\x8E", + "\xE1\xBE\x8F" => "\xE1\xBE\x8F", + "\xE1\xBE\x90" => "\xE1\xBE\x98", + "\xE1\xBE\x91" => "\xE1\xBE\x99", + "\xE1\xBE\x92" => "\xE1\xBE\x9A", + "\xE1\xBE\x93" => "\xE1\xBE\x9B", + "\xE1\xBE\x94" => "\xE1\xBE\x9C", + "\xE1\xBE\x95" => "\xE1\xBE\x9D", + "\xE1\xBE\x96" => "\xE1\xBE\x9E", + "\xE1\xBE\x97" => "\xE1\xBE\x9F", + "\xE1\xBE\x98" => "\xE1\xBE\x98", + "\xE1\xBE\x99" => "\xE1\xBE\x99", + "\xE1\xBE\x9A" => "\xE1\xBE\x9A", + "\xE1\xBE\x9B" => "\xE1\xBE\x9B", + "\xE1\xBE\x9C" => "\xE1\xBE\x9C", + "\xE1\xBE\x9D" => "\xE1\xBE\x9D", + "\xE1\xBE\x9E" => "\xE1\xBE\x9E", + "\xE1\xBE\x9F" => "\xE1\xBE\x9F", + "\xE1\xBE\xA0" => "\xE1\xBE\xA8", + "\xE1\xBE\xA1" => "\xE1\xBE\xA9", + "\xE1\xBE\xA2" => "\xE1\xBE\xAA", + "\xE1\xBE\xA3" => "\xE1\xBE\xAB", + "\xE1\xBE\xA4" => "\xE1\xBE\xAC", + "\xE1\xBE\xA5" => "\xE1\xBE\xAD", + "\xE1\xBE\xA6" => "\xE1\xBE\xAE", + "\xE1\xBE\xA7" => "\xE1\xBE\xAF", + "\xE1\xBE\xA8" => "\xE1\xBE\xA8", + "\xE1\xBE\xA9" => "\xE1\xBE\xA9", + "\xE1\xBE\xAA" => "\xE1\xBE\xAA", + "\xE1\xBE\xAB" => "\xE1\xBE\xAB", + "\xE1\xBE\xAC" => "\xE1\xBE\xAC", + "\xE1\xBE\xAD" => "\xE1\xBE\xAD", + "\xE1\xBE\xAE" => "\xE1\xBE\xAE", + "\xE1\xBE\xAF" => "\xE1\xBE\xAF", + "\xE1\xBE\xB2" => "\xE1\xBE\xBA\xCD\x85", + "\xE1\xBE\xB3" => "\xE1\xBE\xBC", + "\xE1\xBE\xB4" => "\xCE\x86\xCD\x85", + "\xE1\xBE\xB7" => "\xCE\x91\xCD\x82\xCD\x85", + "\xE1\xBE\xBC" => "\xE1\xBE\xBC", + "\xE1\xBF\x82" => "\xE1\xBF\x8A\xCD\x85", + "\xE1\xBF\x83" => "\xE1\xBF\x8C", + "\xE1\xBF\x84" => "\xCE\x89\xCD\x85", + "\xE1\xBF\x87" => "\xCE\x97\xCD\x82\xCD\x85", + "\xE1\xBF\x8C" => "\xE1\xBF\x8C", + "\xE1\xBF\xB2" => "\xE1\xBF\xBA\xCD\x85", + "\xE1\xBF\xB3" => "\xE1\xBF\xBC", + "\xE1\xBF\xB4" => "\xCE\x8F\xCD\x85", + "\xE1\xBF\xB7" => "\xCE\xA9\xCD\x82\xCD\x85", + "\xE1\xBF\xBC" => "\xE1\xBF\xBC", + "\xEF\xAC\x80" => "\x46\x66", + "\xEF\xAC\x81" => "\x46\x69", + "\xEF\xAC\x82" => "\x46\x6C", + "\xEF\xAC\x83" => "\x46\x66\x69", + "\xEF\xAC\x84" => "\x46\x66\x6C", + "\xEF\xAC\x85" => "\x53\x74", + "\xEF\xAC\x86" => "\x53\x74", + "\xEF\xAC\x93" => "\xD5\x84\xD5\xB6", + "\xEF\xAC\x94" => "\xD5\x84\xD5\xA5", + "\xEF\xAC\x95" => "\xD5\x84\xD5\xAB", + "\xEF\xAC\x96" => "\xD5\x8E\xD5\xB6", + "\xEF\xAC\x97" => "\xD5\x84\xD5\xAD", + ); +} + +?> \ No newline at end of file diff --git a/Sources/Unicode/CaseUpper.php b/Sources/Unicode/CaseUpper.php new file mode 100644 index 0000000..b545628 --- /dev/null +++ b/Sources/Unicode/CaseUpper.php @@ -0,0 +1,3021 @@ + "\x41", + "\x62" => "\x42", + "\x63" => "\x43", + "\x64" => "\x44", + "\x65" => "\x45", + "\x66" => "\x46", + "\x67" => "\x47", + "\x68" => "\x48", + "\x69" => "\x49", + "\x6A" => "\x4A", + "\x6B" => "\x4B", + "\x6C" => "\x4C", + "\x6D" => "\x4D", + "\x6E" => "\x4E", + "\x6F" => "\x4F", + "\x70" => "\x50", + "\x71" => "\x51", + "\x72" => "\x52", + "\x73" => "\x53", + "\x74" => "\x54", + "\x75" => "\x55", + "\x76" => "\x56", + "\x77" => "\x57", + "\x78" => "\x58", + "\x79" => "\x59", + "\x7A" => "\x5A", + "\xC2\xB5" => "\xCE\x9C", + "\xC3\xA0" => "\xC3\x80", + "\xC3\xA1" => "\xC3\x81", + "\xC3\xA2" => "\xC3\x82", + "\xC3\xA3" => "\xC3\x83", + "\xC3\xA4" => "\xC3\x84", + "\xC3\xA5" => "\xC3\x85", + "\xC3\xA6" => "\xC3\x86", + "\xC3\xA7" => "\xC3\x87", + "\xC3\xA8" => "\xC3\x88", + "\xC3\xA9" => "\xC3\x89", + "\xC3\xAA" => "\xC3\x8A", + "\xC3\xAB" => "\xC3\x8B", + "\xC3\xAC" => "\xC3\x8C", + "\xC3\xAD" => "\xC3\x8D", + "\xC3\xAE" => "\xC3\x8E", + "\xC3\xAF" => "\xC3\x8F", + "\xC3\xB0" => "\xC3\x90", + "\xC3\xB1" => "\xC3\x91", + "\xC3\xB2" => "\xC3\x92", + "\xC3\xB3" => "\xC3\x93", + "\xC3\xB4" => "\xC3\x94", + "\xC3\xB5" => "\xC3\x95", + "\xC3\xB6" => "\xC3\x96", + "\xC3\xB8" => "\xC3\x98", + "\xC3\xB9" => "\xC3\x99", + "\xC3\xBA" => "\xC3\x9A", + "\xC3\xBB" => "\xC3\x9B", + "\xC3\xBC" => "\xC3\x9C", + "\xC3\xBD" => "\xC3\x9D", + "\xC3\xBE" => "\xC3\x9E", + "\xC3\xBF" => "\xC5\xB8", + "\xC4\x81" => "\xC4\x80", + "\xC4\x83" => "\xC4\x82", + "\xC4\x85" => "\xC4\x84", + "\xC4\x87" => "\xC4\x86", + "\xC4\x89" => "\xC4\x88", + "\xC4\x8B" => "\xC4\x8A", + "\xC4\x8D" => "\xC4\x8C", + "\xC4\x8F" => "\xC4\x8E", + "\xC4\x91" => "\xC4\x90", + "\xC4\x93" => "\xC4\x92", + "\xC4\x95" => "\xC4\x94", + "\xC4\x97" => "\xC4\x96", + "\xC4\x99" => "\xC4\x98", + "\xC4\x9B" => "\xC4\x9A", + "\xC4\x9D" => "\xC4\x9C", + "\xC4\x9F" => "\xC4\x9E", + "\xC4\xA1" => "\xC4\xA0", + "\xC4\xA3" => "\xC4\xA2", + "\xC4\xA5" => "\xC4\xA4", + "\xC4\xA7" => "\xC4\xA6", + "\xC4\xA9" => "\xC4\xA8", + "\xC4\xAB" => "\xC4\xAA", + "\xC4\xAD" => "\xC4\xAC", + "\xC4\xAF" => "\xC4\xAE", + "\xC4\xB1" => "\x49", + "\xC4\xB3" => "\xC4\xB2", + "\xC4\xB5" => "\xC4\xB4", + "\xC4\xB7" => "\xC4\xB6", + "\xC4\xBA" => "\xC4\xB9", + "\xC4\xBC" => "\xC4\xBB", + "\xC4\xBE" => "\xC4\xBD", + "\xC5\x80" => "\xC4\xBF", + "\xC5\x82" => "\xC5\x81", + "\xC5\x84" => "\xC5\x83", + "\xC5\x86" => "\xC5\x85", + "\xC5\x88" => "\xC5\x87", + "\xC5\x8B" => "\xC5\x8A", + "\xC5\x8D" => "\xC5\x8C", + "\xC5\x8F" => "\xC5\x8E", + "\xC5\x91" => "\xC5\x90", + "\xC5\x93" => "\xC5\x92", + "\xC5\x95" => "\xC5\x94", + "\xC5\x97" => "\xC5\x96", + "\xC5\x99" => "\xC5\x98", + "\xC5\x9B" => "\xC5\x9A", + "\xC5\x9D" => "\xC5\x9C", + "\xC5\x9F" => "\xC5\x9E", + "\xC5\xA1" => "\xC5\xA0", + "\xC5\xA3" => "\xC5\xA2", + "\xC5\xA5" => "\xC5\xA4", + "\xC5\xA7" => "\xC5\xA6", + "\xC5\xA9" => "\xC5\xA8", + "\xC5\xAB" => "\xC5\xAA", + "\xC5\xAD" => "\xC5\xAC", + "\xC5\xAF" => "\xC5\xAE", + "\xC5\xB1" => "\xC5\xB0", + "\xC5\xB3" => "\xC5\xB2", + "\xC5\xB5" => "\xC5\xB4", + "\xC5\xB7" => "\xC5\xB6", + "\xC5\xBA" => "\xC5\xB9", + "\xC5\xBC" => "\xC5\xBB", + "\xC5\xBE" => "\xC5\xBD", + "\xC5\xBF" => "\x53", + "\xC6\x80" => "\xC9\x83", + "\xC6\x83" => "\xC6\x82", + "\xC6\x85" => "\xC6\x84", + "\xC6\x88" => "\xC6\x87", + "\xC6\x8C" => "\xC6\x8B", + "\xC6\x92" => "\xC6\x91", + "\xC6\x95" => "\xC7\xB6", + "\xC6\x99" => "\xC6\x98", + "\xC6\x9A" => "\xC8\xBD", + "\xC6\x9E" => "\xC8\xA0", + "\xC6\xA1" => "\xC6\xA0", + "\xC6\xA3" => "\xC6\xA2", + "\xC6\xA5" => "\xC6\xA4", + "\xC6\xA8" => "\xC6\xA7", + "\xC6\xAD" => "\xC6\xAC", + "\xC6\xB0" => "\xC6\xAF", + "\xC6\xB4" => "\xC6\xB3", + "\xC6\xB6" => "\xC6\xB5", + "\xC6\xB9" => "\xC6\xB8", + "\xC6\xBD" => "\xC6\xBC", + "\xC6\xBF" => "\xC7\xB7", + "\xC7\x85" => "\xC7\x84", + "\xC7\x86" => "\xC7\x84", + "\xC7\x88" => "\xC7\x87", + "\xC7\x89" => "\xC7\x87", + "\xC7\x8B" => "\xC7\x8A", + "\xC7\x8C" => "\xC7\x8A", + "\xC7\x8E" => "\xC7\x8D", + "\xC7\x90" => "\xC7\x8F", + "\xC7\x92" => "\xC7\x91", + "\xC7\x94" => "\xC7\x93", + "\xC7\x96" => "\xC7\x95", + "\xC7\x98" => "\xC7\x97", + "\xC7\x9A" => "\xC7\x99", + "\xC7\x9C" => "\xC7\x9B", + "\xC7\x9D" => "\xC6\x8E", + "\xC7\x9F" => "\xC7\x9E", + "\xC7\xA1" => "\xC7\xA0", + "\xC7\xA3" => "\xC7\xA2", + "\xC7\xA5" => "\xC7\xA4", + "\xC7\xA7" => "\xC7\xA6", + "\xC7\xA9" => "\xC7\xA8", + "\xC7\xAB" => "\xC7\xAA", + "\xC7\xAD" => "\xC7\xAC", + "\xC7\xAF" => "\xC7\xAE", + "\xC7\xB2" => "\xC7\xB1", + "\xC7\xB3" => "\xC7\xB1", + "\xC7\xB5" => "\xC7\xB4", + "\xC7\xB9" => "\xC7\xB8", + "\xC7\xBB" => "\xC7\xBA", + "\xC7\xBD" => "\xC7\xBC", + "\xC7\xBF" => "\xC7\xBE", + "\xC8\x81" => "\xC8\x80", + "\xC8\x83" => "\xC8\x82", + "\xC8\x85" => "\xC8\x84", + "\xC8\x87" => "\xC8\x86", + "\xC8\x89" => "\xC8\x88", + "\xC8\x8B" => "\xC8\x8A", + "\xC8\x8D" => "\xC8\x8C", + "\xC8\x8F" => "\xC8\x8E", + "\xC8\x91" => "\xC8\x90", + "\xC8\x93" => "\xC8\x92", + "\xC8\x95" => "\xC8\x94", + "\xC8\x97" => "\xC8\x96", + "\xC8\x99" => "\xC8\x98", + "\xC8\x9B" => "\xC8\x9A", + "\xC8\x9D" => "\xC8\x9C", + "\xC8\x9F" => "\xC8\x9E", + "\xC8\xA3" => "\xC8\xA2", + "\xC8\xA5" => "\xC8\xA4", + "\xC8\xA7" => "\xC8\xA6", + "\xC8\xA9" => "\xC8\xA8", + "\xC8\xAB" => "\xC8\xAA", + "\xC8\xAD" => "\xC8\xAC", + "\xC8\xAF" => "\xC8\xAE", + "\xC8\xB1" => "\xC8\xB0", + "\xC8\xB3" => "\xC8\xB2", + "\xC8\xBC" => "\xC8\xBB", + "\xC8\xBF" => "\xE2\xB1\xBE", + "\xC9\x80" => "\xE2\xB1\xBF", + "\xC9\x82" => "\xC9\x81", + "\xC9\x87" => "\xC9\x86", + "\xC9\x89" => "\xC9\x88", + "\xC9\x8B" => "\xC9\x8A", + "\xC9\x8D" => "\xC9\x8C", + "\xC9\x8F" => "\xC9\x8E", + "\xC9\x90" => "\xE2\xB1\xAF", + "\xC9\x91" => "\xE2\xB1\xAD", + "\xC9\x92" => "\xE2\xB1\xB0", + "\xC9\x93" => "\xC6\x81", + "\xC9\x94" => "\xC6\x86", + "\xC9\x96" => "\xC6\x89", + "\xC9\x97" => "\xC6\x8A", + "\xC9\x99" => "\xC6\x8F", + "\xC9\x9B" => "\xC6\x90", + "\xC9\x9C" => "\xEA\x9E\xAB", + "\xC9\xA0" => "\xC6\x93", + "\xC9\xA1" => "\xEA\x9E\xAC", + "\xC9\xA3" => "\xC6\x94", + "\xC9\xA5" => "\xEA\x9E\x8D", + "\xC9\xA6" => "\xEA\x9E\xAA", + "\xC9\xA8" => "\xC6\x97", + "\xC9\xA9" => "\xC6\x96", + "\xC9\xAA" => "\xEA\x9E\xAE", + "\xC9\xAB" => "\xE2\xB1\xA2", + "\xC9\xAC" => "\xEA\x9E\xAD", + "\xC9\xAF" => "\xC6\x9C", + "\xC9\xB1" => "\xE2\xB1\xAE", + "\xC9\xB2" => "\xC6\x9D", + "\xC9\xB5" => "\xC6\x9F", + "\xC9\xBD" => "\xE2\xB1\xA4", + "\xCA\x80" => "\xC6\xA6", + "\xCA\x82" => "\xEA\x9F\x85", + "\xCA\x83" => "\xC6\xA9", + "\xCA\x87" => "\xEA\x9E\xB1", + "\xCA\x88" => "\xC6\xAE", + "\xCA\x89" => "\xC9\x84", + "\xCA\x8A" => "\xC6\xB1", + "\xCA\x8B" => "\xC6\xB2", + "\xCA\x8C" => "\xC9\x85", + "\xCA\x92" => "\xC6\xB7", + "\xCA\x9D" => "\xEA\x9E\xB2", + "\xCA\x9E" => "\xEA\x9E\xB0", + "\xCD\x85" => "\xCE\x99", + "\xCD\xB1" => "\xCD\xB0", + "\xCD\xB3" => "\xCD\xB2", + "\xCD\xB7" => "\xCD\xB6", + "\xCD\xBB" => "\xCF\xBD", + "\xCD\xBC" => "\xCF\xBE", + "\xCD\xBD" => "\xCF\xBF", + "\xCE\xAC" => "\xCE\x86", + "\xCE\xAD" => "\xCE\x88", + "\xCE\xAE" => "\xCE\x89", + "\xCE\xAF" => "\xCE\x8A", + "\xCE\xB1" => "\xCE\x91", + "\xCE\xB2" => "\xCE\x92", + "\xCE\xB3" => "\xCE\x93", + "\xCE\xB4" => "\xCE\x94", + "\xCE\xB5" => "\xCE\x95", + "\xCE\xB6" => "\xCE\x96", + "\xCE\xB7" => "\xCE\x97", + "\xCE\xB8" => "\xCE\x98", + "\xCE\xB9" => "\xCE\x99", + "\xCE\xBA" => "\xCE\x9A", + "\xCE\xBB" => "\xCE\x9B", + "\xCE\xBC" => "\xCE\x9C", + "\xCE\xBD" => "\xCE\x9D", + "\xCE\xBE" => "\xCE\x9E", + "\xCE\xBF" => "\xCE\x9F", + "\xCF\x80" => "\xCE\xA0", + "\xCF\x81" => "\xCE\xA1", + "\xCF\x82" => "\xCE\xA3", + "\xCF\x83" => "\xCE\xA3", + "\xCF\x84" => "\xCE\xA4", + "\xCF\x85" => "\xCE\xA5", + "\xCF\x86" => "\xCE\xA6", + "\xCF\x87" => "\xCE\xA7", + "\xCF\x88" => "\xCE\xA8", + "\xCF\x89" => "\xCE\xA9", + "\xCF\x8A" => "\xCE\xAA", + "\xCF\x8B" => "\xCE\xAB", + "\xCF\x8C" => "\xCE\x8C", + "\xCF\x8D" => "\xCE\x8E", + "\xCF\x8E" => "\xCE\x8F", + "\xCF\x90" => "\xCE\x92", + "\xCF\x91" => "\xCE\x98", + "\xCF\x95" => "\xCE\xA6", + "\xCF\x96" => "\xCE\xA0", + "\xCF\x97" => "\xCF\x8F", + "\xCF\x99" => "\xCF\x98", + "\xCF\x9B" => "\xCF\x9A", + "\xCF\x9D" => "\xCF\x9C", + "\xCF\x9F" => "\xCF\x9E", + "\xCF\xA1" => "\xCF\xA0", + "\xCF\xA3" => "\xCF\xA2", + "\xCF\xA5" => "\xCF\xA4", + "\xCF\xA7" => "\xCF\xA6", + "\xCF\xA9" => "\xCF\xA8", + "\xCF\xAB" => "\xCF\xAA", + "\xCF\xAD" => "\xCF\xAC", + "\xCF\xAF" => "\xCF\xAE", + "\xCF\xB0" => "\xCE\x9A", + "\xCF\xB1" => "\xCE\xA1", + "\xCF\xB2" => "\xCF\xB9", + "\xCF\xB3" => "\xCD\xBF", + "\xCF\xB5" => "\xCE\x95", + "\xCF\xB8" => "\xCF\xB7", + "\xCF\xBB" => "\xCF\xBA", + "\xD0\xB0" => "\xD0\x90", + "\xD0\xB1" => "\xD0\x91", + "\xD0\xB2" => "\xD0\x92", + "\xD0\xB3" => "\xD0\x93", + "\xD0\xB4" => "\xD0\x94", + "\xD0\xB5" => "\xD0\x95", + "\xD0\xB6" => "\xD0\x96", + "\xD0\xB7" => "\xD0\x97", + "\xD0\xB8" => "\xD0\x98", + "\xD0\xB9" => "\xD0\x99", + "\xD0\xBA" => "\xD0\x9A", + "\xD0\xBB" => "\xD0\x9B", + "\xD0\xBC" => "\xD0\x9C", + "\xD0\xBD" => "\xD0\x9D", + "\xD0\xBE" => "\xD0\x9E", + "\xD0\xBF" => "\xD0\x9F", + "\xD1\x80" => "\xD0\xA0", + "\xD1\x81" => "\xD0\xA1", + "\xD1\x82" => "\xD0\xA2", + "\xD1\x83" => "\xD0\xA3", + "\xD1\x84" => "\xD0\xA4", + "\xD1\x85" => "\xD0\xA5", + "\xD1\x86" => "\xD0\xA6", + "\xD1\x87" => "\xD0\xA7", + "\xD1\x88" => "\xD0\xA8", + "\xD1\x89" => "\xD0\xA9", + "\xD1\x8A" => "\xD0\xAA", + "\xD1\x8B" => "\xD0\xAB", + "\xD1\x8C" => "\xD0\xAC", + "\xD1\x8D" => "\xD0\xAD", + "\xD1\x8E" => "\xD0\xAE", + "\xD1\x8F" => "\xD0\xAF", + "\xD1\x90" => "\xD0\x80", + "\xD1\x91" => "\xD0\x81", + "\xD1\x92" => "\xD0\x82", + "\xD1\x93" => "\xD0\x83", + "\xD1\x94" => "\xD0\x84", + "\xD1\x95" => "\xD0\x85", + "\xD1\x96" => "\xD0\x86", + "\xD1\x97" => "\xD0\x87", + "\xD1\x98" => "\xD0\x88", + "\xD1\x99" => "\xD0\x89", + "\xD1\x9A" => "\xD0\x8A", + "\xD1\x9B" => "\xD0\x8B", + "\xD1\x9C" => "\xD0\x8C", + "\xD1\x9D" => "\xD0\x8D", + "\xD1\x9E" => "\xD0\x8E", + "\xD1\x9F" => "\xD0\x8F", + "\xD1\xA1" => "\xD1\xA0", + "\xD1\xA3" => "\xD1\xA2", + "\xD1\xA5" => "\xD1\xA4", + "\xD1\xA7" => "\xD1\xA6", + "\xD1\xA9" => "\xD1\xA8", + "\xD1\xAB" => "\xD1\xAA", + "\xD1\xAD" => "\xD1\xAC", + "\xD1\xAF" => "\xD1\xAE", + "\xD1\xB1" => "\xD1\xB0", + "\xD1\xB3" => "\xD1\xB2", + "\xD1\xB5" => "\xD1\xB4", + "\xD1\xB7" => "\xD1\xB6", + "\xD1\xB9" => "\xD1\xB8", + "\xD1\xBB" => "\xD1\xBA", + "\xD1\xBD" => "\xD1\xBC", + "\xD1\xBF" => "\xD1\xBE", + "\xD2\x81" => "\xD2\x80", + "\xD2\x8B" => "\xD2\x8A", + "\xD2\x8D" => "\xD2\x8C", + "\xD2\x8F" => "\xD2\x8E", + "\xD2\x91" => "\xD2\x90", + "\xD2\x93" => "\xD2\x92", + "\xD2\x95" => "\xD2\x94", + "\xD2\x97" => "\xD2\x96", + "\xD2\x99" => "\xD2\x98", + "\xD2\x9B" => "\xD2\x9A", + "\xD2\x9D" => "\xD2\x9C", + "\xD2\x9F" => "\xD2\x9E", + "\xD2\xA1" => "\xD2\xA0", + "\xD2\xA3" => "\xD2\xA2", + "\xD2\xA5" => "\xD2\xA4", + "\xD2\xA7" => "\xD2\xA6", + "\xD2\xA9" => "\xD2\xA8", + "\xD2\xAB" => "\xD2\xAA", + "\xD2\xAD" => "\xD2\xAC", + "\xD2\xAF" => "\xD2\xAE", + "\xD2\xB1" => "\xD2\xB0", + "\xD2\xB3" => "\xD2\xB2", + "\xD2\xB5" => "\xD2\xB4", + "\xD2\xB7" => "\xD2\xB6", + "\xD2\xB9" => "\xD2\xB8", + "\xD2\xBB" => "\xD2\xBA", + "\xD2\xBD" => "\xD2\xBC", + "\xD2\xBF" => "\xD2\xBE", + "\xD3\x82" => "\xD3\x81", + "\xD3\x84" => "\xD3\x83", + "\xD3\x86" => "\xD3\x85", + "\xD3\x88" => "\xD3\x87", + "\xD3\x8A" => "\xD3\x89", + "\xD3\x8C" => "\xD3\x8B", + "\xD3\x8E" => "\xD3\x8D", + "\xD3\x8F" => "\xD3\x80", + "\xD3\x91" => "\xD3\x90", + "\xD3\x93" => "\xD3\x92", + "\xD3\x95" => "\xD3\x94", + "\xD3\x97" => "\xD3\x96", + "\xD3\x99" => "\xD3\x98", + "\xD3\x9B" => "\xD3\x9A", + "\xD3\x9D" => "\xD3\x9C", + "\xD3\x9F" => "\xD3\x9E", + "\xD3\xA1" => "\xD3\xA0", + "\xD3\xA3" => "\xD3\xA2", + "\xD3\xA5" => "\xD3\xA4", + "\xD3\xA7" => "\xD3\xA6", + "\xD3\xA9" => "\xD3\xA8", + "\xD3\xAB" => "\xD3\xAA", + "\xD3\xAD" => "\xD3\xAC", + "\xD3\xAF" => "\xD3\xAE", + "\xD3\xB1" => "\xD3\xB0", + "\xD3\xB3" => "\xD3\xB2", + "\xD3\xB5" => "\xD3\xB4", + "\xD3\xB7" => "\xD3\xB6", + "\xD3\xB9" => "\xD3\xB8", + "\xD3\xBB" => "\xD3\xBA", + "\xD3\xBD" => "\xD3\xBC", + "\xD3\xBF" => "\xD3\xBE", + "\xD4\x81" => "\xD4\x80", + "\xD4\x83" => "\xD4\x82", + "\xD4\x85" => "\xD4\x84", + "\xD4\x87" => "\xD4\x86", + "\xD4\x89" => "\xD4\x88", + "\xD4\x8B" => "\xD4\x8A", + "\xD4\x8D" => "\xD4\x8C", + "\xD4\x8F" => "\xD4\x8E", + "\xD4\x91" => "\xD4\x90", + "\xD4\x93" => "\xD4\x92", + "\xD4\x95" => "\xD4\x94", + "\xD4\x97" => "\xD4\x96", + "\xD4\x99" => "\xD4\x98", + "\xD4\x9B" => "\xD4\x9A", + "\xD4\x9D" => "\xD4\x9C", + "\xD4\x9F" => "\xD4\x9E", + "\xD4\xA1" => "\xD4\xA0", + "\xD4\xA3" => "\xD4\xA2", + "\xD4\xA5" => "\xD4\xA4", + "\xD4\xA7" => "\xD4\xA6", + "\xD4\xA9" => "\xD4\xA8", + "\xD4\xAB" => "\xD4\xAA", + "\xD4\xAD" => "\xD4\xAC", + "\xD4\xAF" => "\xD4\xAE", + "\xD5\xA1" => "\xD4\xB1", + "\xD5\xA2" => "\xD4\xB2", + "\xD5\xA3" => "\xD4\xB3", + "\xD5\xA4" => "\xD4\xB4", + "\xD5\xA5" => "\xD4\xB5", + "\xD5\xA6" => "\xD4\xB6", + "\xD5\xA7" => "\xD4\xB7", + "\xD5\xA8" => "\xD4\xB8", + "\xD5\xA9" => "\xD4\xB9", + "\xD5\xAA" => "\xD4\xBA", + "\xD5\xAB" => "\xD4\xBB", + "\xD5\xAC" => "\xD4\xBC", + "\xD5\xAD" => "\xD4\xBD", + "\xD5\xAE" => "\xD4\xBE", + "\xD5\xAF" => "\xD4\xBF", + "\xD5\xB0" => "\xD5\x80", + "\xD5\xB1" => "\xD5\x81", + "\xD5\xB2" => "\xD5\x82", + "\xD5\xB3" => "\xD5\x83", + "\xD5\xB4" => "\xD5\x84", + "\xD5\xB5" => "\xD5\x85", + "\xD5\xB6" => "\xD5\x86", + "\xD5\xB7" => "\xD5\x87", + "\xD5\xB8" => "\xD5\x88", + "\xD5\xB9" => "\xD5\x89", + "\xD5\xBA" => "\xD5\x8A", + "\xD5\xBB" => "\xD5\x8B", + "\xD5\xBC" => "\xD5\x8C", + "\xD5\xBD" => "\xD5\x8D", + "\xD5\xBE" => "\xD5\x8E", + "\xD5\xBF" => "\xD5\x8F", + "\xD6\x80" => "\xD5\x90", + "\xD6\x81" => "\xD5\x91", + "\xD6\x82" => "\xD5\x92", + "\xD6\x83" => "\xD5\x93", + "\xD6\x84" => "\xD5\x94", + "\xD6\x85" => "\xD5\x95", + "\xD6\x86" => "\xD5\x96", + "\xE1\x83\x90" => "\xE1\xB2\x90", + "\xE1\x83\x91" => "\xE1\xB2\x91", + "\xE1\x83\x92" => "\xE1\xB2\x92", + "\xE1\x83\x93" => "\xE1\xB2\x93", + "\xE1\x83\x94" => "\xE1\xB2\x94", + "\xE1\x83\x95" => "\xE1\xB2\x95", + "\xE1\x83\x96" => "\xE1\xB2\x96", + "\xE1\x83\x97" => "\xE1\xB2\x97", + "\xE1\x83\x98" => "\xE1\xB2\x98", + "\xE1\x83\x99" => "\xE1\xB2\x99", + "\xE1\x83\x9A" => "\xE1\xB2\x9A", + "\xE1\x83\x9B" => "\xE1\xB2\x9B", + "\xE1\x83\x9C" => "\xE1\xB2\x9C", + "\xE1\x83\x9D" => "\xE1\xB2\x9D", + "\xE1\x83\x9E" => "\xE1\xB2\x9E", + "\xE1\x83\x9F" => "\xE1\xB2\x9F", + "\xE1\x83\xA0" => "\xE1\xB2\xA0", + "\xE1\x83\xA1" => "\xE1\xB2\xA1", + "\xE1\x83\xA2" => "\xE1\xB2\xA2", + "\xE1\x83\xA3" => "\xE1\xB2\xA3", + "\xE1\x83\xA4" => "\xE1\xB2\xA4", + "\xE1\x83\xA5" => "\xE1\xB2\xA5", + "\xE1\x83\xA6" => "\xE1\xB2\xA6", + "\xE1\x83\xA7" => "\xE1\xB2\xA7", + "\xE1\x83\xA8" => "\xE1\xB2\xA8", + "\xE1\x83\xA9" => "\xE1\xB2\xA9", + "\xE1\x83\xAA" => "\xE1\xB2\xAA", + "\xE1\x83\xAB" => "\xE1\xB2\xAB", + "\xE1\x83\xAC" => "\xE1\xB2\xAC", + "\xE1\x83\xAD" => "\xE1\xB2\xAD", + "\xE1\x83\xAE" => "\xE1\xB2\xAE", + "\xE1\x83\xAF" => "\xE1\xB2\xAF", + "\xE1\x83\xB0" => "\xE1\xB2\xB0", + "\xE1\x83\xB1" => "\xE1\xB2\xB1", + "\xE1\x83\xB2" => "\xE1\xB2\xB2", + "\xE1\x83\xB3" => "\xE1\xB2\xB3", + "\xE1\x83\xB4" => "\xE1\xB2\xB4", + "\xE1\x83\xB5" => "\xE1\xB2\xB5", + "\xE1\x83\xB6" => "\xE1\xB2\xB6", + "\xE1\x83\xB7" => "\xE1\xB2\xB7", + "\xE1\x83\xB8" => "\xE1\xB2\xB8", + "\xE1\x83\xB9" => "\xE1\xB2\xB9", + "\xE1\x83\xBA" => "\xE1\xB2\xBA", + "\xE1\x83\xBD" => "\xE1\xB2\xBD", + "\xE1\x83\xBE" => "\xE1\xB2\xBE", + "\xE1\x83\xBF" => "\xE1\xB2\xBF", + "\xE1\x8F\xB8" => "\xE1\x8F\xB0", + "\xE1\x8F\xB9" => "\xE1\x8F\xB1", + "\xE1\x8F\xBA" => "\xE1\x8F\xB2", + "\xE1\x8F\xBB" => "\xE1\x8F\xB3", + "\xE1\x8F\xBC" => "\xE1\x8F\xB4", + "\xE1\x8F\xBD" => "\xE1\x8F\xB5", + "\xE1\xB2\x80" => "\xD0\x92", + "\xE1\xB2\x81" => "\xD0\x94", + "\xE1\xB2\x82" => "\xD0\x9E", + "\xE1\xB2\x83" => "\xD0\xA1", + "\xE1\xB2\x84" => "\xD0\xA2", + "\xE1\xB2\x85" => "\xD0\xA2", + "\xE1\xB2\x86" => "\xD0\xAA", + "\xE1\xB2\x87" => "\xD1\xA2", + "\xE1\xB2\x88" => "\xEA\x99\x8A", + "\xE1\xB5\xB9" => "\xEA\x9D\xBD", + "\xE1\xB5\xBD" => "\xE2\xB1\xA3", + "\xE1\xB6\x8E" => "\xEA\x9F\x86", + "\xE1\xB8\x81" => "\xE1\xB8\x80", + "\xE1\xB8\x83" => "\xE1\xB8\x82", + "\xE1\xB8\x85" => "\xE1\xB8\x84", + "\xE1\xB8\x87" => "\xE1\xB8\x86", + "\xE1\xB8\x89" => "\xE1\xB8\x88", + "\xE1\xB8\x8B" => "\xE1\xB8\x8A", + "\xE1\xB8\x8D" => "\xE1\xB8\x8C", + "\xE1\xB8\x8F" => "\xE1\xB8\x8E", + "\xE1\xB8\x91" => "\xE1\xB8\x90", + "\xE1\xB8\x93" => "\xE1\xB8\x92", + "\xE1\xB8\x95" => "\xE1\xB8\x94", + "\xE1\xB8\x97" => "\xE1\xB8\x96", + "\xE1\xB8\x99" => "\xE1\xB8\x98", + "\xE1\xB8\x9B" => "\xE1\xB8\x9A", + "\xE1\xB8\x9D" => "\xE1\xB8\x9C", + "\xE1\xB8\x9F" => "\xE1\xB8\x9E", + "\xE1\xB8\xA1" => "\xE1\xB8\xA0", + "\xE1\xB8\xA3" => "\xE1\xB8\xA2", + "\xE1\xB8\xA5" => "\xE1\xB8\xA4", + "\xE1\xB8\xA7" => "\xE1\xB8\xA6", + "\xE1\xB8\xA9" => "\xE1\xB8\xA8", + "\xE1\xB8\xAB" => "\xE1\xB8\xAA", + "\xE1\xB8\xAD" => "\xE1\xB8\xAC", + "\xE1\xB8\xAF" => "\xE1\xB8\xAE", + "\xE1\xB8\xB1" => "\xE1\xB8\xB0", + "\xE1\xB8\xB3" => "\xE1\xB8\xB2", + "\xE1\xB8\xB5" => "\xE1\xB8\xB4", + "\xE1\xB8\xB7" => "\xE1\xB8\xB6", + "\xE1\xB8\xB9" => "\xE1\xB8\xB8", + "\xE1\xB8\xBB" => "\xE1\xB8\xBA", + "\xE1\xB8\xBD" => "\xE1\xB8\xBC", + "\xE1\xB8\xBF" => "\xE1\xB8\xBE", + "\xE1\xB9\x81" => "\xE1\xB9\x80", + "\xE1\xB9\x83" => "\xE1\xB9\x82", + "\xE1\xB9\x85" => "\xE1\xB9\x84", + "\xE1\xB9\x87" => "\xE1\xB9\x86", + "\xE1\xB9\x89" => "\xE1\xB9\x88", + "\xE1\xB9\x8B" => "\xE1\xB9\x8A", + "\xE1\xB9\x8D" => "\xE1\xB9\x8C", + "\xE1\xB9\x8F" => "\xE1\xB9\x8E", + "\xE1\xB9\x91" => "\xE1\xB9\x90", + "\xE1\xB9\x93" => "\xE1\xB9\x92", + "\xE1\xB9\x95" => "\xE1\xB9\x94", + "\xE1\xB9\x97" => "\xE1\xB9\x96", + "\xE1\xB9\x99" => "\xE1\xB9\x98", + "\xE1\xB9\x9B" => "\xE1\xB9\x9A", + "\xE1\xB9\x9D" => "\xE1\xB9\x9C", + "\xE1\xB9\x9F" => "\xE1\xB9\x9E", + "\xE1\xB9\xA1" => "\xE1\xB9\xA0", + "\xE1\xB9\xA3" => "\xE1\xB9\xA2", + "\xE1\xB9\xA5" => "\xE1\xB9\xA4", + "\xE1\xB9\xA7" => "\xE1\xB9\xA6", + "\xE1\xB9\xA9" => "\xE1\xB9\xA8", + "\xE1\xB9\xAB" => "\xE1\xB9\xAA", + "\xE1\xB9\xAD" => "\xE1\xB9\xAC", + "\xE1\xB9\xAF" => "\xE1\xB9\xAE", + "\xE1\xB9\xB1" => "\xE1\xB9\xB0", + "\xE1\xB9\xB3" => "\xE1\xB9\xB2", + "\xE1\xB9\xB5" => "\xE1\xB9\xB4", + "\xE1\xB9\xB7" => "\xE1\xB9\xB6", + "\xE1\xB9\xB9" => "\xE1\xB9\xB8", + "\xE1\xB9\xBB" => "\xE1\xB9\xBA", + "\xE1\xB9\xBD" => "\xE1\xB9\xBC", + "\xE1\xB9\xBF" => "\xE1\xB9\xBE", + "\xE1\xBA\x81" => "\xE1\xBA\x80", + "\xE1\xBA\x83" => "\xE1\xBA\x82", + "\xE1\xBA\x85" => "\xE1\xBA\x84", + "\xE1\xBA\x87" => "\xE1\xBA\x86", + "\xE1\xBA\x89" => "\xE1\xBA\x88", + "\xE1\xBA\x8B" => "\xE1\xBA\x8A", + "\xE1\xBA\x8D" => "\xE1\xBA\x8C", + "\xE1\xBA\x8F" => "\xE1\xBA\x8E", + "\xE1\xBA\x91" => "\xE1\xBA\x90", + "\xE1\xBA\x93" => "\xE1\xBA\x92", + "\xE1\xBA\x95" => "\xE1\xBA\x94", + "\xE1\xBA\x9B" => "\xE1\xB9\xA0", + "\xE1\xBA\xA1" => "\xE1\xBA\xA0", + "\xE1\xBA\xA3" => "\xE1\xBA\xA2", + "\xE1\xBA\xA5" => "\xE1\xBA\xA4", + "\xE1\xBA\xA7" => "\xE1\xBA\xA6", + "\xE1\xBA\xA9" => "\xE1\xBA\xA8", + "\xE1\xBA\xAB" => "\xE1\xBA\xAA", + "\xE1\xBA\xAD" => "\xE1\xBA\xAC", + "\xE1\xBA\xAF" => "\xE1\xBA\xAE", + "\xE1\xBA\xB1" => "\xE1\xBA\xB0", + "\xE1\xBA\xB3" => "\xE1\xBA\xB2", + "\xE1\xBA\xB5" => "\xE1\xBA\xB4", + "\xE1\xBA\xB7" => "\xE1\xBA\xB6", + "\xE1\xBA\xB9" => "\xE1\xBA\xB8", + "\xE1\xBA\xBB" => "\xE1\xBA\xBA", + "\xE1\xBA\xBD" => "\xE1\xBA\xBC", + "\xE1\xBA\xBF" => "\xE1\xBA\xBE", + "\xE1\xBB\x81" => "\xE1\xBB\x80", + "\xE1\xBB\x83" => "\xE1\xBB\x82", + "\xE1\xBB\x85" => "\xE1\xBB\x84", + "\xE1\xBB\x87" => "\xE1\xBB\x86", + "\xE1\xBB\x89" => "\xE1\xBB\x88", + "\xE1\xBB\x8B" => "\xE1\xBB\x8A", + "\xE1\xBB\x8D" => "\xE1\xBB\x8C", + "\xE1\xBB\x8F" => "\xE1\xBB\x8E", + "\xE1\xBB\x91" => "\xE1\xBB\x90", + "\xE1\xBB\x93" => "\xE1\xBB\x92", + "\xE1\xBB\x95" => "\xE1\xBB\x94", + "\xE1\xBB\x97" => "\xE1\xBB\x96", + "\xE1\xBB\x99" => "\xE1\xBB\x98", + "\xE1\xBB\x9B" => "\xE1\xBB\x9A", + "\xE1\xBB\x9D" => "\xE1\xBB\x9C", + "\xE1\xBB\x9F" => "\xE1\xBB\x9E", + "\xE1\xBB\xA1" => "\xE1\xBB\xA0", + "\xE1\xBB\xA3" => "\xE1\xBB\xA2", + "\xE1\xBB\xA5" => "\xE1\xBB\xA4", + "\xE1\xBB\xA7" => "\xE1\xBB\xA6", + "\xE1\xBB\xA9" => "\xE1\xBB\xA8", + "\xE1\xBB\xAB" => "\xE1\xBB\xAA", + "\xE1\xBB\xAD" => "\xE1\xBB\xAC", + "\xE1\xBB\xAF" => "\xE1\xBB\xAE", + "\xE1\xBB\xB1" => "\xE1\xBB\xB0", + "\xE1\xBB\xB3" => "\xE1\xBB\xB2", + "\xE1\xBB\xB5" => "\xE1\xBB\xB4", + "\xE1\xBB\xB7" => "\xE1\xBB\xB6", + "\xE1\xBB\xB9" => "\xE1\xBB\xB8", + "\xE1\xBB\xBB" => "\xE1\xBB\xBA", + "\xE1\xBB\xBD" => "\xE1\xBB\xBC", + "\xE1\xBB\xBF" => "\xE1\xBB\xBE", + "\xE1\xBC\x80" => "\xE1\xBC\x88", + "\xE1\xBC\x81" => "\xE1\xBC\x89", + "\xE1\xBC\x82" => "\xE1\xBC\x8A", + "\xE1\xBC\x83" => "\xE1\xBC\x8B", + "\xE1\xBC\x84" => "\xE1\xBC\x8C", + "\xE1\xBC\x85" => "\xE1\xBC\x8D", + "\xE1\xBC\x86" => "\xE1\xBC\x8E", + "\xE1\xBC\x87" => "\xE1\xBC\x8F", + "\xE1\xBC\x90" => "\xE1\xBC\x98", + "\xE1\xBC\x91" => "\xE1\xBC\x99", + "\xE1\xBC\x92" => "\xE1\xBC\x9A", + "\xE1\xBC\x93" => "\xE1\xBC\x9B", + "\xE1\xBC\x94" => "\xE1\xBC\x9C", + "\xE1\xBC\x95" => "\xE1\xBC\x9D", + "\xE1\xBC\xA0" => "\xE1\xBC\xA8", + "\xE1\xBC\xA1" => "\xE1\xBC\xA9", + "\xE1\xBC\xA2" => "\xE1\xBC\xAA", + "\xE1\xBC\xA3" => "\xE1\xBC\xAB", + "\xE1\xBC\xA4" => "\xE1\xBC\xAC", + "\xE1\xBC\xA5" => "\xE1\xBC\xAD", + "\xE1\xBC\xA6" => "\xE1\xBC\xAE", + "\xE1\xBC\xA7" => "\xE1\xBC\xAF", + "\xE1\xBC\xB0" => "\xE1\xBC\xB8", + "\xE1\xBC\xB1" => "\xE1\xBC\xB9", + "\xE1\xBC\xB2" => "\xE1\xBC\xBA", + "\xE1\xBC\xB3" => "\xE1\xBC\xBB", + "\xE1\xBC\xB4" => "\xE1\xBC\xBC", + "\xE1\xBC\xB5" => "\xE1\xBC\xBD", + "\xE1\xBC\xB6" => "\xE1\xBC\xBE", + "\xE1\xBC\xB7" => "\xE1\xBC\xBF", + "\xE1\xBD\x80" => "\xE1\xBD\x88", + "\xE1\xBD\x81" => "\xE1\xBD\x89", + "\xE1\xBD\x82" => "\xE1\xBD\x8A", + "\xE1\xBD\x83" => "\xE1\xBD\x8B", + "\xE1\xBD\x84" => "\xE1\xBD\x8C", + "\xE1\xBD\x85" => "\xE1\xBD\x8D", + "\xE1\xBD\x91" => "\xE1\xBD\x99", + "\xE1\xBD\x93" => "\xE1\xBD\x9B", + "\xE1\xBD\x95" => "\xE1\xBD\x9D", + "\xE1\xBD\x97" => "\xE1\xBD\x9F", + "\xE1\xBD\xA0" => "\xE1\xBD\xA8", + "\xE1\xBD\xA1" => "\xE1\xBD\xA9", + "\xE1\xBD\xA2" => "\xE1\xBD\xAA", + "\xE1\xBD\xA3" => "\xE1\xBD\xAB", + "\xE1\xBD\xA4" => "\xE1\xBD\xAC", + "\xE1\xBD\xA5" => "\xE1\xBD\xAD", + "\xE1\xBD\xA6" => "\xE1\xBD\xAE", + "\xE1\xBD\xA7" => "\xE1\xBD\xAF", + "\xE1\xBD\xB0" => "\xE1\xBE\xBA", + "\xE1\xBD\xB1" => "\xE1\xBE\xBB", + "\xE1\xBD\xB2" => "\xE1\xBF\x88", + "\xE1\xBD\xB3" => "\xE1\xBF\x89", + "\xE1\xBD\xB4" => "\xE1\xBF\x8A", + "\xE1\xBD\xB5" => "\xE1\xBF\x8B", + "\xE1\xBD\xB6" => "\xE1\xBF\x9A", + "\xE1\xBD\xB7" => "\xE1\xBF\x9B", + "\xE1\xBD\xB8" => "\xE1\xBF\xB8", + "\xE1\xBD\xB9" => "\xE1\xBF\xB9", + "\xE1\xBD\xBA" => "\xE1\xBF\xAA", + "\xE1\xBD\xBB" => "\xE1\xBF\xAB", + "\xE1\xBD\xBC" => "\xE1\xBF\xBA", + "\xE1\xBD\xBD" => "\xE1\xBF\xBB", + "\xE1\xBE\x80" => "\xE1\xBE\x88", + "\xE1\xBE\x81" => "\xE1\xBE\x89", + "\xE1\xBE\x82" => "\xE1\xBE\x8A", + "\xE1\xBE\x83" => "\xE1\xBE\x8B", + "\xE1\xBE\x84" => "\xE1\xBE\x8C", + "\xE1\xBE\x85" => "\xE1\xBE\x8D", + "\xE1\xBE\x86" => "\xE1\xBE\x8E", + "\xE1\xBE\x87" => "\xE1\xBE\x8F", + "\xE1\xBE\x90" => "\xE1\xBE\x98", + "\xE1\xBE\x91" => "\xE1\xBE\x99", + "\xE1\xBE\x92" => "\xE1\xBE\x9A", + "\xE1\xBE\x93" => "\xE1\xBE\x9B", + "\xE1\xBE\x94" => "\xE1\xBE\x9C", + "\xE1\xBE\x95" => "\xE1\xBE\x9D", + "\xE1\xBE\x96" => "\xE1\xBE\x9E", + "\xE1\xBE\x97" => "\xE1\xBE\x9F", + "\xE1\xBE\xA0" => "\xE1\xBE\xA8", + "\xE1\xBE\xA1" => "\xE1\xBE\xA9", + "\xE1\xBE\xA2" => "\xE1\xBE\xAA", + "\xE1\xBE\xA3" => "\xE1\xBE\xAB", + "\xE1\xBE\xA4" => "\xE1\xBE\xAC", + "\xE1\xBE\xA5" => "\xE1\xBE\xAD", + "\xE1\xBE\xA6" => "\xE1\xBE\xAE", + "\xE1\xBE\xA7" => "\xE1\xBE\xAF", + "\xE1\xBE\xB0" => "\xE1\xBE\xB8", + "\xE1\xBE\xB1" => "\xE1\xBE\xB9", + "\xE1\xBE\xB3" => "\xE1\xBE\xBC", + "\xE1\xBE\xBE" => "\xCE\x99", + "\xE1\xBF\x83" => "\xE1\xBF\x8C", + "\xE1\xBF\x90" => "\xE1\xBF\x98", + "\xE1\xBF\x91" => "\xE1\xBF\x99", + "\xE1\xBF\xA0" => "\xE1\xBF\xA8", + "\xE1\xBF\xA1" => "\xE1\xBF\xA9", + "\xE1\xBF\xA5" => "\xE1\xBF\xAC", + "\xE1\xBF\xB3" => "\xE1\xBF\xBC", + "\xE2\x85\x8E" => "\xE2\x84\xB2", + "\xE2\x85\xB0" => "\xE2\x85\xA0", + "\xE2\x85\xB1" => "\xE2\x85\xA1", + "\xE2\x85\xB2" => "\xE2\x85\xA2", + "\xE2\x85\xB3" => "\xE2\x85\xA3", + "\xE2\x85\xB4" => "\xE2\x85\xA4", + "\xE2\x85\xB5" => "\xE2\x85\xA5", + "\xE2\x85\xB6" => "\xE2\x85\xA6", + "\xE2\x85\xB7" => "\xE2\x85\xA7", + "\xE2\x85\xB8" => "\xE2\x85\xA8", + "\xE2\x85\xB9" => "\xE2\x85\xA9", + "\xE2\x85\xBA" => "\xE2\x85\xAA", + "\xE2\x85\xBB" => "\xE2\x85\xAB", + "\xE2\x85\xBC" => "\xE2\x85\xAC", + "\xE2\x85\xBD" => "\xE2\x85\xAD", + "\xE2\x85\xBE" => "\xE2\x85\xAE", + "\xE2\x85\xBF" => "\xE2\x85\xAF", + "\xE2\x86\x84" => "\xE2\x86\x83", + "\xE2\x93\x90" => "\xE2\x92\xB6", + "\xE2\x93\x91" => "\xE2\x92\xB7", + "\xE2\x93\x92" => "\xE2\x92\xB8", + "\xE2\x93\x93" => "\xE2\x92\xB9", + "\xE2\x93\x94" => "\xE2\x92\xBA", + "\xE2\x93\x95" => "\xE2\x92\xBB", + "\xE2\x93\x96" => "\xE2\x92\xBC", + "\xE2\x93\x97" => "\xE2\x92\xBD", + "\xE2\x93\x98" => "\xE2\x92\xBE", + "\xE2\x93\x99" => "\xE2\x92\xBF", + "\xE2\x93\x9A" => "\xE2\x93\x80", + "\xE2\x93\x9B" => "\xE2\x93\x81", + "\xE2\x93\x9C" => "\xE2\x93\x82", + "\xE2\x93\x9D" => "\xE2\x93\x83", + "\xE2\x93\x9E" => "\xE2\x93\x84", + "\xE2\x93\x9F" => "\xE2\x93\x85", + "\xE2\x93\xA0" => "\xE2\x93\x86", + "\xE2\x93\xA1" => "\xE2\x93\x87", + "\xE2\x93\xA2" => "\xE2\x93\x88", + "\xE2\x93\xA3" => "\xE2\x93\x89", + "\xE2\x93\xA4" => "\xE2\x93\x8A", + "\xE2\x93\xA5" => "\xE2\x93\x8B", + "\xE2\x93\xA6" => "\xE2\x93\x8C", + "\xE2\x93\xA7" => "\xE2\x93\x8D", + "\xE2\x93\xA8" => "\xE2\x93\x8E", + "\xE2\x93\xA9" => "\xE2\x93\x8F", + "\xE2\xB0\xB0" => "\xE2\xB0\x80", + "\xE2\xB0\xB1" => "\xE2\xB0\x81", + "\xE2\xB0\xB2" => "\xE2\xB0\x82", + "\xE2\xB0\xB3" => "\xE2\xB0\x83", + "\xE2\xB0\xB4" => "\xE2\xB0\x84", + "\xE2\xB0\xB5" => "\xE2\xB0\x85", + "\xE2\xB0\xB6" => "\xE2\xB0\x86", + "\xE2\xB0\xB7" => "\xE2\xB0\x87", + "\xE2\xB0\xB8" => "\xE2\xB0\x88", + "\xE2\xB0\xB9" => "\xE2\xB0\x89", + "\xE2\xB0\xBA" => "\xE2\xB0\x8A", + "\xE2\xB0\xBB" => "\xE2\xB0\x8B", + "\xE2\xB0\xBC" => "\xE2\xB0\x8C", + "\xE2\xB0\xBD" => "\xE2\xB0\x8D", + "\xE2\xB0\xBE" => "\xE2\xB0\x8E", + "\xE2\xB0\xBF" => "\xE2\xB0\x8F", + "\xE2\xB1\x80" => "\xE2\xB0\x90", + "\xE2\xB1\x81" => "\xE2\xB0\x91", + "\xE2\xB1\x82" => "\xE2\xB0\x92", + "\xE2\xB1\x83" => "\xE2\xB0\x93", + "\xE2\xB1\x84" => "\xE2\xB0\x94", + "\xE2\xB1\x85" => "\xE2\xB0\x95", + "\xE2\xB1\x86" => "\xE2\xB0\x96", + "\xE2\xB1\x87" => "\xE2\xB0\x97", + "\xE2\xB1\x88" => "\xE2\xB0\x98", + "\xE2\xB1\x89" => "\xE2\xB0\x99", + "\xE2\xB1\x8A" => "\xE2\xB0\x9A", + "\xE2\xB1\x8B" => "\xE2\xB0\x9B", + "\xE2\xB1\x8C" => "\xE2\xB0\x9C", + "\xE2\xB1\x8D" => "\xE2\xB0\x9D", + "\xE2\xB1\x8E" => "\xE2\xB0\x9E", + "\xE2\xB1\x8F" => "\xE2\xB0\x9F", + "\xE2\xB1\x90" => "\xE2\xB0\xA0", + "\xE2\xB1\x91" => "\xE2\xB0\xA1", + "\xE2\xB1\x92" => "\xE2\xB0\xA2", + "\xE2\xB1\x93" => "\xE2\xB0\xA3", + "\xE2\xB1\x94" => "\xE2\xB0\xA4", + "\xE2\xB1\x95" => "\xE2\xB0\xA5", + "\xE2\xB1\x96" => "\xE2\xB0\xA6", + "\xE2\xB1\x97" => "\xE2\xB0\xA7", + "\xE2\xB1\x98" => "\xE2\xB0\xA8", + "\xE2\xB1\x99" => "\xE2\xB0\xA9", + "\xE2\xB1\x9A" => "\xE2\xB0\xAA", + "\xE2\xB1\x9B" => "\xE2\xB0\xAB", + "\xE2\xB1\x9C" => "\xE2\xB0\xAC", + "\xE2\xB1\x9D" => "\xE2\xB0\xAD", + "\xE2\xB1\x9E" => "\xE2\xB0\xAE", + "\xE2\xB1\x9F" => "\xE2\xB0\xAF", + "\xE2\xB1\xA1" => "\xE2\xB1\xA0", + "\xE2\xB1\xA5" => "\xC8\xBA", + "\xE2\xB1\xA6" => "\xC8\xBE", + "\xE2\xB1\xA8" => "\xE2\xB1\xA7", + "\xE2\xB1\xAA" => "\xE2\xB1\xA9", + "\xE2\xB1\xAC" => "\xE2\xB1\xAB", + "\xE2\xB1\xB3" => "\xE2\xB1\xB2", + "\xE2\xB1\xB6" => "\xE2\xB1\xB5", + "\xE2\xB2\x81" => "\xE2\xB2\x80", + "\xE2\xB2\x83" => "\xE2\xB2\x82", + "\xE2\xB2\x85" => "\xE2\xB2\x84", + "\xE2\xB2\x87" => "\xE2\xB2\x86", + "\xE2\xB2\x89" => "\xE2\xB2\x88", + "\xE2\xB2\x8B" => "\xE2\xB2\x8A", + "\xE2\xB2\x8D" => "\xE2\xB2\x8C", + "\xE2\xB2\x8F" => "\xE2\xB2\x8E", + "\xE2\xB2\x91" => "\xE2\xB2\x90", + "\xE2\xB2\x93" => "\xE2\xB2\x92", + "\xE2\xB2\x95" => "\xE2\xB2\x94", + "\xE2\xB2\x97" => "\xE2\xB2\x96", + "\xE2\xB2\x99" => "\xE2\xB2\x98", + "\xE2\xB2\x9B" => "\xE2\xB2\x9A", + "\xE2\xB2\x9D" => "\xE2\xB2\x9C", + "\xE2\xB2\x9F" => "\xE2\xB2\x9E", + "\xE2\xB2\xA1" => "\xE2\xB2\xA0", + "\xE2\xB2\xA3" => "\xE2\xB2\xA2", + "\xE2\xB2\xA5" => "\xE2\xB2\xA4", + "\xE2\xB2\xA7" => "\xE2\xB2\xA6", + "\xE2\xB2\xA9" => "\xE2\xB2\xA8", + "\xE2\xB2\xAB" => "\xE2\xB2\xAA", + "\xE2\xB2\xAD" => "\xE2\xB2\xAC", + "\xE2\xB2\xAF" => "\xE2\xB2\xAE", + "\xE2\xB2\xB1" => "\xE2\xB2\xB0", + "\xE2\xB2\xB3" => "\xE2\xB2\xB2", + "\xE2\xB2\xB5" => "\xE2\xB2\xB4", + "\xE2\xB2\xB7" => "\xE2\xB2\xB6", + "\xE2\xB2\xB9" => "\xE2\xB2\xB8", + "\xE2\xB2\xBB" => "\xE2\xB2\xBA", + "\xE2\xB2\xBD" => "\xE2\xB2\xBC", + "\xE2\xB2\xBF" => "\xE2\xB2\xBE", + "\xE2\xB3\x81" => "\xE2\xB3\x80", + "\xE2\xB3\x83" => "\xE2\xB3\x82", + "\xE2\xB3\x85" => "\xE2\xB3\x84", + "\xE2\xB3\x87" => "\xE2\xB3\x86", + "\xE2\xB3\x89" => "\xE2\xB3\x88", + "\xE2\xB3\x8B" => "\xE2\xB3\x8A", + "\xE2\xB3\x8D" => "\xE2\xB3\x8C", + "\xE2\xB3\x8F" => "\xE2\xB3\x8E", + "\xE2\xB3\x91" => "\xE2\xB3\x90", + "\xE2\xB3\x93" => "\xE2\xB3\x92", + "\xE2\xB3\x95" => "\xE2\xB3\x94", + "\xE2\xB3\x97" => "\xE2\xB3\x96", + "\xE2\xB3\x99" => "\xE2\xB3\x98", + "\xE2\xB3\x9B" => "\xE2\xB3\x9A", + "\xE2\xB3\x9D" => "\xE2\xB3\x9C", + "\xE2\xB3\x9F" => "\xE2\xB3\x9E", + "\xE2\xB3\xA1" => "\xE2\xB3\xA0", + "\xE2\xB3\xA3" => "\xE2\xB3\xA2", + "\xE2\xB3\xAC" => "\xE2\xB3\xAB", + "\xE2\xB3\xAE" => "\xE2\xB3\xAD", + "\xE2\xB3\xB3" => "\xE2\xB3\xB2", + "\xE2\xB4\x80" => "\xE1\x82\xA0", + "\xE2\xB4\x81" => "\xE1\x82\xA1", + "\xE2\xB4\x82" => "\xE1\x82\xA2", + "\xE2\xB4\x83" => "\xE1\x82\xA3", + "\xE2\xB4\x84" => "\xE1\x82\xA4", + "\xE2\xB4\x85" => "\xE1\x82\xA5", + "\xE2\xB4\x86" => "\xE1\x82\xA6", + "\xE2\xB4\x87" => "\xE1\x82\xA7", + "\xE2\xB4\x88" => "\xE1\x82\xA8", + "\xE2\xB4\x89" => "\xE1\x82\xA9", + "\xE2\xB4\x8A" => "\xE1\x82\xAA", + "\xE2\xB4\x8B" => "\xE1\x82\xAB", + "\xE2\xB4\x8C" => "\xE1\x82\xAC", + "\xE2\xB4\x8D" => "\xE1\x82\xAD", + "\xE2\xB4\x8E" => "\xE1\x82\xAE", + "\xE2\xB4\x8F" => "\xE1\x82\xAF", + "\xE2\xB4\x90" => "\xE1\x82\xB0", + "\xE2\xB4\x91" => "\xE1\x82\xB1", + "\xE2\xB4\x92" => "\xE1\x82\xB2", + "\xE2\xB4\x93" => "\xE1\x82\xB3", + "\xE2\xB4\x94" => "\xE1\x82\xB4", + "\xE2\xB4\x95" => "\xE1\x82\xB5", + "\xE2\xB4\x96" => "\xE1\x82\xB6", + "\xE2\xB4\x97" => "\xE1\x82\xB7", + "\xE2\xB4\x98" => "\xE1\x82\xB8", + "\xE2\xB4\x99" => "\xE1\x82\xB9", + "\xE2\xB4\x9A" => "\xE1\x82\xBA", + "\xE2\xB4\x9B" => "\xE1\x82\xBB", + "\xE2\xB4\x9C" => "\xE1\x82\xBC", + "\xE2\xB4\x9D" => "\xE1\x82\xBD", + "\xE2\xB4\x9E" => "\xE1\x82\xBE", + "\xE2\xB4\x9F" => "\xE1\x82\xBF", + "\xE2\xB4\xA0" => "\xE1\x83\x80", + "\xE2\xB4\xA1" => "\xE1\x83\x81", + "\xE2\xB4\xA2" => "\xE1\x83\x82", + "\xE2\xB4\xA3" => "\xE1\x83\x83", + "\xE2\xB4\xA4" => "\xE1\x83\x84", + "\xE2\xB4\xA5" => "\xE1\x83\x85", + "\xE2\xB4\xA7" => "\xE1\x83\x87", + "\xE2\xB4\xAD" => "\xE1\x83\x8D", + "\xEA\x99\x81" => "\xEA\x99\x80", + "\xEA\x99\x83" => "\xEA\x99\x82", + "\xEA\x99\x85" => "\xEA\x99\x84", + "\xEA\x99\x87" => "\xEA\x99\x86", + "\xEA\x99\x89" => "\xEA\x99\x88", + "\xEA\x99\x8B" => "\xEA\x99\x8A", + "\xEA\x99\x8D" => "\xEA\x99\x8C", + "\xEA\x99\x8F" => "\xEA\x99\x8E", + "\xEA\x99\x91" => "\xEA\x99\x90", + "\xEA\x99\x93" => "\xEA\x99\x92", + "\xEA\x99\x95" => "\xEA\x99\x94", + "\xEA\x99\x97" => "\xEA\x99\x96", + "\xEA\x99\x99" => "\xEA\x99\x98", + "\xEA\x99\x9B" => "\xEA\x99\x9A", + "\xEA\x99\x9D" => "\xEA\x99\x9C", + "\xEA\x99\x9F" => "\xEA\x99\x9E", + "\xEA\x99\xA1" => "\xEA\x99\xA0", + "\xEA\x99\xA3" => "\xEA\x99\xA2", + "\xEA\x99\xA5" => "\xEA\x99\xA4", + "\xEA\x99\xA7" => "\xEA\x99\xA6", + "\xEA\x99\xA9" => "\xEA\x99\xA8", + "\xEA\x99\xAB" => "\xEA\x99\xAA", + "\xEA\x99\xAD" => "\xEA\x99\xAC", + "\xEA\x9A\x81" => "\xEA\x9A\x80", + "\xEA\x9A\x83" => "\xEA\x9A\x82", + "\xEA\x9A\x85" => "\xEA\x9A\x84", + "\xEA\x9A\x87" => "\xEA\x9A\x86", + "\xEA\x9A\x89" => "\xEA\x9A\x88", + "\xEA\x9A\x8B" => "\xEA\x9A\x8A", + "\xEA\x9A\x8D" => "\xEA\x9A\x8C", + "\xEA\x9A\x8F" => "\xEA\x9A\x8E", + "\xEA\x9A\x91" => "\xEA\x9A\x90", + "\xEA\x9A\x93" => "\xEA\x9A\x92", + "\xEA\x9A\x95" => "\xEA\x9A\x94", + "\xEA\x9A\x97" => "\xEA\x9A\x96", + "\xEA\x9A\x99" => "\xEA\x9A\x98", + "\xEA\x9A\x9B" => "\xEA\x9A\x9A", + "\xEA\x9C\xA3" => "\xEA\x9C\xA2", + "\xEA\x9C\xA5" => "\xEA\x9C\xA4", + "\xEA\x9C\xA7" => "\xEA\x9C\xA6", + "\xEA\x9C\xA9" => "\xEA\x9C\xA8", + "\xEA\x9C\xAB" => "\xEA\x9C\xAA", + "\xEA\x9C\xAD" => "\xEA\x9C\xAC", + "\xEA\x9C\xAF" => "\xEA\x9C\xAE", + "\xEA\x9C\xB3" => "\xEA\x9C\xB2", + "\xEA\x9C\xB5" => "\xEA\x9C\xB4", + "\xEA\x9C\xB7" => "\xEA\x9C\xB6", + "\xEA\x9C\xB9" => "\xEA\x9C\xB8", + "\xEA\x9C\xBB" => "\xEA\x9C\xBA", + "\xEA\x9C\xBD" => "\xEA\x9C\xBC", + "\xEA\x9C\xBF" => "\xEA\x9C\xBE", + "\xEA\x9D\x81" => "\xEA\x9D\x80", + "\xEA\x9D\x83" => "\xEA\x9D\x82", + "\xEA\x9D\x85" => "\xEA\x9D\x84", + "\xEA\x9D\x87" => "\xEA\x9D\x86", + "\xEA\x9D\x89" => "\xEA\x9D\x88", + "\xEA\x9D\x8B" => "\xEA\x9D\x8A", + "\xEA\x9D\x8D" => "\xEA\x9D\x8C", + "\xEA\x9D\x8F" => "\xEA\x9D\x8E", + "\xEA\x9D\x91" => "\xEA\x9D\x90", + "\xEA\x9D\x93" => "\xEA\x9D\x92", + "\xEA\x9D\x95" => "\xEA\x9D\x94", + "\xEA\x9D\x97" => "\xEA\x9D\x96", + "\xEA\x9D\x99" => "\xEA\x9D\x98", + "\xEA\x9D\x9B" => "\xEA\x9D\x9A", + "\xEA\x9D\x9D" => "\xEA\x9D\x9C", + "\xEA\x9D\x9F" => "\xEA\x9D\x9E", + "\xEA\x9D\xA1" => "\xEA\x9D\xA0", + "\xEA\x9D\xA3" => "\xEA\x9D\xA2", + "\xEA\x9D\xA5" => "\xEA\x9D\xA4", + "\xEA\x9D\xA7" => "\xEA\x9D\xA6", + "\xEA\x9D\xA9" => "\xEA\x9D\xA8", + "\xEA\x9D\xAB" => "\xEA\x9D\xAA", + "\xEA\x9D\xAD" => "\xEA\x9D\xAC", + "\xEA\x9D\xAF" => "\xEA\x9D\xAE", + "\xEA\x9D\xBA" => "\xEA\x9D\xB9", + "\xEA\x9D\xBC" => "\xEA\x9D\xBB", + "\xEA\x9D\xBF" => "\xEA\x9D\xBE", + "\xEA\x9E\x81" => "\xEA\x9E\x80", + "\xEA\x9E\x83" => "\xEA\x9E\x82", + "\xEA\x9E\x85" => "\xEA\x9E\x84", + "\xEA\x9E\x87" => "\xEA\x9E\x86", + "\xEA\x9E\x8C" => "\xEA\x9E\x8B", + "\xEA\x9E\x91" => "\xEA\x9E\x90", + "\xEA\x9E\x93" => "\xEA\x9E\x92", + "\xEA\x9E\x94" => "\xEA\x9F\x84", + "\xEA\x9E\x97" => "\xEA\x9E\x96", + "\xEA\x9E\x99" => "\xEA\x9E\x98", + "\xEA\x9E\x9B" => "\xEA\x9E\x9A", + "\xEA\x9E\x9D" => "\xEA\x9E\x9C", + "\xEA\x9E\x9F" => "\xEA\x9E\x9E", + "\xEA\x9E\xA1" => "\xEA\x9E\xA0", + "\xEA\x9E\xA3" => "\xEA\x9E\xA2", + "\xEA\x9E\xA5" => "\xEA\x9E\xA4", + "\xEA\x9E\xA7" => "\xEA\x9E\xA6", + "\xEA\x9E\xA9" => "\xEA\x9E\xA8", + "\xEA\x9E\xB5" => "\xEA\x9E\xB4", + "\xEA\x9E\xB7" => "\xEA\x9E\xB6", + "\xEA\x9E\xB9" => "\xEA\x9E\xB8", + "\xEA\x9E\xBB" => "\xEA\x9E\xBA", + "\xEA\x9E\xBD" => "\xEA\x9E\xBC", + "\xEA\x9E\xBF" => "\xEA\x9E\xBE", + "\xEA\x9F\x81" => "\xEA\x9F\x80", + "\xEA\x9F\x83" => "\xEA\x9F\x82", + "\xEA\x9F\x88" => "\xEA\x9F\x87", + "\xEA\x9F\x8A" => "\xEA\x9F\x89", + "\xEA\x9F\x91" => "\xEA\x9F\x90", + "\xEA\x9F\x97" => "\xEA\x9F\x96", + "\xEA\x9F\x99" => "\xEA\x9F\x98", + "\xEA\x9F\xB6" => "\xEA\x9F\xB5", + "\xEA\xAD\x93" => "\xEA\x9E\xB3", + "\xEA\xAD\xB0" => "\xE1\x8E\xA0", + "\xEA\xAD\xB1" => "\xE1\x8E\xA1", + "\xEA\xAD\xB2" => "\xE1\x8E\xA2", + "\xEA\xAD\xB3" => "\xE1\x8E\xA3", + "\xEA\xAD\xB4" => "\xE1\x8E\xA4", + "\xEA\xAD\xB5" => "\xE1\x8E\xA5", + "\xEA\xAD\xB6" => "\xE1\x8E\xA6", + "\xEA\xAD\xB7" => "\xE1\x8E\xA7", + "\xEA\xAD\xB8" => "\xE1\x8E\xA8", + "\xEA\xAD\xB9" => "\xE1\x8E\xA9", + "\xEA\xAD\xBA" => "\xE1\x8E\xAA", + "\xEA\xAD\xBB" => "\xE1\x8E\xAB", + "\xEA\xAD\xBC" => "\xE1\x8E\xAC", + "\xEA\xAD\xBD" => "\xE1\x8E\xAD", + "\xEA\xAD\xBE" => "\xE1\x8E\xAE", + "\xEA\xAD\xBF" => "\xE1\x8E\xAF", + "\xEA\xAE\x80" => "\xE1\x8E\xB0", + "\xEA\xAE\x81" => "\xE1\x8E\xB1", + "\xEA\xAE\x82" => "\xE1\x8E\xB2", + "\xEA\xAE\x83" => "\xE1\x8E\xB3", + "\xEA\xAE\x84" => "\xE1\x8E\xB4", + "\xEA\xAE\x85" => "\xE1\x8E\xB5", + "\xEA\xAE\x86" => "\xE1\x8E\xB6", + "\xEA\xAE\x87" => "\xE1\x8E\xB7", + "\xEA\xAE\x88" => "\xE1\x8E\xB8", + "\xEA\xAE\x89" => "\xE1\x8E\xB9", + "\xEA\xAE\x8A" => "\xE1\x8E\xBA", + "\xEA\xAE\x8B" => "\xE1\x8E\xBB", + "\xEA\xAE\x8C" => "\xE1\x8E\xBC", + "\xEA\xAE\x8D" => "\xE1\x8E\xBD", + "\xEA\xAE\x8E" => "\xE1\x8E\xBE", + "\xEA\xAE\x8F" => "\xE1\x8E\xBF", + "\xEA\xAE\x90" => "\xE1\x8F\x80", + "\xEA\xAE\x91" => "\xE1\x8F\x81", + "\xEA\xAE\x92" => "\xE1\x8F\x82", + "\xEA\xAE\x93" => "\xE1\x8F\x83", + "\xEA\xAE\x94" => "\xE1\x8F\x84", + "\xEA\xAE\x95" => "\xE1\x8F\x85", + "\xEA\xAE\x96" => "\xE1\x8F\x86", + "\xEA\xAE\x97" => "\xE1\x8F\x87", + "\xEA\xAE\x98" => "\xE1\x8F\x88", + "\xEA\xAE\x99" => "\xE1\x8F\x89", + "\xEA\xAE\x9A" => "\xE1\x8F\x8A", + "\xEA\xAE\x9B" => "\xE1\x8F\x8B", + "\xEA\xAE\x9C" => "\xE1\x8F\x8C", + "\xEA\xAE\x9D" => "\xE1\x8F\x8D", + "\xEA\xAE\x9E" => "\xE1\x8F\x8E", + "\xEA\xAE\x9F" => "\xE1\x8F\x8F", + "\xEA\xAE\xA0" => "\xE1\x8F\x90", + "\xEA\xAE\xA1" => "\xE1\x8F\x91", + "\xEA\xAE\xA2" => "\xE1\x8F\x92", + "\xEA\xAE\xA3" => "\xE1\x8F\x93", + "\xEA\xAE\xA4" => "\xE1\x8F\x94", + "\xEA\xAE\xA5" => "\xE1\x8F\x95", + "\xEA\xAE\xA6" => "\xE1\x8F\x96", + "\xEA\xAE\xA7" => "\xE1\x8F\x97", + "\xEA\xAE\xA8" => "\xE1\x8F\x98", + "\xEA\xAE\xA9" => "\xE1\x8F\x99", + "\xEA\xAE\xAA" => "\xE1\x8F\x9A", + "\xEA\xAE\xAB" => "\xE1\x8F\x9B", + "\xEA\xAE\xAC" => "\xE1\x8F\x9C", + "\xEA\xAE\xAD" => "\xE1\x8F\x9D", + "\xEA\xAE\xAE" => "\xE1\x8F\x9E", + "\xEA\xAE\xAF" => "\xE1\x8F\x9F", + "\xEA\xAE\xB0" => "\xE1\x8F\xA0", + "\xEA\xAE\xB1" => "\xE1\x8F\xA1", + "\xEA\xAE\xB2" => "\xE1\x8F\xA2", + "\xEA\xAE\xB3" => "\xE1\x8F\xA3", + "\xEA\xAE\xB4" => "\xE1\x8F\xA4", + "\xEA\xAE\xB5" => "\xE1\x8F\xA5", + "\xEA\xAE\xB6" => "\xE1\x8F\xA6", + "\xEA\xAE\xB7" => "\xE1\x8F\xA7", + "\xEA\xAE\xB8" => "\xE1\x8F\xA8", + "\xEA\xAE\xB9" => "\xE1\x8F\xA9", + "\xEA\xAE\xBA" => "\xE1\x8F\xAA", + "\xEA\xAE\xBB" => "\xE1\x8F\xAB", + "\xEA\xAE\xBC" => "\xE1\x8F\xAC", + "\xEA\xAE\xBD" => "\xE1\x8F\xAD", + "\xEA\xAE\xBE" => "\xE1\x8F\xAE", + "\xEA\xAE\xBF" => "\xE1\x8F\xAF", + "\xEF\xBD\x81" => "\xEF\xBC\xA1", + "\xEF\xBD\x82" => "\xEF\xBC\xA2", + "\xEF\xBD\x83" => "\xEF\xBC\xA3", + "\xEF\xBD\x84" => "\xEF\xBC\xA4", + "\xEF\xBD\x85" => "\xEF\xBC\xA5", + "\xEF\xBD\x86" => "\xEF\xBC\xA6", + "\xEF\xBD\x87" => "\xEF\xBC\xA7", + "\xEF\xBD\x88" => "\xEF\xBC\xA8", + "\xEF\xBD\x89" => "\xEF\xBC\xA9", + "\xEF\xBD\x8A" => "\xEF\xBC\xAA", + "\xEF\xBD\x8B" => "\xEF\xBC\xAB", + "\xEF\xBD\x8C" => "\xEF\xBC\xAC", + "\xEF\xBD\x8D" => "\xEF\xBC\xAD", + "\xEF\xBD\x8E" => "\xEF\xBC\xAE", + "\xEF\xBD\x8F" => "\xEF\xBC\xAF", + "\xEF\xBD\x90" => "\xEF\xBC\xB0", + "\xEF\xBD\x91" => "\xEF\xBC\xB1", + "\xEF\xBD\x92" => "\xEF\xBC\xB2", + "\xEF\xBD\x93" => "\xEF\xBC\xB3", + "\xEF\xBD\x94" => "\xEF\xBC\xB4", + "\xEF\xBD\x95" => "\xEF\xBC\xB5", + "\xEF\xBD\x96" => "\xEF\xBC\xB6", + "\xEF\xBD\x97" => "\xEF\xBC\xB7", + "\xEF\xBD\x98" => "\xEF\xBC\xB8", + "\xEF\xBD\x99" => "\xEF\xBC\xB9", + "\xEF\xBD\x9A" => "\xEF\xBC\xBA", + "\xF0\x90\x90\xA8" => "\xF0\x90\x90\x80", + "\xF0\x90\x90\xA9" => "\xF0\x90\x90\x81", + "\xF0\x90\x90\xAA" => "\xF0\x90\x90\x82", + "\xF0\x90\x90\xAB" => "\xF0\x90\x90\x83", + "\xF0\x90\x90\xAC" => "\xF0\x90\x90\x84", + "\xF0\x90\x90\xAD" => "\xF0\x90\x90\x85", + "\xF0\x90\x90\xAE" => "\xF0\x90\x90\x86", + "\xF0\x90\x90\xAF" => "\xF0\x90\x90\x87", + "\xF0\x90\x90\xB0" => "\xF0\x90\x90\x88", + "\xF0\x90\x90\xB1" => "\xF0\x90\x90\x89", + "\xF0\x90\x90\xB2" => "\xF0\x90\x90\x8A", + "\xF0\x90\x90\xB3" => "\xF0\x90\x90\x8B", + "\xF0\x90\x90\xB4" => "\xF0\x90\x90\x8C", + "\xF0\x90\x90\xB5" => "\xF0\x90\x90\x8D", + "\xF0\x90\x90\xB6" => "\xF0\x90\x90\x8E", + "\xF0\x90\x90\xB7" => "\xF0\x90\x90\x8F", + "\xF0\x90\x90\xB8" => "\xF0\x90\x90\x90", + "\xF0\x90\x90\xB9" => "\xF0\x90\x90\x91", + "\xF0\x90\x90\xBA" => "\xF0\x90\x90\x92", + "\xF0\x90\x90\xBB" => "\xF0\x90\x90\x93", + "\xF0\x90\x90\xBC" => "\xF0\x90\x90\x94", + "\xF0\x90\x90\xBD" => "\xF0\x90\x90\x95", + "\xF0\x90\x90\xBE" => "\xF0\x90\x90\x96", + "\xF0\x90\x90\xBF" => "\xF0\x90\x90\x97", + "\xF0\x90\x91\x80" => "\xF0\x90\x90\x98", + "\xF0\x90\x91\x81" => "\xF0\x90\x90\x99", + "\xF0\x90\x91\x82" => "\xF0\x90\x90\x9A", + "\xF0\x90\x91\x83" => "\xF0\x90\x90\x9B", + "\xF0\x90\x91\x84" => "\xF0\x90\x90\x9C", + "\xF0\x90\x91\x85" => "\xF0\x90\x90\x9D", + "\xF0\x90\x91\x86" => "\xF0\x90\x90\x9E", + "\xF0\x90\x91\x87" => "\xF0\x90\x90\x9F", + "\xF0\x90\x91\x88" => "\xF0\x90\x90\xA0", + "\xF0\x90\x91\x89" => "\xF0\x90\x90\xA1", + "\xF0\x90\x91\x8A" => "\xF0\x90\x90\xA2", + "\xF0\x90\x91\x8B" => "\xF0\x90\x90\xA3", + "\xF0\x90\x91\x8C" => "\xF0\x90\x90\xA4", + "\xF0\x90\x91\x8D" => "\xF0\x90\x90\xA5", + "\xF0\x90\x91\x8E" => "\xF0\x90\x90\xA6", + "\xF0\x90\x91\x8F" => "\xF0\x90\x90\xA7", + "\xF0\x90\x93\x98" => "\xF0\x90\x92\xB0", + "\xF0\x90\x93\x99" => "\xF0\x90\x92\xB1", + "\xF0\x90\x93\x9A" => "\xF0\x90\x92\xB2", + "\xF0\x90\x93\x9B" => "\xF0\x90\x92\xB3", + "\xF0\x90\x93\x9C" => "\xF0\x90\x92\xB4", + "\xF0\x90\x93\x9D" => "\xF0\x90\x92\xB5", + "\xF0\x90\x93\x9E" => "\xF0\x90\x92\xB6", + "\xF0\x90\x93\x9F" => "\xF0\x90\x92\xB7", + "\xF0\x90\x93\xA0" => "\xF0\x90\x92\xB8", + "\xF0\x90\x93\xA1" => "\xF0\x90\x92\xB9", + "\xF0\x90\x93\xA2" => "\xF0\x90\x92\xBA", + "\xF0\x90\x93\xA3" => "\xF0\x90\x92\xBB", + "\xF0\x90\x93\xA4" => "\xF0\x90\x92\xBC", + "\xF0\x90\x93\xA5" => "\xF0\x90\x92\xBD", + "\xF0\x90\x93\xA6" => "\xF0\x90\x92\xBE", + "\xF0\x90\x93\xA7" => "\xF0\x90\x92\xBF", + "\xF0\x90\x93\xA8" => "\xF0\x90\x93\x80", + "\xF0\x90\x93\xA9" => "\xF0\x90\x93\x81", + "\xF0\x90\x93\xAA" => "\xF0\x90\x93\x82", + "\xF0\x90\x93\xAB" => "\xF0\x90\x93\x83", + "\xF0\x90\x93\xAC" => "\xF0\x90\x93\x84", + "\xF0\x90\x93\xAD" => "\xF0\x90\x93\x85", + "\xF0\x90\x93\xAE" => "\xF0\x90\x93\x86", + "\xF0\x90\x93\xAF" => "\xF0\x90\x93\x87", + "\xF0\x90\x93\xB0" => "\xF0\x90\x93\x88", + "\xF0\x90\x93\xB1" => "\xF0\x90\x93\x89", + "\xF0\x90\x93\xB2" => "\xF0\x90\x93\x8A", + "\xF0\x90\x93\xB3" => "\xF0\x90\x93\x8B", + "\xF0\x90\x93\xB4" => "\xF0\x90\x93\x8C", + "\xF0\x90\x93\xB5" => "\xF0\x90\x93\x8D", + "\xF0\x90\x93\xB6" => "\xF0\x90\x93\x8E", + "\xF0\x90\x93\xB7" => "\xF0\x90\x93\x8F", + "\xF0\x90\x93\xB8" => "\xF0\x90\x93\x90", + "\xF0\x90\x93\xB9" => "\xF0\x90\x93\x91", + "\xF0\x90\x93\xBA" => "\xF0\x90\x93\x92", + "\xF0\x90\x93\xBB" => "\xF0\x90\x93\x93", + "\xF0\x90\x96\x97" => "\xF0\x90\x95\xB0", + "\xF0\x90\x96\x98" => "\xF0\x90\x95\xB1", + "\xF0\x90\x96\x99" => "\xF0\x90\x95\xB2", + "\xF0\x90\x96\x9A" => "\xF0\x90\x95\xB3", + "\xF0\x90\x96\x9B" => "\xF0\x90\x95\xB4", + "\xF0\x90\x96\x9C" => "\xF0\x90\x95\xB5", + "\xF0\x90\x96\x9D" => "\xF0\x90\x95\xB6", + "\xF0\x90\x96\x9E" => "\xF0\x90\x95\xB7", + "\xF0\x90\x96\x9F" => "\xF0\x90\x95\xB8", + "\xF0\x90\x96\xA0" => "\xF0\x90\x95\xB9", + "\xF0\x90\x96\xA1" => "\xF0\x90\x95\xBA", + "\xF0\x90\x96\xA3" => "\xF0\x90\x95\xBC", + "\xF0\x90\x96\xA4" => "\xF0\x90\x95\xBD", + "\xF0\x90\x96\xA5" => "\xF0\x90\x95\xBE", + "\xF0\x90\x96\xA6" => "\xF0\x90\x95\xBF", + "\xF0\x90\x96\xA7" => "\xF0\x90\x96\x80", + "\xF0\x90\x96\xA8" => "\xF0\x90\x96\x81", + "\xF0\x90\x96\xA9" => "\xF0\x90\x96\x82", + "\xF0\x90\x96\xAA" => "\xF0\x90\x96\x83", + "\xF0\x90\x96\xAB" => "\xF0\x90\x96\x84", + "\xF0\x90\x96\xAC" => "\xF0\x90\x96\x85", + "\xF0\x90\x96\xAD" => "\xF0\x90\x96\x86", + "\xF0\x90\x96\xAE" => "\xF0\x90\x96\x87", + "\xF0\x90\x96\xAF" => "\xF0\x90\x96\x88", + "\xF0\x90\x96\xB0" => "\xF0\x90\x96\x89", + "\xF0\x90\x96\xB1" => "\xF0\x90\x96\x8A", + "\xF0\x90\x96\xB3" => "\xF0\x90\x96\x8C", + "\xF0\x90\x96\xB4" => "\xF0\x90\x96\x8D", + "\xF0\x90\x96\xB5" => "\xF0\x90\x96\x8E", + "\xF0\x90\x96\xB6" => "\xF0\x90\x96\x8F", + "\xF0\x90\x96\xB7" => "\xF0\x90\x96\x90", + "\xF0\x90\x96\xB8" => "\xF0\x90\x96\x91", + "\xF0\x90\x96\xB9" => "\xF0\x90\x96\x92", + "\xF0\x90\x96\xBB" => "\xF0\x90\x96\x94", + "\xF0\x90\x96\xBC" => "\xF0\x90\x96\x95", + "\xF0\x90\xB3\x80" => "\xF0\x90\xB2\x80", + "\xF0\x90\xB3\x81" => "\xF0\x90\xB2\x81", + "\xF0\x90\xB3\x82" => "\xF0\x90\xB2\x82", + "\xF0\x90\xB3\x83" => "\xF0\x90\xB2\x83", + "\xF0\x90\xB3\x84" => "\xF0\x90\xB2\x84", + "\xF0\x90\xB3\x85" => "\xF0\x90\xB2\x85", + "\xF0\x90\xB3\x86" => "\xF0\x90\xB2\x86", + "\xF0\x90\xB3\x87" => "\xF0\x90\xB2\x87", + "\xF0\x90\xB3\x88" => "\xF0\x90\xB2\x88", + "\xF0\x90\xB3\x89" => "\xF0\x90\xB2\x89", + "\xF0\x90\xB3\x8A" => "\xF0\x90\xB2\x8A", + "\xF0\x90\xB3\x8B" => "\xF0\x90\xB2\x8B", + "\xF0\x90\xB3\x8C" => "\xF0\x90\xB2\x8C", + "\xF0\x90\xB3\x8D" => "\xF0\x90\xB2\x8D", + "\xF0\x90\xB3\x8E" => "\xF0\x90\xB2\x8E", + "\xF0\x90\xB3\x8F" => "\xF0\x90\xB2\x8F", + "\xF0\x90\xB3\x90" => "\xF0\x90\xB2\x90", + "\xF0\x90\xB3\x91" => "\xF0\x90\xB2\x91", + "\xF0\x90\xB3\x92" => "\xF0\x90\xB2\x92", + "\xF0\x90\xB3\x93" => "\xF0\x90\xB2\x93", + "\xF0\x90\xB3\x94" => "\xF0\x90\xB2\x94", + "\xF0\x90\xB3\x95" => "\xF0\x90\xB2\x95", + "\xF0\x90\xB3\x96" => "\xF0\x90\xB2\x96", + "\xF0\x90\xB3\x97" => "\xF0\x90\xB2\x97", + "\xF0\x90\xB3\x98" => "\xF0\x90\xB2\x98", + "\xF0\x90\xB3\x99" => "\xF0\x90\xB2\x99", + "\xF0\x90\xB3\x9A" => "\xF0\x90\xB2\x9A", + "\xF0\x90\xB3\x9B" => "\xF0\x90\xB2\x9B", + "\xF0\x90\xB3\x9C" => "\xF0\x90\xB2\x9C", + "\xF0\x90\xB3\x9D" => "\xF0\x90\xB2\x9D", + "\xF0\x90\xB3\x9E" => "\xF0\x90\xB2\x9E", + "\xF0\x90\xB3\x9F" => "\xF0\x90\xB2\x9F", + "\xF0\x90\xB3\xA0" => "\xF0\x90\xB2\xA0", + "\xF0\x90\xB3\xA1" => "\xF0\x90\xB2\xA1", + "\xF0\x90\xB3\xA2" => "\xF0\x90\xB2\xA2", + "\xF0\x90\xB3\xA3" => "\xF0\x90\xB2\xA3", + "\xF0\x90\xB3\xA4" => "\xF0\x90\xB2\xA4", + "\xF0\x90\xB3\xA5" => "\xF0\x90\xB2\xA5", + "\xF0\x90\xB3\xA6" => "\xF0\x90\xB2\xA6", + "\xF0\x90\xB3\xA7" => "\xF0\x90\xB2\xA7", + "\xF0\x90\xB3\xA8" => "\xF0\x90\xB2\xA8", + "\xF0\x90\xB3\xA9" => "\xF0\x90\xB2\xA9", + "\xF0\x90\xB3\xAA" => "\xF0\x90\xB2\xAA", + "\xF0\x90\xB3\xAB" => "\xF0\x90\xB2\xAB", + "\xF0\x90\xB3\xAC" => "\xF0\x90\xB2\xAC", + "\xF0\x90\xB3\xAD" => "\xF0\x90\xB2\xAD", + "\xF0\x90\xB3\xAE" => "\xF0\x90\xB2\xAE", + "\xF0\x90\xB3\xAF" => "\xF0\x90\xB2\xAF", + "\xF0\x90\xB3\xB0" => "\xF0\x90\xB2\xB0", + "\xF0\x90\xB3\xB1" => "\xF0\x90\xB2\xB1", + "\xF0\x90\xB3\xB2" => "\xF0\x90\xB2\xB2", + "\xF0\x91\xA3\x80" => "\xF0\x91\xA2\xA0", + "\xF0\x91\xA3\x81" => "\xF0\x91\xA2\xA1", + "\xF0\x91\xA3\x82" => "\xF0\x91\xA2\xA2", + "\xF0\x91\xA3\x83" => "\xF0\x91\xA2\xA3", + "\xF0\x91\xA3\x84" => "\xF0\x91\xA2\xA4", + "\xF0\x91\xA3\x85" => "\xF0\x91\xA2\xA5", + "\xF0\x91\xA3\x86" => "\xF0\x91\xA2\xA6", + "\xF0\x91\xA3\x87" => "\xF0\x91\xA2\xA7", + "\xF0\x91\xA3\x88" => "\xF0\x91\xA2\xA8", + "\xF0\x91\xA3\x89" => "\xF0\x91\xA2\xA9", + "\xF0\x91\xA3\x8A" => "\xF0\x91\xA2\xAA", + "\xF0\x91\xA3\x8B" => "\xF0\x91\xA2\xAB", + "\xF0\x91\xA3\x8C" => "\xF0\x91\xA2\xAC", + "\xF0\x91\xA3\x8D" => "\xF0\x91\xA2\xAD", + "\xF0\x91\xA3\x8E" => "\xF0\x91\xA2\xAE", + "\xF0\x91\xA3\x8F" => "\xF0\x91\xA2\xAF", + "\xF0\x91\xA3\x90" => "\xF0\x91\xA2\xB0", + "\xF0\x91\xA3\x91" => "\xF0\x91\xA2\xB1", + "\xF0\x91\xA3\x92" => "\xF0\x91\xA2\xB2", + "\xF0\x91\xA3\x93" => "\xF0\x91\xA2\xB3", + "\xF0\x91\xA3\x94" => "\xF0\x91\xA2\xB4", + "\xF0\x91\xA3\x95" => "\xF0\x91\xA2\xB5", + "\xF0\x91\xA3\x96" => "\xF0\x91\xA2\xB6", + "\xF0\x91\xA3\x97" => "\xF0\x91\xA2\xB7", + "\xF0\x91\xA3\x98" => "\xF0\x91\xA2\xB8", + "\xF0\x91\xA3\x99" => "\xF0\x91\xA2\xB9", + "\xF0\x91\xA3\x9A" => "\xF0\x91\xA2\xBA", + "\xF0\x91\xA3\x9B" => "\xF0\x91\xA2\xBB", + "\xF0\x91\xA3\x9C" => "\xF0\x91\xA2\xBC", + "\xF0\x91\xA3\x9D" => "\xF0\x91\xA2\xBD", + "\xF0\x91\xA3\x9E" => "\xF0\x91\xA2\xBE", + "\xF0\x91\xA3\x9F" => "\xF0\x91\xA2\xBF", + "\xF0\x96\xB9\xA0" => "\xF0\x96\xB9\x80", + "\xF0\x96\xB9\xA1" => "\xF0\x96\xB9\x81", + "\xF0\x96\xB9\xA2" => "\xF0\x96\xB9\x82", + "\xF0\x96\xB9\xA3" => "\xF0\x96\xB9\x83", + "\xF0\x96\xB9\xA4" => "\xF0\x96\xB9\x84", + "\xF0\x96\xB9\xA5" => "\xF0\x96\xB9\x85", + "\xF0\x96\xB9\xA6" => "\xF0\x96\xB9\x86", + "\xF0\x96\xB9\xA7" => "\xF0\x96\xB9\x87", + "\xF0\x96\xB9\xA8" => "\xF0\x96\xB9\x88", + "\xF0\x96\xB9\xA9" => "\xF0\x96\xB9\x89", + "\xF0\x96\xB9\xAA" => "\xF0\x96\xB9\x8A", + "\xF0\x96\xB9\xAB" => "\xF0\x96\xB9\x8B", + "\xF0\x96\xB9\xAC" => "\xF0\x96\xB9\x8C", + "\xF0\x96\xB9\xAD" => "\xF0\x96\xB9\x8D", + "\xF0\x96\xB9\xAE" => "\xF0\x96\xB9\x8E", + "\xF0\x96\xB9\xAF" => "\xF0\x96\xB9\x8F", + "\xF0\x96\xB9\xB0" => "\xF0\x96\xB9\x90", + "\xF0\x96\xB9\xB1" => "\xF0\x96\xB9\x91", + "\xF0\x96\xB9\xB2" => "\xF0\x96\xB9\x92", + "\xF0\x96\xB9\xB3" => "\xF0\x96\xB9\x93", + "\xF0\x96\xB9\xB4" => "\xF0\x96\xB9\x94", + "\xF0\x96\xB9\xB5" => "\xF0\x96\xB9\x95", + "\xF0\x96\xB9\xB6" => "\xF0\x96\xB9\x96", + "\xF0\x96\xB9\xB7" => "\xF0\x96\xB9\x97", + "\xF0\x96\xB9\xB8" => "\xF0\x96\xB9\x98", + "\xF0\x96\xB9\xB9" => "\xF0\x96\xB9\x99", + "\xF0\x96\xB9\xBA" => "\xF0\x96\xB9\x9A", + "\xF0\x96\xB9\xBB" => "\xF0\x96\xB9\x9B", + "\xF0\x96\xB9\xBC" => "\xF0\x96\xB9\x9C", + "\xF0\x96\xB9\xBD" => "\xF0\x96\xB9\x9D", + "\xF0\x96\xB9\xBE" => "\xF0\x96\xB9\x9E", + "\xF0\x96\xB9\xBF" => "\xF0\x96\xB9\x9F", + "\xF0\x9E\xA4\xA2" => "\xF0\x9E\xA4\x80", + "\xF0\x9E\xA4\xA3" => "\xF0\x9E\xA4\x81", + "\xF0\x9E\xA4\xA4" => "\xF0\x9E\xA4\x82", + "\xF0\x9E\xA4\xA5" => "\xF0\x9E\xA4\x83", + "\xF0\x9E\xA4\xA6" => "\xF0\x9E\xA4\x84", + "\xF0\x9E\xA4\xA7" => "\xF0\x9E\xA4\x85", + "\xF0\x9E\xA4\xA8" => "\xF0\x9E\xA4\x86", + "\xF0\x9E\xA4\xA9" => "\xF0\x9E\xA4\x87", + "\xF0\x9E\xA4\xAA" => "\xF0\x9E\xA4\x88", + "\xF0\x9E\xA4\xAB" => "\xF0\x9E\xA4\x89", + "\xF0\x9E\xA4\xAC" => "\xF0\x9E\xA4\x8A", + "\xF0\x9E\xA4\xAD" => "\xF0\x9E\xA4\x8B", + "\xF0\x9E\xA4\xAE" => "\xF0\x9E\xA4\x8C", + "\xF0\x9E\xA4\xAF" => "\xF0\x9E\xA4\x8D", + "\xF0\x9E\xA4\xB0" => "\xF0\x9E\xA4\x8E", + "\xF0\x9E\xA4\xB1" => "\xF0\x9E\xA4\x8F", + "\xF0\x9E\xA4\xB2" => "\xF0\x9E\xA4\x90", + "\xF0\x9E\xA4\xB3" => "\xF0\x9E\xA4\x91", + "\xF0\x9E\xA4\xB4" => "\xF0\x9E\xA4\x92", + "\xF0\x9E\xA4\xB5" => "\xF0\x9E\xA4\x93", + "\xF0\x9E\xA4\xB6" => "\xF0\x9E\xA4\x94", + "\xF0\x9E\xA4\xB7" => "\xF0\x9E\xA4\x95", + "\xF0\x9E\xA4\xB8" => "\xF0\x9E\xA4\x96", + "\xF0\x9E\xA4\xB9" => "\xF0\x9E\xA4\x97", + "\xF0\x9E\xA4\xBA" => "\xF0\x9E\xA4\x98", + "\xF0\x9E\xA4\xBB" => "\xF0\x9E\xA4\x99", + "\xF0\x9E\xA4\xBC" => "\xF0\x9E\xA4\x9A", + "\xF0\x9E\xA4\xBD" => "\xF0\x9E\xA4\x9B", + "\xF0\x9E\xA4\xBE" => "\xF0\x9E\xA4\x9C", + "\xF0\x9E\xA4\xBF" => "\xF0\x9E\xA4\x9D", + "\xF0\x9E\xA5\x80" => "\xF0\x9E\xA4\x9E", + "\xF0\x9E\xA5\x81" => "\xF0\x9E\xA4\x9F", + "\xF0\x9E\xA5\x82" => "\xF0\x9E\xA4\xA0", + "\xF0\x9E\xA5\x83" => "\xF0\x9E\xA4\xA1", + ); +} + +/** + * Helper function for utf8_strtoupper. + * + * Developers: Do not update the data in this function manually. Instead, + * run "php -f other/update_unicode_data.php" on the command line. + * + * @return array Lowercase to uppercase maps. + */ +function utf8_strtoupper_maps() +{ + return array( + "\x61" => "\x41", + "\x62" => "\x42", + "\x63" => "\x43", + "\x64" => "\x44", + "\x65" => "\x45", + "\x66" => "\x46", + "\x67" => "\x47", + "\x68" => "\x48", + "\x69" => "\x49", + "\x6A" => "\x4A", + "\x6B" => "\x4B", + "\x6C" => "\x4C", + "\x6D" => "\x4D", + "\x6E" => "\x4E", + "\x6F" => "\x4F", + "\x70" => "\x50", + "\x71" => "\x51", + "\x72" => "\x52", + "\x73" => "\x53", + "\x74" => "\x54", + "\x75" => "\x55", + "\x76" => "\x56", + "\x77" => "\x57", + "\x78" => "\x58", + "\x79" => "\x59", + "\x7A" => "\x5A", + "\xC2\xB5" => "\xCE\x9C", + "\xC3\x9F" => "\x53\x53", + "\xC3\xA0" => "\xC3\x80", + "\xC3\xA1" => "\xC3\x81", + "\xC3\xA2" => "\xC3\x82", + "\xC3\xA3" => "\xC3\x83", + "\xC3\xA4" => "\xC3\x84", + "\xC3\xA5" => "\xC3\x85", + "\xC3\xA6" => "\xC3\x86", + "\xC3\xA7" => "\xC3\x87", + "\xC3\xA8" => "\xC3\x88", + "\xC3\xA9" => "\xC3\x89", + "\xC3\xAA" => "\xC3\x8A", + "\xC3\xAB" => "\xC3\x8B", + "\xC3\xAC" => "\xC3\x8C", + "\xC3\xAD" => "\xC3\x8D", + "\xC3\xAE" => "\xC3\x8E", + "\xC3\xAF" => "\xC3\x8F", + "\xC3\xB0" => "\xC3\x90", + "\xC3\xB1" => "\xC3\x91", + "\xC3\xB2" => "\xC3\x92", + "\xC3\xB3" => "\xC3\x93", + "\xC3\xB4" => "\xC3\x94", + "\xC3\xB5" => "\xC3\x95", + "\xC3\xB6" => "\xC3\x96", + "\xC3\xB8" => "\xC3\x98", + "\xC3\xB9" => "\xC3\x99", + "\xC3\xBA" => "\xC3\x9A", + "\xC3\xBB" => "\xC3\x9B", + "\xC3\xBC" => "\xC3\x9C", + "\xC3\xBD" => "\xC3\x9D", + "\xC3\xBE" => "\xC3\x9E", + "\xC3\xBF" => "\xC5\xB8", + "\xC4\x81" => "\xC4\x80", + "\xC4\x83" => "\xC4\x82", + "\xC4\x85" => "\xC4\x84", + "\xC4\x87" => "\xC4\x86", + "\xC4\x89" => "\xC4\x88", + "\xC4\x8B" => "\xC4\x8A", + "\xC4\x8D" => "\xC4\x8C", + "\xC4\x8F" => "\xC4\x8E", + "\xC4\x91" => "\xC4\x90", + "\xC4\x93" => "\xC4\x92", + "\xC4\x95" => "\xC4\x94", + "\xC4\x97" => "\xC4\x96", + "\xC4\x99" => "\xC4\x98", + "\xC4\x9B" => "\xC4\x9A", + "\xC4\x9D" => "\xC4\x9C", + "\xC4\x9F" => "\xC4\x9E", + "\xC4\xA1" => "\xC4\xA0", + "\xC4\xA3" => "\xC4\xA2", + "\xC4\xA5" => "\xC4\xA4", + "\xC4\xA7" => "\xC4\xA6", + "\xC4\xA9" => "\xC4\xA8", + "\xC4\xAB" => "\xC4\xAA", + "\xC4\xAD" => "\xC4\xAC", + "\xC4\xAF" => "\xC4\xAE", + "\xC4\xB0" => "\xC4\xB0", + "\xC4\xB1" => "\x49", + "\xC4\xB3" => "\xC4\xB2", + "\xC4\xB5" => "\xC4\xB4", + "\xC4\xB7" => "\xC4\xB6", + "\xC4\xBA" => "\xC4\xB9", + "\xC4\xBC" => "\xC4\xBB", + "\xC4\xBE" => "\xC4\xBD", + "\xC5\x80" => "\xC4\xBF", + "\xC5\x82" => "\xC5\x81", + "\xC5\x84" => "\xC5\x83", + "\xC5\x86" => "\xC5\x85", + "\xC5\x88" => "\xC5\x87", + "\xC5\x89" => "\xCA\xBC\x4E", + "\xC5\x8B" => "\xC5\x8A", + "\xC5\x8D" => "\xC5\x8C", + "\xC5\x8F" => "\xC5\x8E", + "\xC5\x91" => "\xC5\x90", + "\xC5\x93" => "\xC5\x92", + "\xC5\x95" => "\xC5\x94", + "\xC5\x97" => "\xC5\x96", + "\xC5\x99" => "\xC5\x98", + "\xC5\x9B" => "\xC5\x9A", + "\xC5\x9D" => "\xC5\x9C", + "\xC5\x9F" => "\xC5\x9E", + "\xC5\xA1" => "\xC5\xA0", + "\xC5\xA3" => "\xC5\xA2", + "\xC5\xA5" => "\xC5\xA4", + "\xC5\xA7" => "\xC5\xA6", + "\xC5\xA9" => "\xC5\xA8", + "\xC5\xAB" => "\xC5\xAA", + "\xC5\xAD" => "\xC5\xAC", + "\xC5\xAF" => "\xC5\xAE", + "\xC5\xB1" => "\xC5\xB0", + "\xC5\xB3" => "\xC5\xB2", + "\xC5\xB5" => "\xC5\xB4", + "\xC5\xB7" => "\xC5\xB6", + "\xC5\xBA" => "\xC5\xB9", + "\xC5\xBC" => "\xC5\xBB", + "\xC5\xBE" => "\xC5\xBD", + "\xC5\xBF" => "\x53", + "\xC6\x80" => "\xC9\x83", + "\xC6\x83" => "\xC6\x82", + "\xC6\x85" => "\xC6\x84", + "\xC6\x88" => "\xC6\x87", + "\xC6\x8C" => "\xC6\x8B", + "\xC6\x92" => "\xC6\x91", + "\xC6\x95" => "\xC7\xB6", + "\xC6\x99" => "\xC6\x98", + "\xC6\x9A" => "\xC8\xBD", + "\xC6\x9E" => "\xC8\xA0", + "\xC6\xA1" => "\xC6\xA0", + "\xC6\xA3" => "\xC6\xA2", + "\xC6\xA5" => "\xC6\xA4", + "\xC6\xA8" => "\xC6\xA7", + "\xC6\xAD" => "\xC6\xAC", + "\xC6\xB0" => "\xC6\xAF", + "\xC6\xB4" => "\xC6\xB3", + "\xC6\xB6" => "\xC6\xB5", + "\xC6\xB9" => "\xC6\xB8", + "\xC6\xBD" => "\xC6\xBC", + "\xC6\xBF" => "\xC7\xB7", + "\xC7\x85" => "\xC7\x84", + "\xC7\x86" => "\xC7\x84", + "\xC7\x88" => "\xC7\x87", + "\xC7\x89" => "\xC7\x87", + "\xC7\x8B" => "\xC7\x8A", + "\xC7\x8C" => "\xC7\x8A", + "\xC7\x8E" => "\xC7\x8D", + "\xC7\x90" => "\xC7\x8F", + "\xC7\x92" => "\xC7\x91", + "\xC7\x94" => "\xC7\x93", + "\xC7\x96" => "\xC7\x95", + "\xC7\x98" => "\xC7\x97", + "\xC7\x9A" => "\xC7\x99", + "\xC7\x9C" => "\xC7\x9B", + "\xC7\x9D" => "\xC6\x8E", + "\xC7\x9F" => "\xC7\x9E", + "\xC7\xA1" => "\xC7\xA0", + "\xC7\xA3" => "\xC7\xA2", + "\xC7\xA5" => "\xC7\xA4", + "\xC7\xA7" => "\xC7\xA6", + "\xC7\xA9" => "\xC7\xA8", + "\xC7\xAB" => "\xC7\xAA", + "\xC7\xAD" => "\xC7\xAC", + "\xC7\xAF" => "\xC7\xAE", + "\xC7\xB0" => "\x4A\xCC\x8C", + "\xC7\xB2" => "\xC7\xB1", + "\xC7\xB3" => "\xC7\xB1", + "\xC7\xB5" => "\xC7\xB4", + "\xC7\xB9" => "\xC7\xB8", + "\xC7\xBB" => "\xC7\xBA", + "\xC7\xBD" => "\xC7\xBC", + "\xC7\xBF" => "\xC7\xBE", + "\xC8\x81" => "\xC8\x80", + "\xC8\x83" => "\xC8\x82", + "\xC8\x85" => "\xC8\x84", + "\xC8\x87" => "\xC8\x86", + "\xC8\x89" => "\xC8\x88", + "\xC8\x8B" => "\xC8\x8A", + "\xC8\x8D" => "\xC8\x8C", + "\xC8\x8F" => "\xC8\x8E", + "\xC8\x91" => "\xC8\x90", + "\xC8\x93" => "\xC8\x92", + "\xC8\x95" => "\xC8\x94", + "\xC8\x97" => "\xC8\x96", + "\xC8\x99" => "\xC8\x98", + "\xC8\x9B" => "\xC8\x9A", + "\xC8\x9D" => "\xC8\x9C", + "\xC8\x9F" => "\xC8\x9E", + "\xC8\xA3" => "\xC8\xA2", + "\xC8\xA5" => "\xC8\xA4", + "\xC8\xA7" => "\xC8\xA6", + "\xC8\xA9" => "\xC8\xA8", + "\xC8\xAB" => "\xC8\xAA", + "\xC8\xAD" => "\xC8\xAC", + "\xC8\xAF" => "\xC8\xAE", + "\xC8\xB1" => "\xC8\xB0", + "\xC8\xB3" => "\xC8\xB2", + "\xC8\xBC" => "\xC8\xBB", + "\xC8\xBF" => "\xE2\xB1\xBE", + "\xC9\x80" => "\xE2\xB1\xBF", + "\xC9\x82" => "\xC9\x81", + "\xC9\x87" => "\xC9\x86", + "\xC9\x89" => "\xC9\x88", + "\xC9\x8B" => "\xC9\x8A", + "\xC9\x8D" => "\xC9\x8C", + "\xC9\x8F" => "\xC9\x8E", + "\xC9\x90" => "\xE2\xB1\xAF", + "\xC9\x91" => "\xE2\xB1\xAD", + "\xC9\x92" => "\xE2\xB1\xB0", + "\xC9\x93" => "\xC6\x81", + "\xC9\x94" => "\xC6\x86", + "\xC9\x96" => "\xC6\x89", + "\xC9\x97" => "\xC6\x8A", + "\xC9\x99" => "\xC6\x8F", + "\xC9\x9B" => "\xC6\x90", + "\xC9\x9C" => "\xEA\x9E\xAB", + "\xC9\xA0" => "\xC6\x93", + "\xC9\xA1" => "\xEA\x9E\xAC", + "\xC9\xA3" => "\xC6\x94", + "\xC9\xA5" => "\xEA\x9E\x8D", + "\xC9\xA6" => "\xEA\x9E\xAA", + "\xC9\xA8" => "\xC6\x97", + "\xC9\xA9" => "\xC6\x96", + "\xC9\xAA" => "\xEA\x9E\xAE", + "\xC9\xAB" => "\xE2\xB1\xA2", + "\xC9\xAC" => "\xEA\x9E\xAD", + "\xC9\xAF" => "\xC6\x9C", + "\xC9\xB1" => "\xE2\xB1\xAE", + "\xC9\xB2" => "\xC6\x9D", + "\xC9\xB5" => "\xC6\x9F", + "\xC9\xBD" => "\xE2\xB1\xA4", + "\xCA\x80" => "\xC6\xA6", + "\xCA\x82" => "\xEA\x9F\x85", + "\xCA\x83" => "\xC6\xA9", + "\xCA\x87" => "\xEA\x9E\xB1", + "\xCA\x88" => "\xC6\xAE", + "\xCA\x89" => "\xC9\x84", + "\xCA\x8A" => "\xC6\xB1", + "\xCA\x8B" => "\xC6\xB2", + "\xCA\x8C" => "\xC9\x85", + "\xCA\x92" => "\xC6\xB7", + "\xCA\x9D" => "\xEA\x9E\xB2", + "\xCA\x9E" => "\xEA\x9E\xB0", + "\xCD\x85" => "\xCE\x99", + "\xCD\xB1" => "\xCD\xB0", + "\xCD\xB3" => "\xCD\xB2", + "\xCD\xB7" => "\xCD\xB6", + "\xCD\xBB" => "\xCF\xBD", + "\xCD\xBC" => "\xCF\xBE", + "\xCD\xBD" => "\xCF\xBF", + "\xCE\x90" => "\xCE\x99\xCC\x88\xCC\x81", + "\xCE\xAC" => "\xCE\x86", + "\xCE\xAD" => "\xCE\x88", + "\xCE\xAE" => "\xCE\x89", + "\xCE\xAF" => "\xCE\x8A", + "\xCE\xB0" => "\xCE\xA5\xCC\x88\xCC\x81", + "\xCE\xB1" => "\xCE\x91", + "\xCE\xB2" => "\xCE\x92", + "\xCE\xB3" => "\xCE\x93", + "\xCE\xB4" => "\xCE\x94", + "\xCE\xB5" => "\xCE\x95", + "\xCE\xB6" => "\xCE\x96", + "\xCE\xB7" => "\xCE\x97", + "\xCE\xB8" => "\xCE\x98", + "\xCE\xB9" => "\xCE\x99", + "\xCE\xBA" => "\xCE\x9A", + "\xCE\xBB" => "\xCE\x9B", + "\xCE\xBC" => "\xCE\x9C", + "\xCE\xBD" => "\xCE\x9D", + "\xCE\xBE" => "\xCE\x9E", + "\xCE\xBF" => "\xCE\x9F", + "\xCF\x80" => "\xCE\xA0", + "\xCF\x81" => "\xCE\xA1", + "\xCF\x82" => "\xCE\xA3", + "\xCF\x83" => "\xCE\xA3", + "\xCF\x84" => "\xCE\xA4", + "\xCF\x85" => "\xCE\xA5", + "\xCF\x86" => "\xCE\xA6", + "\xCF\x87" => "\xCE\xA7", + "\xCF\x88" => "\xCE\xA8", + "\xCF\x89" => "\xCE\xA9", + "\xCF\x8A" => "\xCE\xAA", + "\xCF\x8B" => "\xCE\xAB", + "\xCF\x8C" => "\xCE\x8C", + "\xCF\x8D" => "\xCE\x8E", + "\xCF\x8E" => "\xCE\x8F", + "\xCF\x90" => "\xCE\x92", + "\xCF\x91" => "\xCE\x98", + "\xCF\x95" => "\xCE\xA6", + "\xCF\x96" => "\xCE\xA0", + "\xCF\x97" => "\xCF\x8F", + "\xCF\x99" => "\xCF\x98", + "\xCF\x9B" => "\xCF\x9A", + "\xCF\x9D" => "\xCF\x9C", + "\xCF\x9F" => "\xCF\x9E", + "\xCF\xA1" => "\xCF\xA0", + "\xCF\xA3" => "\xCF\xA2", + "\xCF\xA5" => "\xCF\xA4", + "\xCF\xA7" => "\xCF\xA6", + "\xCF\xA9" => "\xCF\xA8", + "\xCF\xAB" => "\xCF\xAA", + "\xCF\xAD" => "\xCF\xAC", + "\xCF\xAF" => "\xCF\xAE", + "\xCF\xB0" => "\xCE\x9A", + "\xCF\xB1" => "\xCE\xA1", + "\xCF\xB2" => "\xCF\xB9", + "\xCF\xB3" => "\xCD\xBF", + "\xCF\xB5" => "\xCE\x95", + "\xCF\xB8" => "\xCF\xB7", + "\xCF\xBB" => "\xCF\xBA", + "\xD0\xB0" => "\xD0\x90", + "\xD0\xB1" => "\xD0\x91", + "\xD0\xB2" => "\xD0\x92", + "\xD0\xB3" => "\xD0\x93", + "\xD0\xB4" => "\xD0\x94", + "\xD0\xB5" => "\xD0\x95", + "\xD0\xB6" => "\xD0\x96", + "\xD0\xB7" => "\xD0\x97", + "\xD0\xB8" => "\xD0\x98", + "\xD0\xB9" => "\xD0\x99", + "\xD0\xBA" => "\xD0\x9A", + "\xD0\xBB" => "\xD0\x9B", + "\xD0\xBC" => "\xD0\x9C", + "\xD0\xBD" => "\xD0\x9D", + "\xD0\xBE" => "\xD0\x9E", + "\xD0\xBF" => "\xD0\x9F", + "\xD1\x80" => "\xD0\xA0", + "\xD1\x81" => "\xD0\xA1", + "\xD1\x82" => "\xD0\xA2", + "\xD1\x83" => "\xD0\xA3", + "\xD1\x84" => "\xD0\xA4", + "\xD1\x85" => "\xD0\xA5", + "\xD1\x86" => "\xD0\xA6", + "\xD1\x87" => "\xD0\xA7", + "\xD1\x88" => "\xD0\xA8", + "\xD1\x89" => "\xD0\xA9", + "\xD1\x8A" => "\xD0\xAA", + "\xD1\x8B" => "\xD0\xAB", + "\xD1\x8C" => "\xD0\xAC", + "\xD1\x8D" => "\xD0\xAD", + "\xD1\x8E" => "\xD0\xAE", + "\xD1\x8F" => "\xD0\xAF", + "\xD1\x90" => "\xD0\x80", + "\xD1\x91" => "\xD0\x81", + "\xD1\x92" => "\xD0\x82", + "\xD1\x93" => "\xD0\x83", + "\xD1\x94" => "\xD0\x84", + "\xD1\x95" => "\xD0\x85", + "\xD1\x96" => "\xD0\x86", + "\xD1\x97" => "\xD0\x87", + "\xD1\x98" => "\xD0\x88", + "\xD1\x99" => "\xD0\x89", + "\xD1\x9A" => "\xD0\x8A", + "\xD1\x9B" => "\xD0\x8B", + "\xD1\x9C" => "\xD0\x8C", + "\xD1\x9D" => "\xD0\x8D", + "\xD1\x9E" => "\xD0\x8E", + "\xD1\x9F" => "\xD0\x8F", + "\xD1\xA1" => "\xD1\xA0", + "\xD1\xA3" => "\xD1\xA2", + "\xD1\xA5" => "\xD1\xA4", + "\xD1\xA7" => "\xD1\xA6", + "\xD1\xA9" => "\xD1\xA8", + "\xD1\xAB" => "\xD1\xAA", + "\xD1\xAD" => "\xD1\xAC", + "\xD1\xAF" => "\xD1\xAE", + "\xD1\xB1" => "\xD1\xB0", + "\xD1\xB3" => "\xD1\xB2", + "\xD1\xB5" => "\xD1\xB4", + "\xD1\xB7" => "\xD1\xB6", + "\xD1\xB9" => "\xD1\xB8", + "\xD1\xBB" => "\xD1\xBA", + "\xD1\xBD" => "\xD1\xBC", + "\xD1\xBF" => "\xD1\xBE", + "\xD2\x81" => "\xD2\x80", + "\xD2\x8B" => "\xD2\x8A", + "\xD2\x8D" => "\xD2\x8C", + "\xD2\x8F" => "\xD2\x8E", + "\xD2\x91" => "\xD2\x90", + "\xD2\x93" => "\xD2\x92", + "\xD2\x95" => "\xD2\x94", + "\xD2\x97" => "\xD2\x96", + "\xD2\x99" => "\xD2\x98", + "\xD2\x9B" => "\xD2\x9A", + "\xD2\x9D" => "\xD2\x9C", + "\xD2\x9F" => "\xD2\x9E", + "\xD2\xA1" => "\xD2\xA0", + "\xD2\xA3" => "\xD2\xA2", + "\xD2\xA5" => "\xD2\xA4", + "\xD2\xA7" => "\xD2\xA6", + "\xD2\xA9" => "\xD2\xA8", + "\xD2\xAB" => "\xD2\xAA", + "\xD2\xAD" => "\xD2\xAC", + "\xD2\xAF" => "\xD2\xAE", + "\xD2\xB1" => "\xD2\xB0", + "\xD2\xB3" => "\xD2\xB2", + "\xD2\xB5" => "\xD2\xB4", + "\xD2\xB7" => "\xD2\xB6", + "\xD2\xB9" => "\xD2\xB8", + "\xD2\xBB" => "\xD2\xBA", + "\xD2\xBD" => "\xD2\xBC", + "\xD2\xBF" => "\xD2\xBE", + "\xD3\x82" => "\xD3\x81", + "\xD3\x84" => "\xD3\x83", + "\xD3\x86" => "\xD3\x85", + "\xD3\x88" => "\xD3\x87", + "\xD3\x8A" => "\xD3\x89", + "\xD3\x8C" => "\xD3\x8B", + "\xD3\x8E" => "\xD3\x8D", + "\xD3\x8F" => "\xD3\x80", + "\xD3\x91" => "\xD3\x90", + "\xD3\x93" => "\xD3\x92", + "\xD3\x95" => "\xD3\x94", + "\xD3\x97" => "\xD3\x96", + "\xD3\x99" => "\xD3\x98", + "\xD3\x9B" => "\xD3\x9A", + "\xD3\x9D" => "\xD3\x9C", + "\xD3\x9F" => "\xD3\x9E", + "\xD3\xA1" => "\xD3\xA0", + "\xD3\xA3" => "\xD3\xA2", + "\xD3\xA5" => "\xD3\xA4", + "\xD3\xA7" => "\xD3\xA6", + "\xD3\xA9" => "\xD3\xA8", + "\xD3\xAB" => "\xD3\xAA", + "\xD3\xAD" => "\xD3\xAC", + "\xD3\xAF" => "\xD3\xAE", + "\xD3\xB1" => "\xD3\xB0", + "\xD3\xB3" => "\xD3\xB2", + "\xD3\xB5" => "\xD3\xB4", + "\xD3\xB7" => "\xD3\xB6", + "\xD3\xB9" => "\xD3\xB8", + "\xD3\xBB" => "\xD3\xBA", + "\xD3\xBD" => "\xD3\xBC", + "\xD3\xBF" => "\xD3\xBE", + "\xD4\x81" => "\xD4\x80", + "\xD4\x83" => "\xD4\x82", + "\xD4\x85" => "\xD4\x84", + "\xD4\x87" => "\xD4\x86", + "\xD4\x89" => "\xD4\x88", + "\xD4\x8B" => "\xD4\x8A", + "\xD4\x8D" => "\xD4\x8C", + "\xD4\x8F" => "\xD4\x8E", + "\xD4\x91" => "\xD4\x90", + "\xD4\x93" => "\xD4\x92", + "\xD4\x95" => "\xD4\x94", + "\xD4\x97" => "\xD4\x96", + "\xD4\x99" => "\xD4\x98", + "\xD4\x9B" => "\xD4\x9A", + "\xD4\x9D" => "\xD4\x9C", + "\xD4\x9F" => "\xD4\x9E", + "\xD4\xA1" => "\xD4\xA0", + "\xD4\xA3" => "\xD4\xA2", + "\xD4\xA5" => "\xD4\xA4", + "\xD4\xA7" => "\xD4\xA6", + "\xD4\xA9" => "\xD4\xA8", + "\xD4\xAB" => "\xD4\xAA", + "\xD4\xAD" => "\xD4\xAC", + "\xD4\xAF" => "\xD4\xAE", + "\xD5\xA1" => "\xD4\xB1", + "\xD5\xA2" => "\xD4\xB2", + "\xD5\xA3" => "\xD4\xB3", + "\xD5\xA4" => "\xD4\xB4", + "\xD5\xA5" => "\xD4\xB5", + "\xD5\xA6" => "\xD4\xB6", + "\xD5\xA7" => "\xD4\xB7", + "\xD5\xA8" => "\xD4\xB8", + "\xD5\xA9" => "\xD4\xB9", + "\xD5\xAA" => "\xD4\xBA", + "\xD5\xAB" => "\xD4\xBB", + "\xD5\xAC" => "\xD4\xBC", + "\xD5\xAD" => "\xD4\xBD", + "\xD5\xAE" => "\xD4\xBE", + "\xD5\xAF" => "\xD4\xBF", + "\xD5\xB0" => "\xD5\x80", + "\xD5\xB1" => "\xD5\x81", + "\xD5\xB2" => "\xD5\x82", + "\xD5\xB3" => "\xD5\x83", + "\xD5\xB4" => "\xD5\x84", + "\xD5\xB5" => "\xD5\x85", + "\xD5\xB6" => "\xD5\x86", + "\xD5\xB7" => "\xD5\x87", + "\xD5\xB8" => "\xD5\x88", + "\xD5\xB9" => "\xD5\x89", + "\xD5\xBA" => "\xD5\x8A", + "\xD5\xBB" => "\xD5\x8B", + "\xD5\xBC" => "\xD5\x8C", + "\xD5\xBD" => "\xD5\x8D", + "\xD5\xBE" => "\xD5\x8E", + "\xD5\xBF" => "\xD5\x8F", + "\xD6\x80" => "\xD5\x90", + "\xD6\x81" => "\xD5\x91", + "\xD6\x82" => "\xD5\x92", + "\xD6\x83" => "\xD5\x93", + "\xD6\x84" => "\xD5\x94", + "\xD6\x85" => "\xD5\x95", + "\xD6\x86" => "\xD5\x96", + "\xD6\x87" => "\xD4\xB5\xD5\x92", + "\xF0\x90\x90\xA8" => "\xF0\x90\x90\x80", + "\xF0\x90\x90\xA9" => "\xF0\x90\x90\x81", + "\xF0\x90\x90\xAA" => "\xF0\x90\x90\x82", + "\xF0\x90\x90\xAB" => "\xF0\x90\x90\x83", + "\xF0\x90\x90\xAC" => "\xF0\x90\x90\x84", + "\xF0\x90\x90\xAD" => "\xF0\x90\x90\x85", + "\xF0\x90\x90\xAE" => "\xF0\x90\x90\x86", + "\xF0\x90\x90\xAF" => "\xF0\x90\x90\x87", + "\xF0\x90\x90\xB0" => "\xF0\x90\x90\x88", + "\xF0\x90\x90\xB1" => "\xF0\x90\x90\x89", + "\xF0\x90\x90\xB2" => "\xF0\x90\x90\x8A", + "\xF0\x90\x90\xB3" => "\xF0\x90\x90\x8B", + "\xF0\x90\x90\xB4" => "\xF0\x90\x90\x8C", + "\xF0\x90\x90\xB5" => "\xF0\x90\x90\x8D", + "\xF0\x90\x90\xB6" => "\xF0\x90\x90\x8E", + "\xF0\x90\x90\xB7" => "\xF0\x90\x90\x8F", + "\xF0\x90\x90\xB8" => "\xF0\x90\x90\x90", + "\xF0\x90\x90\xB9" => "\xF0\x90\x90\x91", + "\xF0\x90\x90\xBA" => "\xF0\x90\x90\x92", + "\xF0\x90\x90\xBB" => "\xF0\x90\x90\x93", + "\xF0\x90\x90\xBC" => "\xF0\x90\x90\x94", + "\xF0\x90\x90\xBD" => "\xF0\x90\x90\x95", + "\xF0\x90\x90\xBE" => "\xF0\x90\x90\x96", + "\xF0\x90\x90\xBF" => "\xF0\x90\x90\x97", + "\xF0\x90\x91\x80" => "\xF0\x90\x90\x98", + "\xF0\x90\x91\x81" => "\xF0\x90\x90\x99", + "\xF0\x90\x91\x82" => "\xF0\x90\x90\x9A", + "\xF0\x90\x91\x83" => "\xF0\x90\x90\x9B", + "\xF0\x90\x91\x84" => "\xF0\x90\x90\x9C", + "\xF0\x90\x91\x85" => "\xF0\x90\x90\x9D", + "\xF0\x90\x91\x86" => "\xF0\x90\x90\x9E", + "\xF0\x90\x91\x87" => "\xF0\x90\x90\x9F", + "\xF0\x90\x91\x88" => "\xF0\x90\x90\xA0", + "\xF0\x90\x91\x89" => "\xF0\x90\x90\xA1", + "\xF0\x90\x91\x8A" => "\xF0\x90\x90\xA2", + "\xF0\x90\x91\x8B" => "\xF0\x90\x90\xA3", + "\xF0\x90\x91\x8C" => "\xF0\x90\x90\xA4", + "\xF0\x90\x91\x8D" => "\xF0\x90\x90\xA5", + "\xF0\x90\x91\x8E" => "\xF0\x90\x90\xA6", + "\xF0\x90\x91\x8F" => "\xF0\x90\x90\xA7", + "\xF0\x90\x93\x98" => "\xF0\x90\x92\xB0", + "\xF0\x90\x93\x99" => "\xF0\x90\x92\xB1", + "\xF0\x90\x93\x9A" => "\xF0\x90\x92\xB2", + "\xF0\x90\x93\x9B" => "\xF0\x90\x92\xB3", + "\xF0\x90\x93\x9C" => "\xF0\x90\x92\xB4", + "\xF0\x90\x93\x9D" => "\xF0\x90\x92\xB5", + "\xF0\x90\x93\x9E" => "\xF0\x90\x92\xB6", + "\xF0\x90\x93\x9F" => "\xF0\x90\x92\xB7", + "\xF0\x90\x93\xA0" => "\xF0\x90\x92\xB8", + "\xF0\x90\x93\xA1" => "\xF0\x90\x92\xB9", + "\xF0\x90\x93\xA2" => "\xF0\x90\x92\xBA", + "\xF0\x90\x93\xA3" => "\xF0\x90\x92\xBB", + "\xF0\x90\x93\xA4" => "\xF0\x90\x92\xBC", + "\xF0\x90\x93\xA5" => "\xF0\x90\x92\xBD", + "\xF0\x90\x93\xA6" => "\xF0\x90\x92\xBE", + "\xF0\x90\x93\xA7" => "\xF0\x90\x92\xBF", + "\xF0\x90\x93\xA8" => "\xF0\x90\x93\x80", + "\xF0\x90\x93\xA9" => "\xF0\x90\x93\x81", + "\xF0\x90\x93\xAA" => "\xF0\x90\x93\x82", + "\xF0\x90\x93\xAB" => "\xF0\x90\x93\x83", + "\xF0\x90\x93\xAC" => "\xF0\x90\x93\x84", + "\xF0\x90\x93\xAD" => "\xF0\x90\x93\x85", + "\xF0\x90\x93\xAE" => "\xF0\x90\x93\x86", + "\xF0\x90\x93\xAF" => "\xF0\x90\x93\x87", + "\xF0\x90\x93\xB0" => "\xF0\x90\x93\x88", + "\xF0\x90\x93\xB1" => "\xF0\x90\x93\x89", + "\xF0\x90\x93\xB2" => "\xF0\x90\x93\x8A", + "\xF0\x90\x93\xB3" => "\xF0\x90\x93\x8B", + "\xF0\x90\x93\xB4" => "\xF0\x90\x93\x8C", + "\xF0\x90\x93\xB5" => "\xF0\x90\x93\x8D", + "\xF0\x90\x93\xB6" => "\xF0\x90\x93\x8E", + "\xF0\x90\x93\xB7" => "\xF0\x90\x93\x8F", + "\xF0\x90\x93\xB8" => "\xF0\x90\x93\x90", + "\xF0\x90\x93\xB9" => "\xF0\x90\x93\x91", + "\xF0\x90\x93\xBA" => "\xF0\x90\x93\x92", + "\xF0\x90\x93\xBB" => "\xF0\x90\x93\x93", + "\xF0\x90\x96\x97" => "\xF0\x90\x95\xB0", + "\xF0\x90\x96\x98" => "\xF0\x90\x95\xB1", + "\xF0\x90\x96\x99" => "\xF0\x90\x95\xB2", + "\xF0\x90\x96\x9A" => "\xF0\x90\x95\xB3", + "\xF0\x90\x96\x9B" => "\xF0\x90\x95\xB4", + "\xF0\x90\x96\x9C" => "\xF0\x90\x95\xB5", + "\xF0\x90\x96\x9D" => "\xF0\x90\x95\xB6", + "\xF0\x90\x96\x9E" => "\xF0\x90\x95\xB7", + "\xF0\x90\x96\x9F" => "\xF0\x90\x95\xB8", + "\xF0\x90\x96\xA0" => "\xF0\x90\x95\xB9", + "\xF0\x90\x96\xA1" => "\xF0\x90\x95\xBA", + "\xF0\x90\x96\xA3" => "\xF0\x90\x95\xBC", + "\xF0\x90\x96\xA4" => "\xF0\x90\x95\xBD", + "\xF0\x90\x96\xA5" => "\xF0\x90\x95\xBE", + "\xF0\x90\x96\xA6" => "\xF0\x90\x95\xBF", + "\xF0\x90\x96\xA7" => "\xF0\x90\x96\x80", + "\xF0\x90\x96\xA8" => "\xF0\x90\x96\x81", + "\xF0\x90\x96\xA9" => "\xF0\x90\x96\x82", + "\xF0\x90\x96\xAA" => "\xF0\x90\x96\x83", + "\xF0\x90\x96\xAB" => "\xF0\x90\x96\x84", + "\xF0\x90\x96\xAC" => "\xF0\x90\x96\x85", + "\xF0\x90\x96\xAD" => "\xF0\x90\x96\x86", + "\xF0\x90\x96\xAE" => "\xF0\x90\x96\x87", + "\xF0\x90\x96\xAF" => "\xF0\x90\x96\x88", + "\xF0\x90\x96\xB0" => "\xF0\x90\x96\x89", + "\xF0\x90\x96\xB1" => "\xF0\x90\x96\x8A", + "\xF0\x90\x96\xB3" => "\xF0\x90\x96\x8C", + "\xF0\x90\x96\xB4" => "\xF0\x90\x96\x8D", + "\xF0\x90\x96\xB5" => "\xF0\x90\x96\x8E", + "\xF0\x90\x96\xB6" => "\xF0\x90\x96\x8F", + "\xF0\x90\x96\xB7" => "\xF0\x90\x96\x90", + "\xF0\x90\x96\xB8" => "\xF0\x90\x96\x91", + "\xF0\x90\x96\xB9" => "\xF0\x90\x96\x92", + "\xF0\x90\x96\xBB" => "\xF0\x90\x96\x94", + "\xF0\x90\x96\xBC" => "\xF0\x90\x96\x95", + "\xF0\x90\xB3\x80" => "\xF0\x90\xB2\x80", + "\xF0\x90\xB3\x81" => "\xF0\x90\xB2\x81", + "\xF0\x90\xB3\x82" => "\xF0\x90\xB2\x82", + "\xF0\x90\xB3\x83" => "\xF0\x90\xB2\x83", + "\xF0\x90\xB3\x84" => "\xF0\x90\xB2\x84", + "\xF0\x90\xB3\x85" => "\xF0\x90\xB2\x85", + "\xF0\x90\xB3\x86" => "\xF0\x90\xB2\x86", + "\xF0\x90\xB3\x87" => "\xF0\x90\xB2\x87", + "\xF0\x90\xB3\x88" => "\xF0\x90\xB2\x88", + "\xF0\x90\xB3\x89" => "\xF0\x90\xB2\x89", + "\xF0\x90\xB3\x8A" => "\xF0\x90\xB2\x8A", + "\xF0\x90\xB3\x8B" => "\xF0\x90\xB2\x8B", + "\xF0\x90\xB3\x8C" => "\xF0\x90\xB2\x8C", + "\xF0\x90\xB3\x8D" => "\xF0\x90\xB2\x8D", + "\xF0\x90\xB3\x8E" => "\xF0\x90\xB2\x8E", + "\xF0\x90\xB3\x8F" => "\xF0\x90\xB2\x8F", + "\xF0\x90\xB3\x90" => "\xF0\x90\xB2\x90", + "\xF0\x90\xB3\x91" => "\xF0\x90\xB2\x91", + "\xF0\x90\xB3\x92" => "\xF0\x90\xB2\x92", + "\xF0\x90\xB3\x93" => "\xF0\x90\xB2\x93", + "\xF0\x90\xB3\x94" => "\xF0\x90\xB2\x94", + "\xF0\x90\xB3\x95" => "\xF0\x90\xB2\x95", + "\xF0\x90\xB3\x96" => "\xF0\x90\xB2\x96", + "\xF0\x90\xB3\x97" => "\xF0\x90\xB2\x97", + "\xF0\x90\xB3\x98" => "\xF0\x90\xB2\x98", + "\xF0\x90\xB3\x99" => "\xF0\x90\xB2\x99", + "\xF0\x90\xB3\x9A" => "\xF0\x90\xB2\x9A", + "\xF0\x90\xB3\x9B" => "\xF0\x90\xB2\x9B", + "\xF0\x90\xB3\x9C" => "\xF0\x90\xB2\x9C", + "\xF0\x90\xB3\x9D" => "\xF0\x90\xB2\x9D", + "\xF0\x90\xB3\x9E" => "\xF0\x90\xB2\x9E", + "\xF0\x90\xB3\x9F" => "\xF0\x90\xB2\x9F", + "\xF0\x90\xB3\xA0" => "\xF0\x90\xB2\xA0", + "\xF0\x90\xB3\xA1" => "\xF0\x90\xB2\xA1", + "\xF0\x90\xB3\xA2" => "\xF0\x90\xB2\xA2", + "\xF0\x90\xB3\xA3" => "\xF0\x90\xB2\xA3", + "\xF0\x90\xB3\xA4" => "\xF0\x90\xB2\xA4", + "\xF0\x90\xB3\xA5" => "\xF0\x90\xB2\xA5", + "\xF0\x90\xB3\xA6" => "\xF0\x90\xB2\xA6", + "\xF0\x90\xB3\xA7" => "\xF0\x90\xB2\xA7", + "\xF0\x90\xB3\xA8" => "\xF0\x90\xB2\xA8", + "\xF0\x90\xB3\xA9" => "\xF0\x90\xB2\xA9", + "\xF0\x90\xB3\xAA" => "\xF0\x90\xB2\xAA", + "\xF0\x90\xB3\xAB" => "\xF0\x90\xB2\xAB", + "\xF0\x90\xB3\xAC" => "\xF0\x90\xB2\xAC", + "\xF0\x90\xB3\xAD" => "\xF0\x90\xB2\xAD", + "\xF0\x90\xB3\xAE" => "\xF0\x90\xB2\xAE", + "\xF0\x90\xB3\xAF" => "\xF0\x90\xB2\xAF", + "\xF0\x90\xB3\xB0" => "\xF0\x90\xB2\xB0", + "\xF0\x90\xB3\xB1" => "\xF0\x90\xB2\xB1", + "\xF0\x90\xB3\xB2" => "\xF0\x90\xB2\xB2", + "\xE1\x83\x90" => "\xE1\xB2\x90", + "\xE1\x83\x91" => "\xE1\xB2\x91", + "\xE1\x83\x92" => "\xE1\xB2\x92", + "\xE1\x83\x93" => "\xE1\xB2\x93", + "\xE1\x83\x94" => "\xE1\xB2\x94", + "\xE1\x83\x95" => "\xE1\xB2\x95", + "\xE1\x83\x96" => "\xE1\xB2\x96", + "\xE1\x83\x97" => "\xE1\xB2\x97", + "\xE1\x83\x98" => "\xE1\xB2\x98", + "\xE1\x83\x99" => "\xE1\xB2\x99", + "\xE1\x83\x9A" => "\xE1\xB2\x9A", + "\xE1\x83\x9B" => "\xE1\xB2\x9B", + "\xE1\x83\x9C" => "\xE1\xB2\x9C", + "\xE1\x83\x9D" => "\xE1\xB2\x9D", + "\xE1\x83\x9E" => "\xE1\xB2\x9E", + "\xE1\x83\x9F" => "\xE1\xB2\x9F", + "\xE1\x83\xA0" => "\xE1\xB2\xA0", + "\xE1\x83\xA1" => "\xE1\xB2\xA1", + "\xE1\x83\xA2" => "\xE1\xB2\xA2", + "\xE1\x83\xA3" => "\xE1\xB2\xA3", + "\xE1\x83\xA4" => "\xE1\xB2\xA4", + "\xE1\x83\xA5" => "\xE1\xB2\xA5", + "\xE1\x83\xA6" => "\xE1\xB2\xA6", + "\xE1\x83\xA7" => "\xE1\xB2\xA7", + "\xE1\x83\xA8" => "\xE1\xB2\xA8", + "\xE1\x83\xA9" => "\xE1\xB2\xA9", + "\xE1\x83\xAA" => "\xE1\xB2\xAA", + "\xE1\x83\xAB" => "\xE1\xB2\xAB", + "\xE1\x83\xAC" => "\xE1\xB2\xAC", + "\xE1\x83\xAD" => "\xE1\xB2\xAD", + "\xE1\x83\xAE" => "\xE1\xB2\xAE", + "\xE1\x83\xAF" => "\xE1\xB2\xAF", + "\xE1\x83\xB0" => "\xE1\xB2\xB0", + "\xE1\x83\xB1" => "\xE1\xB2\xB1", + "\xE1\x83\xB2" => "\xE1\xB2\xB2", + "\xE1\x83\xB3" => "\xE1\xB2\xB3", + "\xE1\x83\xB4" => "\xE1\xB2\xB4", + "\xE1\x83\xB5" => "\xE1\xB2\xB5", + "\xE1\x83\xB6" => "\xE1\xB2\xB6", + "\xE1\x83\xB7" => "\xE1\xB2\xB7", + "\xE1\x83\xB8" => "\xE1\xB2\xB8", + "\xE1\x83\xB9" => "\xE1\xB2\xB9", + "\xE1\x83\xBA" => "\xE1\xB2\xBA", + "\xE1\x83\xBD" => "\xE1\xB2\xBD", + "\xE1\x83\xBE" => "\xE1\xB2\xBE", + "\xE1\x83\xBF" => "\xE1\xB2\xBF", + "\xF0\x91\xA3\x80" => "\xF0\x91\xA2\xA0", + "\xF0\x91\xA3\x81" => "\xF0\x91\xA2\xA1", + "\xF0\x91\xA3\x82" => "\xF0\x91\xA2\xA2", + "\xF0\x91\xA3\x83" => "\xF0\x91\xA2\xA3", + "\xF0\x91\xA3\x84" => "\xF0\x91\xA2\xA4", + "\xF0\x91\xA3\x85" => "\xF0\x91\xA2\xA5", + "\xF0\x91\xA3\x86" => "\xF0\x91\xA2\xA6", + "\xF0\x91\xA3\x87" => "\xF0\x91\xA2\xA7", + "\xF0\x91\xA3\x88" => "\xF0\x91\xA2\xA8", + "\xF0\x91\xA3\x89" => "\xF0\x91\xA2\xA9", + "\xF0\x91\xA3\x8A" => "\xF0\x91\xA2\xAA", + "\xF0\x91\xA3\x8B" => "\xF0\x91\xA2\xAB", + "\xF0\x91\xA3\x8C" => "\xF0\x91\xA2\xAC", + "\xF0\x91\xA3\x8D" => "\xF0\x91\xA2\xAD", + "\xF0\x91\xA3\x8E" => "\xF0\x91\xA2\xAE", + "\xF0\x91\xA3\x8F" => "\xF0\x91\xA2\xAF", + "\xF0\x91\xA3\x90" => "\xF0\x91\xA2\xB0", + "\xF0\x91\xA3\x91" => "\xF0\x91\xA2\xB1", + "\xF0\x91\xA3\x92" => "\xF0\x91\xA2\xB2", + "\xF0\x91\xA3\x93" => "\xF0\x91\xA2\xB3", + "\xF0\x91\xA3\x94" => "\xF0\x91\xA2\xB4", + "\xF0\x91\xA3\x95" => "\xF0\x91\xA2\xB5", + "\xF0\x91\xA3\x96" => "\xF0\x91\xA2\xB6", + "\xF0\x91\xA3\x97" => "\xF0\x91\xA2\xB7", + "\xF0\x91\xA3\x98" => "\xF0\x91\xA2\xB8", + "\xF0\x91\xA3\x99" => "\xF0\x91\xA2\xB9", + "\xF0\x91\xA3\x9A" => "\xF0\x91\xA2\xBA", + "\xF0\x91\xA3\x9B" => "\xF0\x91\xA2\xBB", + "\xF0\x91\xA3\x9C" => "\xF0\x91\xA2\xBC", + "\xF0\x91\xA3\x9D" => "\xF0\x91\xA2\xBD", + "\xF0\x91\xA3\x9E" => "\xF0\x91\xA2\xBE", + "\xF0\x91\xA3\x9F" => "\xF0\x91\xA2\xBF", + "\xE1\x8F\xB8" => "\xE1\x8F\xB0", + "\xE1\x8F\xB9" => "\xE1\x8F\xB1", + "\xE1\x8F\xBA" => "\xE1\x8F\xB2", + "\xE1\x8F\xBB" => "\xE1\x8F\xB3", + "\xE1\x8F\xBC" => "\xE1\x8F\xB4", + "\xE1\x8F\xBD" => "\xE1\x8F\xB5", + "\xF0\x96\xB9\xA0" => "\xF0\x96\xB9\x80", + "\xF0\x96\xB9\xA1" => "\xF0\x96\xB9\x81", + "\xF0\x96\xB9\xA2" => "\xF0\x96\xB9\x82", + "\xF0\x96\xB9\xA3" => "\xF0\x96\xB9\x83", + "\xF0\x96\xB9\xA4" => "\xF0\x96\xB9\x84", + "\xF0\x96\xB9\xA5" => "\xF0\x96\xB9\x85", + "\xF0\x96\xB9\xA6" => "\xF0\x96\xB9\x86", + "\xF0\x96\xB9\xA7" => "\xF0\x96\xB9\x87", + "\xF0\x96\xB9\xA8" => "\xF0\x96\xB9\x88", + "\xF0\x96\xB9\xA9" => "\xF0\x96\xB9\x89", + "\xF0\x96\xB9\xAA" => "\xF0\x96\xB9\x8A", + "\xF0\x96\xB9\xAB" => "\xF0\x96\xB9\x8B", + "\xF0\x96\xB9\xAC" => "\xF0\x96\xB9\x8C", + "\xF0\x96\xB9\xAD" => "\xF0\x96\xB9\x8D", + "\xF0\x96\xB9\xAE" => "\xF0\x96\xB9\x8E", + "\xF0\x96\xB9\xAF" => "\xF0\x96\xB9\x8F", + "\xF0\x96\xB9\xB0" => "\xF0\x96\xB9\x90", + "\xF0\x96\xB9\xB1" => "\xF0\x96\xB9\x91", + "\xF0\x96\xB9\xB2" => "\xF0\x96\xB9\x92", + "\xF0\x96\xB9\xB3" => "\xF0\x96\xB9\x93", + "\xF0\x96\xB9\xB4" => "\xF0\x96\xB9\x94", + "\xF0\x96\xB9\xB5" => "\xF0\x96\xB9\x95", + "\xF0\x96\xB9\xB6" => "\xF0\x96\xB9\x96", + "\xF0\x96\xB9\xB7" => "\xF0\x96\xB9\x97", + "\xF0\x96\xB9\xB8" => "\xF0\x96\xB9\x98", + "\xF0\x96\xB9\xB9" => "\xF0\x96\xB9\x99", + "\xF0\x96\xB9\xBA" => "\xF0\x96\xB9\x9A", + "\xF0\x96\xB9\xBB" => "\xF0\x96\xB9\x9B", + "\xF0\x96\xB9\xBC" => "\xF0\x96\xB9\x9C", + "\xF0\x96\xB9\xBD" => "\xF0\x96\xB9\x9D", + "\xF0\x96\xB9\xBE" => "\xF0\x96\xB9\x9E", + "\xF0\x96\xB9\xBF" => "\xF0\x96\xB9\x9F", + "\xE1\xB2\x80" => "\xD0\x92", + "\xE1\xB2\x81" => "\xD0\x94", + "\xE1\xB2\x82" => "\xD0\x9E", + "\xE1\xB2\x83" => "\xD0\xA1", + "\xE1\xB2\x84" => "\xD0\xA2", + "\xE1\xB2\x85" => "\xD0\xA2", + "\xE1\xB2\x86" => "\xD0\xAA", + "\xE1\xB2\x87" => "\xD1\xA2", + "\xE1\xB2\x88" => "\xEA\x99\x8A", + "\xE1\xB5\xB9" => "\xEA\x9D\xBD", + "\xE1\xB5\xBD" => "\xE2\xB1\xA3", + "\xE1\xB6\x8E" => "\xEA\x9F\x86", + "\xE1\xB8\x81" => "\xE1\xB8\x80", + "\xE1\xB8\x83" => "\xE1\xB8\x82", + "\xE1\xB8\x85" => "\xE1\xB8\x84", + "\xE1\xB8\x87" => "\xE1\xB8\x86", + "\xE1\xB8\x89" => "\xE1\xB8\x88", + "\xE1\xB8\x8B" => "\xE1\xB8\x8A", + "\xE1\xB8\x8D" => "\xE1\xB8\x8C", + "\xE1\xB8\x8F" => "\xE1\xB8\x8E", + "\xE1\xB8\x91" => "\xE1\xB8\x90", + "\xE1\xB8\x93" => "\xE1\xB8\x92", + "\xE1\xB8\x95" => "\xE1\xB8\x94", + "\xE1\xB8\x97" => "\xE1\xB8\x96", + "\xE1\xB8\x99" => "\xE1\xB8\x98", + "\xE1\xB8\x9B" => "\xE1\xB8\x9A", + "\xE1\xB8\x9D" => "\xE1\xB8\x9C", + "\xE1\xB8\x9F" => "\xE1\xB8\x9E", + "\xE1\xB8\xA1" => "\xE1\xB8\xA0", + "\xE1\xB8\xA3" => "\xE1\xB8\xA2", + "\xE1\xB8\xA5" => "\xE1\xB8\xA4", + "\xE1\xB8\xA7" => "\xE1\xB8\xA6", + "\xE1\xB8\xA9" => "\xE1\xB8\xA8", + "\xE1\xB8\xAB" => "\xE1\xB8\xAA", + "\xE1\xB8\xAD" => "\xE1\xB8\xAC", + "\xE1\xB8\xAF" => "\xE1\xB8\xAE", + "\xE1\xB8\xB1" => "\xE1\xB8\xB0", + "\xE1\xB8\xB3" => "\xE1\xB8\xB2", + "\xE1\xB8\xB5" => "\xE1\xB8\xB4", + "\xE1\xB8\xB7" => "\xE1\xB8\xB6", + "\xE1\xB8\xB9" => "\xE1\xB8\xB8", + "\xE1\xB8\xBB" => "\xE1\xB8\xBA", + "\xE1\xB8\xBD" => "\xE1\xB8\xBC", + "\xE1\xB8\xBF" => "\xE1\xB8\xBE", + "\xE1\xB9\x81" => "\xE1\xB9\x80", + "\xE1\xB9\x83" => "\xE1\xB9\x82", + "\xE1\xB9\x85" => "\xE1\xB9\x84", + "\xE1\xB9\x87" => "\xE1\xB9\x86", + "\xE1\xB9\x89" => "\xE1\xB9\x88", + "\xE1\xB9\x8B" => "\xE1\xB9\x8A", + "\xE1\xB9\x8D" => "\xE1\xB9\x8C", + "\xE1\xB9\x8F" => "\xE1\xB9\x8E", + "\xE1\xB9\x91" => "\xE1\xB9\x90", + "\xE1\xB9\x93" => "\xE1\xB9\x92", + "\xE1\xB9\x95" => "\xE1\xB9\x94", + "\xE1\xB9\x97" => "\xE1\xB9\x96", + "\xE1\xB9\x99" => "\xE1\xB9\x98", + "\xE1\xB9\x9B" => "\xE1\xB9\x9A", + "\xE1\xB9\x9D" => "\xE1\xB9\x9C", + "\xE1\xB9\x9F" => "\xE1\xB9\x9E", + "\xE1\xB9\xA1" => "\xE1\xB9\xA0", + "\xE1\xB9\xA3" => "\xE1\xB9\xA2", + "\xE1\xB9\xA5" => "\xE1\xB9\xA4", + "\xE1\xB9\xA7" => "\xE1\xB9\xA6", + "\xE1\xB9\xA9" => "\xE1\xB9\xA8", + "\xE1\xB9\xAB" => "\xE1\xB9\xAA", + "\xE1\xB9\xAD" => "\xE1\xB9\xAC", + "\xE1\xB9\xAF" => "\xE1\xB9\xAE", + "\xE1\xB9\xB1" => "\xE1\xB9\xB0", + "\xE1\xB9\xB3" => "\xE1\xB9\xB2", + "\xE1\xB9\xB5" => "\xE1\xB9\xB4", + "\xE1\xB9\xB7" => "\xE1\xB9\xB6", + "\xE1\xB9\xB9" => "\xE1\xB9\xB8", + "\xE1\xB9\xBB" => "\xE1\xB9\xBA", + "\xE1\xB9\xBD" => "\xE1\xB9\xBC", + "\xE1\xB9\xBF" => "\xE1\xB9\xBE", + "\xE1\xBA\x81" => "\xE1\xBA\x80", + "\xE1\xBA\x83" => "\xE1\xBA\x82", + "\xE1\xBA\x85" => "\xE1\xBA\x84", + "\xE1\xBA\x87" => "\xE1\xBA\x86", + "\xE1\xBA\x89" => "\xE1\xBA\x88", + "\xE1\xBA\x8B" => "\xE1\xBA\x8A", + "\xE1\xBA\x8D" => "\xE1\xBA\x8C", + "\xE1\xBA\x8F" => "\xE1\xBA\x8E", + "\xE1\xBA\x91" => "\xE1\xBA\x90", + "\xF0\x9E\xA4\xA2" => "\xF0\x9E\xA4\x80", + "\xF0\x9E\xA4\xA3" => "\xF0\x9E\xA4\x81", + "\xF0\x9E\xA4\xA4" => "\xF0\x9E\xA4\x82", + "\xF0\x9E\xA4\xA5" => "\xF0\x9E\xA4\x83", + "\xF0\x9E\xA4\xA6" => "\xF0\x9E\xA4\x84", + "\xF0\x9E\xA4\xA7" => "\xF0\x9E\xA4\x85", + "\xF0\x9E\xA4\xA8" => "\xF0\x9E\xA4\x86", + "\xF0\x9E\xA4\xA9" => "\xF0\x9E\xA4\x87", + "\xF0\x9E\xA4\xAA" => "\xF0\x9E\xA4\x88", + "\xF0\x9E\xA4\xAB" => "\xF0\x9E\xA4\x89", + "\xF0\x9E\xA4\xAC" => "\xF0\x9E\xA4\x8A", + "\xF0\x9E\xA4\xAD" => "\xF0\x9E\xA4\x8B", + "\xF0\x9E\xA4\xAE" => "\xF0\x9E\xA4\x8C", + "\xF0\x9E\xA4\xAF" => "\xF0\x9E\xA4\x8D", + "\xF0\x9E\xA4\xB0" => "\xF0\x9E\xA4\x8E", + "\xF0\x9E\xA4\xB1" => "\xF0\x9E\xA4\x8F", + "\xF0\x9E\xA4\xB2" => "\xF0\x9E\xA4\x90", + "\xF0\x9E\xA4\xB3" => "\xF0\x9E\xA4\x91", + "\xF0\x9E\xA4\xB4" => "\xF0\x9E\xA4\x92", + "\xF0\x9E\xA4\xB5" => "\xF0\x9E\xA4\x93", + "\xF0\x9E\xA4\xB6" => "\xF0\x9E\xA4\x94", + "\xF0\x9E\xA4\xB7" => "\xF0\x9E\xA4\x95", + "\xF0\x9E\xA4\xB8" => "\xF0\x9E\xA4\x96", + "\xF0\x9E\xA4\xB9" => "\xF0\x9E\xA4\x97", + "\xE1\xBA\x93" => "\xE1\xBA\x92", + "\xF0\x9E\xA4\xBA" => "\xF0\x9E\xA4\x98", + "\xF0\x9E\xA4\xBB" => "\xF0\x9E\xA4\x99", + "\xF0\x9E\xA4\xBC" => "\xF0\x9E\xA4\x9A", + "\xF0\x9E\xA4\xBD" => "\xF0\x9E\xA4\x9B", + "\xF0\x9E\xA4\xBE" => "\xF0\x9E\xA4\x9C", + "\xF0\x9E\xA4\xBF" => "\xF0\x9E\xA4\x9D", + "\xF0\x9E\xA5\x80" => "\xF0\x9E\xA4\x9E", + "\xF0\x9E\xA5\x81" => "\xF0\x9E\xA4\x9F", + "\xF0\x9E\xA5\x82" => "\xF0\x9E\xA4\xA0", + "\xF0\x9E\xA5\x83" => "\xF0\x9E\xA4\xA1", + "\xE1\xBA\x95" => "\xE1\xBA\x94", + "\xE1\xBA\x96" => "\x48\xCC\xB1", + "\xE1\xBA\x97" => "\x54\xCC\x88", + "\xE1\xBA\x98" => "\x57\xCC\x8A", + "\xE1\xBA\x99" => "\x59\xCC\x8A", + "\xE1\xBA\x9A" => "\x41\xCA\xBE", + "\xE1\xBA\x9B" => "\xE1\xB9\xA0", + "\xE1\xBA\xA1" => "\xE1\xBA\xA0", + "\xE1\xBA\xA3" => "\xE1\xBA\xA2", + "\xE1\xBA\xA5" => "\xE1\xBA\xA4", + "\xE1\xBA\xA7" => "\xE1\xBA\xA6", + "\xE1\xBA\xA9" => "\xE1\xBA\xA8", + "\xE1\xBA\xAB" => "\xE1\xBA\xAA", + "\xE1\xBA\xAD" => "\xE1\xBA\xAC", + "\xE1\xBA\xAF" => "\xE1\xBA\xAE", + "\xE1\xBA\xB1" => "\xE1\xBA\xB0", + "\xE1\xBA\xB3" => "\xE1\xBA\xB2", + "\xE1\xBA\xB5" => "\xE1\xBA\xB4", + "\xE1\xBA\xB7" => "\xE1\xBA\xB6", + "\xE1\xBA\xB9" => "\xE1\xBA\xB8", + "\xE1\xBA\xBB" => "\xE1\xBA\xBA", + "\xE1\xBA\xBD" => "\xE1\xBA\xBC", + "\xE1\xBA\xBF" => "\xE1\xBA\xBE", + "\xE1\xBB\x81" => "\xE1\xBB\x80", + "\xE1\xBB\x83" => "\xE1\xBB\x82", + "\xE1\xBB\x85" => "\xE1\xBB\x84", + "\xE1\xBB\x87" => "\xE1\xBB\x86", + "\xE1\xBB\x89" => "\xE1\xBB\x88", + "\xE1\xBB\x8B" => "\xE1\xBB\x8A", + "\xE1\xBB\x8D" => "\xE1\xBB\x8C", + "\xE1\xBB\x8F" => "\xE1\xBB\x8E", + "\xE1\xBB\x91" => "\xE1\xBB\x90", + "\xE1\xBB\x93" => "\xE1\xBB\x92", + "\xE1\xBB\x95" => "\xE1\xBB\x94", + "\xE1\xBB\x97" => "\xE1\xBB\x96", + "\xE1\xBB\x99" => "\xE1\xBB\x98", + "\xE1\xBB\x9B" => "\xE1\xBB\x9A", + "\xE1\xBB\x9D" => "\xE1\xBB\x9C", + "\xE1\xBB\x9F" => "\xE1\xBB\x9E", + "\xE1\xBB\xA1" => "\xE1\xBB\xA0", + "\xE1\xBB\xA3" => "\xE1\xBB\xA2", + "\xE1\xBB\xA5" => "\xE1\xBB\xA4", + "\xE1\xBB\xA7" => "\xE1\xBB\xA6", + "\xE1\xBB\xA9" => "\xE1\xBB\xA8", + "\xE1\xBB\xAB" => "\xE1\xBB\xAA", + "\xE1\xBB\xAD" => "\xE1\xBB\xAC", + "\xE1\xBB\xAF" => "\xE1\xBB\xAE", + "\xE1\xBB\xB1" => "\xE1\xBB\xB0", + "\xE1\xBB\xB3" => "\xE1\xBB\xB2", + "\xE1\xBB\xB5" => "\xE1\xBB\xB4", + "\xE1\xBB\xB7" => "\xE1\xBB\xB6", + "\xE1\xBB\xB9" => "\xE1\xBB\xB8", + "\xE1\xBB\xBB" => "\xE1\xBB\xBA", + "\xE1\xBB\xBD" => "\xE1\xBB\xBC", + "\xE1\xBB\xBF" => "\xE1\xBB\xBE", + "\xE1\xBC\x80" => "\xE1\xBC\x88", + "\xE1\xBC\x81" => "\xE1\xBC\x89", + "\xE1\xBC\x82" => "\xE1\xBC\x8A", + "\xE1\xBC\x83" => "\xE1\xBC\x8B", + "\xE1\xBC\x84" => "\xE1\xBC\x8C", + "\xE1\xBC\x85" => "\xE1\xBC\x8D", + "\xE1\xBC\x86" => "\xE1\xBC\x8E", + "\xE1\xBC\x87" => "\xE1\xBC\x8F", + "\xE1\xBC\x90" => "\xE1\xBC\x98", + "\xE1\xBC\x91" => "\xE1\xBC\x99", + "\xE1\xBC\x92" => "\xE1\xBC\x9A", + "\xE1\xBC\x93" => "\xE1\xBC\x9B", + "\xE1\xBC\x94" => "\xE1\xBC\x9C", + "\xE1\xBC\x95" => "\xE1\xBC\x9D", + "\xE1\xBC\xA0" => "\xE1\xBC\xA8", + "\xE1\xBC\xA1" => "\xE1\xBC\xA9", + "\xE1\xBC\xA2" => "\xE1\xBC\xAA", + "\xE1\xBC\xA3" => "\xE1\xBC\xAB", + "\xE1\xBC\xA4" => "\xE1\xBC\xAC", + "\xE1\xBC\xA5" => "\xE1\xBC\xAD", + "\xE1\xBC\xA6" => "\xE1\xBC\xAE", + "\xE1\xBC\xA7" => "\xE1\xBC\xAF", + "\xE1\xBC\xB0" => "\xE1\xBC\xB8", + "\xE1\xBC\xB1" => "\xE1\xBC\xB9", + "\xE1\xBC\xB2" => "\xE1\xBC\xBA", + "\xE1\xBC\xB3" => "\xE1\xBC\xBB", + "\xE1\xBC\xB4" => "\xE1\xBC\xBC", + "\xE1\xBC\xB5" => "\xE1\xBC\xBD", + "\xE1\xBC\xB6" => "\xE1\xBC\xBE", + "\xE1\xBC\xB7" => "\xE1\xBC\xBF", + "\xE1\xBD\x80" => "\xE1\xBD\x88", + "\xE1\xBD\x81" => "\xE1\xBD\x89", + "\xE1\xBD\x82" => "\xE1\xBD\x8A", + "\xE1\xBD\x83" => "\xE1\xBD\x8B", + "\xE1\xBD\x84" => "\xE1\xBD\x8C", + "\xE1\xBD\x85" => "\xE1\xBD\x8D", + "\xE1\xBD\x90" => "\xCE\xA5\xCC\x93", + "\xE1\xBD\x91" => "\xE1\xBD\x99", + "\xE1\xBD\x92" => "\xCE\xA5\xCC\x93\xCC\x80", + "\xE1\xBD\x93" => "\xE1\xBD\x9B", + "\xE1\xBD\x94" => "\xCE\xA5\xCC\x93\xCC\x81", + "\xE1\xBD\x95" => "\xE1\xBD\x9D", + "\xE1\xBD\x96" => "\xCE\xA5\xCC\x93\xCD\x82", + "\xE1\xBD\x97" => "\xE1\xBD\x9F", + "\xE1\xBD\xA0" => "\xE1\xBD\xA8", + "\xE1\xBD\xA1" => "\xE1\xBD\xA9", + "\xE1\xBD\xA2" => "\xE1\xBD\xAA", + "\xE1\xBD\xA3" => "\xE1\xBD\xAB", + "\xE1\xBD\xA4" => "\xE1\xBD\xAC", + "\xE1\xBD\xA5" => "\xE1\xBD\xAD", + "\xE1\xBD\xA6" => "\xE1\xBD\xAE", + "\xE1\xBD\xA7" => "\xE1\xBD\xAF", + "\xE1\xBD\xB0" => "\xE1\xBE\xBA", + "\xE1\xBD\xB1" => "\xE1\xBE\xBB", + "\xE1\xBD\xB2" => "\xE1\xBF\x88", + "\xE1\xBD\xB3" => "\xE1\xBF\x89", + "\xE1\xBD\xB4" => "\xE1\xBF\x8A", + "\xE1\xBD\xB5" => "\xE1\xBF\x8B", + "\xE1\xBD\xB6" => "\xE1\xBF\x9A", + "\xE1\xBD\xB7" => "\xE1\xBF\x9B", + "\xE1\xBD\xB8" => "\xE1\xBF\xB8", + "\xE1\xBD\xB9" => "\xE1\xBF\xB9", + "\xE1\xBD\xBA" => "\xE1\xBF\xAA", + "\xE1\xBD\xBB" => "\xE1\xBF\xAB", + "\xE1\xBD\xBC" => "\xE1\xBF\xBA", + "\xE1\xBD\xBD" => "\xE1\xBF\xBB", + "\xE1\xBE\x80" => "\xE1\xBC\x88\xCE\x99", + "\xE1\xBE\x81" => "\xE1\xBC\x89\xCE\x99", + "\xE1\xBE\x82" => "\xE1\xBC\x8A\xCE\x99", + "\xE1\xBE\x83" => "\xE1\xBC\x8B\xCE\x99", + "\xE1\xBE\x84" => "\xE1\xBC\x8C\xCE\x99", + "\xE1\xBE\x85" => "\xE1\xBC\x8D\xCE\x99", + "\xE1\xBE\x86" => "\xE1\xBC\x8E\xCE\x99", + "\xE1\xBE\x87" => "\xE1\xBC\x8F\xCE\x99", + "\xE1\xBE\x88" => "\xE1\xBC\x88\xCE\x99", + "\xE1\xBE\x89" => "\xE1\xBC\x89\xCE\x99", + "\xE1\xBE\x8A" => "\xE1\xBC\x8A\xCE\x99", + "\xE1\xBE\x8B" => "\xE1\xBC\x8B\xCE\x99", + "\xE1\xBE\x8C" => "\xE1\xBC\x8C\xCE\x99", + "\xE1\xBE\x8D" => "\xE1\xBC\x8D\xCE\x99", + "\xE1\xBE\x8E" => "\xE1\xBC\x8E\xCE\x99", + "\xE1\xBE\x8F" => "\xE1\xBC\x8F\xCE\x99", + "\xE1\xBE\x90" => "\xE1\xBC\xA8\xCE\x99", + "\xE1\xBE\x91" => "\xE1\xBC\xA9\xCE\x99", + "\xE1\xBE\x92" => "\xE1\xBC\xAA\xCE\x99", + "\xE1\xBE\x93" => "\xE1\xBC\xAB\xCE\x99", + "\xE1\xBE\x94" => "\xE1\xBC\xAC\xCE\x99", + "\xE1\xBE\x95" => "\xE1\xBC\xAD\xCE\x99", + "\xE1\xBE\x96" => "\xE1\xBC\xAE\xCE\x99", + "\xE1\xBE\x97" => "\xE1\xBC\xAF\xCE\x99", + "\xE1\xBE\x98" => "\xE1\xBC\xA8\xCE\x99", + "\xE1\xBE\x99" => "\xE1\xBC\xA9\xCE\x99", + "\xE1\xBE\x9A" => "\xE1\xBC\xAA\xCE\x99", + "\xE1\xBE\x9B" => "\xE1\xBC\xAB\xCE\x99", + "\xE1\xBE\x9C" => "\xE1\xBC\xAC\xCE\x99", + "\xE1\xBE\x9D" => "\xE1\xBC\xAD\xCE\x99", + "\xE1\xBE\x9E" => "\xE1\xBC\xAE\xCE\x99", + "\xE1\xBE\x9F" => "\xE1\xBC\xAF\xCE\x99", + "\xE1\xBE\xA0" => "\xE1\xBD\xA8\xCE\x99", + "\xE1\xBE\xA1" => "\xE1\xBD\xA9\xCE\x99", + "\xE1\xBE\xA2" => "\xE1\xBD\xAA\xCE\x99", + "\xE1\xBE\xA3" => "\xE1\xBD\xAB\xCE\x99", + "\xE1\xBE\xA4" => "\xE1\xBD\xAC\xCE\x99", + "\xE1\xBE\xA5" => "\xE1\xBD\xAD\xCE\x99", + "\xE1\xBE\xA6" => "\xE1\xBD\xAE\xCE\x99", + "\xE1\xBE\xA7" => "\xE1\xBD\xAF\xCE\x99", + "\xE1\xBE\xA8" => "\xE1\xBD\xA8\xCE\x99", + "\xE1\xBE\xA9" => "\xE1\xBD\xA9\xCE\x99", + "\xE1\xBE\xAA" => "\xE1\xBD\xAA\xCE\x99", + "\xE1\xBE\xAB" => "\xE1\xBD\xAB\xCE\x99", + "\xE1\xBE\xAC" => "\xE1\xBD\xAC\xCE\x99", + "\xE1\xBE\xAD" => "\xE1\xBD\xAD\xCE\x99", + "\xE1\xBE\xAE" => "\xE1\xBD\xAE\xCE\x99", + "\xE1\xBE\xAF" => "\xE1\xBD\xAF\xCE\x99", + "\xE1\xBE\xB0" => "\xE1\xBE\xB8", + "\xE1\xBE\xB1" => "\xE1\xBE\xB9", + "\xE1\xBE\xB2" => "\xE1\xBE\xBA\xCE\x99", + "\xE1\xBE\xB3" => "\xCE\x91\xCE\x99", + "\xE1\xBE\xB4" => "\xCE\x86\xCE\x99", + "\xE1\xBE\xB6" => "\xCE\x91\xCD\x82", + "\xE1\xBE\xB7" => "\xCE\x91\xCD\x82\xCE\x99", + "\xE1\xBE\xBC" => "\xCE\x91\xCE\x99", + "\xE1\xBE\xBE" => "\xCE\x99", + "\xE1\xBF\x82" => "\xE1\xBF\x8A\xCE\x99", + "\xE1\xBF\x83" => "\xCE\x97\xCE\x99", + "\xE1\xBF\x84" => "\xCE\x89\xCE\x99", + "\xE1\xBF\x86" => "\xCE\x97\xCD\x82", + "\xE1\xBF\x87" => "\xCE\x97\xCD\x82\xCE\x99", + "\xE1\xBF\x8C" => "\xCE\x97\xCE\x99", + "\xE1\xBF\x90" => "\xE1\xBF\x98", + "\xE1\xBF\x91" => "\xE1\xBF\x99", + "\xE1\xBF\x92" => "\xCE\x99\xCC\x88\xCC\x80", + "\xE1\xBF\x93" => "\xCE\x99\xCC\x88\xCC\x81", + "\xE1\xBF\x96" => "\xCE\x99\xCD\x82", + "\xE1\xBF\x97" => "\xCE\x99\xCC\x88\xCD\x82", + "\xE1\xBF\xA0" => "\xE1\xBF\xA8", + "\xE1\xBF\xA1" => "\xE1\xBF\xA9", + "\xE1\xBF\xA2" => "\xCE\xA5\xCC\x88\xCC\x80", + "\xE1\xBF\xA3" => "\xCE\xA5\xCC\x88\xCC\x81", + "\xE1\xBF\xA4" => "\xCE\xA1\xCC\x93", + "\xE1\xBF\xA5" => "\xE1\xBF\xAC", + "\xE1\xBF\xA6" => "\xCE\xA5\xCD\x82", + "\xE1\xBF\xA7" => "\xCE\xA5\xCC\x88\xCD\x82", + "\xE1\xBF\xB2" => "\xE1\xBF\xBA\xCE\x99", + "\xE1\xBF\xB3" => "\xCE\xA9\xCE\x99", + "\xE1\xBF\xB4" => "\xCE\x8F\xCE\x99", + "\xE1\xBF\xB6" => "\xCE\xA9\xCD\x82", + "\xE1\xBF\xB7" => "\xCE\xA9\xCD\x82\xCE\x99", + "\xE1\xBF\xBC" => "\xCE\xA9\xCE\x99", + "\xE2\x85\x8E" => "\xE2\x84\xB2", + "\xE2\x85\xB0" => "\xE2\x85\xA0", + "\xE2\x85\xB1" => "\xE2\x85\xA1", + "\xE2\x85\xB2" => "\xE2\x85\xA2", + "\xE2\x85\xB3" => "\xE2\x85\xA3", + "\xE2\x85\xB4" => "\xE2\x85\xA4", + "\xE2\x85\xB5" => "\xE2\x85\xA5", + "\xE2\x85\xB6" => "\xE2\x85\xA6", + "\xE2\x85\xB7" => "\xE2\x85\xA7", + "\xE2\x85\xB8" => "\xE2\x85\xA8", + "\xE2\x85\xB9" => "\xE2\x85\xA9", + "\xE2\x85\xBA" => "\xE2\x85\xAA", + "\xE2\x85\xBB" => "\xE2\x85\xAB", + "\xE2\x85\xBC" => "\xE2\x85\xAC", + "\xE2\x85\xBD" => "\xE2\x85\xAD", + "\xE2\x85\xBE" => "\xE2\x85\xAE", + "\xE2\x85\xBF" => "\xE2\x85\xAF", + "\xE2\x86\x84" => "\xE2\x86\x83", + "\xE2\x93\x90" => "\xE2\x92\xB6", + "\xE2\x93\x91" => "\xE2\x92\xB7", + "\xE2\x93\x92" => "\xE2\x92\xB8", + "\xE2\x93\x93" => "\xE2\x92\xB9", + "\xE2\x93\x94" => "\xE2\x92\xBA", + "\xE2\x93\x95" => "\xE2\x92\xBB", + "\xE2\x93\x96" => "\xE2\x92\xBC", + "\xE2\x93\x97" => "\xE2\x92\xBD", + "\xE2\x93\x98" => "\xE2\x92\xBE", + "\xE2\x93\x99" => "\xE2\x92\xBF", + "\xE2\x93\x9A" => "\xE2\x93\x80", + "\xE2\x93\x9B" => "\xE2\x93\x81", + "\xE2\x93\x9C" => "\xE2\x93\x82", + "\xE2\x93\x9D" => "\xE2\x93\x83", + "\xE2\x93\x9E" => "\xE2\x93\x84", + "\xE2\x93\x9F" => "\xE2\x93\x85", + "\xE2\x93\xA0" => "\xE2\x93\x86", + "\xE2\x93\xA1" => "\xE2\x93\x87", + "\xE2\x93\xA2" => "\xE2\x93\x88", + "\xE2\x93\xA3" => "\xE2\x93\x89", + "\xE2\x93\xA4" => "\xE2\x93\x8A", + "\xE2\x93\xA5" => "\xE2\x93\x8B", + "\xE2\x93\xA6" => "\xE2\x93\x8C", + "\xE2\x93\xA7" => "\xE2\x93\x8D", + "\xE2\x93\xA8" => "\xE2\x93\x8E", + "\xE2\x93\xA9" => "\xE2\x93\x8F", + "\xE2\xB0\xB0" => "\xE2\xB0\x80", + "\xE2\xB0\xB1" => "\xE2\xB0\x81", + "\xE2\xB0\xB2" => "\xE2\xB0\x82", + "\xE2\xB0\xB3" => "\xE2\xB0\x83", + "\xE2\xB0\xB4" => "\xE2\xB0\x84", + "\xE2\xB0\xB5" => "\xE2\xB0\x85", + "\xE2\xB0\xB6" => "\xE2\xB0\x86", + "\xE2\xB0\xB7" => "\xE2\xB0\x87", + "\xE2\xB0\xB8" => "\xE2\xB0\x88", + "\xE2\xB0\xB9" => "\xE2\xB0\x89", + "\xE2\xB0\xBA" => "\xE2\xB0\x8A", + "\xE2\xB0\xBB" => "\xE2\xB0\x8B", + "\xE2\xB0\xBC" => "\xE2\xB0\x8C", + "\xE2\xB0\xBD" => "\xE2\xB0\x8D", + "\xE2\xB0\xBE" => "\xE2\xB0\x8E", + "\xE2\xB0\xBF" => "\xE2\xB0\x8F", + "\xE2\xB1\x80" => "\xE2\xB0\x90", + "\xE2\xB1\x81" => "\xE2\xB0\x91", + "\xE2\xB1\x82" => "\xE2\xB0\x92", + "\xE2\xB1\x83" => "\xE2\xB0\x93", + "\xE2\xB1\x84" => "\xE2\xB0\x94", + "\xE2\xB1\x85" => "\xE2\xB0\x95", + "\xE2\xB1\x86" => "\xE2\xB0\x96", + "\xE2\xB1\x87" => "\xE2\xB0\x97", + "\xE2\xB1\x88" => "\xE2\xB0\x98", + "\xE2\xB1\x89" => "\xE2\xB0\x99", + "\xE2\xB1\x8A" => "\xE2\xB0\x9A", + "\xE2\xB1\x8B" => "\xE2\xB0\x9B", + "\xE2\xB1\x8C" => "\xE2\xB0\x9C", + "\xE2\xB1\x8D" => "\xE2\xB0\x9D", + "\xE2\xB1\x8E" => "\xE2\xB0\x9E", + "\xE2\xB1\x8F" => "\xE2\xB0\x9F", + "\xE2\xB1\x90" => "\xE2\xB0\xA0", + "\xE2\xB1\x91" => "\xE2\xB0\xA1", + "\xE2\xB1\x92" => "\xE2\xB0\xA2", + "\xE2\xB1\x93" => "\xE2\xB0\xA3", + "\xE2\xB1\x94" => "\xE2\xB0\xA4", + "\xE2\xB1\x95" => "\xE2\xB0\xA5", + "\xE2\xB1\x96" => "\xE2\xB0\xA6", + "\xE2\xB1\x97" => "\xE2\xB0\xA7", + "\xE2\xB1\x98" => "\xE2\xB0\xA8", + "\xE2\xB1\x99" => "\xE2\xB0\xA9", + "\xE2\xB1\x9A" => "\xE2\xB0\xAA", + "\xE2\xB1\x9B" => "\xE2\xB0\xAB", + "\xE2\xB1\x9C" => "\xE2\xB0\xAC", + "\xE2\xB1\x9D" => "\xE2\xB0\xAD", + "\xE2\xB1\x9E" => "\xE2\xB0\xAE", + "\xE2\xB1\x9F" => "\xE2\xB0\xAF", + "\xE2\xB1\xA1" => "\xE2\xB1\xA0", + "\xE2\xB1\xA5" => "\xC8\xBA", + "\xE2\xB1\xA6" => "\xC8\xBE", + "\xE2\xB1\xA8" => "\xE2\xB1\xA7", + "\xE2\xB1\xAA" => "\xE2\xB1\xA9", + "\xE2\xB1\xAC" => "\xE2\xB1\xAB", + "\xE2\xB1\xB3" => "\xE2\xB1\xB2", + "\xE2\xB1\xB6" => "\xE2\xB1\xB5", + "\xE2\xB2\x81" => "\xE2\xB2\x80", + "\xE2\xB2\x83" => "\xE2\xB2\x82", + "\xE2\xB2\x85" => "\xE2\xB2\x84", + "\xE2\xB2\x87" => "\xE2\xB2\x86", + "\xE2\xB2\x89" => "\xE2\xB2\x88", + "\xE2\xB2\x8B" => "\xE2\xB2\x8A", + "\xE2\xB2\x8D" => "\xE2\xB2\x8C", + "\xE2\xB2\x8F" => "\xE2\xB2\x8E", + "\xE2\xB2\x91" => "\xE2\xB2\x90", + "\xE2\xB2\x93" => "\xE2\xB2\x92", + "\xE2\xB2\x95" => "\xE2\xB2\x94", + "\xE2\xB2\x97" => "\xE2\xB2\x96", + "\xE2\xB2\x99" => "\xE2\xB2\x98", + "\xE2\xB2\x9B" => "\xE2\xB2\x9A", + "\xE2\xB2\x9D" => "\xE2\xB2\x9C", + "\xE2\xB2\x9F" => "\xE2\xB2\x9E", + "\xE2\xB2\xA1" => "\xE2\xB2\xA0", + "\xE2\xB2\xA3" => "\xE2\xB2\xA2", + "\xE2\xB2\xA5" => "\xE2\xB2\xA4", + "\xE2\xB2\xA7" => "\xE2\xB2\xA6", + "\xE2\xB2\xA9" => "\xE2\xB2\xA8", + "\xE2\xB2\xAB" => "\xE2\xB2\xAA", + "\xE2\xB2\xAD" => "\xE2\xB2\xAC", + "\xE2\xB2\xAF" => "\xE2\xB2\xAE", + "\xE2\xB2\xB1" => "\xE2\xB2\xB0", + "\xE2\xB2\xB3" => "\xE2\xB2\xB2", + "\xE2\xB2\xB5" => "\xE2\xB2\xB4", + "\xE2\xB2\xB7" => "\xE2\xB2\xB6", + "\xE2\xB2\xB9" => "\xE2\xB2\xB8", + "\xE2\xB2\xBB" => "\xE2\xB2\xBA", + "\xE2\xB2\xBD" => "\xE2\xB2\xBC", + "\xE2\xB2\xBF" => "\xE2\xB2\xBE", + "\xE2\xB3\x81" => "\xE2\xB3\x80", + "\xE2\xB3\x83" => "\xE2\xB3\x82", + "\xE2\xB3\x85" => "\xE2\xB3\x84", + "\xE2\xB3\x87" => "\xE2\xB3\x86", + "\xE2\xB3\x89" => "\xE2\xB3\x88", + "\xE2\xB3\x8B" => "\xE2\xB3\x8A", + "\xE2\xB3\x8D" => "\xE2\xB3\x8C", + "\xE2\xB3\x8F" => "\xE2\xB3\x8E", + "\xE2\xB3\x91" => "\xE2\xB3\x90", + "\xE2\xB3\x93" => "\xE2\xB3\x92", + "\xE2\xB3\x95" => "\xE2\xB3\x94", + "\xE2\xB3\x97" => "\xE2\xB3\x96", + "\xE2\xB3\x99" => "\xE2\xB3\x98", + "\xE2\xB3\x9B" => "\xE2\xB3\x9A", + "\xE2\xB3\x9D" => "\xE2\xB3\x9C", + "\xE2\xB3\x9F" => "\xE2\xB3\x9E", + "\xE2\xB3\xA1" => "\xE2\xB3\xA0", + "\xE2\xB3\xA3" => "\xE2\xB3\xA2", + "\xE2\xB3\xAC" => "\xE2\xB3\xAB", + "\xE2\xB3\xAE" => "\xE2\xB3\xAD", + "\xE2\xB3\xB3" => "\xE2\xB3\xB2", + "\xE2\xB4\x80" => "\xE1\x82\xA0", + "\xE2\xB4\x81" => "\xE1\x82\xA1", + "\xE2\xB4\x82" => "\xE1\x82\xA2", + "\xE2\xB4\x83" => "\xE1\x82\xA3", + "\xE2\xB4\x84" => "\xE1\x82\xA4", + "\xE2\xB4\x85" => "\xE1\x82\xA5", + "\xE2\xB4\x86" => "\xE1\x82\xA6", + "\xE2\xB4\x87" => "\xE1\x82\xA7", + "\xE2\xB4\x88" => "\xE1\x82\xA8", + "\xE2\xB4\x89" => "\xE1\x82\xA9", + "\xE2\xB4\x8A" => "\xE1\x82\xAA", + "\xE2\xB4\x8B" => "\xE1\x82\xAB", + "\xE2\xB4\x8C" => "\xE1\x82\xAC", + "\xE2\xB4\x8D" => "\xE1\x82\xAD", + "\xE2\xB4\x8E" => "\xE1\x82\xAE", + "\xE2\xB4\x8F" => "\xE1\x82\xAF", + "\xE2\xB4\x90" => "\xE1\x82\xB0", + "\xE2\xB4\x91" => "\xE1\x82\xB1", + "\xE2\xB4\x92" => "\xE1\x82\xB2", + "\xE2\xB4\x93" => "\xE1\x82\xB3", + "\xE2\xB4\x94" => "\xE1\x82\xB4", + "\xE2\xB4\x95" => "\xE1\x82\xB5", + "\xE2\xB4\x96" => "\xE1\x82\xB6", + "\xE2\xB4\x97" => "\xE1\x82\xB7", + "\xE2\xB4\x98" => "\xE1\x82\xB8", + "\xE2\xB4\x99" => "\xE1\x82\xB9", + "\xE2\xB4\x9A" => "\xE1\x82\xBA", + "\xE2\xB4\x9B" => "\xE1\x82\xBB", + "\xE2\xB4\x9C" => "\xE1\x82\xBC", + "\xE2\xB4\x9D" => "\xE1\x82\xBD", + "\xE2\xB4\x9E" => "\xE1\x82\xBE", + "\xE2\xB4\x9F" => "\xE1\x82\xBF", + "\xE2\xB4\xA0" => "\xE1\x83\x80", + "\xE2\xB4\xA1" => "\xE1\x83\x81", + "\xE2\xB4\xA2" => "\xE1\x83\x82", + "\xE2\xB4\xA3" => "\xE1\x83\x83", + "\xE2\xB4\xA4" => "\xE1\x83\x84", + "\xE2\xB4\xA5" => "\xE1\x83\x85", + "\xE2\xB4\xA7" => "\xE1\x83\x87", + "\xE2\xB4\xAD" => "\xE1\x83\x8D", + "\xEA\x99\x81" => "\xEA\x99\x80", + "\xEA\x99\x83" => "\xEA\x99\x82", + "\xEA\x99\x85" => "\xEA\x99\x84", + "\xEA\x99\x87" => "\xEA\x99\x86", + "\xEA\x99\x89" => "\xEA\x99\x88", + "\xEA\x99\x8B" => "\xEA\x99\x8A", + "\xEA\x99\x8D" => "\xEA\x99\x8C", + "\xEA\x99\x8F" => "\xEA\x99\x8E", + "\xEA\x99\x91" => "\xEA\x99\x90", + "\xEA\x99\x93" => "\xEA\x99\x92", + "\xEA\x99\x95" => "\xEA\x99\x94", + "\xEA\x99\x97" => "\xEA\x99\x96", + "\xEA\x99\x99" => "\xEA\x99\x98", + "\xEA\x99\x9B" => "\xEA\x99\x9A", + "\xEA\x99\x9D" => "\xEA\x99\x9C", + "\xEA\x99\x9F" => "\xEA\x99\x9E", + "\xEA\x99\xA1" => "\xEA\x99\xA0", + "\xEA\x99\xA3" => "\xEA\x99\xA2", + "\xEA\x99\xA5" => "\xEA\x99\xA4", + "\xEA\x99\xA7" => "\xEA\x99\xA6", + "\xEA\x99\xA9" => "\xEA\x99\xA8", + "\xEA\x99\xAB" => "\xEA\x99\xAA", + "\xEA\x99\xAD" => "\xEA\x99\xAC", + "\xEA\x9A\x81" => "\xEA\x9A\x80", + "\xEA\x9A\x83" => "\xEA\x9A\x82", + "\xEA\x9A\x85" => "\xEA\x9A\x84", + "\xEA\x9A\x87" => "\xEA\x9A\x86", + "\xEA\x9A\x89" => "\xEA\x9A\x88", + "\xEA\x9A\x8B" => "\xEA\x9A\x8A", + "\xEA\x9A\x8D" => "\xEA\x9A\x8C", + "\xEA\x9A\x8F" => "\xEA\x9A\x8E", + "\xEA\x9A\x91" => "\xEA\x9A\x90", + "\xEA\x9A\x93" => "\xEA\x9A\x92", + "\xEA\x9A\x95" => "\xEA\x9A\x94", + "\xEA\x9A\x97" => "\xEA\x9A\x96", + "\xEA\x9A\x99" => "\xEA\x9A\x98", + "\xEA\x9A\x9B" => "\xEA\x9A\x9A", + "\xEA\x9C\xA3" => "\xEA\x9C\xA2", + "\xEA\x9C\xA5" => "\xEA\x9C\xA4", + "\xEA\x9C\xA7" => "\xEA\x9C\xA6", + "\xEA\x9C\xA9" => "\xEA\x9C\xA8", + "\xEA\x9C\xAB" => "\xEA\x9C\xAA", + "\xEA\x9C\xAD" => "\xEA\x9C\xAC", + "\xEA\x9C\xAF" => "\xEA\x9C\xAE", + "\xEA\x9C\xB3" => "\xEA\x9C\xB2", + "\xEA\x9C\xB5" => "\xEA\x9C\xB4", + "\xEA\x9C\xB7" => "\xEA\x9C\xB6", + "\xEA\x9C\xB9" => "\xEA\x9C\xB8", + "\xEA\x9C\xBB" => "\xEA\x9C\xBA", + "\xEA\x9C\xBD" => "\xEA\x9C\xBC", + "\xEA\x9C\xBF" => "\xEA\x9C\xBE", + "\xEA\x9D\x81" => "\xEA\x9D\x80", + "\xEA\x9D\x83" => "\xEA\x9D\x82", + "\xEA\x9D\x85" => "\xEA\x9D\x84", + "\xEA\x9D\x87" => "\xEA\x9D\x86", + "\xEA\x9D\x89" => "\xEA\x9D\x88", + "\xEA\x9D\x8B" => "\xEA\x9D\x8A", + "\xEA\x9D\x8D" => "\xEA\x9D\x8C", + "\xEA\x9D\x8F" => "\xEA\x9D\x8E", + "\xEA\x9D\x91" => "\xEA\x9D\x90", + "\xEA\x9D\x93" => "\xEA\x9D\x92", + "\xEA\x9D\x95" => "\xEA\x9D\x94", + "\xEA\x9D\x97" => "\xEA\x9D\x96", + "\xEA\x9D\x99" => "\xEA\x9D\x98", + "\xEA\x9D\x9B" => "\xEA\x9D\x9A", + "\xEA\x9D\x9D" => "\xEA\x9D\x9C", + "\xEA\x9D\x9F" => "\xEA\x9D\x9E", + "\xEA\x9D\xA1" => "\xEA\x9D\xA0", + "\xEA\x9D\xA3" => "\xEA\x9D\xA2", + "\xEA\x9D\xA5" => "\xEA\x9D\xA4", + "\xEA\x9D\xA7" => "\xEA\x9D\xA6", + "\xEA\x9D\xA9" => "\xEA\x9D\xA8", + "\xEA\x9D\xAB" => "\xEA\x9D\xAA", + "\xEA\x9D\xAD" => "\xEA\x9D\xAC", + "\xEA\x9D\xAF" => "\xEA\x9D\xAE", + "\xEA\x9D\xBA" => "\xEA\x9D\xB9", + "\xEA\x9D\xBC" => "\xEA\x9D\xBB", + "\xEA\x9D\xBF" => "\xEA\x9D\xBE", + "\xEA\x9E\x81" => "\xEA\x9E\x80", + "\xEA\x9E\x83" => "\xEA\x9E\x82", + "\xEA\x9E\x85" => "\xEA\x9E\x84", + "\xEA\x9E\x87" => "\xEA\x9E\x86", + "\xEA\x9E\x8C" => "\xEA\x9E\x8B", + "\xEA\x9E\x91" => "\xEA\x9E\x90", + "\xEA\x9E\x93" => "\xEA\x9E\x92", + "\xEA\x9E\x94" => "\xEA\x9F\x84", + "\xEA\x9E\x97" => "\xEA\x9E\x96", + "\xEA\x9E\x99" => "\xEA\x9E\x98", + "\xEA\x9E\x9B" => "\xEA\x9E\x9A", + "\xEA\x9E\x9D" => "\xEA\x9E\x9C", + "\xEA\x9E\x9F" => "\xEA\x9E\x9E", + "\xEA\x9E\xA1" => "\xEA\x9E\xA0", + "\xEA\x9E\xA3" => "\xEA\x9E\xA2", + "\xEA\x9E\xA5" => "\xEA\x9E\xA4", + "\xEA\x9E\xA7" => "\xEA\x9E\xA6", + "\xEA\x9E\xA9" => "\xEA\x9E\xA8", + "\xEA\x9E\xB5" => "\xEA\x9E\xB4", + "\xEA\x9E\xB7" => "\xEA\x9E\xB6", + "\xEA\x9E\xB9" => "\xEA\x9E\xB8", + "\xEA\x9E\xBB" => "\xEA\x9E\xBA", + "\xEA\x9E\xBD" => "\xEA\x9E\xBC", + "\xEA\x9E\xBF" => "\xEA\x9E\xBE", + "\xEA\x9F\x81" => "\xEA\x9F\x80", + "\xEA\x9F\x83" => "\xEA\x9F\x82", + "\xEA\x9F\x88" => "\xEA\x9F\x87", + "\xEA\x9F\x8A" => "\xEA\x9F\x89", + "\xEA\x9F\x91" => "\xEA\x9F\x90", + "\xEA\x9F\x97" => "\xEA\x9F\x96", + "\xEA\x9F\x99" => "\xEA\x9F\x98", + "\xEA\x9F\xB6" => "\xEA\x9F\xB5", + "\xEA\xAD\x93" => "\xEA\x9E\xB3", + "\xEA\xAD\xB0" => "\xE1\x8E\xA0", + "\xEA\xAD\xB1" => "\xE1\x8E\xA1", + "\xEA\xAD\xB2" => "\xE1\x8E\xA2", + "\xEA\xAD\xB3" => "\xE1\x8E\xA3", + "\xEA\xAD\xB4" => "\xE1\x8E\xA4", + "\xEA\xAD\xB5" => "\xE1\x8E\xA5", + "\xEA\xAD\xB6" => "\xE1\x8E\xA6", + "\xEA\xAD\xB7" => "\xE1\x8E\xA7", + "\xEA\xAD\xB8" => "\xE1\x8E\xA8", + "\xEA\xAD\xB9" => "\xE1\x8E\xA9", + "\xEA\xAD\xBA" => "\xE1\x8E\xAA", + "\xEA\xAD\xBB" => "\xE1\x8E\xAB", + "\xEA\xAD\xBC" => "\xE1\x8E\xAC", + "\xEA\xAD\xBD" => "\xE1\x8E\xAD", + "\xEA\xAD\xBE" => "\xE1\x8E\xAE", + "\xEA\xAD\xBF" => "\xE1\x8E\xAF", + "\xEA\xAE\x80" => "\xE1\x8E\xB0", + "\xEA\xAE\x81" => "\xE1\x8E\xB1", + "\xEA\xAE\x82" => "\xE1\x8E\xB2", + "\xEA\xAE\x83" => "\xE1\x8E\xB3", + "\xEA\xAE\x84" => "\xE1\x8E\xB4", + "\xEA\xAE\x85" => "\xE1\x8E\xB5", + "\xEA\xAE\x86" => "\xE1\x8E\xB6", + "\xEA\xAE\x87" => "\xE1\x8E\xB7", + "\xEA\xAE\x88" => "\xE1\x8E\xB8", + "\xEA\xAE\x89" => "\xE1\x8E\xB9", + "\xEA\xAE\x8A" => "\xE1\x8E\xBA", + "\xEA\xAE\x8B" => "\xE1\x8E\xBB", + "\xEA\xAE\x8C" => "\xE1\x8E\xBC", + "\xEA\xAE\x8D" => "\xE1\x8E\xBD", + "\xEA\xAE\x8E" => "\xE1\x8E\xBE", + "\xEA\xAE\x8F" => "\xE1\x8E\xBF", + "\xEA\xAE\x90" => "\xE1\x8F\x80", + "\xEA\xAE\x91" => "\xE1\x8F\x81", + "\xEA\xAE\x92" => "\xE1\x8F\x82", + "\xEA\xAE\x93" => "\xE1\x8F\x83", + "\xEA\xAE\x94" => "\xE1\x8F\x84", + "\xEA\xAE\x95" => "\xE1\x8F\x85", + "\xEA\xAE\x96" => "\xE1\x8F\x86", + "\xEA\xAE\x97" => "\xE1\x8F\x87", + "\xEA\xAE\x98" => "\xE1\x8F\x88", + "\xEA\xAE\x99" => "\xE1\x8F\x89", + "\xEA\xAE\x9A" => "\xE1\x8F\x8A", + "\xEA\xAE\x9B" => "\xE1\x8F\x8B", + "\xEA\xAE\x9C" => "\xE1\x8F\x8C", + "\xEA\xAE\x9D" => "\xE1\x8F\x8D", + "\xEA\xAE\x9E" => "\xE1\x8F\x8E", + "\xEA\xAE\x9F" => "\xE1\x8F\x8F", + "\xEA\xAE\xA0" => "\xE1\x8F\x90", + "\xEA\xAE\xA1" => "\xE1\x8F\x91", + "\xEA\xAE\xA2" => "\xE1\x8F\x92", + "\xEA\xAE\xA3" => "\xE1\x8F\x93", + "\xEA\xAE\xA4" => "\xE1\x8F\x94", + "\xEA\xAE\xA5" => "\xE1\x8F\x95", + "\xEA\xAE\xA6" => "\xE1\x8F\x96", + "\xEA\xAE\xA7" => "\xE1\x8F\x97", + "\xEA\xAE\xA8" => "\xE1\x8F\x98", + "\xEA\xAE\xA9" => "\xE1\x8F\x99", + "\xEA\xAE\xAA" => "\xE1\x8F\x9A", + "\xEA\xAE\xAB" => "\xE1\x8F\x9B", + "\xEA\xAE\xAC" => "\xE1\x8F\x9C", + "\xEA\xAE\xAD" => "\xE1\x8F\x9D", + "\xEA\xAE\xAE" => "\xE1\x8F\x9E", + "\xEA\xAE\xAF" => "\xE1\x8F\x9F", + "\xEA\xAE\xB0" => "\xE1\x8F\xA0", + "\xEA\xAE\xB1" => "\xE1\x8F\xA1", + "\xEA\xAE\xB2" => "\xE1\x8F\xA2", + "\xEA\xAE\xB3" => "\xE1\x8F\xA3", + "\xEA\xAE\xB4" => "\xE1\x8F\xA4", + "\xEA\xAE\xB5" => "\xE1\x8F\xA5", + "\xEA\xAE\xB6" => "\xE1\x8F\xA6", + "\xEA\xAE\xB7" => "\xE1\x8F\xA7", + "\xEA\xAE\xB8" => "\xE1\x8F\xA8", + "\xEA\xAE\xB9" => "\xE1\x8F\xA9", + "\xEA\xAE\xBA" => "\xE1\x8F\xAA", + "\xEA\xAE\xBB" => "\xE1\x8F\xAB", + "\xEA\xAE\xBC" => "\xE1\x8F\xAC", + "\xEA\xAE\xBD" => "\xE1\x8F\xAD", + "\xEA\xAE\xBE" => "\xE1\x8F\xAE", + "\xEA\xAE\xBF" => "\xE1\x8F\xAF", + "\xEF\xAC\x80" => "\x46\x46", + "\xEF\xAC\x81" => "\x46\x49", + "\xEF\xAC\x82" => "\x46\x4C", + "\xEF\xAC\x83" => "\x46\x46\x49", + "\xEF\xAC\x84" => "\x46\x46\x4C", + "\xEF\xAC\x85" => "\x53\x54", + "\xEF\xAC\x86" => "\x53\x54", + "\xEF\xAC\x93" => "\xD5\x84\xD5\x86", + "\xEF\xAC\x94" => "\xD5\x84\xD4\xB5", + "\xEF\xAC\x95" => "\xD5\x84\xD4\xBB", + "\xEF\xAC\x96" => "\xD5\x8E\xD5\x86", + "\xEF\xAC\x97" => "\xD5\x84\xD4\xBD", + "\xEF\xBD\x81" => "\xEF\xBC\xA1", + "\xEF\xBD\x82" => "\xEF\xBC\xA2", + "\xEF\xBD\x83" => "\xEF\xBC\xA3", + "\xEF\xBD\x84" => "\xEF\xBC\xA4", + "\xEF\xBD\x85" => "\xEF\xBC\xA5", + "\xEF\xBD\x86" => "\xEF\xBC\xA6", + "\xEF\xBD\x87" => "\xEF\xBC\xA7", + "\xEF\xBD\x88" => "\xEF\xBC\xA8", + "\xEF\xBD\x89" => "\xEF\xBC\xA9", + "\xEF\xBD\x8A" => "\xEF\xBC\xAA", + "\xEF\xBD\x8B" => "\xEF\xBC\xAB", + "\xEF\xBD\x8C" => "\xEF\xBC\xAC", + "\xEF\xBD\x8D" => "\xEF\xBC\xAD", + "\xEF\xBD\x8E" => "\xEF\xBC\xAE", + "\xEF\xBD\x8F" => "\xEF\xBC\xAF", + "\xEF\xBD\x90" => "\xEF\xBC\xB0", + "\xEF\xBD\x91" => "\xEF\xBC\xB1", + "\xEF\xBD\x92" => "\xEF\xBC\xB2", + "\xEF\xBD\x93" => "\xEF\xBC\xB3", + "\xEF\xBD\x94" => "\xEF\xBC\xB4", + "\xEF\xBD\x95" => "\xEF\xBC\xB5", + "\xEF\xBD\x96" => "\xEF\xBC\xB6", + "\xEF\xBD\x97" => "\xEF\xBC\xB7", + "\xEF\xBD\x98" => "\xEF\xBC\xB8", + "\xEF\xBD\x99" => "\xEF\xBC\xB9", + "\xEF\xBD\x9A" => "\xEF\xBC\xBA", + ); +} + +?> \ No newline at end of file diff --git a/Sources/Unicode/CombiningClasses.php b/Sources/Unicode/CombiningClasses.php new file mode 100644 index 0000000..d6af747 --- /dev/null +++ b/Sources/Unicode/CombiningClasses.php @@ -0,0 +1,953 @@ + 230, + "\xCC\x81" => 230, + "\xCC\x82" => 230, + "\xCC\x83" => 230, + "\xCC\x84" => 230, + "\xCC\x85" => 230, + "\xCC\x86" => 230, + "\xCC\x87" => 230, + "\xCC\x88" => 230, + "\xCC\x89" => 230, + "\xCC\x8A" => 230, + "\xCC\x8B" => 230, + "\xCC\x8C" => 230, + "\xCC\x8D" => 230, + "\xCC\x8E" => 230, + "\xCC\x8F" => 230, + "\xCC\x90" => 230, + "\xCC\x91" => 230, + "\xCC\x92" => 230, + "\xCC\x93" => 230, + "\xCC\x94" => 230, + "\xCC\x95" => 232, + "\xCC\x96" => 220, + "\xCC\x97" => 220, + "\xCC\x98" => 220, + "\xCC\x99" => 220, + "\xCC\x9A" => 232, + "\xCC\x9B" => 216, + "\xCC\x9C" => 220, + "\xCC\x9D" => 220, + "\xCC\x9E" => 220, + "\xCC\x9F" => 220, + "\xCC\xA0" => 220, + "\xCC\xA1" => 202, + "\xCC\xA2" => 202, + "\xCC\xA3" => 220, + "\xCC\xA4" => 220, + "\xCC\xA5" => 220, + "\xCC\xA6" => 220, + "\xCC\xA7" => 202, + "\xCC\xA8" => 202, + "\xCC\xA9" => 220, + "\xCC\xAA" => 220, + "\xCC\xAB" => 220, + "\xCC\xAC" => 220, + "\xCC\xAD" => 220, + "\xCC\xAE" => 220, + "\xCC\xAF" => 220, + "\xCC\xB0" => 220, + "\xCC\xB1" => 220, + "\xCC\xB2" => 220, + "\xCC\xB3" => 220, + "\xCC\xB4" => 1, + "\xCC\xB5" => 1, + "\xCC\xB6" => 1, + "\xCC\xB7" => 1, + "\xCC\xB8" => 1, + "\xCC\xB9" => 220, + "\xCC\xBA" => 220, + "\xCC\xBB" => 220, + "\xCC\xBC" => 220, + "\xCC\xBD" => 230, + "\xCC\xBE" => 230, + "\xCC\xBF" => 230, + "\xCD\x80" => 230, + "\xCD\x81" => 230, + "\xCD\x82" => 230, + "\xCD\x83" => 230, + "\xCD\x84" => 230, + "\xCD\x85" => 240, + "\xCD\x86" => 230, + "\xCD\x87" => 220, + "\xCD\x88" => 220, + "\xCD\x89" => 220, + "\xCD\x8A" => 230, + "\xCD\x8B" => 230, + "\xCD\x8C" => 230, + "\xCD\x8D" => 220, + "\xCD\x8E" => 220, + "\xCD\x90" => 230, + "\xCD\x91" => 230, + "\xCD\x92" => 230, + "\xCD\x93" => 220, + "\xCD\x94" => 220, + "\xCD\x95" => 220, + "\xCD\x96" => 220, + "\xCD\x97" => 230, + "\xCD\x98" => 232, + "\xCD\x99" => 220, + "\xCD\x9A" => 220, + "\xCD\x9B" => 230, + "\xCD\x9C" => 233, + "\xCD\x9D" => 234, + "\xCD\x9E" => 234, + "\xCD\x9F" => 233, + "\xCD\xA0" => 234, + "\xCD\xA1" => 234, + "\xCD\xA2" => 233, + "\xCD\xA3" => 230, + "\xCD\xA4" => 230, + "\xCD\xA5" => 230, + "\xCD\xA6" => 230, + "\xCD\xA7" => 230, + "\xCD\xA8" => 230, + "\xCD\xA9" => 230, + "\xCD\xAA" => 230, + "\xCD\xAB" => 230, + "\xCD\xAC" => 230, + "\xCD\xAD" => 230, + "\xCD\xAE" => 230, + "\xCD\xAF" => 230, + "\xD2\x83" => 230, + "\xD2\x84" => 230, + "\xD2\x85" => 230, + "\xD2\x86" => 230, + "\xD2\x87" => 230, + "\xD6\x91" => 220, + "\xD6\x92" => 230, + "\xD6\x93" => 230, + "\xD6\x94" => 230, + "\xD6\x95" => 230, + "\xD6\x96" => 220, + "\xD6\x97" => 230, + "\xD6\x98" => 230, + "\xD6\x99" => 230, + "\xD6\x9A" => 222, + "\xD6\x9B" => 220, + "\xD6\x9C" => 230, + "\xD6\x9D" => 230, + "\xD6\x9E" => 230, + "\xD6\x9F" => 230, + "\xD6\xA0" => 230, + "\xD6\xA1" => 230, + "\xD6\xA2" => 220, + "\xD6\xA3" => 220, + "\xD6\xA4" => 220, + "\xD6\xA5" => 220, + "\xD6\xA6" => 220, + "\xD6\xA7" => 220, + "\xD6\xA8" => 230, + "\xD6\xA9" => 230, + "\xD6\xAA" => 220, + "\xD6\xAB" => 230, + "\xD6\xAC" => 230, + "\xD6\xAD" => 222, + "\xD6\xAE" => 228, + "\xD6\xAF" => 230, + "\xD6\xB0" => 10, + "\xD6\xB1" => 11, + "\xD6\xB2" => 12, + "\xD6\xB3" => 13, + "\xD6\xB4" => 14, + "\xD6\xB5" => 15, + "\xD6\xB6" => 16, + "\xD6\xB7" => 17, + "\xD6\xB8" => 18, + "\xD6\xB9" => 19, + "\xD6\xBA" => 19, + "\xD6\xBB" => 20, + "\xD6\xBC" => 21, + "\xD6\xBD" => 22, + "\xD6\xBF" => 23, + "\xD7\x81" => 24, + "\xD7\x82" => 25, + "\xD7\x84" => 230, + "\xD7\x85" => 220, + "\xD7\x87" => 18, + "\xD8\x90" => 230, + "\xD8\x91" => 230, + "\xD8\x92" => 230, + "\xD8\x93" => 230, + "\xD8\x94" => 230, + "\xD8\x95" => 230, + "\xD8\x96" => 230, + "\xD8\x97" => 230, + "\xD8\x98" => 30, + "\xD8\x99" => 31, + "\xD8\x9A" => 32, + "\xD9\x8B" => 27, + "\xD9\x8C" => 28, + "\xD9\x8D" => 29, + "\xD9\x8E" => 30, + "\xD9\x8F" => 31, + "\xD9\x90" => 32, + "\xD9\x91" => 33, + "\xD9\x92" => 34, + "\xD9\x93" => 230, + "\xD9\x94" => 230, + "\xD9\x95" => 220, + "\xD9\x96" => 220, + "\xD9\x97" => 230, + "\xD9\x98" => 230, + "\xD9\x99" => 230, + "\xD9\x9A" => 230, + "\xD9\x9B" => 230, + "\xD9\x9C" => 220, + "\xD9\x9D" => 230, + "\xD9\x9E" => 230, + "\xD9\x9F" => 220, + "\xD9\xB0" => 35, + "\xDB\x96" => 230, + "\xDB\x97" => 230, + "\xDB\x98" => 230, + "\xDB\x99" => 230, + "\xDB\x9A" => 230, + "\xDB\x9B" => 230, + "\xDB\x9C" => 230, + "\xDB\x9F" => 230, + "\xDB\xA0" => 230, + "\xDB\xA1" => 230, + "\xDB\xA2" => 230, + "\xDB\xA3" => 220, + "\xDB\xA4" => 230, + "\xDB\xA7" => 230, + "\xDB\xA8" => 230, + "\xDB\xAA" => 220, + "\xDB\xAB" => 230, + "\xDB\xAC" => 230, + "\xDB\xAD" => 220, + "\xDC\x91" => 36, + "\xDC\xB0" => 230, + "\xDC\xB1" => 220, + "\xDC\xB2" => 230, + "\xDC\xB3" => 230, + "\xDC\xB4" => 220, + "\xDC\xB5" => 230, + "\xDC\xB6" => 230, + "\xDC\xB7" => 220, + "\xDC\xB8" => 220, + "\xDC\xB9" => 220, + "\xDC\xBA" => 230, + "\xDC\xBB" => 220, + "\xDC\xBC" => 220, + "\xDC\xBD" => 230, + "\xDC\xBE" => 220, + "\xDC\xBF" => 230, + "\xDD\x80" => 230, + "\xDD\x81" => 230, + "\xDD\x82" => 220, + "\xDD\x83" => 230, + "\xDD\x84" => 220, + "\xDD\x85" => 230, + "\xDD\x86" => 220, + "\xDD\x87" => 230, + "\xDD\x88" => 220, + "\xDD\x89" => 230, + "\xDD\x8A" => 230, + "\xDF\xAB" => 230, + "\xDF\xAC" => 230, + "\xDF\xAD" => 230, + "\xDF\xAE" => 230, + "\xDF\xAF" => 230, + "\xDF\xB0" => 230, + "\xDF\xB1" => 230, + "\xDF\xB2" => 220, + "\xDF\xB3" => 230, + "\xDF\xBD" => 220, + "\xE0\xA0\x96" => 230, + "\xE0\xA0\x97" => 230, + "\xE0\xA0\x98" => 230, + "\xE0\xA0\x99" => 230, + "\xE0\xA0\x9B" => 230, + "\xE0\xA0\x9C" => 230, + "\xE0\xA0\x9D" => 230, + "\xE0\xA0\x9E" => 230, + "\xE0\xA0\x9F" => 230, + "\xE0\xA0\xA0" => 230, + "\xE0\xA0\xA1" => 230, + "\xE0\xA0\xA2" => 230, + "\xE0\xA0\xA3" => 230, + "\xE0\xA0\xA5" => 230, + "\xE0\xA0\xA6" => 230, + "\xE0\xA0\xA7" => 230, + "\xE0\xA0\xA9" => 230, + "\xE0\xA0\xAA" => 230, + "\xE0\xA0\xAB" => 230, + "\xE0\xA0\xAC" => 230, + "\xE0\xA0\xAD" => 230, + "\xE0\xA1\x99" => 220, + "\xE0\xA1\x9A" => 220, + "\xE0\xA1\x9B" => 220, + "\xE0\xA2\x98" => 230, + "\xE0\xA2\x99" => 220, + "\xE0\xA2\x9A" => 220, + "\xE0\xA2\x9B" => 220, + "\xE0\xA2\x9C" => 230, + "\xE0\xA2\x9D" => 230, + "\xE0\xA2\x9E" => 230, + "\xE0\xA2\x9F" => 230, + "\xE0\xA3\x8A" => 230, + "\xE0\xA3\x8B" => 230, + "\xE0\xA3\x8C" => 230, + "\xE0\xA3\x8D" => 230, + "\xE0\xA3\x8E" => 230, + "\xE0\xA3\x8F" => 220, + "\xE0\xA3\x90" => 220, + "\xE0\xA3\x91" => 220, + "\xE0\xA3\x92" => 220, + "\xE0\xA3\x93" => 220, + "\xE0\xA3\x94" => 230, + "\xE0\xA3\x95" => 230, + "\xE0\xA3\x96" => 230, + "\xE0\xA3\x97" => 230, + "\xE0\xA3\x98" => 230, + "\xE0\xA3\x99" => 230, + "\xE0\xA3\x9A" => 230, + "\xE0\xA3\x9B" => 230, + "\xE0\xA3\x9C" => 230, + "\xE0\xA3\x9D" => 230, + "\xE0\xA3\x9E" => 230, + "\xE0\xA3\x9F" => 230, + "\xE0\xA3\xA0" => 230, + "\xE0\xA3\xA1" => 230, + "\xE0\xA3\xA3" => 220, + "\xE0\xA3\xA4" => 230, + "\xE0\xA3\xA5" => 230, + "\xE0\xA3\xA6" => 220, + "\xE0\xA3\xA7" => 230, + "\xE0\xA3\xA8" => 230, + "\xE0\xA3\xA9" => 220, + "\xE0\xA3\xAA" => 230, + "\xE0\xA3\xAB" => 230, + "\xE0\xA3\xAC" => 230, + "\xE0\xA3\xAD" => 220, + "\xE0\xA3\xAE" => 220, + "\xE0\xA3\xAF" => 220, + "\xE0\xA3\xB0" => 27, + "\xE0\xA3\xB1" => 28, + "\xE0\xA3\xB2" => 29, + "\xE0\xA3\xB3" => 230, + "\xE0\xA3\xB4" => 230, + "\xE0\xA3\xB5" => 230, + "\xE0\xA3\xB6" => 220, + "\xE0\xA3\xB7" => 230, + "\xE0\xA3\xB8" => 230, + "\xE0\xA3\xB9" => 220, + "\xE0\xA3\xBA" => 220, + "\xE0\xA3\xBB" => 230, + "\xE0\xA3\xBC" => 230, + "\xE0\xA3\xBD" => 230, + "\xE0\xA3\xBE" => 230, + "\xE0\xA3\xBF" => 230, + "\xE0\xA4\xBC" => 7, + "\xE0\xA5\x8D" => 9, + "\xE0\xA5\x91" => 230, + "\xE0\xA5\x92" => 220, + "\xE0\xA5\x93" => 230, + "\xE0\xA5\x94" => 230, + "\xE0\xA6\xBC" => 7, + "\xE0\xA7\x8D" => 9, + "\xE0\xA7\xBE" => 230, + "\xE0\xA8\xBC" => 7, + "\xE0\xA9\x8D" => 9, + "\xE0\xAA\xBC" => 7, + "\xE0\xAB\x8D" => 9, + "\xE0\xAC\xBC" => 7, + "\xE0\xAD\x8D" => 9, + "\xE0\xAF\x8D" => 9, + "\xE0\xB0\xBC" => 7, + "\xE0\xB1\x8D" => 9, + "\xE0\xB1\x95" => 84, + "\xE0\xB1\x96" => 91, + "\xE0\xB2\xBC" => 7, + "\xE0\xB3\x8D" => 9, + "\xE0\xB4\xBB" => 9, + "\xE0\xB4\xBC" => 9, + "\xE0\xB5\x8D" => 9, + "\xE0\xB7\x8A" => 9, + "\xE0\xB8\xB8" => 103, + "\xE0\xB8\xB9" => 103, + "\xE0\xB8\xBA" => 9, + "\xE0\xB9\x88" => 107, + "\xE0\xB9\x89" => 107, + "\xE0\xB9\x8A" => 107, + "\xE0\xB9\x8B" => 107, + "\xE0\xBA\xB8" => 118, + "\xE0\xBA\xB9" => 118, + "\xE0\xBA\xBA" => 9, + "\xE0\xBB\x88" => 122, + "\xE0\xBB\x89" => 122, + "\xE0\xBB\x8A" => 122, + "\xE0\xBB\x8B" => 122, + "\xE0\xBC\x98" => 220, + "\xE0\xBC\x99" => 220, + "\xE0\xBC\xB5" => 220, + "\xE0\xBC\xB7" => 220, + "\xE0\xBC\xB9" => 216, + "\xE0\xBD\xB1" => 129, + "\xE0\xBD\xB2" => 130, + "\xE0\xBD\xB4" => 132, + "\xE0\xBD\xBA" => 130, + "\xE0\xBD\xBB" => 130, + "\xE0\xBD\xBC" => 130, + "\xE0\xBD\xBD" => 130, + "\xE0\xBE\x80" => 130, + "\xE0\xBE\x82" => 230, + "\xE0\xBE\x83" => 230, + "\xE0\xBE\x84" => 9, + "\xE0\xBE\x86" => 230, + "\xE0\xBE\x87" => 230, + "\xE0\xBF\x86" => 220, + "\xE1\x80\xB7" => 7, + "\xE1\x80\xB9" => 9, + "\xE1\x80\xBA" => 9, + "\xE1\x82\x8D" => 220, + "\xE1\x8D\x9D" => 230, + "\xE1\x8D\x9E" => 230, + "\xE1\x8D\x9F" => 230, + "\xE1\x9C\x94" => 9, + "\xE1\x9C\x95" => 9, + "\xE1\x9C\xB4" => 9, + "\xE1\x9F\x92" => 9, + "\xE1\x9F\x9D" => 230, + "\xE1\xA2\xA9" => 228, + "\xE1\xA4\xB9" => 222, + "\xE1\xA4\xBA" => 230, + "\xE1\xA4\xBB" => 220, + "\xE1\xA8\x97" => 230, + "\xE1\xA8\x98" => 220, + "\xE1\xA9\xA0" => 9, + "\xE1\xA9\xB5" => 230, + "\xE1\xA9\xB6" => 230, + "\xE1\xA9\xB7" => 230, + "\xE1\xA9\xB8" => 230, + "\xE1\xA9\xB9" => 230, + "\xE1\xA9\xBA" => 230, + "\xE1\xA9\xBB" => 230, + "\xE1\xA9\xBC" => 230, + "\xE1\xA9\xBF" => 220, + "\xE1\xAA\xB0" => 230, + "\xE1\xAA\xB1" => 230, + "\xE1\xAA\xB2" => 230, + "\xE1\xAA\xB3" => 230, + "\xE1\xAA\xB4" => 230, + "\xE1\xAA\xB5" => 220, + "\xE1\xAA\xB6" => 220, + "\xE1\xAA\xB7" => 220, + "\xE1\xAA\xB8" => 220, + "\xE1\xAA\xB9" => 220, + "\xE1\xAA\xBA" => 220, + "\xE1\xAA\xBB" => 230, + "\xE1\xAA\xBC" => 230, + "\xE1\xAA\xBD" => 220, + "\xE1\xAA\xBF" => 220, + "\xE1\xAB\x80" => 220, + "\xE1\xAB\x81" => 230, + "\xE1\xAB\x82" => 230, + "\xE1\xAB\x83" => 220, + "\xE1\xAB\x84" => 220, + "\xE1\xAB\x85" => 230, + "\xE1\xAB\x86" => 230, + "\xE1\xAB\x87" => 230, + "\xE1\xAB\x88" => 230, + "\xE1\xAB\x89" => 230, + "\xE1\xAB\x8A" => 220, + "\xE1\xAB\x8B" => 230, + "\xE1\xAB\x8C" => 230, + "\xE1\xAB\x8D" => 230, + "\xE1\xAB\x8E" => 230, + "\xE1\xAC\xB4" => 7, + "\xE1\xAD\x84" => 9, + "\xE1\xAD\xAB" => 230, + "\xE1\xAD\xAC" => 220, + "\xE1\xAD\xAD" => 230, + "\xE1\xAD\xAE" => 230, + "\xE1\xAD\xAF" => 230, + "\xE1\xAD\xB0" => 230, + "\xE1\xAD\xB1" => 230, + "\xE1\xAD\xB2" => 230, + "\xE1\xAD\xB3" => 230, + "\xE1\xAE\xAA" => 9, + "\xE1\xAE\xAB" => 9, + "\xE1\xAF\xA6" => 7, + "\xE1\xAF\xB2" => 9, + "\xE1\xAF\xB3" => 9, + "\xE1\xB0\xB7" => 7, + "\xE1\xB3\x90" => 230, + "\xE1\xB3\x91" => 230, + "\xE1\xB3\x92" => 230, + "\xE1\xB3\x94" => 1, + "\xE1\xB3\x95" => 220, + "\xE1\xB3\x96" => 220, + "\xE1\xB3\x97" => 220, + "\xE1\xB3\x98" => 220, + "\xE1\xB3\x99" => 220, + "\xE1\xB3\x9A" => 230, + "\xE1\xB3\x9B" => 230, + "\xE1\xB3\x9C" => 220, + "\xE1\xB3\x9D" => 220, + "\xE1\xB3\x9E" => 220, + "\xE1\xB3\x9F" => 220, + "\xE1\xB3\xA0" => 230, + "\xE1\xB3\xA2" => 1, + "\xE1\xB3\xA3" => 1, + "\xE1\xB3\xA4" => 1, + "\xE1\xB3\xA5" => 1, + "\xE1\xB3\xA6" => 1, + "\xE1\xB3\xA7" => 1, + "\xE1\xB3\xA8" => 1, + "\xE1\xB3\xAD" => 220, + "\xE1\xB3\xB4" => 230, + "\xE1\xB3\xB8" => 230, + "\xE1\xB3\xB9" => 230, + "\xE1\xB7\x80" => 230, + "\xE1\xB7\x81" => 230, + "\xE1\xB7\x82" => 220, + "\xE1\xB7\x83" => 230, + "\xE1\xB7\x84" => 230, + "\xE1\xB7\x85" => 230, + "\xE1\xB7\x86" => 230, + "\xE1\xB7\x87" => 230, + "\xE1\xB7\x88" => 230, + "\xE1\xB7\x89" => 230, + "\xE1\xB7\x8A" => 220, + "\xE1\xB7\x8B" => 230, + "\xE1\xB7\x8C" => 230, + "\xE1\xB7\x8D" => 234, + "\xE1\xB7\x8E" => 214, + "\xE1\xB7\x8F" => 220, + "\xE1\xB7\x90" => 202, + "\xE1\xB7\x91" => 230, + "\xE1\xB7\x92" => 230, + "\xE1\xB7\x93" => 230, + "\xE1\xB7\x94" => 230, + "\xE1\xB7\x95" => 230, + "\xE1\xB7\x96" => 230, + "\xE1\xB7\x97" => 230, + "\xE1\xB7\x98" => 230, + "\xE1\xB7\x99" => 230, + "\xE1\xB7\x9A" => 230, + "\xE1\xB7\x9B" => 230, + "\xE1\xB7\x9C" => 230, + "\xE1\xB7\x9D" => 230, + "\xE1\xB7\x9E" => 230, + "\xE1\xB7\x9F" => 230, + "\xE1\xB7\xA0" => 230, + "\xE1\xB7\xA1" => 230, + "\xE1\xB7\xA2" => 230, + "\xE1\xB7\xA3" => 230, + "\xE1\xB7\xA4" => 230, + "\xE1\xB7\xA5" => 230, + "\xE1\xB7\xA6" => 230, + "\xE1\xB7\xA7" => 230, + "\xE1\xB7\xA8" => 230, + "\xE1\xB7\xA9" => 230, + "\xE1\xB7\xAA" => 230, + "\xE1\xB7\xAB" => 230, + "\xE1\xB7\xAC" => 230, + "\xE1\xB7\xAD" => 230, + "\xE1\xB7\xAE" => 230, + "\xE1\xB7\xAF" => 230, + "\xE1\xB7\xB0" => 230, + "\xE1\xB7\xB1" => 230, + "\xE1\xB7\xB2" => 230, + "\xE1\xB7\xB3" => 230, + "\xE1\xB7\xB4" => 230, + "\xE1\xB7\xB5" => 230, + "\xE1\xB7\xB6" => 232, + "\xE1\xB7\xB7" => 228, + "\xE1\xB7\xB8" => 228, + "\xE1\xB7\xB9" => 220, + "\xE1\xB7\xBA" => 218, + "\xE1\xB7\xBB" => 230, + "\xE1\xB7\xBC" => 233, + "\xE1\xB7\xBD" => 220, + "\xE1\xB7\xBE" => 230, + "\xE1\xB7\xBF" => 220, + "\xE2\x83\x90" => 230, + "\xE2\x83\x91" => 230, + "\xE2\x83\x92" => 1, + "\xE2\x83\x93" => 1, + "\xE2\x83\x94" => 230, + "\xE2\x83\x95" => 230, + "\xE2\x83\x96" => 230, + "\xE2\x83\x97" => 230, + "\xE2\x83\x98" => 1, + "\xE2\x83\x99" => 1, + "\xE2\x83\x9A" => 1, + "\xE2\x83\x9B" => 230, + "\xE2\x83\x9C" => 230, + "\xE2\x83\xA1" => 230, + "\xE2\x83\xA5" => 1, + "\xE2\x83\xA6" => 1, + "\xE2\x83\xA7" => 230, + "\xE2\x83\xA8" => 220, + "\xE2\x83\xA9" => 230, + "\xE2\x83\xAA" => 1, + "\xE2\x83\xAB" => 1, + "\xE2\x83\xAC" => 220, + "\xE2\x83\xAD" => 220, + "\xE2\x83\xAE" => 220, + "\xE2\x83\xAF" => 220, + "\xE2\x83\xB0" => 230, + "\xE2\xB3\xAF" => 230, + "\xE2\xB3\xB0" => 230, + "\xE2\xB3\xB1" => 230, + "\xE2\xB5\xBF" => 9, + "\xE2\xB7\xA0" => 230, + "\xE2\xB7\xA1" => 230, + "\xE2\xB7\xA2" => 230, + "\xE2\xB7\xA3" => 230, + "\xE2\xB7\xA4" => 230, + "\xE2\xB7\xA5" => 230, + "\xE2\xB7\xA6" => 230, + "\xE2\xB7\xA7" => 230, + "\xE2\xB7\xA8" => 230, + "\xE2\xB7\xA9" => 230, + "\xE2\xB7\xAA" => 230, + "\xE2\xB7\xAB" => 230, + "\xE2\xB7\xAC" => 230, + "\xE2\xB7\xAD" => 230, + "\xE2\xB7\xAE" => 230, + "\xE2\xB7\xAF" => 230, + "\xE2\xB7\xB0" => 230, + "\xE2\xB7\xB1" => 230, + "\xE2\xB7\xB2" => 230, + "\xE2\xB7\xB3" => 230, + "\xE2\xB7\xB4" => 230, + "\xE2\xB7\xB5" => 230, + "\xE2\xB7\xB6" => 230, + "\xE2\xB7\xB7" => 230, + "\xE2\xB7\xB8" => 230, + "\xE2\xB7\xB9" => 230, + "\xE2\xB7\xBA" => 230, + "\xE2\xB7\xBB" => 230, + "\xE2\xB7\xBC" => 230, + "\xE2\xB7\xBD" => 230, + "\xE2\xB7\xBE" => 230, + "\xE2\xB7\xBF" => 230, + "\xE3\x80\xAA" => 218, + "\xE3\x80\xAB" => 228, + "\xE3\x80\xAC" => 232, + "\xE3\x80\xAD" => 222, + "\xE3\x80\xAE" => 224, + "\xE3\x80\xAF" => 224, + "\xE3\x82\x99" => 8, + "\xE3\x82\x9A" => 8, + "\xEA\x99\xAF" => 230, + "\xEA\x99\xB4" => 230, + "\xEA\x99\xB5" => 230, + "\xEA\x99\xB6" => 230, + "\xEA\x99\xB7" => 230, + "\xEA\x99\xB8" => 230, + "\xEA\x99\xB9" => 230, + "\xEA\x99\xBA" => 230, + "\xEA\x99\xBB" => 230, + "\xEA\x99\xBC" => 230, + "\xEA\x99\xBD" => 230, + "\xEA\x9A\x9E" => 230, + "\xEA\x9A\x9F" => 230, + "\xEA\x9B\xB0" => 230, + "\xEA\x9B\xB1" => 230, + "\xEA\xA0\x86" => 9, + "\xEA\xA0\xAC" => 9, + "\xEA\xA3\x84" => 9, + "\xEA\xA3\xA0" => 230, + "\xEA\xA3\xA1" => 230, + "\xEA\xA3\xA2" => 230, + "\xEA\xA3\xA3" => 230, + "\xEA\xA3\xA4" => 230, + "\xEA\xA3\xA5" => 230, + "\xEA\xA3\xA6" => 230, + "\xEA\xA3\xA7" => 230, + "\xEA\xA3\xA8" => 230, + "\xEA\xA3\xA9" => 230, + "\xEA\xA3\xAA" => 230, + "\xEA\xA3\xAB" => 230, + "\xEA\xA3\xAC" => 230, + "\xEA\xA3\xAD" => 230, + "\xEA\xA3\xAE" => 230, + "\xEA\xA3\xAF" => 230, + "\xEA\xA3\xB0" => 230, + "\xEA\xA3\xB1" => 230, + "\xEA\xA4\xAB" => 220, + "\xEA\xA4\xAC" => 220, + "\xEA\xA4\xAD" => 220, + "\xEA\xA5\x93" => 9, + "\xEA\xA6\xB3" => 7, + "\xEA\xA7\x80" => 9, + "\xEA\xAA\xB0" => 230, + "\xEA\xAA\xB2" => 230, + "\xEA\xAA\xB3" => 230, + "\xEA\xAA\xB4" => 220, + "\xEA\xAA\xB7" => 230, + "\xEA\xAA\xB8" => 230, + "\xEA\xAA\xBE" => 230, + "\xEA\xAA\xBF" => 230, + "\xEA\xAB\x81" => 230, + "\xEA\xAB\xB6" => 9, + "\xEA\xAF\xAD" => 9, + "\xEF\xAC\x9E" => 26, + "\xEF\xB8\xA0" => 230, + "\xEF\xB8\xA1" => 230, + "\xEF\xB8\xA2" => 230, + "\xEF\xB8\xA3" => 230, + "\xEF\xB8\xA4" => 230, + "\xEF\xB8\xA5" => 230, + "\xEF\xB8\xA6" => 230, + "\xEF\xB8\xA7" => 220, + "\xEF\xB8\xA8" => 220, + "\xEF\xB8\xA9" => 220, + "\xEF\xB8\xAA" => 220, + "\xEF\xB8\xAB" => 220, + "\xEF\xB8\xAC" => 220, + "\xEF\xB8\xAD" => 220, + "\xEF\xB8\xAE" => 230, + "\xEF\xB8\xAF" => 230, + "\xF0\x90\x87\xBD" => 220, + "\xF0\x90\x8B\xA0" => 220, + "\xF0\x90\x8D\xB6" => 230, + "\xF0\x90\x8D\xB7" => 230, + "\xF0\x90\x8D\xB8" => 230, + "\xF0\x90\x8D\xB9" => 230, + "\xF0\x90\x8D\xBA" => 230, + "\xF0\x90\xA8\x8D" => 220, + "\xF0\x90\xA8\x8F" => 230, + "\xF0\x90\xA8\xB8" => 230, + "\xF0\x90\xA8\xB9" => 1, + "\xF0\x90\xA8\xBA" => 220, + "\xF0\x90\xA8\xBF" => 9, + "\xF0\x90\xAB\xA5" => 230, + "\xF0\x90\xAB\xA6" => 220, + "\xF0\x90\xB4\xA4" => 230, + "\xF0\x90\xB4\xA5" => 230, + "\xF0\x90\xB4\xA6" => 230, + "\xF0\x90\xB4\xA7" => 230, + "\xF0\x90\xBA\xAB" => 230, + "\xF0\x90\xBA\xAC" => 230, + "\xF0\x90\xBB\xBD" => 220, + "\xF0\x90\xBB\xBE" => 220, + "\xF0\x90\xBB\xBF" => 220, + "\xF0\x90\xBD\x86" => 220, + "\xF0\x90\xBD\x87" => 220, + "\xF0\x90\xBD\x88" => 230, + "\xF0\x90\xBD\x89" => 230, + "\xF0\x90\xBD\x8A" => 230, + "\xF0\x90\xBD\x8B" => 220, + "\xF0\x90\xBD\x8C" => 230, + "\xF0\x90\xBD\x8D" => 220, + "\xF0\x90\xBD\x8E" => 220, + "\xF0\x90\xBD\x8F" => 220, + "\xF0\x90\xBD\x90" => 220, + "\xF0\x90\xBE\x82" => 230, + "\xF0\x90\xBE\x83" => 220, + "\xF0\x90\xBE\x84" => 230, + "\xF0\x90\xBE\x85" => 220, + "\xF0\x91\x81\x86" => 9, + "\xF0\x91\x81\xB0" => 9, + "\xF0\x91\x81\xBF" => 9, + "\xF0\x91\x82\xB9" => 9, + "\xF0\x91\x82\xBA" => 7, + "\xF0\x91\x84\x80" => 230, + "\xF0\x91\x84\x81" => 230, + "\xF0\x91\x84\x82" => 230, + "\xF0\x91\x84\xB3" => 9, + "\xF0\x91\x84\xB4" => 9, + "\xF0\x91\x85\xB3" => 7, + "\xF0\x91\x87\x80" => 9, + "\xF0\x91\x87\x8A" => 7, + "\xF0\x91\x88\xB5" => 9, + "\xF0\x91\x88\xB6" => 7, + "\xF0\x91\x8B\xA9" => 7, + "\xF0\x91\x8B\xAA" => 9, + "\xF0\x91\x8C\xBB" => 7, + "\xF0\x91\x8C\xBC" => 7, + "\xF0\x91\x8D\x8D" => 9, + "\xF0\x91\x8D\xA6" => 230, + "\xF0\x91\x8D\xA7" => 230, + "\xF0\x91\x8D\xA8" => 230, + "\xF0\x91\x8D\xA9" => 230, + "\xF0\x91\x8D\xAA" => 230, + "\xF0\x91\x8D\xAB" => 230, + "\xF0\x91\x8D\xAC" => 230, + "\xF0\x91\x8D\xB0" => 230, + "\xF0\x91\x8D\xB1" => 230, + "\xF0\x91\x8D\xB2" => 230, + "\xF0\x91\x8D\xB3" => 230, + "\xF0\x91\x8D\xB4" => 230, + "\xF0\x91\x91\x82" => 9, + "\xF0\x91\x91\x86" => 7, + "\xF0\x91\x91\x9E" => 230, + "\xF0\x91\x93\x82" => 9, + "\xF0\x91\x93\x83" => 7, + "\xF0\x91\x96\xBF" => 9, + "\xF0\x91\x97\x80" => 7, + "\xF0\x91\x98\xBF" => 9, + "\xF0\x91\x9A\xB6" => 9, + "\xF0\x91\x9A\xB7" => 7, + "\xF0\x91\x9C\xAB" => 9, + "\xF0\x91\xA0\xB9" => 9, + "\xF0\x91\xA0\xBA" => 7, + "\xF0\x91\xA4\xBD" => 9, + "\xF0\x91\xA4\xBE" => 9, + "\xF0\x91\xA5\x83" => 7, + "\xF0\x91\xA7\xA0" => 9, + "\xF0\x91\xA8\xB4" => 9, + "\xF0\x91\xA9\x87" => 9, + "\xF0\x91\xAA\x99" => 9, + "\xF0\x91\xB0\xBF" => 9, + "\xF0\x91\xB5\x82" => 7, + "\xF0\x91\xB5\x84" => 9, + "\xF0\x91\xB5\x85" => 9, + "\xF0\x91\xB6\x97" => 9, + "\xF0\x91\xBD\x81" => 9, + "\xF0\x91\xBD\x82" => 9, + "\xF0\x96\xAB\xB0" => 1, + "\xF0\x96\xAB\xB1" => 1, + "\xF0\x96\xAB\xB2" => 1, + "\xF0\x96\xAB\xB3" => 1, + "\xF0\x96\xAB\xB4" => 1, + "\xF0\x96\xAC\xB0" => 230, + "\xF0\x96\xAC\xB1" => 230, + "\xF0\x96\xAC\xB2" => 230, + "\xF0\x96\xAC\xB3" => 230, + "\xF0\x96\xAC\xB4" => 230, + "\xF0\x96\xAC\xB5" => 230, + "\xF0\x96\xAC\xB6" => 230, + "\xF0\x96\xBF\xB0" => 6, + "\xF0\x96\xBF\xB1" => 6, + "\xF0\x9B\xB2\x9E" => 1, + "\xF0\x9D\x85\xA5" => 216, + "\xF0\x9D\x85\xA6" => 216, + "\xF0\x9D\x85\xA7" => 1, + "\xF0\x9D\x85\xA8" => 1, + "\xF0\x9D\x85\xA9" => 1, + "\xF0\x9D\x85\xAD" => 226, + "\xF0\x9D\x85\xAE" => 216, + "\xF0\x9D\x85\xAF" => 216, + "\xF0\x9D\x85\xB0" => 216, + "\xF0\x9D\x85\xB1" => 216, + "\xF0\x9D\x85\xB2" => 216, + "\xF0\x9D\x85\xBB" => 220, + "\xF0\x9D\x85\xBC" => 220, + "\xF0\x9D\x85\xBD" => 220, + "\xF0\x9D\x85\xBE" => 220, + "\xF0\x9D\x85\xBF" => 220, + "\xF0\x9D\x86\x80" => 220, + "\xF0\x9D\x86\x81" => 220, + "\xF0\x9D\x86\x82" => 220, + "\xF0\x9D\x86\x85" => 230, + "\xF0\x9D\x86\x86" => 230, + "\xF0\x9D\x86\x87" => 230, + "\xF0\x9D\x86\x88" => 230, + "\xF0\x9D\x86\x89" => 230, + "\xF0\x9D\x86\x8A" => 220, + "\xF0\x9D\x86\x8B" => 220, + "\xF0\x9D\x86\xAA" => 230, + "\xF0\x9D\x86\xAB" => 230, + "\xF0\x9D\x86\xAC" => 230, + "\xF0\x9D\x86\xAD" => 230, + "\xF0\x9D\x89\x82" => 230, + "\xF0\x9D\x89\x83" => 230, + "\xF0\x9D\x89\x84" => 230, + "\xF0\x9E\x80\x80" => 230, + "\xF0\x9E\x80\x81" => 230, + "\xF0\x9E\x80\x82" => 230, + "\xF0\x9E\x80\x83" => 230, + "\xF0\x9E\x80\x84" => 230, + "\xF0\x9E\x80\x85" => 230, + "\xF0\x9E\x80\x86" => 230, + "\xF0\x9E\x80\x88" => 230, + "\xF0\x9E\x80\x89" => 230, + "\xF0\x9E\x80\x8A" => 230, + "\xF0\x9E\x80\x8B" => 230, + "\xF0\x9E\x80\x8C" => 230, + "\xF0\x9E\x80\x8D" => 230, + "\xF0\x9E\x80\x8E" => 230, + "\xF0\x9E\x80\x8F" => 230, + "\xF0\x9E\x80\x90" => 230, + "\xF0\x9E\x80\x91" => 230, + "\xF0\x9E\x80\x92" => 230, + "\xF0\x9E\x80\x93" => 230, + "\xF0\x9E\x80\x94" => 230, + "\xF0\x9E\x80\x95" => 230, + "\xF0\x9E\x80\x96" => 230, + "\xF0\x9E\x80\x97" => 230, + "\xF0\x9E\x80\x98" => 230, + "\xF0\x9E\x80\x9B" => 230, + "\xF0\x9E\x80\x9C" => 230, + "\xF0\x9E\x80\x9D" => 230, + "\xF0\x9E\x80\x9E" => 230, + "\xF0\x9E\x80\x9F" => 230, + "\xF0\x9E\x80\xA0" => 230, + "\xF0\x9E\x80\xA1" => 230, + "\xF0\x9E\x80\xA3" => 230, + "\xF0\x9E\x80\xA4" => 230, + "\xF0\x9E\x80\xA6" => 230, + "\xF0\x9E\x80\xA7" => 230, + "\xF0\x9E\x80\xA8" => 230, + "\xF0\x9E\x80\xA9" => 230, + "\xF0\x9E\x80\xAA" => 230, + "\xF0\x9E\x82\x8F" => 230, + "\xF0\x9E\x84\xB0" => 230, + "\xF0\x9E\x84\xB1" => 230, + "\xF0\x9E\x84\xB2" => 230, + "\xF0\x9E\x84\xB3" => 230, + "\xF0\x9E\x84\xB4" => 230, + "\xF0\x9E\x84\xB5" => 230, + "\xF0\x9E\x84\xB6" => 230, + "\xF0\x9E\x8A\xAE" => 230, + "\xF0\x9E\x8B\xAC" => 230, + "\xF0\x9E\x8B\xAD" => 230, + "\xF0\x9E\x8B\xAE" => 230, + "\xF0\x9E\x8B\xAF" => 230, + "\xF0\x9E\x93\xAC" => 232, + "\xF0\x9E\x93\xAD" => 232, + "\xF0\x9E\x93\xAE" => 220, + "\xF0\x9E\x93\xAF" => 230, + "\xF0\x9E\xA3\x90" => 220, + "\xF0\x9E\xA3\x91" => 220, + "\xF0\x9E\xA3\x92" => 220, + "\xF0\x9E\xA3\x93" => 220, + "\xF0\x9E\xA3\x94" => 220, + "\xF0\x9E\xA3\x95" => 220, + "\xF0\x9E\xA3\x96" => 220, + "\xF0\x9E\xA5\x84" => 230, + "\xF0\x9E\xA5\x85" => 230, + "\xF0\x9E\xA5\x86" => 230, + "\xF0\x9E\xA5\x87" => 230, + "\xF0\x9E\xA5\x88" => 230, + "\xF0\x9E\xA5\x89" => 230, + "\xF0\x9E\xA5\x8A" => 7, + ); +} + +?> \ No newline at end of file diff --git a/Sources/Unicode/Composition.php b/Sources/Unicode/Composition.php new file mode 100644 index 0000000..0bcf219 --- /dev/null +++ b/Sources/Unicode/Composition.php @@ -0,0 +1,972 @@ + "\xC3\x80", + "\x41\xCC\x81" => "\xC3\x81", + "\x41\xCC\x82" => "\xC3\x82", + "\x41\xCC\x83" => "\xC3\x83", + "\x41\xCC\x88" => "\xC3\x84", + "\x41\xCC\x8A" => "\xC3\x85", + "\x43\xCC\xA7" => "\xC3\x87", + "\x45\xCC\x80" => "\xC3\x88", + "\x45\xCC\x81" => "\xC3\x89", + "\x45\xCC\x82" => "\xC3\x8A", + "\x45\xCC\x88" => "\xC3\x8B", + "\x49\xCC\x80" => "\xC3\x8C", + "\x49\xCC\x81" => "\xC3\x8D", + "\x49\xCC\x82" => "\xC3\x8E", + "\x49\xCC\x88" => "\xC3\x8F", + "\x4E\xCC\x83" => "\xC3\x91", + "\x4F\xCC\x80" => "\xC3\x92", + "\x4F\xCC\x81" => "\xC3\x93", + "\x4F\xCC\x82" => "\xC3\x94", + "\x4F\xCC\x83" => "\xC3\x95", + "\x4F\xCC\x88" => "\xC3\x96", + "\x55\xCC\x80" => "\xC3\x99", + "\x55\xCC\x81" => "\xC3\x9A", + "\x55\xCC\x82" => "\xC3\x9B", + "\x55\xCC\x88" => "\xC3\x9C", + "\x59\xCC\x81" => "\xC3\x9D", + "\x61\xCC\x80" => "\xC3\xA0", + "\x61\xCC\x81" => "\xC3\xA1", + "\x61\xCC\x82" => "\xC3\xA2", + "\x61\xCC\x83" => "\xC3\xA3", + "\x61\xCC\x88" => "\xC3\xA4", + "\x61\xCC\x8A" => "\xC3\xA5", + "\x63\xCC\xA7" => "\xC3\xA7", + "\x65\xCC\x80" => "\xC3\xA8", + "\x65\xCC\x81" => "\xC3\xA9", + "\x65\xCC\x82" => "\xC3\xAA", + "\x65\xCC\x88" => "\xC3\xAB", + "\x69\xCC\x80" => "\xC3\xAC", + "\x69\xCC\x81" => "\xC3\xAD", + "\x69\xCC\x82" => "\xC3\xAE", + "\x69\xCC\x88" => "\xC3\xAF", + "\x6E\xCC\x83" => "\xC3\xB1", + "\x6F\xCC\x80" => "\xC3\xB2", + "\x6F\xCC\x81" => "\xC3\xB3", + "\x6F\xCC\x82" => "\xC3\xB4", + "\x6F\xCC\x83" => "\xC3\xB5", + "\x6F\xCC\x88" => "\xC3\xB6", + "\x75\xCC\x80" => "\xC3\xB9", + "\x75\xCC\x81" => "\xC3\xBA", + "\x75\xCC\x82" => "\xC3\xBB", + "\x75\xCC\x88" => "\xC3\xBC", + "\x79\xCC\x81" => "\xC3\xBD", + "\x79\xCC\x88" => "\xC3\xBF", + "\x41\xCC\x84" => "\xC4\x80", + "\x61\xCC\x84" => "\xC4\x81", + "\x41\xCC\x86" => "\xC4\x82", + "\x61\xCC\x86" => "\xC4\x83", + "\x41\xCC\xA8" => "\xC4\x84", + "\x61\xCC\xA8" => "\xC4\x85", + "\x43\xCC\x81" => "\xC4\x86", + "\x63\xCC\x81" => "\xC4\x87", + "\x43\xCC\x82" => "\xC4\x88", + "\x63\xCC\x82" => "\xC4\x89", + "\x43\xCC\x87" => "\xC4\x8A", + "\x63\xCC\x87" => "\xC4\x8B", + "\x43\xCC\x8C" => "\xC4\x8C", + "\x63\xCC\x8C" => "\xC4\x8D", + "\x44\xCC\x8C" => "\xC4\x8E", + "\x64\xCC\x8C" => "\xC4\x8F", + "\x45\xCC\x84" => "\xC4\x92", + "\x65\xCC\x84" => "\xC4\x93", + "\x45\xCC\x86" => "\xC4\x94", + "\x65\xCC\x86" => "\xC4\x95", + "\x45\xCC\x87" => "\xC4\x96", + "\x65\xCC\x87" => "\xC4\x97", + "\x45\xCC\xA8" => "\xC4\x98", + "\x65\xCC\xA8" => "\xC4\x99", + "\x45\xCC\x8C" => "\xC4\x9A", + "\x65\xCC\x8C" => "\xC4\x9B", + "\x47\xCC\x82" => "\xC4\x9C", + "\x67\xCC\x82" => "\xC4\x9D", + "\x47\xCC\x86" => "\xC4\x9E", + "\x67\xCC\x86" => "\xC4\x9F", + "\x47\xCC\x87" => "\xC4\xA0", + "\x67\xCC\x87" => "\xC4\xA1", + "\x47\xCC\xA7" => "\xC4\xA2", + "\x67\xCC\xA7" => "\xC4\xA3", + "\x48\xCC\x82" => "\xC4\xA4", + "\x68\xCC\x82" => "\xC4\xA5", + "\x49\xCC\x83" => "\xC4\xA8", + "\x69\xCC\x83" => "\xC4\xA9", + "\x49\xCC\x84" => "\xC4\xAA", + "\x69\xCC\x84" => "\xC4\xAB", + "\x49\xCC\x86" => "\xC4\xAC", + "\x69\xCC\x86" => "\xC4\xAD", + "\x49\xCC\xA8" => "\xC4\xAE", + "\x69\xCC\xA8" => "\xC4\xAF", + "\x49\xCC\x87" => "\xC4\xB0", + "\x4A\xCC\x82" => "\xC4\xB4", + "\x6A\xCC\x82" => "\xC4\xB5", + "\x4B\xCC\xA7" => "\xC4\xB6", + "\x6B\xCC\xA7" => "\xC4\xB7", + "\x4C\xCC\x81" => "\xC4\xB9", + "\x6C\xCC\x81" => "\xC4\xBA", + "\x4C\xCC\xA7" => "\xC4\xBB", + "\x6C\xCC\xA7" => "\xC4\xBC", + "\x4C\xCC\x8C" => "\xC4\xBD", + "\x6C\xCC\x8C" => "\xC4\xBE", + "\x4E\xCC\x81" => "\xC5\x83", + "\x6E\xCC\x81" => "\xC5\x84", + "\x4E\xCC\xA7" => "\xC5\x85", + "\x6E\xCC\xA7" => "\xC5\x86", + "\x4E\xCC\x8C" => "\xC5\x87", + "\x6E\xCC\x8C" => "\xC5\x88", + "\x4F\xCC\x84" => "\xC5\x8C", + "\x6F\xCC\x84" => "\xC5\x8D", + "\x4F\xCC\x86" => "\xC5\x8E", + "\x6F\xCC\x86" => "\xC5\x8F", + "\x4F\xCC\x8B" => "\xC5\x90", + "\x6F\xCC\x8B" => "\xC5\x91", + "\x52\xCC\x81" => "\xC5\x94", + "\x72\xCC\x81" => "\xC5\x95", + "\x52\xCC\xA7" => "\xC5\x96", + "\x72\xCC\xA7" => "\xC5\x97", + "\x52\xCC\x8C" => "\xC5\x98", + "\x72\xCC\x8C" => "\xC5\x99", + "\x53\xCC\x81" => "\xC5\x9A", + "\x73\xCC\x81" => "\xC5\x9B", + "\x53\xCC\x82" => "\xC5\x9C", + "\x73\xCC\x82" => "\xC5\x9D", + "\x53\xCC\xA7" => "\xC5\x9E", + "\x73\xCC\xA7" => "\xC5\x9F", + "\x53\xCC\x8C" => "\xC5\xA0", + "\x73\xCC\x8C" => "\xC5\xA1", + "\x54\xCC\xA7" => "\xC5\xA2", + "\x74\xCC\xA7" => "\xC5\xA3", + "\x54\xCC\x8C" => "\xC5\xA4", + "\x74\xCC\x8C" => "\xC5\xA5", + "\x55\xCC\x83" => "\xC5\xA8", + "\x75\xCC\x83" => "\xC5\xA9", + "\x55\xCC\x84" => "\xC5\xAA", + "\x75\xCC\x84" => "\xC5\xAB", + "\x55\xCC\x86" => "\xC5\xAC", + "\x75\xCC\x86" => "\xC5\xAD", + "\x55\xCC\x8A" => "\xC5\xAE", + "\x75\xCC\x8A" => "\xC5\xAF", + "\x55\xCC\x8B" => "\xC5\xB0", + "\x75\xCC\x8B" => "\xC5\xB1", + "\x55\xCC\xA8" => "\xC5\xB2", + "\x75\xCC\xA8" => "\xC5\xB3", + "\x57\xCC\x82" => "\xC5\xB4", + "\x77\xCC\x82" => "\xC5\xB5", + "\x59\xCC\x82" => "\xC5\xB6", + "\x79\xCC\x82" => "\xC5\xB7", + "\x59\xCC\x88" => "\xC5\xB8", + "\x5A\xCC\x81" => "\xC5\xB9", + "\x7A\xCC\x81" => "\xC5\xBA", + "\x5A\xCC\x87" => "\xC5\xBB", + "\x7A\xCC\x87" => "\xC5\xBC", + "\x5A\xCC\x8C" => "\xC5\xBD", + "\x7A\xCC\x8C" => "\xC5\xBE", + "\x4F\xCC\x9B" => "\xC6\xA0", + "\x6F\xCC\x9B" => "\xC6\xA1", + "\x55\xCC\x9B" => "\xC6\xAF", + "\x75\xCC\x9B" => "\xC6\xB0", + "\x41\xCC\x8C" => "\xC7\x8D", + "\x61\xCC\x8C" => "\xC7\x8E", + "\x49\xCC\x8C" => "\xC7\x8F", + "\x69\xCC\x8C" => "\xC7\x90", + "\x4F\xCC\x8C" => "\xC7\x91", + "\x6F\xCC\x8C" => "\xC7\x92", + "\x55\xCC\x8C" => "\xC7\x93", + "\x75\xCC\x8C" => "\xC7\x94", + "\xC3\x9C\xCC\x84" => "\xC7\x95", + "\xC3\xBC\xCC\x84" => "\xC7\x96", + "\xC3\x9C\xCC\x81" => "\xC7\x97", + "\xC3\xBC\xCC\x81" => "\xC7\x98", + "\xC3\x9C\xCC\x8C" => "\xC7\x99", + "\xC3\xBC\xCC\x8C" => "\xC7\x9A", + "\xC3\x9C\xCC\x80" => "\xC7\x9B", + "\xC3\xBC\xCC\x80" => "\xC7\x9C", + "\xC3\x84\xCC\x84" => "\xC7\x9E", + "\xC3\xA4\xCC\x84" => "\xC7\x9F", + "\xC8\xA6\xCC\x84" => "\xC7\xA0", + "\xC8\xA7\xCC\x84" => "\xC7\xA1", + "\xC3\x86\xCC\x84" => "\xC7\xA2", + "\xC3\xA6\xCC\x84" => "\xC7\xA3", + "\x47\xCC\x8C" => "\xC7\xA6", + "\x67\xCC\x8C" => "\xC7\xA7", + "\x4B\xCC\x8C" => "\xC7\xA8", + "\x6B\xCC\x8C" => "\xC7\xA9", + "\x4F\xCC\xA8" => "\xC7\xAA", + "\x6F\xCC\xA8" => "\xC7\xAB", + "\xC7\xAA\xCC\x84" => "\xC7\xAC", + "\xC7\xAB\xCC\x84" => "\xC7\xAD", + "\xC6\xB7\xCC\x8C" => "\xC7\xAE", + "\xCA\x92\xCC\x8C" => "\xC7\xAF", + "\x6A\xCC\x8C" => "\xC7\xB0", + "\x47\xCC\x81" => "\xC7\xB4", + "\x67\xCC\x81" => "\xC7\xB5", + "\x4E\xCC\x80" => "\xC7\xB8", + "\x6E\xCC\x80" => "\xC7\xB9", + "\xC3\x85\xCC\x81" => "\xC7\xBA", + "\xC3\xA5\xCC\x81" => "\xC7\xBB", + "\xC3\x86\xCC\x81" => "\xC7\xBC", + "\xC3\xA6\xCC\x81" => "\xC7\xBD", + "\xC3\x98\xCC\x81" => "\xC7\xBE", + "\xC3\xB8\xCC\x81" => "\xC7\xBF", + "\x41\xCC\x8F" => "\xC8\x80", + "\x61\xCC\x8F" => "\xC8\x81", + "\x41\xCC\x91" => "\xC8\x82", + "\x61\xCC\x91" => "\xC8\x83", + "\x45\xCC\x8F" => "\xC8\x84", + "\x65\xCC\x8F" => "\xC8\x85", + "\x45\xCC\x91" => "\xC8\x86", + "\x65\xCC\x91" => "\xC8\x87", + "\x49\xCC\x8F" => "\xC8\x88", + "\x69\xCC\x8F" => "\xC8\x89", + "\x49\xCC\x91" => "\xC8\x8A", + "\x69\xCC\x91" => "\xC8\x8B", + "\x4F\xCC\x8F" => "\xC8\x8C", + "\x6F\xCC\x8F" => "\xC8\x8D", + "\x4F\xCC\x91" => "\xC8\x8E", + "\x6F\xCC\x91" => "\xC8\x8F", + "\x52\xCC\x8F" => "\xC8\x90", + "\x72\xCC\x8F" => "\xC8\x91", + "\x52\xCC\x91" => "\xC8\x92", + "\x72\xCC\x91" => "\xC8\x93", + "\x55\xCC\x8F" => "\xC8\x94", + "\x75\xCC\x8F" => "\xC8\x95", + "\x55\xCC\x91" => "\xC8\x96", + "\x75\xCC\x91" => "\xC8\x97", + "\x53\xCC\xA6" => "\xC8\x98", + "\x73\xCC\xA6" => "\xC8\x99", + "\x54\xCC\xA6" => "\xC8\x9A", + "\x74\xCC\xA6" => "\xC8\x9B", + "\x48\xCC\x8C" => "\xC8\x9E", + "\x68\xCC\x8C" => "\xC8\x9F", + "\x41\xCC\x87" => "\xC8\xA6", + "\x61\xCC\x87" => "\xC8\xA7", + "\x45\xCC\xA7" => "\xC8\xA8", + "\x65\xCC\xA7" => "\xC8\xA9", + "\xC3\x96\xCC\x84" => "\xC8\xAA", + "\xC3\xB6\xCC\x84" => "\xC8\xAB", + "\xC3\x95\xCC\x84" => "\xC8\xAC", + "\xC3\xB5\xCC\x84" => "\xC8\xAD", + "\x4F\xCC\x87" => "\xC8\xAE", + "\x6F\xCC\x87" => "\xC8\xAF", + "\xC8\xAE\xCC\x84" => "\xC8\xB0", + "\xC8\xAF\xCC\x84" => "\xC8\xB1", + "\x59\xCC\x84" => "\xC8\xB2", + "\x79\xCC\x84" => "\xC8\xB3", + "\xC2\xA8\xCC\x81" => "\xCE\x85", + "\xCE\x91\xCC\x81" => "\xCE\x86", + "\xCE\x95\xCC\x81" => "\xCE\x88", + "\xCE\x97\xCC\x81" => "\xCE\x89", + "\xCE\x99\xCC\x81" => "\xCE\x8A", + "\xCE\x9F\xCC\x81" => "\xCE\x8C", + "\xCE\xA5\xCC\x81" => "\xCE\x8E", + "\xCE\xA9\xCC\x81" => "\xCE\x8F", + "\xCF\x8A\xCC\x81" => "\xCE\x90", + "\xCE\x99\xCC\x88" => "\xCE\xAA", + "\xCE\xA5\xCC\x88" => "\xCE\xAB", + "\xCE\xB1\xCC\x81" => "\xCE\xAC", + "\xCE\xB5\xCC\x81" => "\xCE\xAD", + "\xCE\xB7\xCC\x81" => "\xCE\xAE", + "\xCE\xB9\xCC\x81" => "\xCE\xAF", + "\xCF\x8B\xCC\x81" => "\xCE\xB0", + "\xCE\xB9\xCC\x88" => "\xCF\x8A", + "\xCF\x85\xCC\x88" => "\xCF\x8B", + "\xCE\xBF\xCC\x81" => "\xCF\x8C", + "\xCF\x85\xCC\x81" => "\xCF\x8D", + "\xCF\x89\xCC\x81" => "\xCF\x8E", + "\xCF\x92\xCC\x81" => "\xCF\x93", + "\xCF\x92\xCC\x88" => "\xCF\x94", + "\xD0\x95\xCC\x80" => "\xD0\x80", + "\xD0\x95\xCC\x88" => "\xD0\x81", + "\xD0\x93\xCC\x81" => "\xD0\x83", + "\xD0\x86\xCC\x88" => "\xD0\x87", + "\xD0\x9A\xCC\x81" => "\xD0\x8C", + "\xD0\x98\xCC\x80" => "\xD0\x8D", + "\xD0\xA3\xCC\x86" => "\xD0\x8E", + "\xD0\x98\xCC\x86" => "\xD0\x99", + "\xD0\xB8\xCC\x86" => "\xD0\xB9", + "\xD0\xB5\xCC\x80" => "\xD1\x90", + "\xD0\xB5\xCC\x88" => "\xD1\x91", + "\xD0\xB3\xCC\x81" => "\xD1\x93", + "\xD1\x96\xCC\x88" => "\xD1\x97", + "\xD0\xBA\xCC\x81" => "\xD1\x9C", + "\xD0\xB8\xCC\x80" => "\xD1\x9D", + "\xD1\x83\xCC\x86" => "\xD1\x9E", + "\xD1\xB4\xCC\x8F" => "\xD1\xB6", + "\xD1\xB5\xCC\x8F" => "\xD1\xB7", + "\xD0\x96\xCC\x86" => "\xD3\x81", + "\xD0\xB6\xCC\x86" => "\xD3\x82", + "\xD0\x90\xCC\x86" => "\xD3\x90", + "\xD0\xB0\xCC\x86" => "\xD3\x91", + "\xD0\x90\xCC\x88" => "\xD3\x92", + "\xD0\xB0\xCC\x88" => "\xD3\x93", + "\xD0\x95\xCC\x86" => "\xD3\x96", + "\xD0\xB5\xCC\x86" => "\xD3\x97", + "\xD3\x98\xCC\x88" => "\xD3\x9A", + "\xD3\x99\xCC\x88" => "\xD3\x9B", + "\xD0\x96\xCC\x88" => "\xD3\x9C", + "\xD0\xB6\xCC\x88" => "\xD3\x9D", + "\xD0\x97\xCC\x88" => "\xD3\x9E", + "\xD0\xB7\xCC\x88" => "\xD3\x9F", + "\xD0\x98\xCC\x84" => "\xD3\xA2", + "\xD0\xB8\xCC\x84" => "\xD3\xA3", + "\xD0\x98\xCC\x88" => "\xD3\xA4", + "\xD0\xB8\xCC\x88" => "\xD3\xA5", + "\xD0\x9E\xCC\x88" => "\xD3\xA6", + "\xD0\xBE\xCC\x88" => "\xD3\xA7", + "\xD3\xA8\xCC\x88" => "\xD3\xAA", + "\xD3\xA9\xCC\x88" => "\xD3\xAB", + "\xD0\xAD\xCC\x88" => "\xD3\xAC", + "\xD1\x8D\xCC\x88" => "\xD3\xAD", + "\xD0\xA3\xCC\x84" => "\xD3\xAE", + "\xD1\x83\xCC\x84" => "\xD3\xAF", + "\xD0\xA3\xCC\x88" => "\xD3\xB0", + "\xD1\x83\xCC\x88" => "\xD3\xB1", + "\xD0\xA3\xCC\x8B" => "\xD3\xB2", + "\xD1\x83\xCC\x8B" => "\xD3\xB3", + "\xD0\xA7\xCC\x88" => "\xD3\xB4", + "\xD1\x87\xCC\x88" => "\xD3\xB5", + "\xD0\xAB\xCC\x88" => "\xD3\xB8", + "\xD1\x8B\xCC\x88" => "\xD3\xB9", + "\xD8\xA7\xD9\x93" => "\xD8\xA2", + "\xD8\xA7\xD9\x94" => "\xD8\xA3", + "\xD9\x88\xD9\x94" => "\xD8\xA4", + "\xD8\xA7\xD9\x95" => "\xD8\xA5", + "\xD9\x8A\xD9\x94" => "\xD8\xA6", + "\xDB\x95\xD9\x94" => "\xDB\x80", + "\xDB\x81\xD9\x94" => "\xDB\x82", + "\xDB\x92\xD9\x94" => "\xDB\x93", + "\xE0\xA4\xA8\xE0\xA4\xBC" => "\xE0\xA4\xA9", + "\xE0\xA4\xB0\xE0\xA4\xBC" => "\xE0\xA4\xB1", + "\xE0\xA4\xB3\xE0\xA4\xBC" => "\xE0\xA4\xB4", + "\xE0\xA7\x87\xE0\xA6\xBE" => "\xE0\xA7\x8B", + "\xE0\xA7\x87\xE0\xA7\x97" => "\xE0\xA7\x8C", + "\xE0\xAD\x87\xE0\xAD\x96" => "\xE0\xAD\x88", + "\xE0\xAD\x87\xE0\xAC\xBE" => "\xE0\xAD\x8B", + "\xE0\xAD\x87\xE0\xAD\x97" => "\xE0\xAD\x8C", + "\xE0\xAE\x92\xE0\xAF\x97" => "\xE0\xAE\x94", + "\xE0\xAF\x86\xE0\xAE\xBE" => "\xE0\xAF\x8A", + "\xE0\xAF\x87\xE0\xAE\xBE" => "\xE0\xAF\x8B", + "\xE0\xAF\x86\xE0\xAF\x97" => "\xE0\xAF\x8C", + "\xE0\xB1\x86\xE0\xB1\x96" => "\xE0\xB1\x88", + "\xE0\xB2\xBF\xE0\xB3\x95" => "\xE0\xB3\x80", + "\xE0\xB3\x86\xE0\xB3\x95" => "\xE0\xB3\x87", + "\xE0\xB3\x86\xE0\xB3\x96" => "\xE0\xB3\x88", + "\xE0\xB3\x86\xE0\xB3\x82" => "\xE0\xB3\x8A", + "\xE0\xB3\x8A\xE0\xB3\x95" => "\xE0\xB3\x8B", + "\xE0\xB5\x86\xE0\xB4\xBE" => "\xE0\xB5\x8A", + "\xE0\xB5\x87\xE0\xB4\xBE" => "\xE0\xB5\x8B", + "\xE0\xB5\x86\xE0\xB5\x97" => "\xE0\xB5\x8C", + "\xE0\xB7\x99\xE0\xB7\x8A" => "\xE0\xB7\x9A", + "\xE0\xB7\x99\xE0\xB7\x8F" => "\xE0\xB7\x9C", + "\xE0\xB7\x9C\xE0\xB7\x8A" => "\xE0\xB7\x9D", + "\xE0\xB7\x99\xE0\xB7\x9F" => "\xE0\xB7\x9E", + "\xE1\x80\xA5\xE1\x80\xAE" => "\xE1\x80\xA6", + "\xE1\xAC\x85\xE1\xAC\xB5" => "\xE1\xAC\x86", + "\xE1\xAC\x87\xE1\xAC\xB5" => "\xE1\xAC\x88", + "\xE1\xAC\x89\xE1\xAC\xB5" => "\xE1\xAC\x8A", + "\xE1\xAC\x8B\xE1\xAC\xB5" => "\xE1\xAC\x8C", + "\xE1\xAC\x8D\xE1\xAC\xB5" => "\xE1\xAC\x8E", + "\xE1\xAC\x91\xE1\xAC\xB5" => "\xE1\xAC\x92", + "\xE1\xAC\xBA\xE1\xAC\xB5" => "\xE1\xAC\xBB", + "\xE1\xAC\xBC\xE1\xAC\xB5" => "\xE1\xAC\xBD", + "\xE1\xAC\xBE\xE1\xAC\xB5" => "\xE1\xAD\x80", + "\xE1\xAC\xBF\xE1\xAC\xB5" => "\xE1\xAD\x81", + "\xE1\xAD\x82\xE1\xAC\xB5" => "\xE1\xAD\x83", + "\x41\xCC\xA5" => "\xE1\xB8\x80", + "\x61\xCC\xA5" => "\xE1\xB8\x81", + "\x42\xCC\x87" => "\xE1\xB8\x82", + "\x62\xCC\x87" => "\xE1\xB8\x83", + "\x42\xCC\xA3" => "\xE1\xB8\x84", + "\x62\xCC\xA3" => "\xE1\xB8\x85", + "\x42\xCC\xB1" => "\xE1\xB8\x86", + "\x62\xCC\xB1" => "\xE1\xB8\x87", + "\xC3\x87\xCC\x81" => "\xE1\xB8\x88", + "\xC3\xA7\xCC\x81" => "\xE1\xB8\x89", + "\x44\xCC\x87" => "\xE1\xB8\x8A", + "\x64\xCC\x87" => "\xE1\xB8\x8B", + "\x44\xCC\xA3" => "\xE1\xB8\x8C", + "\x64\xCC\xA3" => "\xE1\xB8\x8D", + "\x44\xCC\xB1" => "\xE1\xB8\x8E", + "\x64\xCC\xB1" => "\xE1\xB8\x8F", + "\x44\xCC\xA7" => "\xE1\xB8\x90", + "\x64\xCC\xA7" => "\xE1\xB8\x91", + "\x44\xCC\xAD" => "\xE1\xB8\x92", + "\x64\xCC\xAD" => "\xE1\xB8\x93", + "\xC4\x92\xCC\x80" => "\xE1\xB8\x94", + "\xC4\x93\xCC\x80" => "\xE1\xB8\x95", + "\xC4\x92\xCC\x81" => "\xE1\xB8\x96", + "\xC4\x93\xCC\x81" => "\xE1\xB8\x97", + "\x45\xCC\xAD" => "\xE1\xB8\x98", + "\x65\xCC\xAD" => "\xE1\xB8\x99", + "\x45\xCC\xB0" => "\xE1\xB8\x9A", + "\x65\xCC\xB0" => "\xE1\xB8\x9B", + "\xC8\xA8\xCC\x86" => "\xE1\xB8\x9C", + "\xC8\xA9\xCC\x86" => "\xE1\xB8\x9D", + "\x46\xCC\x87" => "\xE1\xB8\x9E", + "\x66\xCC\x87" => "\xE1\xB8\x9F", + "\x47\xCC\x84" => "\xE1\xB8\xA0", + "\x67\xCC\x84" => "\xE1\xB8\xA1", + "\x48\xCC\x87" => "\xE1\xB8\xA2", + "\x68\xCC\x87" => "\xE1\xB8\xA3", + "\x48\xCC\xA3" => "\xE1\xB8\xA4", + "\x68\xCC\xA3" => "\xE1\xB8\xA5", + "\x48\xCC\x88" => "\xE1\xB8\xA6", + "\x68\xCC\x88" => "\xE1\xB8\xA7", + "\x48\xCC\xA7" => "\xE1\xB8\xA8", + "\x68\xCC\xA7" => "\xE1\xB8\xA9", + "\x48\xCC\xAE" => "\xE1\xB8\xAA", + "\x68\xCC\xAE" => "\xE1\xB8\xAB", + "\x49\xCC\xB0" => "\xE1\xB8\xAC", + "\x69\xCC\xB0" => "\xE1\xB8\xAD", + "\xC3\x8F\xCC\x81" => "\xE1\xB8\xAE", + "\xC3\xAF\xCC\x81" => "\xE1\xB8\xAF", + "\x4B\xCC\x81" => "\xE1\xB8\xB0", + "\x6B\xCC\x81" => "\xE1\xB8\xB1", + "\x4B\xCC\xA3" => "\xE1\xB8\xB2", + "\x6B\xCC\xA3" => "\xE1\xB8\xB3", + "\x4B\xCC\xB1" => "\xE1\xB8\xB4", + "\x6B\xCC\xB1" => "\xE1\xB8\xB5", + "\x4C\xCC\xA3" => "\xE1\xB8\xB6", + "\x6C\xCC\xA3" => "\xE1\xB8\xB7", + "\xE1\xB8\xB6\xCC\x84" => "\xE1\xB8\xB8", + "\xE1\xB8\xB7\xCC\x84" => "\xE1\xB8\xB9", + "\x4C\xCC\xB1" => "\xE1\xB8\xBA", + "\x6C\xCC\xB1" => "\xE1\xB8\xBB", + "\x4C\xCC\xAD" => "\xE1\xB8\xBC", + "\x6C\xCC\xAD" => "\xE1\xB8\xBD", + "\x4D\xCC\x81" => "\xE1\xB8\xBE", + "\x6D\xCC\x81" => "\xE1\xB8\xBF", + "\x4D\xCC\x87" => "\xE1\xB9\x80", + "\x6D\xCC\x87" => "\xE1\xB9\x81", + "\x4D\xCC\xA3" => "\xE1\xB9\x82", + "\x6D\xCC\xA3" => "\xE1\xB9\x83", + "\x4E\xCC\x87" => "\xE1\xB9\x84", + "\x6E\xCC\x87" => "\xE1\xB9\x85", + "\x4E\xCC\xA3" => "\xE1\xB9\x86", + "\x6E\xCC\xA3" => "\xE1\xB9\x87", + "\x4E\xCC\xB1" => "\xE1\xB9\x88", + "\x6E\xCC\xB1" => "\xE1\xB9\x89", + "\x4E\xCC\xAD" => "\xE1\xB9\x8A", + "\x6E\xCC\xAD" => "\xE1\xB9\x8B", + "\xC3\x95\xCC\x81" => "\xE1\xB9\x8C", + "\xC3\xB5\xCC\x81" => "\xE1\xB9\x8D", + "\xC3\x95\xCC\x88" => "\xE1\xB9\x8E", + "\xC3\xB5\xCC\x88" => "\xE1\xB9\x8F", + "\xC5\x8C\xCC\x80" => "\xE1\xB9\x90", + "\xC5\x8D\xCC\x80" => "\xE1\xB9\x91", + "\xC5\x8C\xCC\x81" => "\xE1\xB9\x92", + "\xC5\x8D\xCC\x81" => "\xE1\xB9\x93", + "\x50\xCC\x81" => "\xE1\xB9\x94", + "\x70\xCC\x81" => "\xE1\xB9\x95", + "\x50\xCC\x87" => "\xE1\xB9\x96", + "\x70\xCC\x87" => "\xE1\xB9\x97", + "\x52\xCC\x87" => "\xE1\xB9\x98", + "\x72\xCC\x87" => "\xE1\xB9\x99", + "\x52\xCC\xA3" => "\xE1\xB9\x9A", + "\x72\xCC\xA3" => "\xE1\xB9\x9B", + "\xE1\xB9\x9A\xCC\x84" => "\xE1\xB9\x9C", + "\xE1\xB9\x9B\xCC\x84" => "\xE1\xB9\x9D", + "\x52\xCC\xB1" => "\xE1\xB9\x9E", + "\x72\xCC\xB1" => "\xE1\xB9\x9F", + "\x53\xCC\x87" => "\xE1\xB9\xA0", + "\x73\xCC\x87" => "\xE1\xB9\xA1", + "\x53\xCC\xA3" => "\xE1\xB9\xA2", + "\x73\xCC\xA3" => "\xE1\xB9\xA3", + "\xC5\x9A\xCC\x87" => "\xE1\xB9\xA4", + "\xC5\x9B\xCC\x87" => "\xE1\xB9\xA5", + "\xC5\xA0\xCC\x87" => "\xE1\xB9\xA6", + "\xC5\xA1\xCC\x87" => "\xE1\xB9\xA7", + "\xE1\xB9\xA2\xCC\x87" => "\xE1\xB9\xA8", + "\xE1\xB9\xA3\xCC\x87" => "\xE1\xB9\xA9", + "\x54\xCC\x87" => "\xE1\xB9\xAA", + "\x74\xCC\x87" => "\xE1\xB9\xAB", + "\x54\xCC\xA3" => "\xE1\xB9\xAC", + "\x74\xCC\xA3" => "\xE1\xB9\xAD", + "\x54\xCC\xB1" => "\xE1\xB9\xAE", + "\x74\xCC\xB1" => "\xE1\xB9\xAF", + "\x54\xCC\xAD" => "\xE1\xB9\xB0", + "\x74\xCC\xAD" => "\xE1\xB9\xB1", + "\x55\xCC\xA4" => "\xE1\xB9\xB2", + "\x75\xCC\xA4" => "\xE1\xB9\xB3", + "\x55\xCC\xB0" => "\xE1\xB9\xB4", + "\x75\xCC\xB0" => "\xE1\xB9\xB5", + "\x55\xCC\xAD" => "\xE1\xB9\xB6", + "\x75\xCC\xAD" => "\xE1\xB9\xB7", + "\xC5\xA8\xCC\x81" => "\xE1\xB9\xB8", + "\xC5\xA9\xCC\x81" => "\xE1\xB9\xB9", + "\xC5\xAA\xCC\x88" => "\xE1\xB9\xBA", + "\xC5\xAB\xCC\x88" => "\xE1\xB9\xBB", + "\x56\xCC\x83" => "\xE1\xB9\xBC", + "\x76\xCC\x83" => "\xE1\xB9\xBD", + "\x56\xCC\xA3" => "\xE1\xB9\xBE", + "\x76\xCC\xA3" => "\xE1\xB9\xBF", + "\x57\xCC\x80" => "\xE1\xBA\x80", + "\x77\xCC\x80" => "\xE1\xBA\x81", + "\x57\xCC\x81" => "\xE1\xBA\x82", + "\x77\xCC\x81" => "\xE1\xBA\x83", + "\x57\xCC\x88" => "\xE1\xBA\x84", + "\x77\xCC\x88" => "\xE1\xBA\x85", + "\x57\xCC\x87" => "\xE1\xBA\x86", + "\x77\xCC\x87" => "\xE1\xBA\x87", + "\x57\xCC\xA3" => "\xE1\xBA\x88", + "\x77\xCC\xA3" => "\xE1\xBA\x89", + "\x58\xCC\x87" => "\xE1\xBA\x8A", + "\x78\xCC\x87" => "\xE1\xBA\x8B", + "\x58\xCC\x88" => "\xE1\xBA\x8C", + "\x78\xCC\x88" => "\xE1\xBA\x8D", + "\x59\xCC\x87" => "\xE1\xBA\x8E", + "\x79\xCC\x87" => "\xE1\xBA\x8F", + "\x5A\xCC\x82" => "\xE1\xBA\x90", + "\x7A\xCC\x82" => "\xE1\xBA\x91", + "\x5A\xCC\xA3" => "\xE1\xBA\x92", + "\x7A\xCC\xA3" => "\xE1\xBA\x93", + "\x5A\xCC\xB1" => "\xE1\xBA\x94", + "\x7A\xCC\xB1" => "\xE1\xBA\x95", + "\x68\xCC\xB1" => "\xE1\xBA\x96", + "\x74\xCC\x88" => "\xE1\xBA\x97", + "\x77\xCC\x8A" => "\xE1\xBA\x98", + "\x79\xCC\x8A" => "\xE1\xBA\x99", + "\xC5\xBF\xCC\x87" => "\xE1\xBA\x9B", + "\x41\xCC\xA3" => "\xE1\xBA\xA0", + "\x61\xCC\xA3" => "\xE1\xBA\xA1", + "\x41\xCC\x89" => "\xE1\xBA\xA2", + "\x61\xCC\x89" => "\xE1\xBA\xA3", + "\xC3\x82\xCC\x81" => "\xE1\xBA\xA4", + "\xC3\xA2\xCC\x81" => "\xE1\xBA\xA5", + "\xC3\x82\xCC\x80" => "\xE1\xBA\xA6", + "\xC3\xA2\xCC\x80" => "\xE1\xBA\xA7", + "\xC3\x82\xCC\x89" => "\xE1\xBA\xA8", + "\xC3\xA2\xCC\x89" => "\xE1\xBA\xA9", + "\xC3\x82\xCC\x83" => "\xE1\xBA\xAA", + "\xC3\xA2\xCC\x83" => "\xE1\xBA\xAB", + "\xE1\xBA\xA0\xCC\x82" => "\xE1\xBA\xAC", + "\xE1\xBA\xA1\xCC\x82" => "\xE1\xBA\xAD", + "\xC4\x82\xCC\x81" => "\xE1\xBA\xAE", + "\xC4\x83\xCC\x81" => "\xE1\xBA\xAF", + "\xC4\x82\xCC\x80" => "\xE1\xBA\xB0", + "\xC4\x83\xCC\x80" => "\xE1\xBA\xB1", + "\xC4\x82\xCC\x89" => "\xE1\xBA\xB2", + "\xC4\x83\xCC\x89" => "\xE1\xBA\xB3", + "\xC4\x82\xCC\x83" => "\xE1\xBA\xB4", + "\xC4\x83\xCC\x83" => "\xE1\xBA\xB5", + "\xE1\xBA\xA0\xCC\x86" => "\xE1\xBA\xB6", + "\xE1\xBA\xA1\xCC\x86" => "\xE1\xBA\xB7", + "\x45\xCC\xA3" => "\xE1\xBA\xB8", + "\x65\xCC\xA3" => "\xE1\xBA\xB9", + "\x45\xCC\x89" => "\xE1\xBA\xBA", + "\x65\xCC\x89" => "\xE1\xBA\xBB", + "\x45\xCC\x83" => "\xE1\xBA\xBC", + "\x65\xCC\x83" => "\xE1\xBA\xBD", + "\xC3\x8A\xCC\x81" => "\xE1\xBA\xBE", + "\xC3\xAA\xCC\x81" => "\xE1\xBA\xBF", + "\xC3\x8A\xCC\x80" => "\xE1\xBB\x80", + "\xC3\xAA\xCC\x80" => "\xE1\xBB\x81", + "\xC3\x8A\xCC\x89" => "\xE1\xBB\x82", + "\xC3\xAA\xCC\x89" => "\xE1\xBB\x83", + "\xC3\x8A\xCC\x83" => "\xE1\xBB\x84", + "\xC3\xAA\xCC\x83" => "\xE1\xBB\x85", + "\xE1\xBA\xB8\xCC\x82" => "\xE1\xBB\x86", + "\xE1\xBA\xB9\xCC\x82" => "\xE1\xBB\x87", + "\x49\xCC\x89" => "\xE1\xBB\x88", + "\x69\xCC\x89" => "\xE1\xBB\x89", + "\x49\xCC\xA3" => "\xE1\xBB\x8A", + "\x69\xCC\xA3" => "\xE1\xBB\x8B", + "\x4F\xCC\xA3" => "\xE1\xBB\x8C", + "\x6F\xCC\xA3" => "\xE1\xBB\x8D", + "\x4F\xCC\x89" => "\xE1\xBB\x8E", + "\x6F\xCC\x89" => "\xE1\xBB\x8F", + "\xC3\x94\xCC\x81" => "\xE1\xBB\x90", + "\xC3\xB4\xCC\x81" => "\xE1\xBB\x91", + "\xC3\x94\xCC\x80" => "\xE1\xBB\x92", + "\xC3\xB4\xCC\x80" => "\xE1\xBB\x93", + "\xC3\x94\xCC\x89" => "\xE1\xBB\x94", + "\xC3\xB4\xCC\x89" => "\xE1\xBB\x95", + "\xC3\x94\xCC\x83" => "\xE1\xBB\x96", + "\xC3\xB4\xCC\x83" => "\xE1\xBB\x97", + "\xE1\xBB\x8C\xCC\x82" => "\xE1\xBB\x98", + "\xE1\xBB\x8D\xCC\x82" => "\xE1\xBB\x99", + "\xC6\xA0\xCC\x81" => "\xE1\xBB\x9A", + "\xC6\xA1\xCC\x81" => "\xE1\xBB\x9B", + "\xC6\xA0\xCC\x80" => "\xE1\xBB\x9C", + "\xC6\xA1\xCC\x80" => "\xE1\xBB\x9D", + "\xC6\xA0\xCC\x89" => "\xE1\xBB\x9E", + "\xC6\xA1\xCC\x89" => "\xE1\xBB\x9F", + "\xC6\xA0\xCC\x83" => "\xE1\xBB\xA0", + "\xC6\xA1\xCC\x83" => "\xE1\xBB\xA1", + "\xC6\xA0\xCC\xA3" => "\xE1\xBB\xA2", + "\xC6\xA1\xCC\xA3" => "\xE1\xBB\xA3", + "\x55\xCC\xA3" => "\xE1\xBB\xA4", + "\x75\xCC\xA3" => "\xE1\xBB\xA5", + "\x55\xCC\x89" => "\xE1\xBB\xA6", + "\x75\xCC\x89" => "\xE1\xBB\xA7", + "\xC6\xAF\xCC\x81" => "\xE1\xBB\xA8", + "\xC6\xB0\xCC\x81" => "\xE1\xBB\xA9", + "\xC6\xAF\xCC\x80" => "\xE1\xBB\xAA", + "\xC6\xB0\xCC\x80" => "\xE1\xBB\xAB", + "\xC6\xAF\xCC\x89" => "\xE1\xBB\xAC", + "\xC6\xB0\xCC\x89" => "\xE1\xBB\xAD", + "\xC6\xAF\xCC\x83" => "\xE1\xBB\xAE", + "\xC6\xB0\xCC\x83" => "\xE1\xBB\xAF", + "\xC6\xAF\xCC\xA3" => "\xE1\xBB\xB0", + "\xC6\xB0\xCC\xA3" => "\xE1\xBB\xB1", + "\x59\xCC\x80" => "\xE1\xBB\xB2", + "\x79\xCC\x80" => "\xE1\xBB\xB3", + "\x59\xCC\xA3" => "\xE1\xBB\xB4", + "\x79\xCC\xA3" => "\xE1\xBB\xB5", + "\x59\xCC\x89" => "\xE1\xBB\xB6", + "\x79\xCC\x89" => "\xE1\xBB\xB7", + "\x59\xCC\x83" => "\xE1\xBB\xB8", + "\x79\xCC\x83" => "\xE1\xBB\xB9", + "\xCE\xB1\xCC\x93" => "\xE1\xBC\x80", + "\xCE\xB1\xCC\x94" => "\xE1\xBC\x81", + "\xE1\xBC\x80\xCC\x80" => "\xE1\xBC\x82", + "\xE1\xBC\x81\xCC\x80" => "\xE1\xBC\x83", + "\xE1\xBC\x80\xCC\x81" => "\xE1\xBC\x84", + "\xE1\xBC\x81\xCC\x81" => "\xE1\xBC\x85", + "\xE1\xBC\x80\xCD\x82" => "\xE1\xBC\x86", + "\xE1\xBC\x81\xCD\x82" => "\xE1\xBC\x87", + "\xCE\x91\xCC\x93" => "\xE1\xBC\x88", + "\xCE\x91\xCC\x94" => "\xE1\xBC\x89", + "\xE1\xBC\x88\xCC\x80" => "\xE1\xBC\x8A", + "\xE1\xBC\x89\xCC\x80" => "\xE1\xBC\x8B", + "\xE1\xBC\x88\xCC\x81" => "\xE1\xBC\x8C", + "\xE1\xBC\x89\xCC\x81" => "\xE1\xBC\x8D", + "\xE1\xBC\x88\xCD\x82" => "\xE1\xBC\x8E", + "\xE1\xBC\x89\xCD\x82" => "\xE1\xBC\x8F", + "\xCE\xB5\xCC\x93" => "\xE1\xBC\x90", + "\xCE\xB5\xCC\x94" => "\xE1\xBC\x91", + "\xE1\xBC\x90\xCC\x80" => "\xE1\xBC\x92", + "\xE1\xBC\x91\xCC\x80" => "\xE1\xBC\x93", + "\xE1\xBC\x90\xCC\x81" => "\xE1\xBC\x94", + "\xE1\xBC\x91\xCC\x81" => "\xE1\xBC\x95", + "\xCE\x95\xCC\x93" => "\xE1\xBC\x98", + "\xCE\x95\xCC\x94" => "\xE1\xBC\x99", + "\xE1\xBC\x98\xCC\x80" => "\xE1\xBC\x9A", + "\xE1\xBC\x99\xCC\x80" => "\xE1\xBC\x9B", + "\xE1\xBC\x98\xCC\x81" => "\xE1\xBC\x9C", + "\xE1\xBC\x99\xCC\x81" => "\xE1\xBC\x9D", + "\xCE\xB7\xCC\x93" => "\xE1\xBC\xA0", + "\xCE\xB7\xCC\x94" => "\xE1\xBC\xA1", + "\xE1\xBC\xA0\xCC\x80" => "\xE1\xBC\xA2", + "\xE1\xBC\xA1\xCC\x80" => "\xE1\xBC\xA3", + "\xE1\xBC\xA0\xCC\x81" => "\xE1\xBC\xA4", + "\xE1\xBC\xA1\xCC\x81" => "\xE1\xBC\xA5", + "\xE1\xBC\xA0\xCD\x82" => "\xE1\xBC\xA6", + "\xE1\xBC\xA1\xCD\x82" => "\xE1\xBC\xA7", + "\xCE\x97\xCC\x93" => "\xE1\xBC\xA8", + "\xCE\x97\xCC\x94" => "\xE1\xBC\xA9", + "\xE1\xBC\xA8\xCC\x80" => "\xE1\xBC\xAA", + "\xE1\xBC\xA9\xCC\x80" => "\xE1\xBC\xAB", + "\xE1\xBC\xA8\xCC\x81" => "\xE1\xBC\xAC", + "\xE1\xBC\xA9\xCC\x81" => "\xE1\xBC\xAD", + "\xE1\xBC\xA8\xCD\x82" => "\xE1\xBC\xAE", + "\xE1\xBC\xA9\xCD\x82" => "\xE1\xBC\xAF", + "\xCE\xB9\xCC\x93" => "\xE1\xBC\xB0", + "\xCE\xB9\xCC\x94" => "\xE1\xBC\xB1", + "\xE1\xBC\xB0\xCC\x80" => "\xE1\xBC\xB2", + "\xE1\xBC\xB1\xCC\x80" => "\xE1\xBC\xB3", + "\xE1\xBC\xB0\xCC\x81" => "\xE1\xBC\xB4", + "\xE1\xBC\xB1\xCC\x81" => "\xE1\xBC\xB5", + "\xE1\xBC\xB0\xCD\x82" => "\xE1\xBC\xB6", + "\xE1\xBC\xB1\xCD\x82" => "\xE1\xBC\xB7", + "\xCE\x99\xCC\x93" => "\xE1\xBC\xB8", + "\xCE\x99\xCC\x94" => "\xE1\xBC\xB9", + "\xE1\xBC\xB8\xCC\x80" => "\xE1\xBC\xBA", + "\xE1\xBC\xB9\xCC\x80" => "\xE1\xBC\xBB", + "\xE1\xBC\xB8\xCC\x81" => "\xE1\xBC\xBC", + "\xE1\xBC\xB9\xCC\x81" => "\xE1\xBC\xBD", + "\xE1\xBC\xB8\xCD\x82" => "\xE1\xBC\xBE", + "\xE1\xBC\xB9\xCD\x82" => "\xE1\xBC\xBF", + "\xCE\xBF\xCC\x93" => "\xE1\xBD\x80", + "\xCE\xBF\xCC\x94" => "\xE1\xBD\x81", + "\xE1\xBD\x80\xCC\x80" => "\xE1\xBD\x82", + "\xE1\xBD\x81\xCC\x80" => "\xE1\xBD\x83", + "\xE1\xBD\x80\xCC\x81" => "\xE1\xBD\x84", + "\xE1\xBD\x81\xCC\x81" => "\xE1\xBD\x85", + "\xCE\x9F\xCC\x93" => "\xE1\xBD\x88", + "\xCE\x9F\xCC\x94" => "\xE1\xBD\x89", + "\xE1\xBD\x88\xCC\x80" => "\xE1\xBD\x8A", + "\xE1\xBD\x89\xCC\x80" => "\xE1\xBD\x8B", + "\xE1\xBD\x88\xCC\x81" => "\xE1\xBD\x8C", + "\xE1\xBD\x89\xCC\x81" => "\xE1\xBD\x8D", + "\xCF\x85\xCC\x93" => "\xE1\xBD\x90", + "\xCF\x85\xCC\x94" => "\xE1\xBD\x91", + "\xE1\xBD\x90\xCC\x80" => "\xE1\xBD\x92", + "\xE1\xBD\x91\xCC\x80" => "\xE1\xBD\x93", + "\xE1\xBD\x90\xCC\x81" => "\xE1\xBD\x94", + "\xE1\xBD\x91\xCC\x81" => "\xE1\xBD\x95", + "\xE1\xBD\x90\xCD\x82" => "\xE1\xBD\x96", + "\xE1\xBD\x91\xCD\x82" => "\xE1\xBD\x97", + "\xCE\xA5\xCC\x94" => "\xE1\xBD\x99", + "\xE1\xBD\x99\xCC\x80" => "\xE1\xBD\x9B", + "\xE1\xBD\x99\xCC\x81" => "\xE1\xBD\x9D", + "\xE1\xBD\x99\xCD\x82" => "\xE1\xBD\x9F", + "\xCF\x89\xCC\x93" => "\xE1\xBD\xA0", + "\xCF\x89\xCC\x94" => "\xE1\xBD\xA1", + "\xE1\xBD\xA0\xCC\x80" => "\xE1\xBD\xA2", + "\xE1\xBD\xA1\xCC\x80" => "\xE1\xBD\xA3", + "\xE1\xBD\xA0\xCC\x81" => "\xE1\xBD\xA4", + "\xE1\xBD\xA1\xCC\x81" => "\xE1\xBD\xA5", + "\xE1\xBD\xA0\xCD\x82" => "\xE1\xBD\xA6", + "\xE1\xBD\xA1\xCD\x82" => "\xE1\xBD\xA7", + "\xCE\xA9\xCC\x93" => "\xE1\xBD\xA8", + "\xCE\xA9\xCC\x94" => "\xE1\xBD\xA9", + "\xE1\xBD\xA8\xCC\x80" => "\xE1\xBD\xAA", + "\xE1\xBD\xA9\xCC\x80" => "\xE1\xBD\xAB", + "\xE1\xBD\xA8\xCC\x81" => "\xE1\xBD\xAC", + "\xE1\xBD\xA9\xCC\x81" => "\xE1\xBD\xAD", + "\xE1\xBD\xA8\xCD\x82" => "\xE1\xBD\xAE", + "\xE1\xBD\xA9\xCD\x82" => "\xE1\xBD\xAF", + "\xCE\xB1\xCC\x80" => "\xE1\xBD\xB0", + "\xCE\xB5\xCC\x80" => "\xE1\xBD\xB2", + "\xCE\xB7\xCC\x80" => "\xE1\xBD\xB4", + "\xCE\xB9\xCC\x80" => "\xE1\xBD\xB6", + "\xCE\xBF\xCC\x80" => "\xE1\xBD\xB8", + "\xCF\x85\xCC\x80" => "\xE1\xBD\xBA", + "\xCF\x89\xCC\x80" => "\xE1\xBD\xBC", + "\xE1\xBC\x80\xCD\x85" => "\xE1\xBE\x80", + "\xE1\xBC\x81\xCD\x85" => "\xE1\xBE\x81", + "\xE1\xBC\x82\xCD\x85" => "\xE1\xBE\x82", + "\xE1\xBC\x83\xCD\x85" => "\xE1\xBE\x83", + "\xE1\xBC\x84\xCD\x85" => "\xE1\xBE\x84", + "\xE1\xBC\x85\xCD\x85" => "\xE1\xBE\x85", + "\xE1\xBC\x86\xCD\x85" => "\xE1\xBE\x86", + "\xE1\xBC\x87\xCD\x85" => "\xE1\xBE\x87", + "\xE1\xBC\x88\xCD\x85" => "\xE1\xBE\x88", + "\xE1\xBC\x89\xCD\x85" => "\xE1\xBE\x89", + "\xE1\xBC\x8A\xCD\x85" => "\xE1\xBE\x8A", + "\xE1\xBC\x8B\xCD\x85" => "\xE1\xBE\x8B", + "\xE1\xBC\x8C\xCD\x85" => "\xE1\xBE\x8C", + "\xE1\xBC\x8D\xCD\x85" => "\xE1\xBE\x8D", + "\xE1\xBC\x8E\xCD\x85" => "\xE1\xBE\x8E", + "\xE1\xBC\x8F\xCD\x85" => "\xE1\xBE\x8F", + "\xE1\xBC\xA0\xCD\x85" => "\xE1\xBE\x90", + "\xE1\xBC\xA1\xCD\x85" => "\xE1\xBE\x91", + "\xE1\xBC\xA2\xCD\x85" => "\xE1\xBE\x92", + "\xE1\xBC\xA3\xCD\x85" => "\xE1\xBE\x93", + "\xE1\xBC\xA4\xCD\x85" => "\xE1\xBE\x94", + "\xE1\xBC\xA5\xCD\x85" => "\xE1\xBE\x95", + "\xE1\xBC\xA6\xCD\x85" => "\xE1\xBE\x96", + "\xE1\xBC\xA7\xCD\x85" => "\xE1\xBE\x97", + "\xE1\xBC\xA8\xCD\x85" => "\xE1\xBE\x98", + "\xE1\xBC\xA9\xCD\x85" => "\xE1\xBE\x99", + "\xE1\xBC\xAA\xCD\x85" => "\xE1\xBE\x9A", + "\xE1\xBC\xAB\xCD\x85" => "\xE1\xBE\x9B", + "\xE1\xBC\xAC\xCD\x85" => "\xE1\xBE\x9C", + "\xE1\xBC\xAD\xCD\x85" => "\xE1\xBE\x9D", + "\xE1\xBC\xAE\xCD\x85" => "\xE1\xBE\x9E", + "\xE1\xBC\xAF\xCD\x85" => "\xE1\xBE\x9F", + "\xE1\xBD\xA0\xCD\x85" => "\xE1\xBE\xA0", + "\xE1\xBD\xA1\xCD\x85" => "\xE1\xBE\xA1", + "\xE1\xBD\xA2\xCD\x85" => "\xE1\xBE\xA2", + "\xE1\xBD\xA3\xCD\x85" => "\xE1\xBE\xA3", + "\xE1\xBD\xA4\xCD\x85" => "\xE1\xBE\xA4", + "\xE1\xBD\xA5\xCD\x85" => "\xE1\xBE\xA5", + "\xE1\xBD\xA6\xCD\x85" => "\xE1\xBE\xA6", + "\xE1\xBD\xA7\xCD\x85" => "\xE1\xBE\xA7", + "\xE1\xBD\xA8\xCD\x85" => "\xE1\xBE\xA8", + "\xE1\xBD\xA9\xCD\x85" => "\xE1\xBE\xA9", + "\xE1\xBD\xAA\xCD\x85" => "\xE1\xBE\xAA", + "\xE1\xBD\xAB\xCD\x85" => "\xE1\xBE\xAB", + "\xE1\xBD\xAC\xCD\x85" => "\xE1\xBE\xAC", + "\xE1\xBD\xAD\xCD\x85" => "\xE1\xBE\xAD", + "\xE1\xBD\xAE\xCD\x85" => "\xE1\xBE\xAE", + "\xE1\xBD\xAF\xCD\x85" => "\xE1\xBE\xAF", + "\xCE\xB1\xCC\x86" => "\xE1\xBE\xB0", + "\xCE\xB1\xCC\x84" => "\xE1\xBE\xB1", + "\xE1\xBD\xB0\xCD\x85" => "\xE1\xBE\xB2", + "\xCE\xB1\xCD\x85" => "\xE1\xBE\xB3", + "\xCE\xAC\xCD\x85" => "\xE1\xBE\xB4", + "\xCE\xB1\xCD\x82" => "\xE1\xBE\xB6", + "\xE1\xBE\xB6\xCD\x85" => "\xE1\xBE\xB7", + "\xCE\x91\xCC\x86" => "\xE1\xBE\xB8", + "\xCE\x91\xCC\x84" => "\xE1\xBE\xB9", + "\xCE\x91\xCC\x80" => "\xE1\xBE\xBA", + "\xCE\x91\xCD\x85" => "\xE1\xBE\xBC", + "\xC2\xA8\xCD\x82" => "\xE1\xBF\x81", + "\xE1\xBD\xB4\xCD\x85" => "\xE1\xBF\x82", + "\xCE\xB7\xCD\x85" => "\xE1\xBF\x83", + "\xCE\xAE\xCD\x85" => "\xE1\xBF\x84", + "\xCE\xB7\xCD\x82" => "\xE1\xBF\x86", + "\xE1\xBF\x86\xCD\x85" => "\xE1\xBF\x87", + "\xCE\x95\xCC\x80" => "\xE1\xBF\x88", + "\xCE\x97\xCC\x80" => "\xE1\xBF\x8A", + "\xCE\x97\xCD\x85" => "\xE1\xBF\x8C", + "\xE1\xBE\xBF\xCC\x80" => "\xE1\xBF\x8D", + "\xE1\xBE\xBF\xCC\x81" => "\xE1\xBF\x8E", + "\xE1\xBE\xBF\xCD\x82" => "\xE1\xBF\x8F", + "\xCE\xB9\xCC\x86" => "\xE1\xBF\x90", + "\xCE\xB9\xCC\x84" => "\xE1\xBF\x91", + "\xCF\x8A\xCC\x80" => "\xE1\xBF\x92", + "\xCE\xB9\xCD\x82" => "\xE1\xBF\x96", + "\xCF\x8A\xCD\x82" => "\xE1\xBF\x97", + "\xCE\x99\xCC\x86" => "\xE1\xBF\x98", + "\xCE\x99\xCC\x84" => "\xE1\xBF\x99", + "\xCE\x99\xCC\x80" => "\xE1\xBF\x9A", + "\xE1\xBF\xBE\xCC\x80" => "\xE1\xBF\x9D", + "\xE1\xBF\xBE\xCC\x81" => "\xE1\xBF\x9E", + "\xE1\xBF\xBE\xCD\x82" => "\xE1\xBF\x9F", + "\xCF\x85\xCC\x86" => "\xE1\xBF\xA0", + "\xCF\x85\xCC\x84" => "\xE1\xBF\xA1", + "\xCF\x8B\xCC\x80" => "\xE1\xBF\xA2", + "\xCF\x81\xCC\x93" => "\xE1\xBF\xA4", + "\xCF\x81\xCC\x94" => "\xE1\xBF\xA5", + "\xCF\x85\xCD\x82" => "\xE1\xBF\xA6", + "\xCF\x8B\xCD\x82" => "\xE1\xBF\xA7", + "\xCE\xA5\xCC\x86" => "\xE1\xBF\xA8", + "\xCE\xA5\xCC\x84" => "\xE1\xBF\xA9", + "\xCE\xA5\xCC\x80" => "\xE1\xBF\xAA", + "\xCE\xA1\xCC\x94" => "\xE1\xBF\xAC", + "\xC2\xA8\xCC\x80" => "\xE1\xBF\xAD", + "\xE1\xBD\xBC\xCD\x85" => "\xE1\xBF\xB2", + "\xCF\x89\xCD\x85" => "\xE1\xBF\xB3", + "\xCF\x8E\xCD\x85" => "\xE1\xBF\xB4", + "\xCF\x89\xCD\x82" => "\xE1\xBF\xB6", + "\xE1\xBF\xB6\xCD\x85" => "\xE1\xBF\xB7", + "\xCE\x9F\xCC\x80" => "\xE1\xBF\xB8", + "\xCE\xA9\xCC\x80" => "\xE1\xBF\xBA", + "\xCE\xA9\xCD\x85" => "\xE1\xBF\xBC", + "\xE2\x86\x90\xCC\xB8" => "\xE2\x86\x9A", + "\xE2\x86\x92\xCC\xB8" => "\xE2\x86\x9B", + "\xE2\x86\x94\xCC\xB8" => "\xE2\x86\xAE", + "\xE2\x87\x90\xCC\xB8" => "\xE2\x87\x8D", + "\xE2\x87\x94\xCC\xB8" => "\xE2\x87\x8E", + "\xE2\x87\x92\xCC\xB8" => "\xE2\x87\x8F", + "\xE2\x88\x83\xCC\xB8" => "\xE2\x88\x84", + "\xE2\x88\x88\xCC\xB8" => "\xE2\x88\x89", + "\xE2\x88\x8B\xCC\xB8" => "\xE2\x88\x8C", + "\xE2\x88\xA3\xCC\xB8" => "\xE2\x88\xA4", + "\xE2\x88\xA5\xCC\xB8" => "\xE2\x88\xA6", + "\xE2\x88\xBC\xCC\xB8" => "\xE2\x89\x81", + "\xE2\x89\x83\xCC\xB8" => "\xE2\x89\x84", + "\xE2\x89\x85\xCC\xB8" => "\xE2\x89\x87", + "\xE2\x89\x88\xCC\xB8" => "\xE2\x89\x89", + "\x3D\xCC\xB8" => "\xE2\x89\xA0", + "\xE2\x89\xA1\xCC\xB8" => "\xE2\x89\xA2", + "\xE2\x89\x8D\xCC\xB8" => "\xE2\x89\xAD", + "\x3C\xCC\xB8" => "\xE2\x89\xAE", + "\x3E\xCC\xB8" => "\xE2\x89\xAF", + "\xE2\x89\xA4\xCC\xB8" => "\xE2\x89\xB0", + "\xE2\x89\xA5\xCC\xB8" => "\xE2\x89\xB1", + "\xE2\x89\xB2\xCC\xB8" => "\xE2\x89\xB4", + "\xE2\x89\xB3\xCC\xB8" => "\xE2\x89\xB5", + "\xE2\x89\xB6\xCC\xB8" => "\xE2\x89\xB8", + "\xE2\x89\xB7\xCC\xB8" => "\xE2\x89\xB9", + "\xE2\x89\xBA\xCC\xB8" => "\xE2\x8A\x80", + "\xE2\x89\xBB\xCC\xB8" => "\xE2\x8A\x81", + "\xE2\x8A\x82\xCC\xB8" => "\xE2\x8A\x84", + "\xE2\x8A\x83\xCC\xB8" => "\xE2\x8A\x85", + "\xE2\x8A\x86\xCC\xB8" => "\xE2\x8A\x88", + "\xE2\x8A\x87\xCC\xB8" => "\xE2\x8A\x89", + "\xE2\x8A\xA2\xCC\xB8" => "\xE2\x8A\xAC", + "\xE2\x8A\xA8\xCC\xB8" => "\xE2\x8A\xAD", + "\xE2\x8A\xA9\xCC\xB8" => "\xE2\x8A\xAE", + "\xE2\x8A\xAB\xCC\xB8" => "\xE2\x8A\xAF", + "\xE2\x89\xBC\xCC\xB8" => "\xE2\x8B\xA0", + "\xE2\x89\xBD\xCC\xB8" => "\xE2\x8B\xA1", + "\xE2\x8A\x91\xCC\xB8" => "\xE2\x8B\xA2", + "\xE2\x8A\x92\xCC\xB8" => "\xE2\x8B\xA3", + "\xE2\x8A\xB2\xCC\xB8" => "\xE2\x8B\xAA", + "\xE2\x8A\xB3\xCC\xB8" => "\xE2\x8B\xAB", + "\xE2\x8A\xB4\xCC\xB8" => "\xE2\x8B\xAC", + "\xE2\x8A\xB5\xCC\xB8" => "\xE2\x8B\xAD", + "\xE3\x81\x8B\xE3\x82\x99" => "\xE3\x81\x8C", + "\xE3\x81\x8D\xE3\x82\x99" => "\xE3\x81\x8E", + "\xE3\x81\x8F\xE3\x82\x99" => "\xE3\x81\x90", + "\xE3\x81\x91\xE3\x82\x99" => "\xE3\x81\x92", + "\xE3\x81\x93\xE3\x82\x99" => "\xE3\x81\x94", + "\xE3\x81\x95\xE3\x82\x99" => "\xE3\x81\x96", + "\xE3\x81\x97\xE3\x82\x99" => "\xE3\x81\x98", + "\xE3\x81\x99\xE3\x82\x99" => "\xE3\x81\x9A", + "\xE3\x81\x9B\xE3\x82\x99" => "\xE3\x81\x9C", + "\xE3\x81\x9D\xE3\x82\x99" => "\xE3\x81\x9E", + "\xE3\x81\x9F\xE3\x82\x99" => "\xE3\x81\xA0", + "\xE3\x81\xA1\xE3\x82\x99" => "\xE3\x81\xA2", + "\xE3\x81\xA4\xE3\x82\x99" => "\xE3\x81\xA5", + "\xE3\x81\xA6\xE3\x82\x99" => "\xE3\x81\xA7", + "\xE3\x81\xA8\xE3\x82\x99" => "\xE3\x81\xA9", + "\xE3\x81\xAF\xE3\x82\x99" => "\xE3\x81\xB0", + "\xE3\x81\xAF\xE3\x82\x9A" => "\xE3\x81\xB1", + "\xE3\x81\xB2\xE3\x82\x99" => "\xE3\x81\xB3", + "\xE3\x81\xB2\xE3\x82\x9A" => "\xE3\x81\xB4", + "\xE3\x81\xB5\xE3\x82\x99" => "\xE3\x81\xB6", + "\xE3\x81\xB5\xE3\x82\x9A" => "\xE3\x81\xB7", + "\xE3\x81\xB8\xE3\x82\x99" => "\xE3\x81\xB9", + "\xE3\x81\xB8\xE3\x82\x9A" => "\xE3\x81\xBA", + "\xE3\x81\xBB\xE3\x82\x99" => "\xE3\x81\xBC", + "\xE3\x81\xBB\xE3\x82\x9A" => "\xE3\x81\xBD", + "\xE3\x81\x86\xE3\x82\x99" => "\xE3\x82\x94", + "\xE3\x82\x9D\xE3\x82\x99" => "\xE3\x82\x9E", + "\xE3\x82\xAB\xE3\x82\x99" => "\xE3\x82\xAC", + "\xE3\x82\xAD\xE3\x82\x99" => "\xE3\x82\xAE", + "\xE3\x82\xAF\xE3\x82\x99" => "\xE3\x82\xB0", + "\xE3\x82\xB1\xE3\x82\x99" => "\xE3\x82\xB2", + "\xE3\x82\xB3\xE3\x82\x99" => "\xE3\x82\xB4", + "\xE3\x82\xB5\xE3\x82\x99" => "\xE3\x82\xB6", + "\xE3\x82\xB7\xE3\x82\x99" => "\xE3\x82\xB8", + "\xE3\x82\xB9\xE3\x82\x99" => "\xE3\x82\xBA", + "\xE3\x82\xBB\xE3\x82\x99" => "\xE3\x82\xBC", + "\xE3\x82\xBD\xE3\x82\x99" => "\xE3\x82\xBE", + "\xE3\x82\xBF\xE3\x82\x99" => "\xE3\x83\x80", + "\xE3\x83\x81\xE3\x82\x99" => "\xE3\x83\x82", + "\xE3\x83\x84\xE3\x82\x99" => "\xE3\x83\x85", + "\xE3\x83\x86\xE3\x82\x99" => "\xE3\x83\x87", + "\xE3\x83\x88\xE3\x82\x99" => "\xE3\x83\x89", + "\xE3\x83\x8F\xE3\x82\x99" => "\xE3\x83\x90", + "\xE3\x83\x8F\xE3\x82\x9A" => "\xE3\x83\x91", + "\xE3\x83\x92\xE3\x82\x99" => "\xE3\x83\x93", + "\xE3\x83\x92\xE3\x82\x9A" => "\xE3\x83\x94", + "\xE3\x83\x95\xE3\x82\x99" => "\xE3\x83\x96", + "\xE3\x83\x95\xE3\x82\x9A" => "\xE3\x83\x97", + "\xE3\x83\x98\xE3\x82\x99" => "\xE3\x83\x99", + "\xE3\x83\x98\xE3\x82\x9A" => "\xE3\x83\x9A", + "\xE3\x83\x9B\xE3\x82\x99" => "\xE3\x83\x9C", + "\xE3\x83\x9B\xE3\x82\x9A" => "\xE3\x83\x9D", + "\xE3\x82\xA6\xE3\x82\x99" => "\xE3\x83\xB4", + "\xE3\x83\xAF\xE3\x82\x99" => "\xE3\x83\xB7", + "\xE3\x83\xB0\xE3\x82\x99" => "\xE3\x83\xB8", + "\xE3\x83\xB1\xE3\x82\x99" => "\xE3\x83\xB9", + "\xE3\x83\xB2\xE3\x82\x99" => "\xE3\x83\xBA", + "\xE3\x83\xBD\xE3\x82\x99" => "\xE3\x83\xBE", + "\xF0\x91\x82\x99\xF0\x91\x82\xBA" => "\xF0\x91\x82\x9A", + "\xF0\x91\x82\x9B\xF0\x91\x82\xBA" => "\xF0\x91\x82\x9C", + "\xF0\x91\x82\xA5\xF0\x91\x82\xBA" => "\xF0\x91\x82\xAB", + "\xF0\x91\x84\xB1\xF0\x91\x84\xA7" => "\xF0\x91\x84\xAE", + "\xF0\x91\x84\xB2\xF0\x91\x84\xA7" => "\xF0\x91\x84\xAF", + "\xF0\x91\x8D\x87\xF0\x91\x8C\xBE" => "\xF0\x91\x8D\x8B", + "\xF0\x91\x8D\x87\xF0\x91\x8D\x97" => "\xF0\x91\x8D\x8C", + "\xF0\x91\x92\xB9\xF0\x91\x92\xBA" => "\xF0\x91\x92\xBB", + "\xF0\x91\x92\xB9\xF0\x91\x92\xB0" => "\xF0\x91\x92\xBC", + "\xF0\x91\x92\xB9\xF0\x91\x92\xBD" => "\xF0\x91\x92\xBE", + "\xF0\x91\x96\xB8\xF0\x91\x96\xAF" => "\xF0\x91\x96\xBA", + "\xF0\x91\x96\xB9\xF0\x91\x96\xAF" => "\xF0\x91\x96\xBB", + "\xF0\x91\xA4\xB5\xF0\x91\xA4\xB0" => "\xF0\x91\xA4\xB8", + ); +} + +?> \ No newline at end of file diff --git a/Sources/Unicode/DecompositionCanonical.php b/Sources/Unicode/DecompositionCanonical.php new file mode 100644 index 0000000..2f990e0 --- /dev/null +++ b/Sources/Unicode/DecompositionCanonical.php @@ -0,0 +1,2092 @@ + "\x41\xCC\x80", + "\xC3\x81" => "\x41\xCC\x81", + "\xC3\x82" => "\x41\xCC\x82", + "\xC3\x83" => "\x41\xCC\x83", + "\xC3\x84" => "\x41\xCC\x88", + "\xC3\x85" => "\x41\xCC\x8A", + "\xC3\x87" => "\x43\xCC\xA7", + "\xC3\x88" => "\x45\xCC\x80", + "\xC3\x89" => "\x45\xCC\x81", + "\xC3\x8A" => "\x45\xCC\x82", + "\xC3\x8B" => "\x45\xCC\x88", + "\xC3\x8C" => "\x49\xCC\x80", + "\xC3\x8D" => "\x49\xCC\x81", + "\xC3\x8E" => "\x49\xCC\x82", + "\xC3\x8F" => "\x49\xCC\x88", + "\xC3\x91" => "\x4E\xCC\x83", + "\xC3\x92" => "\x4F\xCC\x80", + "\xC3\x93" => "\x4F\xCC\x81", + "\xC3\x94" => "\x4F\xCC\x82", + "\xC3\x95" => "\x4F\xCC\x83", + "\xC3\x96" => "\x4F\xCC\x88", + "\xC3\x99" => "\x55\xCC\x80", + "\xC3\x9A" => "\x55\xCC\x81", + "\xC3\x9B" => "\x55\xCC\x82", + "\xC3\x9C" => "\x55\xCC\x88", + "\xC3\x9D" => "\x59\xCC\x81", + "\xC3\xA0" => "\x61\xCC\x80", + "\xC3\xA1" => "\x61\xCC\x81", + "\xC3\xA2" => "\x61\xCC\x82", + "\xC3\xA3" => "\x61\xCC\x83", + "\xC3\xA4" => "\x61\xCC\x88", + "\xC3\xA5" => "\x61\xCC\x8A", + "\xC3\xA7" => "\x63\xCC\xA7", + "\xC3\xA8" => "\x65\xCC\x80", + "\xC3\xA9" => "\x65\xCC\x81", + "\xC3\xAA" => "\x65\xCC\x82", + "\xC3\xAB" => "\x65\xCC\x88", + "\xC3\xAC" => "\x69\xCC\x80", + "\xC3\xAD" => "\x69\xCC\x81", + "\xC3\xAE" => "\x69\xCC\x82", + "\xC3\xAF" => "\x69\xCC\x88", + "\xC3\xB1" => "\x6E\xCC\x83", + "\xC3\xB2" => "\x6F\xCC\x80", + "\xC3\xB3" => "\x6F\xCC\x81", + "\xC3\xB4" => "\x6F\xCC\x82", + "\xC3\xB5" => "\x6F\xCC\x83", + "\xC3\xB6" => "\x6F\xCC\x88", + "\xC3\xB9" => "\x75\xCC\x80", + "\xC3\xBA" => "\x75\xCC\x81", + "\xC3\xBB" => "\x75\xCC\x82", + "\xC3\xBC" => "\x75\xCC\x88", + "\xC3\xBD" => "\x79\xCC\x81", + "\xC3\xBF" => "\x79\xCC\x88", + "\xC4\x80" => "\x41\xCC\x84", + "\xC4\x81" => "\x61\xCC\x84", + "\xC4\x82" => "\x41\xCC\x86", + "\xC4\x83" => "\x61\xCC\x86", + "\xC4\x84" => "\x41\xCC\xA8", + "\xC4\x85" => "\x61\xCC\xA8", + "\xC4\x86" => "\x43\xCC\x81", + "\xC4\x87" => "\x63\xCC\x81", + "\xC4\x88" => "\x43\xCC\x82", + "\xC4\x89" => "\x63\xCC\x82", + "\xC4\x8A" => "\x43\xCC\x87", + "\xC4\x8B" => "\x63\xCC\x87", + "\xC4\x8C" => "\x43\xCC\x8C", + "\xC4\x8D" => "\x63\xCC\x8C", + "\xC4\x8E" => "\x44\xCC\x8C", + "\xC4\x8F" => "\x64\xCC\x8C", + "\xC4\x92" => "\x45\xCC\x84", + "\xC4\x93" => "\x65\xCC\x84", + "\xC4\x94" => "\x45\xCC\x86", + "\xC4\x95" => "\x65\xCC\x86", + "\xC4\x96" => "\x45\xCC\x87", + "\xC4\x97" => "\x65\xCC\x87", + "\xC4\x98" => "\x45\xCC\xA8", + "\xC4\x99" => "\x65\xCC\xA8", + "\xC4\x9A" => "\x45\xCC\x8C", + "\xC4\x9B" => "\x65\xCC\x8C", + "\xC4\x9C" => "\x47\xCC\x82", + "\xC4\x9D" => "\x67\xCC\x82", + "\xC4\x9E" => "\x47\xCC\x86", + "\xC4\x9F" => "\x67\xCC\x86", + "\xC4\xA0" => "\x47\xCC\x87", + "\xC4\xA1" => "\x67\xCC\x87", + "\xC4\xA2" => "\x47\xCC\xA7", + "\xC4\xA3" => "\x67\xCC\xA7", + "\xC4\xA4" => "\x48\xCC\x82", + "\xC4\xA5" => "\x68\xCC\x82", + "\xC4\xA8" => "\x49\xCC\x83", + "\xC4\xA9" => "\x69\xCC\x83", + "\xC4\xAA" => "\x49\xCC\x84", + "\xC4\xAB" => "\x69\xCC\x84", + "\xC4\xAC" => "\x49\xCC\x86", + "\xC4\xAD" => "\x69\xCC\x86", + "\xC4\xAE" => "\x49\xCC\xA8", + "\xC4\xAF" => "\x69\xCC\xA8", + "\xC4\xB0" => "\x49\xCC\x87", + "\xC4\xB4" => "\x4A\xCC\x82", + "\xC4\xB5" => "\x6A\xCC\x82", + "\xC4\xB6" => "\x4B\xCC\xA7", + "\xC4\xB7" => "\x6B\xCC\xA7", + "\xC4\xB9" => "\x4C\xCC\x81", + "\xC4\xBA" => "\x6C\xCC\x81", + "\xC4\xBB" => "\x4C\xCC\xA7", + "\xC4\xBC" => "\x6C\xCC\xA7", + "\xC4\xBD" => "\x4C\xCC\x8C", + "\xC4\xBE" => "\x6C\xCC\x8C", + "\xC5\x83" => "\x4E\xCC\x81", + "\xC5\x84" => "\x6E\xCC\x81", + "\xC5\x85" => "\x4E\xCC\xA7", + "\xC5\x86" => "\x6E\xCC\xA7", + "\xC5\x87" => "\x4E\xCC\x8C", + "\xC5\x88" => "\x6E\xCC\x8C", + "\xC5\x8C" => "\x4F\xCC\x84", + "\xC5\x8D" => "\x6F\xCC\x84", + "\xC5\x8E" => "\x4F\xCC\x86", + "\xC5\x8F" => "\x6F\xCC\x86", + "\xC5\x90" => "\x4F\xCC\x8B", + "\xC5\x91" => "\x6F\xCC\x8B", + "\xC5\x94" => "\x52\xCC\x81", + "\xC5\x95" => "\x72\xCC\x81", + "\xC5\x96" => "\x52\xCC\xA7", + "\xC5\x97" => "\x72\xCC\xA7", + "\xC5\x98" => "\x52\xCC\x8C", + "\xC5\x99" => "\x72\xCC\x8C", + "\xC5\x9A" => "\x53\xCC\x81", + "\xC5\x9B" => "\x73\xCC\x81", + "\xC5\x9C" => "\x53\xCC\x82", + "\xC5\x9D" => "\x73\xCC\x82", + "\xC5\x9E" => "\x53\xCC\xA7", + "\xC5\x9F" => "\x73\xCC\xA7", + "\xC5\xA0" => "\x53\xCC\x8C", + "\xC5\xA1" => "\x73\xCC\x8C", + "\xC5\xA2" => "\x54\xCC\xA7", + "\xC5\xA3" => "\x74\xCC\xA7", + "\xC5\xA4" => "\x54\xCC\x8C", + "\xC5\xA5" => "\x74\xCC\x8C", + "\xC5\xA8" => "\x55\xCC\x83", + "\xC5\xA9" => "\x75\xCC\x83", + "\xC5\xAA" => "\x55\xCC\x84", + "\xC5\xAB" => "\x75\xCC\x84", + "\xC5\xAC" => "\x55\xCC\x86", + "\xC5\xAD" => "\x75\xCC\x86", + "\xC5\xAE" => "\x55\xCC\x8A", + "\xC5\xAF" => "\x75\xCC\x8A", + "\xC5\xB0" => "\x55\xCC\x8B", + "\xC5\xB1" => "\x75\xCC\x8B", + "\xC5\xB2" => "\x55\xCC\xA8", + "\xC5\xB3" => "\x75\xCC\xA8", + "\xC5\xB4" => "\x57\xCC\x82", + "\xC5\xB5" => "\x77\xCC\x82", + "\xC5\xB6" => "\x59\xCC\x82", + "\xC5\xB7" => "\x79\xCC\x82", + "\xC5\xB8" => "\x59\xCC\x88", + "\xC5\xB9" => "\x5A\xCC\x81", + "\xC5\xBA" => "\x7A\xCC\x81", + "\xC5\xBB" => "\x5A\xCC\x87", + "\xC5\xBC" => "\x7A\xCC\x87", + "\xC5\xBD" => "\x5A\xCC\x8C", + "\xC5\xBE" => "\x7A\xCC\x8C", + "\xC6\xA0" => "\x4F\xCC\x9B", + "\xC6\xA1" => "\x6F\xCC\x9B", + "\xC6\xAF" => "\x55\xCC\x9B", + "\xC6\xB0" => "\x75\xCC\x9B", + "\xC7\x8D" => "\x41\xCC\x8C", + "\xC7\x8E" => "\x61\xCC\x8C", + "\xC7\x8F" => "\x49\xCC\x8C", + "\xC7\x90" => "\x69\xCC\x8C", + "\xC7\x91" => "\x4F\xCC\x8C", + "\xC7\x92" => "\x6F\xCC\x8C", + "\xC7\x93" => "\x55\xCC\x8C", + "\xC7\x94" => "\x75\xCC\x8C", + "\xC7\x95" => "\x55\xCC\x88\xCC\x84", + "\xC7\x96" => "\x75\xCC\x88\xCC\x84", + "\xC7\x97" => "\x55\xCC\x88\xCC\x81", + "\xC7\x98" => "\x75\xCC\x88\xCC\x81", + "\xC7\x99" => "\x55\xCC\x88\xCC\x8C", + "\xC7\x9A" => "\x75\xCC\x88\xCC\x8C", + "\xC7\x9B" => "\x55\xCC\x88\xCC\x80", + "\xC7\x9C" => "\x75\xCC\x88\xCC\x80", + "\xC7\x9E" => "\x41\xCC\x88\xCC\x84", + "\xC7\x9F" => "\x61\xCC\x88\xCC\x84", + "\xC7\xA0" => "\x41\xCC\x87\xCC\x84", + "\xC7\xA1" => "\x61\xCC\x87\xCC\x84", + "\xC7\xA2" => "\xC3\x86\xCC\x84", + "\xC7\xA3" => "\xC3\xA6\xCC\x84", + "\xC7\xA6" => "\x47\xCC\x8C", + "\xC7\xA7" => "\x67\xCC\x8C", + "\xC7\xA8" => "\x4B\xCC\x8C", + "\xC7\xA9" => "\x6B\xCC\x8C", + "\xC7\xAA" => "\x4F\xCC\xA8", + "\xC7\xAB" => "\x6F\xCC\xA8", + "\xC7\xAC" => "\x4F\xCC\xA8\xCC\x84", + "\xC7\xAD" => "\x6F\xCC\xA8\xCC\x84", + "\xC7\xAE" => "\xC6\xB7\xCC\x8C", + "\xC7\xAF" => "\xCA\x92\xCC\x8C", + "\xC7\xB0" => "\x6A\xCC\x8C", + "\xC7\xB4" => "\x47\xCC\x81", + "\xC7\xB5" => "\x67\xCC\x81", + "\xC7\xB8" => "\x4E\xCC\x80", + "\xC7\xB9" => "\x6E\xCC\x80", + "\xC7\xBA" => "\x41\xCC\x8A\xCC\x81", + "\xC7\xBB" => "\x61\xCC\x8A\xCC\x81", + "\xC7\xBC" => "\xC3\x86\xCC\x81", + "\xC7\xBD" => "\xC3\xA6\xCC\x81", + "\xC7\xBE" => "\xC3\x98\xCC\x81", + "\xC7\xBF" => "\xC3\xB8\xCC\x81", + "\xC8\x80" => "\x41\xCC\x8F", + "\xC8\x81" => "\x61\xCC\x8F", + "\xC8\x82" => "\x41\xCC\x91", + "\xC8\x83" => "\x61\xCC\x91", + "\xC8\x84" => "\x45\xCC\x8F", + "\xC8\x85" => "\x65\xCC\x8F", + "\xC8\x86" => "\x45\xCC\x91", + "\xC8\x87" => "\x65\xCC\x91", + "\xC8\x88" => "\x49\xCC\x8F", + "\xC8\x89" => "\x69\xCC\x8F", + "\xC8\x8A" => "\x49\xCC\x91", + "\xC8\x8B" => "\x69\xCC\x91", + "\xC8\x8C" => "\x4F\xCC\x8F", + "\xC8\x8D" => "\x6F\xCC\x8F", + "\xC8\x8E" => "\x4F\xCC\x91", + "\xC8\x8F" => "\x6F\xCC\x91", + "\xC8\x90" => "\x52\xCC\x8F", + "\xC8\x91" => "\x72\xCC\x8F", + "\xC8\x92" => "\x52\xCC\x91", + "\xC8\x93" => "\x72\xCC\x91", + "\xC8\x94" => "\x55\xCC\x8F", + "\xC8\x95" => "\x75\xCC\x8F", + "\xC8\x96" => "\x55\xCC\x91", + "\xC8\x97" => "\x75\xCC\x91", + "\xC8\x98" => "\x53\xCC\xA6", + "\xC8\x99" => "\x73\xCC\xA6", + "\xC8\x9A" => "\x54\xCC\xA6", + "\xC8\x9B" => "\x74\xCC\xA6", + "\xC8\x9E" => "\x48\xCC\x8C", + "\xC8\x9F" => "\x68\xCC\x8C", + "\xC8\xA6" => "\x41\xCC\x87", + "\xC8\xA7" => "\x61\xCC\x87", + "\xC8\xA8" => "\x45\xCC\xA7", + "\xC8\xA9" => "\x65\xCC\xA7", + "\xC8\xAA" => "\x4F\xCC\x88\xCC\x84", + "\xC8\xAB" => "\x6F\xCC\x88\xCC\x84", + "\xC8\xAC" => "\x4F\xCC\x83\xCC\x84", + "\xC8\xAD" => "\x6F\xCC\x83\xCC\x84", + "\xC8\xAE" => "\x4F\xCC\x87", + "\xC8\xAF" => "\x6F\xCC\x87", + "\xC8\xB0" => "\x4F\xCC\x87\xCC\x84", + "\xC8\xB1" => "\x6F\xCC\x87\xCC\x84", + "\xC8\xB2" => "\x59\xCC\x84", + "\xC8\xB3" => "\x79\xCC\x84", + "\xCD\x80" => "\xCC\x80", + "\xCD\x81" => "\xCC\x81", + "\xCD\x83" => "\xCC\x93", + "\xCD\x84" => "\xCC\x88\xCC\x81", + "\xCD\xB4" => "\xCA\xB9", + "\xCD\xBE" => "\x3B", + "\xCE\x85" => "\xC2\xA8\xCC\x81", + "\xCE\x86" => "\xCE\x91\xCC\x81", + "\xCE\x87" => "\xC2\xB7", + "\xCE\x88" => "\xCE\x95\xCC\x81", + "\xCE\x89" => "\xCE\x97\xCC\x81", + "\xCE\x8A" => "\xCE\x99\xCC\x81", + "\xCE\x8C" => "\xCE\x9F\xCC\x81", + "\xCE\x8E" => "\xCE\xA5\xCC\x81", + "\xCE\x8F" => "\xCE\xA9\xCC\x81", + "\xCE\x90" => "\xCE\xB9\xCC\x88\xCC\x81", + "\xCE\xAA" => "\xCE\x99\xCC\x88", + "\xCE\xAB" => "\xCE\xA5\xCC\x88", + "\xCE\xAC" => "\xCE\xB1\xCC\x81", + "\xCE\xAD" => "\xCE\xB5\xCC\x81", + "\xCE\xAE" => "\xCE\xB7\xCC\x81", + "\xCE\xAF" => "\xCE\xB9\xCC\x81", + "\xCE\xB0" => "\xCF\x85\xCC\x88\xCC\x81", + "\xCF\x8A" => "\xCE\xB9\xCC\x88", + "\xCF\x8B" => "\xCF\x85\xCC\x88", + "\xCF\x8C" => "\xCE\xBF\xCC\x81", + "\xCF\x8D" => "\xCF\x85\xCC\x81", + "\xCF\x8E" => "\xCF\x89\xCC\x81", + "\xCF\x93" => "\xCF\x92\xCC\x81", + "\xCF\x94" => "\xCF\x92\xCC\x88", + "\xD0\x80" => "\xD0\x95\xCC\x80", + "\xD0\x81" => "\xD0\x95\xCC\x88", + "\xD0\x83" => "\xD0\x93\xCC\x81", + "\xD0\x87" => "\xD0\x86\xCC\x88", + "\xD0\x8C" => "\xD0\x9A\xCC\x81", + "\xD0\x8D" => "\xD0\x98\xCC\x80", + "\xD0\x8E" => "\xD0\xA3\xCC\x86", + "\xD0\x99" => "\xD0\x98\xCC\x86", + "\xD0\xB9" => "\xD0\xB8\xCC\x86", + "\xD1\x90" => "\xD0\xB5\xCC\x80", + "\xD1\x91" => "\xD0\xB5\xCC\x88", + "\xD1\x93" => "\xD0\xB3\xCC\x81", + "\xD1\x97" => "\xD1\x96\xCC\x88", + "\xD1\x9C" => "\xD0\xBA\xCC\x81", + "\xD1\x9D" => "\xD0\xB8\xCC\x80", + "\xD1\x9E" => "\xD1\x83\xCC\x86", + "\xD1\xB6" => "\xD1\xB4\xCC\x8F", + "\xD1\xB7" => "\xD1\xB5\xCC\x8F", + "\xD3\x81" => "\xD0\x96\xCC\x86", + "\xD3\x82" => "\xD0\xB6\xCC\x86", + "\xD3\x90" => "\xD0\x90\xCC\x86", + "\xD3\x91" => "\xD0\xB0\xCC\x86", + "\xD3\x92" => "\xD0\x90\xCC\x88", + "\xD3\x93" => "\xD0\xB0\xCC\x88", + "\xD3\x96" => "\xD0\x95\xCC\x86", + "\xD3\x97" => "\xD0\xB5\xCC\x86", + "\xD3\x9A" => "\xD3\x98\xCC\x88", + "\xD3\x9B" => "\xD3\x99\xCC\x88", + "\xD3\x9C" => "\xD0\x96\xCC\x88", + "\xD3\x9D" => "\xD0\xB6\xCC\x88", + "\xD3\x9E" => "\xD0\x97\xCC\x88", + "\xD3\x9F" => "\xD0\xB7\xCC\x88", + "\xD3\xA2" => "\xD0\x98\xCC\x84", + "\xD3\xA3" => "\xD0\xB8\xCC\x84", + "\xD3\xA4" => "\xD0\x98\xCC\x88", + "\xD3\xA5" => "\xD0\xB8\xCC\x88", + "\xD3\xA6" => "\xD0\x9E\xCC\x88", + "\xD3\xA7" => "\xD0\xBE\xCC\x88", + "\xD3\xAA" => "\xD3\xA8\xCC\x88", + "\xD3\xAB" => "\xD3\xA9\xCC\x88", + "\xD3\xAC" => "\xD0\xAD\xCC\x88", + "\xD3\xAD" => "\xD1\x8D\xCC\x88", + "\xD3\xAE" => "\xD0\xA3\xCC\x84", + "\xD3\xAF" => "\xD1\x83\xCC\x84", + "\xD3\xB0" => "\xD0\xA3\xCC\x88", + "\xD3\xB1" => "\xD1\x83\xCC\x88", + "\xD3\xB2" => "\xD0\xA3\xCC\x8B", + "\xD3\xB3" => "\xD1\x83\xCC\x8B", + "\xD3\xB4" => "\xD0\xA7\xCC\x88", + "\xD3\xB5" => "\xD1\x87\xCC\x88", + "\xD3\xB8" => "\xD0\xAB\xCC\x88", + "\xD3\xB9" => "\xD1\x8B\xCC\x88", + "\xD8\xA2" => "\xD8\xA7\xD9\x93", + "\xD8\xA3" => "\xD8\xA7\xD9\x94", + "\xD8\xA4" => "\xD9\x88\xD9\x94", + "\xD8\xA5" => "\xD8\xA7\xD9\x95", + "\xD8\xA6" => "\xD9\x8A\xD9\x94", + "\xDB\x80" => "\xDB\x95\xD9\x94", + "\xDB\x82" => "\xDB\x81\xD9\x94", + "\xDB\x93" => "\xDB\x92\xD9\x94", + "\xE0\xA4\xA9" => "\xE0\xA4\xA8\xE0\xA4\xBC", + "\xE0\xA4\xB1" => "\xE0\xA4\xB0\xE0\xA4\xBC", + "\xE0\xA4\xB4" => "\xE0\xA4\xB3\xE0\xA4\xBC", + "\xE0\xA5\x98" => "\xE0\xA4\x95\xE0\xA4\xBC", + "\xE0\xA5\x99" => "\xE0\xA4\x96\xE0\xA4\xBC", + "\xE0\xA5\x9A" => "\xE0\xA4\x97\xE0\xA4\xBC", + "\xE0\xA5\x9B" => "\xE0\xA4\x9C\xE0\xA4\xBC", + "\xE0\xA5\x9C" => "\xE0\xA4\xA1\xE0\xA4\xBC", + "\xE0\xA5\x9D" => "\xE0\xA4\xA2\xE0\xA4\xBC", + "\xE0\xA5\x9E" => "\xE0\xA4\xAB\xE0\xA4\xBC", + "\xE0\xA5\x9F" => "\xE0\xA4\xAF\xE0\xA4\xBC", + "\xE0\xA7\x8B" => "\xE0\xA7\x87\xE0\xA6\xBE", + "\xE0\xA7\x8C" => "\xE0\xA7\x87\xE0\xA7\x97", + "\xE0\xA7\x9C" => "\xE0\xA6\xA1\xE0\xA6\xBC", + "\xE0\xA7\x9D" => "\xE0\xA6\xA2\xE0\xA6\xBC", + "\xE0\xA7\x9F" => "\xE0\xA6\xAF\xE0\xA6\xBC", + "\xE0\xA8\xB3" => "\xE0\xA8\xB2\xE0\xA8\xBC", + "\xE0\xA8\xB6" => "\xE0\xA8\xB8\xE0\xA8\xBC", + "\xE0\xA9\x99" => "\xE0\xA8\x96\xE0\xA8\xBC", + "\xE0\xA9\x9A" => "\xE0\xA8\x97\xE0\xA8\xBC", + "\xE0\xA9\x9B" => "\xE0\xA8\x9C\xE0\xA8\xBC", + "\xE0\xA9\x9E" => "\xE0\xA8\xAB\xE0\xA8\xBC", + "\xE0\xAD\x88" => "\xE0\xAD\x87\xE0\xAD\x96", + "\xE0\xAD\x8B" => "\xE0\xAD\x87\xE0\xAC\xBE", + "\xE0\xAD\x8C" => "\xE0\xAD\x87\xE0\xAD\x97", + "\xE0\xAD\x9C" => "\xE0\xAC\xA1\xE0\xAC\xBC", + "\xE0\xAD\x9D" => "\xE0\xAC\xA2\xE0\xAC\xBC", + "\xE0\xAE\x94" => "\xE0\xAE\x92\xE0\xAF\x97", + "\xE0\xAF\x8A" => "\xE0\xAF\x86\xE0\xAE\xBE", + "\xE0\xAF\x8B" => "\xE0\xAF\x87\xE0\xAE\xBE", + "\xE0\xAF\x8C" => "\xE0\xAF\x86\xE0\xAF\x97", + "\xE0\xB1\x88" => "\xE0\xB1\x86\xE0\xB1\x96", + "\xE0\xB3\x80" => "\xE0\xB2\xBF\xE0\xB3\x95", + "\xE0\xB3\x87" => "\xE0\xB3\x86\xE0\xB3\x95", + "\xE0\xB3\x88" => "\xE0\xB3\x86\xE0\xB3\x96", + "\xE0\xB3\x8A" => "\xE0\xB3\x86\xE0\xB3\x82", + "\xE0\xB3\x8B" => "\xE0\xB3\x86\xE0\xB3\x82\xE0\xB3\x95", + "\xE0\xB5\x8A" => "\xE0\xB5\x86\xE0\xB4\xBE", + "\xE0\xB5\x8B" => "\xE0\xB5\x87\xE0\xB4\xBE", + "\xE0\xB5\x8C" => "\xE0\xB5\x86\xE0\xB5\x97", + "\xE0\xB7\x9A" => "\xE0\xB7\x99\xE0\xB7\x8A", + "\xE0\xB7\x9C" => "\xE0\xB7\x99\xE0\xB7\x8F", + "\xE0\xB7\x9D" => "\xE0\xB7\x99\xE0\xB7\x8F\xE0\xB7\x8A", + "\xE0\xB7\x9E" => "\xE0\xB7\x99\xE0\xB7\x9F", + "\xE0\xBD\x83" => "\xE0\xBD\x82\xE0\xBE\xB7", + "\xE0\xBD\x8D" => "\xE0\xBD\x8C\xE0\xBE\xB7", + "\xE0\xBD\x92" => "\xE0\xBD\x91\xE0\xBE\xB7", + "\xE0\xBD\x97" => "\xE0\xBD\x96\xE0\xBE\xB7", + "\xE0\xBD\x9C" => "\xE0\xBD\x9B\xE0\xBE\xB7", + "\xE0\xBD\xA9" => "\xE0\xBD\x80\xE0\xBE\xB5", + "\xE0\xBD\xB3" => "\xE0\xBD\xB1\xE0\xBD\xB2", + "\xE0\xBD\xB5" => "\xE0\xBD\xB1\xE0\xBD\xB4", + "\xE0\xBD\xB6" => "\xE0\xBE\xB2\xE0\xBE\x80", + "\xE0\xBD\xB8" => "\xE0\xBE\xB3\xE0\xBE\x80", + "\xE0\xBE\x81" => "\xE0\xBD\xB1\xE0\xBE\x80", + "\xE0\xBE\x93" => "\xE0\xBE\x92\xE0\xBE\xB7", + "\xE0\xBE\x9D" => "\xE0\xBE\x9C\xE0\xBE\xB7", + "\xE0\xBE\xA2" => "\xE0\xBE\xA1\xE0\xBE\xB7", + "\xE0\xBE\xA7" => "\xE0\xBE\xA6\xE0\xBE\xB7", + "\xE0\xBE\xAC" => "\xE0\xBE\xAB\xE0\xBE\xB7", + "\xE0\xBE\xB9" => "\xE0\xBE\x90\xE0\xBE\xB5", + "\xE1\x80\xA6" => "\xE1\x80\xA5\xE1\x80\xAE", + "\xE1\xAC\x86" => "\xE1\xAC\x85\xE1\xAC\xB5", + "\xE1\xAC\x88" => "\xE1\xAC\x87\xE1\xAC\xB5", + "\xE1\xAC\x8A" => "\xE1\xAC\x89\xE1\xAC\xB5", + "\xE1\xAC\x8C" => "\xE1\xAC\x8B\xE1\xAC\xB5", + "\xE1\xAC\x8E" => "\xE1\xAC\x8D\xE1\xAC\xB5", + "\xE1\xAC\x92" => "\xE1\xAC\x91\xE1\xAC\xB5", + "\xE1\xAC\xBB" => "\xE1\xAC\xBA\xE1\xAC\xB5", + "\xE1\xAC\xBD" => "\xE1\xAC\xBC\xE1\xAC\xB5", + "\xE1\xAD\x80" => "\xE1\xAC\xBE\xE1\xAC\xB5", + "\xE1\xAD\x81" => "\xE1\xAC\xBF\xE1\xAC\xB5", + "\xE1\xAD\x83" => "\xE1\xAD\x82\xE1\xAC\xB5", + "\xE1\xB8\x80" => "\x41\xCC\xA5", + "\xE1\xB8\x81" => "\x61\xCC\xA5", + "\xE1\xB8\x82" => "\x42\xCC\x87", + "\xE1\xB8\x83" => "\x62\xCC\x87", + "\xE1\xB8\x84" => "\x42\xCC\xA3", + "\xE1\xB8\x85" => "\x62\xCC\xA3", + "\xE1\xB8\x86" => "\x42\xCC\xB1", + "\xE1\xB8\x87" => "\x62\xCC\xB1", + "\xE1\xB8\x88" => "\x43\xCC\xA7\xCC\x81", + "\xE1\xB8\x89" => "\x63\xCC\xA7\xCC\x81", + "\xE1\xB8\x8A" => "\x44\xCC\x87", + "\xE1\xB8\x8B" => "\x64\xCC\x87", + "\xE1\xB8\x8C" => "\x44\xCC\xA3", + "\xE1\xB8\x8D" => "\x64\xCC\xA3", + "\xE1\xB8\x8E" => "\x44\xCC\xB1", + "\xE1\xB8\x8F" => "\x64\xCC\xB1", + "\xE1\xB8\x90" => "\x44\xCC\xA7", + "\xE1\xB8\x91" => "\x64\xCC\xA7", + "\xE1\xB8\x92" => "\x44\xCC\xAD", + "\xE1\xB8\x93" => "\x64\xCC\xAD", + "\xE1\xB8\x94" => "\x45\xCC\x84\xCC\x80", + "\xE1\xB8\x95" => "\x65\xCC\x84\xCC\x80", + "\xE1\xB8\x96" => "\x45\xCC\x84\xCC\x81", + "\xE1\xB8\x97" => "\x65\xCC\x84\xCC\x81", + "\xE1\xB8\x98" => "\x45\xCC\xAD", + "\xE1\xB8\x99" => "\x65\xCC\xAD", + "\xE1\xB8\x9A" => "\x45\xCC\xB0", + "\xE1\xB8\x9B" => "\x65\xCC\xB0", + "\xE1\xB8\x9C" => "\x45\xCC\xA7\xCC\x86", + "\xE1\xB8\x9D" => "\x65\xCC\xA7\xCC\x86", + "\xE1\xB8\x9E" => "\x46\xCC\x87", + "\xE1\xB8\x9F" => "\x66\xCC\x87", + "\xE1\xB8\xA0" => "\x47\xCC\x84", + "\xE1\xB8\xA1" => "\x67\xCC\x84", + "\xE1\xB8\xA2" => "\x48\xCC\x87", + "\xE1\xB8\xA3" => "\x68\xCC\x87", + "\xE1\xB8\xA4" => "\x48\xCC\xA3", + "\xE1\xB8\xA5" => "\x68\xCC\xA3", + "\xE1\xB8\xA6" => "\x48\xCC\x88", + "\xE1\xB8\xA7" => "\x68\xCC\x88", + "\xE1\xB8\xA8" => "\x48\xCC\xA7", + "\xE1\xB8\xA9" => "\x68\xCC\xA7", + "\xE1\xB8\xAA" => "\x48\xCC\xAE", + "\xE1\xB8\xAB" => "\x68\xCC\xAE", + "\xE1\xB8\xAC" => "\x49\xCC\xB0", + "\xE1\xB8\xAD" => "\x69\xCC\xB0", + "\xE1\xB8\xAE" => "\x49\xCC\x88\xCC\x81", + "\xE1\xB8\xAF" => "\x69\xCC\x88\xCC\x81", + "\xE1\xB8\xB0" => "\x4B\xCC\x81", + "\xE1\xB8\xB1" => "\x6B\xCC\x81", + "\xE1\xB8\xB2" => "\x4B\xCC\xA3", + "\xE1\xB8\xB3" => "\x6B\xCC\xA3", + "\xE1\xB8\xB4" => "\x4B\xCC\xB1", + "\xE1\xB8\xB5" => "\x6B\xCC\xB1", + "\xE1\xB8\xB6" => "\x4C\xCC\xA3", + "\xE1\xB8\xB7" => "\x6C\xCC\xA3", + "\xE1\xB8\xB8" => "\x4C\xCC\xA3\xCC\x84", + "\xE1\xB8\xB9" => "\x6C\xCC\xA3\xCC\x84", + "\xE1\xB8\xBA" => "\x4C\xCC\xB1", + "\xE1\xB8\xBB" => "\x6C\xCC\xB1", + "\xE1\xB8\xBC" => "\x4C\xCC\xAD", + "\xE1\xB8\xBD" => "\x6C\xCC\xAD", + "\xE1\xB8\xBE" => "\x4D\xCC\x81", + "\xE1\xB8\xBF" => "\x6D\xCC\x81", + "\xE1\xB9\x80" => "\x4D\xCC\x87", + "\xE1\xB9\x81" => "\x6D\xCC\x87", + "\xE1\xB9\x82" => "\x4D\xCC\xA3", + "\xE1\xB9\x83" => "\x6D\xCC\xA3", + "\xE1\xB9\x84" => "\x4E\xCC\x87", + "\xE1\xB9\x85" => "\x6E\xCC\x87", + "\xE1\xB9\x86" => "\x4E\xCC\xA3", + "\xE1\xB9\x87" => "\x6E\xCC\xA3", + "\xE1\xB9\x88" => "\x4E\xCC\xB1", + "\xE1\xB9\x89" => "\x6E\xCC\xB1", + "\xE1\xB9\x8A" => "\x4E\xCC\xAD", + "\xE1\xB9\x8B" => "\x6E\xCC\xAD", + "\xE1\xB9\x8C" => "\x4F\xCC\x83\xCC\x81", + "\xE1\xB9\x8D" => "\x6F\xCC\x83\xCC\x81", + "\xE1\xB9\x8E" => "\x4F\xCC\x83\xCC\x88", + "\xE1\xB9\x8F" => "\x6F\xCC\x83\xCC\x88", + "\xE1\xB9\x90" => "\x4F\xCC\x84\xCC\x80", + "\xE1\xB9\x91" => "\x6F\xCC\x84\xCC\x80", + "\xE1\xB9\x92" => "\x4F\xCC\x84\xCC\x81", + "\xE1\xB9\x93" => "\x6F\xCC\x84\xCC\x81", + "\xE1\xB9\x94" => "\x50\xCC\x81", + "\xE1\xB9\x95" => "\x70\xCC\x81", + "\xE1\xB9\x96" => "\x50\xCC\x87", + "\xE1\xB9\x97" => "\x70\xCC\x87", + "\xE1\xB9\x98" => "\x52\xCC\x87", + "\xE1\xB9\x99" => "\x72\xCC\x87", + "\xE1\xB9\x9A" => "\x52\xCC\xA3", + "\xE1\xB9\x9B" => "\x72\xCC\xA3", + "\xE1\xB9\x9C" => "\x52\xCC\xA3\xCC\x84", + "\xE1\xB9\x9D" => "\x72\xCC\xA3\xCC\x84", + "\xE1\xB9\x9E" => "\x52\xCC\xB1", + "\xE1\xB9\x9F" => "\x72\xCC\xB1", + "\xE1\xB9\xA0" => "\x53\xCC\x87", + "\xE1\xB9\xA1" => "\x73\xCC\x87", + "\xE1\xB9\xA2" => "\x53\xCC\xA3", + "\xE1\xB9\xA3" => "\x73\xCC\xA3", + "\xE1\xB9\xA4" => "\x53\xCC\x81\xCC\x87", + "\xE1\xB9\xA5" => "\x73\xCC\x81\xCC\x87", + "\xE1\xB9\xA6" => "\x53\xCC\x8C\xCC\x87", + "\xE1\xB9\xA7" => "\x73\xCC\x8C\xCC\x87", + "\xE1\xB9\xA8" => "\x53\xCC\xA3\xCC\x87", + "\xE1\xB9\xA9" => "\x73\xCC\xA3\xCC\x87", + "\xE1\xB9\xAA" => "\x54\xCC\x87", + "\xE1\xB9\xAB" => "\x74\xCC\x87", + "\xE1\xB9\xAC" => "\x54\xCC\xA3", + "\xE1\xB9\xAD" => "\x74\xCC\xA3", + "\xE1\xB9\xAE" => "\x54\xCC\xB1", + "\xE1\xB9\xAF" => "\x74\xCC\xB1", + "\xE1\xB9\xB0" => "\x54\xCC\xAD", + "\xE1\xB9\xB1" => "\x74\xCC\xAD", + "\xE1\xB9\xB2" => "\x55\xCC\xA4", + "\xE1\xB9\xB3" => "\x75\xCC\xA4", + "\xE1\xB9\xB4" => "\x55\xCC\xB0", + "\xE1\xB9\xB5" => "\x75\xCC\xB0", + "\xE1\xB9\xB6" => "\x55\xCC\xAD", + "\xE1\xB9\xB7" => "\x75\xCC\xAD", + "\xE1\xB9\xB8" => "\x55\xCC\x83\xCC\x81", + "\xE1\xB9\xB9" => "\x75\xCC\x83\xCC\x81", + "\xE1\xB9\xBA" => "\x55\xCC\x84\xCC\x88", + "\xE1\xB9\xBB" => "\x75\xCC\x84\xCC\x88", + "\xE1\xB9\xBC" => "\x56\xCC\x83", + "\xE1\xB9\xBD" => "\x76\xCC\x83", + "\xE1\xB9\xBE" => "\x56\xCC\xA3", + "\xE1\xB9\xBF" => "\x76\xCC\xA3", + "\xE1\xBA\x80" => "\x57\xCC\x80", + "\xE1\xBA\x81" => "\x77\xCC\x80", + "\xE1\xBA\x82" => "\x57\xCC\x81", + "\xE1\xBA\x83" => "\x77\xCC\x81", + "\xE1\xBA\x84" => "\x57\xCC\x88", + "\xE1\xBA\x85" => "\x77\xCC\x88", + "\xE1\xBA\x86" => "\x57\xCC\x87", + "\xE1\xBA\x87" => "\x77\xCC\x87", + "\xE1\xBA\x88" => "\x57\xCC\xA3", + "\xE1\xBA\x89" => "\x77\xCC\xA3", + "\xE1\xBA\x8A" => "\x58\xCC\x87", + "\xE1\xBA\x8B" => "\x78\xCC\x87", + "\xE1\xBA\x8C" => "\x58\xCC\x88", + "\xE1\xBA\x8D" => "\x78\xCC\x88", + "\xE1\xBA\x8E" => "\x59\xCC\x87", + "\xE1\xBA\x8F" => "\x79\xCC\x87", + "\xE1\xBA\x90" => "\x5A\xCC\x82", + "\xE1\xBA\x91" => "\x7A\xCC\x82", + "\xE1\xBA\x92" => "\x5A\xCC\xA3", + "\xE1\xBA\x93" => "\x7A\xCC\xA3", + "\xE1\xBA\x94" => "\x5A\xCC\xB1", + "\xE1\xBA\x95" => "\x7A\xCC\xB1", + "\xE1\xBA\x96" => "\x68\xCC\xB1", + "\xE1\xBA\x97" => "\x74\xCC\x88", + "\xE1\xBA\x98" => "\x77\xCC\x8A", + "\xE1\xBA\x99" => "\x79\xCC\x8A", + "\xE1\xBA\x9B" => "\xC5\xBF\xCC\x87", + "\xE1\xBA\xA0" => "\x41\xCC\xA3", + "\xE1\xBA\xA1" => "\x61\xCC\xA3", + "\xE1\xBA\xA2" => "\x41\xCC\x89", + "\xE1\xBA\xA3" => "\x61\xCC\x89", + "\xE1\xBA\xA4" => "\x41\xCC\x82\xCC\x81", + "\xE1\xBA\xA5" => "\x61\xCC\x82\xCC\x81", + "\xE1\xBA\xA6" => "\x41\xCC\x82\xCC\x80", + "\xE1\xBA\xA7" => "\x61\xCC\x82\xCC\x80", + "\xE1\xBA\xA8" => "\x41\xCC\x82\xCC\x89", + "\xE1\xBA\xA9" => "\x61\xCC\x82\xCC\x89", + "\xE1\xBA\xAA" => "\x41\xCC\x82\xCC\x83", + "\xE1\xBA\xAB" => "\x61\xCC\x82\xCC\x83", + "\xE1\xBA\xAC" => "\x41\xCC\xA3\xCC\x82", + "\xE1\xBA\xAD" => "\x61\xCC\xA3\xCC\x82", + "\xE1\xBA\xAE" => "\x41\xCC\x86\xCC\x81", + "\xE1\xBA\xAF" => "\x61\xCC\x86\xCC\x81", + "\xE1\xBA\xB0" => "\x41\xCC\x86\xCC\x80", + "\xE1\xBA\xB1" => "\x61\xCC\x86\xCC\x80", + "\xE1\xBA\xB2" => "\x41\xCC\x86\xCC\x89", + "\xE1\xBA\xB3" => "\x61\xCC\x86\xCC\x89", + "\xE1\xBA\xB4" => "\x41\xCC\x86\xCC\x83", + "\xE1\xBA\xB5" => "\x61\xCC\x86\xCC\x83", + "\xE1\xBA\xB6" => "\x41\xCC\xA3\xCC\x86", + "\xE1\xBA\xB7" => "\x61\xCC\xA3\xCC\x86", + "\xE1\xBA\xB8" => "\x45\xCC\xA3", + "\xE1\xBA\xB9" => "\x65\xCC\xA3", + "\xE1\xBA\xBA" => "\x45\xCC\x89", + "\xE1\xBA\xBB" => "\x65\xCC\x89", + "\xE1\xBA\xBC" => "\x45\xCC\x83", + "\xE1\xBA\xBD" => "\x65\xCC\x83", + "\xE1\xBA\xBE" => "\x45\xCC\x82\xCC\x81", + "\xE1\xBA\xBF" => "\x65\xCC\x82\xCC\x81", + "\xE1\xBB\x80" => "\x45\xCC\x82\xCC\x80", + "\xE1\xBB\x81" => "\x65\xCC\x82\xCC\x80", + "\xE1\xBB\x82" => "\x45\xCC\x82\xCC\x89", + "\xE1\xBB\x83" => "\x65\xCC\x82\xCC\x89", + "\xE1\xBB\x84" => "\x45\xCC\x82\xCC\x83", + "\xE1\xBB\x85" => "\x65\xCC\x82\xCC\x83", + "\xE1\xBB\x86" => "\x45\xCC\xA3\xCC\x82", + "\xE1\xBB\x87" => "\x65\xCC\xA3\xCC\x82", + "\xE1\xBB\x88" => "\x49\xCC\x89", + "\xE1\xBB\x89" => "\x69\xCC\x89", + "\xE1\xBB\x8A" => "\x49\xCC\xA3", + "\xE1\xBB\x8B" => "\x69\xCC\xA3", + "\xE1\xBB\x8C" => "\x4F\xCC\xA3", + "\xE1\xBB\x8D" => "\x6F\xCC\xA3", + "\xE1\xBB\x8E" => "\x4F\xCC\x89", + "\xE1\xBB\x8F" => "\x6F\xCC\x89", + "\xE1\xBB\x90" => "\x4F\xCC\x82\xCC\x81", + "\xE1\xBB\x91" => "\x6F\xCC\x82\xCC\x81", + "\xE1\xBB\x92" => "\x4F\xCC\x82\xCC\x80", + "\xE1\xBB\x93" => "\x6F\xCC\x82\xCC\x80", + "\xE1\xBB\x94" => "\x4F\xCC\x82\xCC\x89", + "\xE1\xBB\x95" => "\x6F\xCC\x82\xCC\x89", + "\xE1\xBB\x96" => "\x4F\xCC\x82\xCC\x83", + "\xE1\xBB\x97" => "\x6F\xCC\x82\xCC\x83", + "\xE1\xBB\x98" => "\x4F\xCC\xA3\xCC\x82", + "\xE1\xBB\x99" => "\x6F\xCC\xA3\xCC\x82", + "\xE1\xBB\x9A" => "\x4F\xCC\x9B\xCC\x81", + "\xE1\xBB\x9B" => "\x6F\xCC\x9B\xCC\x81", + "\xE1\xBB\x9C" => "\x4F\xCC\x9B\xCC\x80", + "\xE1\xBB\x9D" => "\x6F\xCC\x9B\xCC\x80", + "\xE1\xBB\x9E" => "\x4F\xCC\x9B\xCC\x89", + "\xE1\xBB\x9F" => "\x6F\xCC\x9B\xCC\x89", + "\xE1\xBB\xA0" => "\x4F\xCC\x9B\xCC\x83", + "\xE1\xBB\xA1" => "\x6F\xCC\x9B\xCC\x83", + "\xE1\xBB\xA2" => "\x4F\xCC\x9B\xCC\xA3", + "\xE1\xBB\xA3" => "\x6F\xCC\x9B\xCC\xA3", + "\xE1\xBB\xA4" => "\x55\xCC\xA3", + "\xE1\xBB\xA5" => "\x75\xCC\xA3", + "\xE1\xBB\xA6" => "\x55\xCC\x89", + "\xE1\xBB\xA7" => "\x75\xCC\x89", + "\xE1\xBB\xA8" => "\x55\xCC\x9B\xCC\x81", + "\xE1\xBB\xA9" => "\x75\xCC\x9B\xCC\x81", + "\xE1\xBB\xAA" => "\x55\xCC\x9B\xCC\x80", + "\xE1\xBB\xAB" => "\x75\xCC\x9B\xCC\x80", + "\xE1\xBB\xAC" => "\x55\xCC\x9B\xCC\x89", + "\xE1\xBB\xAD" => "\x75\xCC\x9B\xCC\x89", + "\xE1\xBB\xAE" => "\x55\xCC\x9B\xCC\x83", + "\xE1\xBB\xAF" => "\x75\xCC\x9B\xCC\x83", + "\xE1\xBB\xB0" => "\x55\xCC\x9B\xCC\xA3", + "\xE1\xBB\xB1" => "\x75\xCC\x9B\xCC\xA3", + "\xE1\xBB\xB2" => "\x59\xCC\x80", + "\xE1\xBB\xB3" => "\x79\xCC\x80", + "\xE1\xBB\xB4" => "\x59\xCC\xA3", + "\xE1\xBB\xB5" => "\x79\xCC\xA3", + "\xE1\xBB\xB6" => "\x59\xCC\x89", + "\xE1\xBB\xB7" => "\x79\xCC\x89", + "\xE1\xBB\xB8" => "\x59\xCC\x83", + "\xE1\xBB\xB9" => "\x79\xCC\x83", + "\xE1\xBC\x80" => "\xCE\xB1\xCC\x93", + "\xE1\xBC\x81" => "\xCE\xB1\xCC\x94", + "\xE1\xBC\x82" => "\xCE\xB1\xCC\x93\xCC\x80", + "\xE1\xBC\x83" => "\xCE\xB1\xCC\x94\xCC\x80", + "\xE1\xBC\x84" => "\xCE\xB1\xCC\x93\xCC\x81", + "\xE1\xBC\x85" => "\xCE\xB1\xCC\x94\xCC\x81", + "\xE1\xBC\x86" => "\xCE\xB1\xCC\x93\xCD\x82", + "\xE1\xBC\x87" => "\xCE\xB1\xCC\x94\xCD\x82", + "\xE1\xBC\x88" => "\xCE\x91\xCC\x93", + "\xE1\xBC\x89" => "\xCE\x91\xCC\x94", + "\xE1\xBC\x8A" => "\xCE\x91\xCC\x93\xCC\x80", + "\xE1\xBC\x8B" => "\xCE\x91\xCC\x94\xCC\x80", + "\xE1\xBC\x8C" => "\xCE\x91\xCC\x93\xCC\x81", + "\xE1\xBC\x8D" => "\xCE\x91\xCC\x94\xCC\x81", + "\xE1\xBC\x8E" => "\xCE\x91\xCC\x93\xCD\x82", + "\xE1\xBC\x8F" => "\xCE\x91\xCC\x94\xCD\x82", + "\xE1\xBC\x90" => "\xCE\xB5\xCC\x93", + "\xE1\xBC\x91" => "\xCE\xB5\xCC\x94", + "\xE1\xBC\x92" => "\xCE\xB5\xCC\x93\xCC\x80", + "\xE1\xBC\x93" => "\xCE\xB5\xCC\x94\xCC\x80", + "\xE1\xBC\x94" => "\xCE\xB5\xCC\x93\xCC\x81", + "\xE1\xBC\x95" => "\xCE\xB5\xCC\x94\xCC\x81", + "\xE1\xBC\x98" => "\xCE\x95\xCC\x93", + "\xE1\xBC\x99" => "\xCE\x95\xCC\x94", + "\xE1\xBC\x9A" => "\xCE\x95\xCC\x93\xCC\x80", + "\xE1\xBC\x9B" => "\xCE\x95\xCC\x94\xCC\x80", + "\xE1\xBC\x9C" => "\xCE\x95\xCC\x93\xCC\x81", + "\xE1\xBC\x9D" => "\xCE\x95\xCC\x94\xCC\x81", + "\xE1\xBC\xA0" => "\xCE\xB7\xCC\x93", + "\xE1\xBC\xA1" => "\xCE\xB7\xCC\x94", + "\xE1\xBC\xA2" => "\xCE\xB7\xCC\x93\xCC\x80", + "\xE1\xBC\xA3" => "\xCE\xB7\xCC\x94\xCC\x80", + "\xE1\xBC\xA4" => "\xCE\xB7\xCC\x93\xCC\x81", + "\xE1\xBC\xA5" => "\xCE\xB7\xCC\x94\xCC\x81", + "\xE1\xBC\xA6" => "\xCE\xB7\xCC\x93\xCD\x82", + "\xE1\xBC\xA7" => "\xCE\xB7\xCC\x94\xCD\x82", + "\xE1\xBC\xA8" => "\xCE\x97\xCC\x93", + "\xE1\xBC\xA9" => "\xCE\x97\xCC\x94", + "\xE1\xBC\xAA" => "\xCE\x97\xCC\x93\xCC\x80", + "\xE1\xBC\xAB" => "\xCE\x97\xCC\x94\xCC\x80", + "\xE1\xBC\xAC" => "\xCE\x97\xCC\x93\xCC\x81", + "\xE1\xBC\xAD" => "\xCE\x97\xCC\x94\xCC\x81", + "\xE1\xBC\xAE" => "\xCE\x97\xCC\x93\xCD\x82", + "\xE1\xBC\xAF" => "\xCE\x97\xCC\x94\xCD\x82", + "\xE1\xBC\xB0" => "\xCE\xB9\xCC\x93", + "\xE1\xBC\xB1" => "\xCE\xB9\xCC\x94", + "\xE1\xBC\xB2" => "\xCE\xB9\xCC\x93\xCC\x80", + "\xE1\xBC\xB3" => "\xCE\xB9\xCC\x94\xCC\x80", + "\xE1\xBC\xB4" => "\xCE\xB9\xCC\x93\xCC\x81", + "\xE1\xBC\xB5" => "\xCE\xB9\xCC\x94\xCC\x81", + "\xE1\xBC\xB6" => "\xCE\xB9\xCC\x93\xCD\x82", + "\xE1\xBC\xB7" => "\xCE\xB9\xCC\x94\xCD\x82", + "\xE1\xBC\xB8" => "\xCE\x99\xCC\x93", + "\xE1\xBC\xB9" => "\xCE\x99\xCC\x94", + "\xE1\xBC\xBA" => "\xCE\x99\xCC\x93\xCC\x80", + "\xE1\xBC\xBB" => "\xCE\x99\xCC\x94\xCC\x80", + "\xE1\xBC\xBC" => "\xCE\x99\xCC\x93\xCC\x81", + "\xE1\xBC\xBD" => "\xCE\x99\xCC\x94\xCC\x81", + "\xE1\xBC\xBE" => "\xCE\x99\xCC\x93\xCD\x82", + "\xE1\xBC\xBF" => "\xCE\x99\xCC\x94\xCD\x82", + "\xE1\xBD\x80" => "\xCE\xBF\xCC\x93", + "\xE1\xBD\x81" => "\xCE\xBF\xCC\x94", + "\xE1\xBD\x82" => "\xCE\xBF\xCC\x93\xCC\x80", + "\xE1\xBD\x83" => "\xCE\xBF\xCC\x94\xCC\x80", + "\xE1\xBD\x84" => "\xCE\xBF\xCC\x93\xCC\x81", + "\xE1\xBD\x85" => "\xCE\xBF\xCC\x94\xCC\x81", + "\xE1\xBD\x88" => "\xCE\x9F\xCC\x93", + "\xE1\xBD\x89" => "\xCE\x9F\xCC\x94", + "\xE1\xBD\x8A" => "\xCE\x9F\xCC\x93\xCC\x80", + "\xE1\xBD\x8B" => "\xCE\x9F\xCC\x94\xCC\x80", + "\xE1\xBD\x8C" => "\xCE\x9F\xCC\x93\xCC\x81", + "\xE1\xBD\x8D" => "\xCE\x9F\xCC\x94\xCC\x81", + "\xE1\xBD\x90" => "\xCF\x85\xCC\x93", + "\xE1\xBD\x91" => "\xCF\x85\xCC\x94", + "\xE1\xBD\x92" => "\xCF\x85\xCC\x93\xCC\x80", + "\xE1\xBD\x93" => "\xCF\x85\xCC\x94\xCC\x80", + "\xE1\xBD\x94" => "\xCF\x85\xCC\x93\xCC\x81", + "\xE1\xBD\x95" => "\xCF\x85\xCC\x94\xCC\x81", + "\xE1\xBD\x96" => "\xCF\x85\xCC\x93\xCD\x82", + "\xE1\xBD\x97" => "\xCF\x85\xCC\x94\xCD\x82", + "\xE1\xBD\x99" => "\xCE\xA5\xCC\x94", + "\xE1\xBD\x9B" => "\xCE\xA5\xCC\x94\xCC\x80", + "\xE1\xBD\x9D" => "\xCE\xA5\xCC\x94\xCC\x81", + "\xE1\xBD\x9F" => "\xCE\xA5\xCC\x94\xCD\x82", + "\xE1\xBD\xA0" => "\xCF\x89\xCC\x93", + "\xE1\xBD\xA1" => "\xCF\x89\xCC\x94", + "\xE1\xBD\xA2" => "\xCF\x89\xCC\x93\xCC\x80", + "\xE1\xBD\xA3" => "\xCF\x89\xCC\x94\xCC\x80", + "\xE1\xBD\xA4" => "\xCF\x89\xCC\x93\xCC\x81", + "\xE1\xBD\xA5" => "\xCF\x89\xCC\x94\xCC\x81", + "\xE1\xBD\xA6" => "\xCF\x89\xCC\x93\xCD\x82", + "\xE1\xBD\xA7" => "\xCF\x89\xCC\x94\xCD\x82", + "\xE1\xBD\xA8" => "\xCE\xA9\xCC\x93", + "\xE1\xBD\xA9" => "\xCE\xA9\xCC\x94", + "\xE1\xBD\xAA" => "\xCE\xA9\xCC\x93\xCC\x80", + "\xE1\xBD\xAB" => "\xCE\xA9\xCC\x94\xCC\x80", + "\xE1\xBD\xAC" => "\xCE\xA9\xCC\x93\xCC\x81", + "\xE1\xBD\xAD" => "\xCE\xA9\xCC\x94\xCC\x81", + "\xE1\xBD\xAE" => "\xCE\xA9\xCC\x93\xCD\x82", + "\xE1\xBD\xAF" => "\xCE\xA9\xCC\x94\xCD\x82", + "\xE1\xBD\xB0" => "\xCE\xB1\xCC\x80", + "\xE1\xBD\xB1" => "\xCE\xB1\xCC\x81", + "\xE1\xBD\xB2" => "\xCE\xB5\xCC\x80", + "\xE1\xBD\xB3" => "\xCE\xB5\xCC\x81", + "\xE1\xBD\xB4" => "\xCE\xB7\xCC\x80", + "\xE1\xBD\xB5" => "\xCE\xB7\xCC\x81", + "\xE1\xBD\xB6" => "\xCE\xB9\xCC\x80", + "\xE1\xBD\xB7" => "\xCE\xB9\xCC\x81", + "\xE1\xBD\xB8" => "\xCE\xBF\xCC\x80", + "\xE1\xBD\xB9" => "\xCE\xBF\xCC\x81", + "\xE1\xBD\xBA" => "\xCF\x85\xCC\x80", + "\xE1\xBD\xBB" => "\xCF\x85\xCC\x81", + "\xE1\xBD\xBC" => "\xCF\x89\xCC\x80", + "\xE1\xBD\xBD" => "\xCF\x89\xCC\x81", + "\xE1\xBE\x80" => "\xCE\xB1\xCC\x93\xCD\x85", + "\xE1\xBE\x81" => "\xCE\xB1\xCC\x94\xCD\x85", + "\xE1\xBE\x82" => "\xCE\xB1\xCC\x93\xCC\x80\xCD\x85", + "\xE1\xBE\x83" => "\xCE\xB1\xCC\x94\xCC\x80\xCD\x85", + "\xE1\xBE\x84" => "\xCE\xB1\xCC\x93\xCC\x81\xCD\x85", + "\xE1\xBE\x85" => "\xCE\xB1\xCC\x94\xCC\x81\xCD\x85", + "\xE1\xBE\x86" => "\xCE\xB1\xCC\x93\xCD\x82\xCD\x85", + "\xE1\xBE\x87" => "\xCE\xB1\xCC\x94\xCD\x82\xCD\x85", + "\xE1\xBE\x88" => "\xCE\x91\xCC\x93\xCD\x85", + "\xE1\xBE\x89" => "\xCE\x91\xCC\x94\xCD\x85", + "\xE1\xBE\x8A" => "\xCE\x91\xCC\x93\xCC\x80\xCD\x85", + "\xE1\xBE\x8B" => "\xCE\x91\xCC\x94\xCC\x80\xCD\x85", + "\xE1\xBE\x8C" => "\xCE\x91\xCC\x93\xCC\x81\xCD\x85", + "\xE1\xBE\x8D" => "\xCE\x91\xCC\x94\xCC\x81\xCD\x85", + "\xE1\xBE\x8E" => "\xCE\x91\xCC\x93\xCD\x82\xCD\x85", + "\xE1\xBE\x8F" => "\xCE\x91\xCC\x94\xCD\x82\xCD\x85", + "\xE1\xBE\x90" => "\xCE\xB7\xCC\x93\xCD\x85", + "\xE1\xBE\x91" => "\xCE\xB7\xCC\x94\xCD\x85", + "\xE1\xBE\x92" => "\xCE\xB7\xCC\x93\xCC\x80\xCD\x85", + "\xE1\xBE\x93" => "\xCE\xB7\xCC\x94\xCC\x80\xCD\x85", + "\xE1\xBE\x94" => "\xCE\xB7\xCC\x93\xCC\x81\xCD\x85", + "\xE1\xBE\x95" => "\xCE\xB7\xCC\x94\xCC\x81\xCD\x85", + "\xE1\xBE\x96" => "\xCE\xB7\xCC\x93\xCD\x82\xCD\x85", + "\xE1\xBE\x97" => "\xCE\xB7\xCC\x94\xCD\x82\xCD\x85", + "\xE1\xBE\x98" => "\xCE\x97\xCC\x93\xCD\x85", + "\xE1\xBE\x99" => "\xCE\x97\xCC\x94\xCD\x85", + "\xE1\xBE\x9A" => "\xCE\x97\xCC\x93\xCC\x80\xCD\x85", + "\xE1\xBE\x9B" => "\xCE\x97\xCC\x94\xCC\x80\xCD\x85", + "\xE1\xBE\x9C" => "\xCE\x97\xCC\x93\xCC\x81\xCD\x85", + "\xE1\xBE\x9D" => "\xCE\x97\xCC\x94\xCC\x81\xCD\x85", + "\xE1\xBE\x9E" => "\xCE\x97\xCC\x93\xCD\x82\xCD\x85", + "\xE1\xBE\x9F" => "\xCE\x97\xCC\x94\xCD\x82\xCD\x85", + "\xE1\xBE\xA0" => "\xCF\x89\xCC\x93\xCD\x85", + "\xE1\xBE\xA1" => "\xCF\x89\xCC\x94\xCD\x85", + "\xE1\xBE\xA2" => "\xCF\x89\xCC\x93\xCC\x80\xCD\x85", + "\xE1\xBE\xA3" => "\xCF\x89\xCC\x94\xCC\x80\xCD\x85", + "\xE1\xBE\xA4" => "\xCF\x89\xCC\x93\xCC\x81\xCD\x85", + "\xE1\xBE\xA5" => "\xCF\x89\xCC\x94\xCC\x81\xCD\x85", + "\xE1\xBE\xA6" => "\xCF\x89\xCC\x93\xCD\x82\xCD\x85", + "\xE1\xBE\xA7" => "\xCF\x89\xCC\x94\xCD\x82\xCD\x85", + "\xE1\xBE\xA8" => "\xCE\xA9\xCC\x93\xCD\x85", + "\xE1\xBE\xA9" => "\xCE\xA9\xCC\x94\xCD\x85", + "\xE1\xBE\xAA" => "\xCE\xA9\xCC\x93\xCC\x80\xCD\x85", + "\xE1\xBE\xAB" => "\xCE\xA9\xCC\x94\xCC\x80\xCD\x85", + "\xE1\xBE\xAC" => "\xCE\xA9\xCC\x93\xCC\x81\xCD\x85", + "\xE1\xBE\xAD" => "\xCE\xA9\xCC\x94\xCC\x81\xCD\x85", + "\xE1\xBE\xAE" => "\xCE\xA9\xCC\x93\xCD\x82\xCD\x85", + "\xE1\xBE\xAF" => "\xCE\xA9\xCC\x94\xCD\x82\xCD\x85", + "\xE1\xBE\xB0" => "\xCE\xB1\xCC\x86", + "\xE1\xBE\xB1" => "\xCE\xB1\xCC\x84", + "\xE1\xBE\xB2" => "\xCE\xB1\xCC\x80\xCD\x85", + "\xE1\xBE\xB3" => "\xCE\xB1\xCD\x85", + "\xE1\xBE\xB4" => "\xCE\xB1\xCC\x81\xCD\x85", + "\xE1\xBE\xB6" => "\xCE\xB1\xCD\x82", + "\xE1\xBE\xB7" => "\xCE\xB1\xCD\x82\xCD\x85", + "\xE1\xBE\xB8" => "\xCE\x91\xCC\x86", + "\xE1\xBE\xB9" => "\xCE\x91\xCC\x84", + "\xE1\xBE\xBA" => "\xCE\x91\xCC\x80", + "\xE1\xBE\xBB" => "\xCE\x91\xCC\x81", + "\xE1\xBE\xBC" => "\xCE\x91\xCD\x85", + "\xE1\xBE\xBE" => "\xCE\xB9", + "\xE1\xBF\x81" => "\xC2\xA8\xCD\x82", + "\xE1\xBF\x82" => "\xCE\xB7\xCC\x80\xCD\x85", + "\xE1\xBF\x83" => "\xCE\xB7\xCD\x85", + "\xE1\xBF\x84" => "\xCE\xB7\xCC\x81\xCD\x85", + "\xE1\xBF\x86" => "\xCE\xB7\xCD\x82", + "\xE1\xBF\x87" => "\xCE\xB7\xCD\x82\xCD\x85", + "\xE1\xBF\x88" => "\xCE\x95\xCC\x80", + "\xE1\xBF\x89" => "\xCE\x95\xCC\x81", + "\xE1\xBF\x8A" => "\xCE\x97\xCC\x80", + "\xE1\xBF\x8B" => "\xCE\x97\xCC\x81", + "\xE1\xBF\x8C" => "\xCE\x97\xCD\x85", + "\xE1\xBF\x8D" => "\xE1\xBE\xBF\xCC\x80", + "\xE1\xBF\x8E" => "\xE1\xBE\xBF\xCC\x81", + "\xE1\xBF\x8F" => "\xE1\xBE\xBF\xCD\x82", + "\xE1\xBF\x90" => "\xCE\xB9\xCC\x86", + "\xE1\xBF\x91" => "\xCE\xB9\xCC\x84", + "\xE1\xBF\x92" => "\xCE\xB9\xCC\x88\xCC\x80", + "\xE1\xBF\x93" => "\xCE\xB9\xCC\x88\xCC\x81", + "\xE1\xBF\x96" => "\xCE\xB9\xCD\x82", + "\xE1\xBF\x97" => "\xCE\xB9\xCC\x88\xCD\x82", + "\xE1\xBF\x98" => "\xCE\x99\xCC\x86", + "\xE1\xBF\x99" => "\xCE\x99\xCC\x84", + "\xE1\xBF\x9A" => "\xCE\x99\xCC\x80", + "\xE1\xBF\x9B" => "\xCE\x99\xCC\x81", + "\xE1\xBF\x9D" => "\xE1\xBF\xBE\xCC\x80", + "\xE1\xBF\x9E" => "\xE1\xBF\xBE\xCC\x81", + "\xE1\xBF\x9F" => "\xE1\xBF\xBE\xCD\x82", + "\xE1\xBF\xA0" => "\xCF\x85\xCC\x86", + "\xE1\xBF\xA1" => "\xCF\x85\xCC\x84", + "\xE1\xBF\xA2" => "\xCF\x85\xCC\x88\xCC\x80", + "\xE1\xBF\xA3" => "\xCF\x85\xCC\x88\xCC\x81", + "\xE1\xBF\xA4" => "\xCF\x81\xCC\x93", + "\xE1\xBF\xA5" => "\xCF\x81\xCC\x94", + "\xE1\xBF\xA6" => "\xCF\x85\xCD\x82", + "\xE1\xBF\xA7" => "\xCF\x85\xCC\x88\xCD\x82", + "\xE1\xBF\xA8" => "\xCE\xA5\xCC\x86", + "\xE1\xBF\xA9" => "\xCE\xA5\xCC\x84", + "\xE1\xBF\xAA" => "\xCE\xA5\xCC\x80", + "\xE1\xBF\xAB" => "\xCE\xA5\xCC\x81", + "\xE1\xBF\xAC" => "\xCE\xA1\xCC\x94", + "\xE1\xBF\xAD" => "\xC2\xA8\xCC\x80", + "\xE1\xBF\xAE" => "\xC2\xA8\xCC\x81", + "\xE1\xBF\xAF" => "\x60", + "\xE1\xBF\xB2" => "\xCF\x89\xCC\x80\xCD\x85", + "\xE1\xBF\xB3" => "\xCF\x89\xCD\x85", + "\xE1\xBF\xB4" => "\xCF\x89\xCC\x81\xCD\x85", + "\xE1\xBF\xB6" => "\xCF\x89\xCD\x82", + "\xE1\xBF\xB7" => "\xCF\x89\xCD\x82\xCD\x85", + "\xE1\xBF\xB8" => "\xCE\x9F\xCC\x80", + "\xE1\xBF\xB9" => "\xCE\x9F\xCC\x81", + "\xE1\xBF\xBA" => "\xCE\xA9\xCC\x80", + "\xE1\xBF\xBB" => "\xCE\xA9\xCC\x81", + "\xE1\xBF\xBC" => "\xCE\xA9\xCD\x85", + "\xE1\xBF\xBD" => "\xC2\xB4", + "\xE2\x80\x80" => "\xE2\x80\x82", + "\xE2\x80\x81" => "\xE2\x80\x83", + "\xE2\x84\xA6" => "\xCE\xA9", + "\xE2\x84\xAA" => "\x4B", + "\xE2\x84\xAB" => "\x41\xCC\x8A", + "\xE2\x86\x9A" => "\xE2\x86\x90\xCC\xB8", + "\xE2\x86\x9B" => "\xE2\x86\x92\xCC\xB8", + "\xE2\x86\xAE" => "\xE2\x86\x94\xCC\xB8", + "\xE2\x87\x8D" => "\xE2\x87\x90\xCC\xB8", + "\xE2\x87\x8E" => "\xE2\x87\x94\xCC\xB8", + "\xE2\x87\x8F" => "\xE2\x87\x92\xCC\xB8", + "\xE2\x88\x84" => "\xE2\x88\x83\xCC\xB8", + "\xE2\x88\x89" => "\xE2\x88\x88\xCC\xB8", + "\xE2\x88\x8C" => "\xE2\x88\x8B\xCC\xB8", + "\xE2\x88\xA4" => "\xE2\x88\xA3\xCC\xB8", + "\xE2\x88\xA6" => "\xE2\x88\xA5\xCC\xB8", + "\xE2\x89\x81" => "\xE2\x88\xBC\xCC\xB8", + "\xE2\x89\x84" => "\xE2\x89\x83\xCC\xB8", + "\xE2\x89\x87" => "\xE2\x89\x85\xCC\xB8", + "\xE2\x89\x89" => "\xE2\x89\x88\xCC\xB8", + "\xE2\x89\xA0" => "\x3D\xCC\xB8", + "\xE2\x89\xA2" => "\xE2\x89\xA1\xCC\xB8", + "\xE2\x89\xAD" => "\xE2\x89\x8D\xCC\xB8", + "\xE2\x89\xAE" => "\x3C\xCC\xB8", + "\xE2\x89\xAF" => "\x3E\xCC\xB8", + "\xE2\x89\xB0" => "\xE2\x89\xA4\xCC\xB8", + "\xE2\x89\xB1" => "\xE2\x89\xA5\xCC\xB8", + "\xE2\x89\xB4" => "\xE2\x89\xB2\xCC\xB8", + "\xE2\x89\xB5" => "\xE2\x89\xB3\xCC\xB8", + "\xE2\x89\xB8" => "\xE2\x89\xB6\xCC\xB8", + "\xE2\x89\xB9" => "\xE2\x89\xB7\xCC\xB8", + "\xE2\x8A\x80" => "\xE2\x89\xBA\xCC\xB8", + "\xE2\x8A\x81" => "\xE2\x89\xBB\xCC\xB8", + "\xE2\x8A\x84" => "\xE2\x8A\x82\xCC\xB8", + "\xE2\x8A\x85" => "\xE2\x8A\x83\xCC\xB8", + "\xE2\x8A\x88" => "\xE2\x8A\x86\xCC\xB8", + "\xE2\x8A\x89" => "\xE2\x8A\x87\xCC\xB8", + "\xE2\x8A\xAC" => "\xE2\x8A\xA2\xCC\xB8", + "\xE2\x8A\xAD" => "\xE2\x8A\xA8\xCC\xB8", + "\xE2\x8A\xAE" => "\xE2\x8A\xA9\xCC\xB8", + "\xE2\x8A\xAF" => "\xE2\x8A\xAB\xCC\xB8", + "\xE2\x8B\xA0" => "\xE2\x89\xBC\xCC\xB8", + "\xE2\x8B\xA1" => "\xE2\x89\xBD\xCC\xB8", + "\xE2\x8B\xA2" => "\xE2\x8A\x91\xCC\xB8", + "\xE2\x8B\xA3" => "\xE2\x8A\x92\xCC\xB8", + "\xE2\x8B\xAA" => "\xE2\x8A\xB2\xCC\xB8", + "\xE2\x8B\xAB" => "\xE2\x8A\xB3\xCC\xB8", + "\xE2\x8B\xAC" => "\xE2\x8A\xB4\xCC\xB8", + "\xE2\x8B\xAD" => "\xE2\x8A\xB5\xCC\xB8", + "\xE2\x8C\xA9" => "\xE3\x80\x88", + "\xE2\x8C\xAA" => "\xE3\x80\x89", + "\xE2\xAB\x9C" => "\xE2\xAB\x9D\xCC\xB8", + "\xE3\x81\x8C" => "\xE3\x81\x8B\xE3\x82\x99", + "\xE3\x81\x8E" => "\xE3\x81\x8D\xE3\x82\x99", + "\xE3\x81\x90" => "\xE3\x81\x8F\xE3\x82\x99", + "\xE3\x81\x92" => "\xE3\x81\x91\xE3\x82\x99", + "\xE3\x81\x94" => "\xE3\x81\x93\xE3\x82\x99", + "\xE3\x81\x96" => "\xE3\x81\x95\xE3\x82\x99", + "\xE3\x81\x98" => "\xE3\x81\x97\xE3\x82\x99", + "\xE3\x81\x9A" => "\xE3\x81\x99\xE3\x82\x99", + "\xE3\x81\x9C" => "\xE3\x81\x9B\xE3\x82\x99", + "\xE3\x81\x9E" => "\xE3\x81\x9D\xE3\x82\x99", + "\xE3\x81\xA0" => "\xE3\x81\x9F\xE3\x82\x99", + "\xE3\x81\xA2" => "\xE3\x81\xA1\xE3\x82\x99", + "\xE3\x81\xA5" => "\xE3\x81\xA4\xE3\x82\x99", + "\xE3\x81\xA7" => "\xE3\x81\xA6\xE3\x82\x99", + "\xE3\x81\xA9" => "\xE3\x81\xA8\xE3\x82\x99", + "\xE3\x81\xB0" => "\xE3\x81\xAF\xE3\x82\x99", + "\xE3\x81\xB1" => "\xE3\x81\xAF\xE3\x82\x9A", + "\xE3\x81\xB3" => "\xE3\x81\xB2\xE3\x82\x99", + "\xE3\x81\xB4" => "\xE3\x81\xB2\xE3\x82\x9A", + "\xE3\x81\xB6" => "\xE3\x81\xB5\xE3\x82\x99", + "\xE3\x81\xB7" => "\xE3\x81\xB5\xE3\x82\x9A", + "\xE3\x81\xB9" => "\xE3\x81\xB8\xE3\x82\x99", + "\xE3\x81\xBA" => "\xE3\x81\xB8\xE3\x82\x9A", + "\xE3\x81\xBC" => "\xE3\x81\xBB\xE3\x82\x99", + "\xE3\x81\xBD" => "\xE3\x81\xBB\xE3\x82\x9A", + "\xE3\x82\x94" => "\xE3\x81\x86\xE3\x82\x99", + "\xE3\x82\x9E" => "\xE3\x82\x9D\xE3\x82\x99", + "\xE3\x82\xAC" => "\xE3\x82\xAB\xE3\x82\x99", + "\xE3\x82\xAE" => "\xE3\x82\xAD\xE3\x82\x99", + "\xE3\x82\xB0" => "\xE3\x82\xAF\xE3\x82\x99", + "\xE3\x82\xB2" => "\xE3\x82\xB1\xE3\x82\x99", + "\xE3\x82\xB4" => "\xE3\x82\xB3\xE3\x82\x99", + "\xE3\x82\xB6" => "\xE3\x82\xB5\xE3\x82\x99", + "\xE3\x82\xB8" => "\xE3\x82\xB7\xE3\x82\x99", + "\xE3\x82\xBA" => "\xE3\x82\xB9\xE3\x82\x99", + "\xE3\x82\xBC" => "\xE3\x82\xBB\xE3\x82\x99", + "\xE3\x82\xBE" => "\xE3\x82\xBD\xE3\x82\x99", + "\xE3\x83\x80" => "\xE3\x82\xBF\xE3\x82\x99", + "\xE3\x83\x82" => "\xE3\x83\x81\xE3\x82\x99", + "\xE3\x83\x85" => "\xE3\x83\x84\xE3\x82\x99", + "\xE3\x83\x87" => "\xE3\x83\x86\xE3\x82\x99", + "\xE3\x83\x89" => "\xE3\x83\x88\xE3\x82\x99", + "\xE3\x83\x90" => "\xE3\x83\x8F\xE3\x82\x99", + "\xE3\x83\x91" => "\xE3\x83\x8F\xE3\x82\x9A", + "\xE3\x83\x93" => "\xE3\x83\x92\xE3\x82\x99", + "\xE3\x83\x94" => "\xE3\x83\x92\xE3\x82\x9A", + "\xE3\x83\x96" => "\xE3\x83\x95\xE3\x82\x99", + "\xE3\x83\x97" => "\xE3\x83\x95\xE3\x82\x9A", + "\xE3\x83\x99" => "\xE3\x83\x98\xE3\x82\x99", + "\xE3\x83\x9A" => "\xE3\x83\x98\xE3\x82\x9A", + "\xE3\x83\x9C" => "\xE3\x83\x9B\xE3\x82\x99", + "\xE3\x83\x9D" => "\xE3\x83\x9B\xE3\x82\x9A", + "\xE3\x83\xB4" => "\xE3\x82\xA6\xE3\x82\x99", + "\xE3\x83\xB7" => "\xE3\x83\xAF\xE3\x82\x99", + "\xE3\x83\xB8" => "\xE3\x83\xB0\xE3\x82\x99", + "\xE3\x83\xB9" => "\xE3\x83\xB1\xE3\x82\x99", + "\xE3\x83\xBA" => "\xE3\x83\xB2\xE3\x82\x99", + "\xE3\x83\xBE" => "\xE3\x83\xBD\xE3\x82\x99", + "\xEF\xA4\x80" => "\xE8\xB1\x88", + "\xEF\xA4\x81" => "\xE6\x9B\xB4", + "\xEF\xA4\x82" => "\xE8\xBB\x8A", + "\xEF\xA4\x83" => "\xE8\xB3\x88", + "\xEF\xA4\x84" => "\xE6\xBB\x91", + "\xEF\xA4\x85" => "\xE4\xB8\xB2", + "\xEF\xA4\x86" => "\xE5\x8F\xA5", + "\xEF\xA4\x87" => "\xE9\xBE\x9C", + "\xEF\xA4\x88" => "\xE9\xBE\x9C", + "\xEF\xA4\x89" => "\xE5\xA5\x91", + "\xEF\xA4\x8A" => "\xE9\x87\x91", + "\xEF\xA4\x8B" => "\xE5\x96\x87", + "\xEF\xA4\x8C" => "\xE5\xA5\x88", + "\xEF\xA4\x8D" => "\xE6\x87\xB6", + "\xEF\xA4\x8E" => "\xE7\x99\xA9", + "\xEF\xA4\x8F" => "\xE7\xBE\x85", + "\xEF\xA4\x90" => "\xE8\x98\xBF", + "\xEF\xA4\x91" => "\xE8\x9E\xBA", + "\xEF\xA4\x92" => "\xE8\xA3\xB8", + "\xEF\xA4\x93" => "\xE9\x82\x8F", + "\xEF\xA4\x94" => "\xE6\xA8\x82", + "\xEF\xA4\x95" => "\xE6\xB4\x9B", + "\xEF\xA4\x96" => "\xE7\x83\x99", + "\xEF\xA4\x97" => "\xE7\x8F\x9E", + "\xEF\xA4\x98" => "\xE8\x90\xBD", + "\xEF\xA4\x99" => "\xE9\x85\xAA", + "\xEF\xA4\x9A" => "\xE9\xA7\xB1", + "\xEF\xA4\x9B" => "\xE4\xBA\x82", + "\xEF\xA4\x9C" => "\xE5\x8D\xB5", + "\xEF\xA4\x9D" => "\xE6\xAC\x84", + "\xEF\xA4\x9E" => "\xE7\x88\x9B", + "\xEF\xA4\x9F" => "\xE8\x98\xAD", + "\xEF\xA4\xA0" => "\xE9\xB8\x9E", + "\xEF\xA4\xA1" => "\xE5\xB5\x90", + "\xEF\xA4\xA2" => "\xE6\xBF\xAB", + "\xEF\xA4\xA3" => "\xE8\x97\x8D", + "\xEF\xA4\xA4" => "\xE8\xA5\xA4", + "\xEF\xA4\xA5" => "\xE6\x8B\x89", + "\xEF\xA4\xA6" => "\xE8\x87\x98", + "\xEF\xA4\xA7" => "\xE8\xA0\x9F", + "\xEF\xA4\xA8" => "\xE5\xBB\x8A", + "\xEF\xA4\xA9" => "\xE6\x9C\x97", + "\xEF\xA4\xAA" => "\xE6\xB5\xAA", + "\xEF\xA4\xAB" => "\xE7\x8B\xBC", + "\xEF\xA4\xAC" => "\xE9\x83\x8E", + "\xEF\xA4\xAD" => "\xE4\xBE\x86", + "\xEF\xA4\xAE" => "\xE5\x86\xB7", + "\xEF\xA4\xAF" => "\xE5\x8B\x9E", + "\xEF\xA4\xB0" => "\xE6\x93\x84", + "\xEF\xA4\xB1" => "\xE6\xAB\x93", + "\xEF\xA4\xB2" => "\xE7\x88\x90", + "\xEF\xA4\xB3" => "\xE7\x9B\xA7", + "\xEF\xA4\xB4" => "\xE8\x80\x81", + "\xEF\xA4\xB5" => "\xE8\x98\x86", + "\xEF\xA4\xB6" => "\xE8\x99\x9C", + "\xEF\xA4\xB7" => "\xE8\xB7\xAF", + "\xEF\xA4\xB8" => "\xE9\x9C\xB2", + "\xEF\xA4\xB9" => "\xE9\xAD\xAF", + "\xEF\xA4\xBA" => "\xE9\xB7\xBA", + "\xEF\xA4\xBB" => "\xE7\xA2\x8C", + "\xEF\xA4\xBC" => "\xE7\xA5\xBF", + "\xEF\xA4\xBD" => "\xE7\xB6\xA0", + "\xEF\xA4\xBE" => "\xE8\x8F\x89", + "\xEF\xA4\xBF" => "\xE9\x8C\x84", + "\xEF\xA5\x80" => "\xE9\xB9\xBF", + "\xEF\xA5\x81" => "\xE8\xAB\x96", + "\xEF\xA5\x82" => "\xE5\xA3\x9F", + "\xEF\xA5\x83" => "\xE5\xBC\x84", + "\xEF\xA5\x84" => "\xE7\xB1\xA0", + "\xEF\xA5\x85" => "\xE8\x81\xBE", + "\xEF\xA5\x86" => "\xE7\x89\xA2", + "\xEF\xA5\x87" => "\xE7\xA3\x8A", + "\xEF\xA5\x88" => "\xE8\xB3\x82", + "\xEF\xA5\x89" => "\xE9\x9B\xB7", + "\xEF\xA5\x8A" => "\xE5\xA3\x98", + "\xEF\xA5\x8B" => "\xE5\xB1\xA2", + "\xEF\xA5\x8C" => "\xE6\xA8\x93", + "\xEF\xA5\x8D" => "\xE6\xB7\x9A", + "\xEF\xA5\x8E" => "\xE6\xBC\x8F", + "\xEF\xA5\x8F" => "\xE7\xB4\xAF", + "\xEF\xA5\x90" => "\xE7\xB8\xB7", + "\xEF\xA5\x91" => "\xE9\x99\x8B", + "\xEF\xA5\x92" => "\xE5\x8B\x92", + "\xEF\xA5\x93" => "\xE8\x82\x8B", + "\xEF\xA5\x94" => "\xE5\x87\x9C", + "\xEF\xA5\x95" => "\xE5\x87\x8C", + "\xEF\xA5\x96" => "\xE7\xA8\x9C", + "\xEF\xA5\x97" => "\xE7\xB6\xBE", + "\xEF\xA5\x98" => "\xE8\x8F\xB1", + "\xEF\xA5\x99" => "\xE9\x99\xB5", + "\xEF\xA5\x9A" => "\xE8\xAE\x80", + "\xEF\xA5\x9B" => "\xE6\x8B\x8F", + "\xEF\xA5\x9C" => "\xE6\xA8\x82", + "\xEF\xA5\x9D" => "\xE8\xAB\xBE", + "\xEF\xA5\x9E" => "\xE4\xB8\xB9", + "\xEF\xA5\x9F" => "\xE5\xAF\xA7", + "\xEF\xA5\xA0" => "\xE6\x80\x92", + "\xEF\xA5\xA1" => "\xE7\x8E\x87", + "\xEF\xA5\xA2" => "\xE7\x95\xB0", + "\xEF\xA5\xA3" => "\xE5\x8C\x97", + "\xEF\xA5\xA4" => "\xE7\xA3\xBB", + "\xEF\xA5\xA5" => "\xE4\xBE\xBF", + "\xEF\xA5\xA6" => "\xE5\xBE\xA9", + "\xEF\xA5\xA7" => "\xE4\xB8\x8D", + "\xEF\xA5\xA8" => "\xE6\xB3\x8C", + "\xEF\xA5\xA9" => "\xE6\x95\xB8", + "\xEF\xA5\xAA" => "\xE7\xB4\xA2", + "\xEF\xA5\xAB" => "\xE5\x8F\x83", + "\xEF\xA5\xAC" => "\xE5\xA1\x9E", + "\xEF\xA5\xAD" => "\xE7\x9C\x81", + "\xEF\xA5\xAE" => "\xE8\x91\x89", + "\xEF\xA5\xAF" => "\xE8\xAA\xAA", + "\xEF\xA5\xB0" => "\xE6\xAE\xBA", + "\xEF\xA5\xB1" => "\xE8\xBE\xB0", + "\xEF\xA5\xB2" => "\xE6\xB2\x88", + "\xEF\xA5\xB3" => "\xE6\x8B\xBE", + "\xEF\xA5\xB4" => "\xE8\x8B\xA5", + "\xEF\xA5\xB5" => "\xE6\x8E\xA0", + "\xEF\xA5\xB6" => "\xE7\x95\xA5", + "\xEF\xA5\xB7" => "\xE4\xBA\xAE", + "\xEF\xA5\xB8" => "\xE5\x85\xA9", + "\xEF\xA5\xB9" => "\xE5\x87\x89", + "\xEF\xA5\xBA" => "\xE6\xA2\x81", + "\xEF\xA5\xBB" => "\xE7\xB3\xA7", + "\xEF\xA5\xBC" => "\xE8\x89\xAF", + "\xEF\xA5\xBD" => "\xE8\xAB\x92", + "\xEF\xA5\xBE" => "\xE9\x87\x8F", + "\xEF\xA5\xBF" => "\xE5\x8B\xB5", + "\xEF\xA6\x80" => "\xE5\x91\x82", + "\xEF\xA6\x81" => "\xE5\xA5\xB3", + "\xEF\xA6\x82" => "\xE5\xBB\xAC", + "\xEF\xA6\x83" => "\xE6\x97\x85", + "\xEF\xA6\x84" => "\xE6\xBF\xBE", + "\xEF\xA6\x85" => "\xE7\xA4\xAA", + "\xEF\xA6\x86" => "\xE9\x96\xAD", + "\xEF\xA6\x87" => "\xE9\xA9\xAA", + "\xEF\xA6\x88" => "\xE9\xBA\x97", + "\xEF\xA6\x89" => "\xE9\xBB\x8E", + "\xEF\xA6\x8A" => "\xE5\x8A\x9B", + "\xEF\xA6\x8B" => "\xE6\x9B\x86", + "\xEF\xA6\x8C" => "\xE6\xAD\xB7", + "\xEF\xA6\x8D" => "\xE8\xBD\xA2", + "\xEF\xA6\x8E" => "\xE5\xB9\xB4", + "\xEF\xA6\x8F" => "\xE6\x86\x90", + "\xEF\xA6\x90" => "\xE6\x88\x80", + "\xEF\xA6\x91" => "\xE6\x92\x9A", + "\xEF\xA6\x92" => "\xE6\xBC\xA3", + "\xEF\xA6\x93" => "\xE7\x85\x89", + "\xEF\xA6\x94" => "\xE7\x92\x89", + "\xEF\xA6\x95" => "\xE7\xA7\x8A", + "\xEF\xA6\x96" => "\xE7\xB7\xB4", + "\xEF\xA6\x97" => "\xE8\x81\xAF", + "\xEF\xA6\x98" => "\xE8\xBC\xA6", + "\xEF\xA6\x99" => "\xE8\x93\xAE", + "\xEF\xA6\x9A" => "\xE9\x80\xA3", + "\xEF\xA6\x9B" => "\xE9\x8D\x8A", + "\xEF\xA6\x9C" => "\xE5\x88\x97", + "\xEF\xA6\x9D" => "\xE5\x8A\xA3", + "\xEF\xA6\x9E" => "\xE5\x92\xBD", + "\xEF\xA6\x9F" => "\xE7\x83\x88", + "\xEF\xA6\xA0" => "\xE8\xA3\x82", + "\xEF\xA6\xA1" => "\xE8\xAA\xAA", + "\xEF\xA6\xA2" => "\xE5\xBB\x89", + "\xEF\xA6\xA3" => "\xE5\xBF\xB5", + "\xEF\xA6\xA4" => "\xE6\x8D\xBB", + "\xEF\xA6\xA5" => "\xE6\xAE\xAE", + "\xEF\xA6\xA6" => "\xE7\xB0\xBE", + "\xEF\xA6\xA7" => "\xE7\x8D\xB5", + "\xEF\xA6\xA8" => "\xE4\xBB\xA4", + "\xEF\xA6\xA9" => "\xE5\x9B\xB9", + "\xEF\xA6\xAA" => "\xE5\xAF\xA7", + "\xEF\xA6\xAB" => "\xE5\xB6\xBA", + "\xEF\xA6\xAC" => "\xE6\x80\x9C", + "\xEF\xA6\xAD" => "\xE7\x8E\xB2", + "\xEF\xA6\xAE" => "\xE7\x91\xA9", + "\xEF\xA6\xAF" => "\xE7\xBE\x9A", + "\xEF\xA6\xB0" => "\xE8\x81\x86", + "\xEF\xA6\xB1" => "\xE9\x88\xB4", + "\xEF\xA6\xB2" => "\xE9\x9B\xB6", + "\xEF\xA6\xB3" => "\xE9\x9D\x88", + "\xEF\xA6\xB4" => "\xE9\xA0\x98", + "\xEF\xA6\xB5" => "\xE4\xBE\x8B", + "\xEF\xA6\xB6" => "\xE7\xA6\xAE", + "\xEF\xA6\xB7" => "\xE9\x86\xB4", + "\xEF\xA6\xB8" => "\xE9\x9A\xB8", + "\xEF\xA6\xB9" => "\xE6\x83\xA1", + "\xEF\xA6\xBA" => "\xE4\xBA\x86", + "\xEF\xA6\xBB" => "\xE5\x83\x9A", + "\xEF\xA6\xBC" => "\xE5\xAF\xAE", + "\xEF\xA6\xBD" => "\xE5\xB0\xBF", + "\xEF\xA6\xBE" => "\xE6\x96\x99", + "\xEF\xA6\xBF" => "\xE6\xA8\x82", + "\xEF\xA7\x80" => "\xE7\x87\x8E", + "\xEF\xA7\x81" => "\xE7\x99\x82", + "\xEF\xA7\x82" => "\xE8\x93\xBC", + "\xEF\xA7\x83" => "\xE9\x81\xBC", + "\xEF\xA7\x84" => "\xE9\xBE\x8D", + "\xEF\xA7\x85" => "\xE6\x9A\x88", + "\xEF\xA7\x86" => "\xE9\x98\xAE", + "\xEF\xA7\x87" => "\xE5\x8A\x89", + "\xEF\xA7\x88" => "\xE6\x9D\xBB", + "\xEF\xA7\x89" => "\xE6\x9F\xB3", + "\xEF\xA7\x8A" => "\xE6\xB5\x81", + "\xEF\xA7\x8B" => "\xE6\xBA\x9C", + "\xEF\xA7\x8C" => "\xE7\x90\x89", + "\xEF\xA7\x8D" => "\xE7\x95\x99", + "\xEF\xA7\x8E" => "\xE7\xA1\xAB", + "\xEF\xA7\x8F" => "\xE7\xB4\x90", + "\xEF\xA7\x90" => "\xE9\xA1\x9E", + "\xEF\xA7\x91" => "\xE5\x85\xAD", + "\xEF\xA7\x92" => "\xE6\x88\xAE", + "\xEF\xA7\x93" => "\xE9\x99\xB8", + "\xEF\xA7\x94" => "\xE5\x80\xAB", + "\xEF\xA7\x95" => "\xE5\xB4\x99", + "\xEF\xA7\x96" => "\xE6\xB7\xAA", + "\xEF\xA7\x97" => "\xE8\xBC\xAA", + "\xEF\xA7\x98" => "\xE5\xBE\x8B", + "\xEF\xA7\x99" => "\xE6\x85\x84", + "\xEF\xA7\x9A" => "\xE6\xA0\x97", + "\xEF\xA7\x9B" => "\xE7\x8E\x87", + "\xEF\xA7\x9C" => "\xE9\x9A\x86", + "\xEF\xA7\x9D" => "\xE5\x88\xA9", + "\xEF\xA7\x9E" => "\xE5\x90\x8F", + "\xEF\xA7\x9F" => "\xE5\xB1\xA5", + "\xEF\xA7\xA0" => "\xE6\x98\x93", + "\xEF\xA7\xA1" => "\xE6\x9D\x8E", + "\xEF\xA7\xA2" => "\xE6\xA2\xA8", + "\xEF\xA7\xA3" => "\xE6\xB3\xA5", + "\xEF\xA7\xA4" => "\xE7\x90\x86", + "\xEF\xA7\xA5" => "\xE7\x97\xA2", + "\xEF\xA7\xA6" => "\xE7\xBD\xB9", + "\xEF\xA7\xA7" => "\xE8\xA3\x8F", + "\xEF\xA7\xA8" => "\xE8\xA3\xA1", + "\xEF\xA7\xA9" => "\xE9\x87\x8C", + "\xEF\xA7\xAA" => "\xE9\x9B\xA2", + "\xEF\xA7\xAB" => "\xE5\x8C\xBF", + "\xEF\xA7\xAC" => "\xE6\xBA\xBA", + "\xEF\xA7\xAD" => "\xE5\x90\x9D", + "\xEF\xA7\xAE" => "\xE7\x87\x90", + "\xEF\xA7\xAF" => "\xE7\x92\x98", + "\xEF\xA7\xB0" => "\xE8\x97\xBA", + "\xEF\xA7\xB1" => "\xE9\x9A\xA3", + "\xEF\xA7\xB2" => "\xE9\xB1\x97", + "\xEF\xA7\xB3" => "\xE9\xBA\x9F", + "\xEF\xA7\xB4" => "\xE6\x9E\x97", + "\xEF\xA7\xB5" => "\xE6\xB7\x8B", + "\xEF\xA7\xB6" => "\xE8\x87\xA8", + "\xEF\xA7\xB7" => "\xE7\xAB\x8B", + "\xEF\xA7\xB8" => "\xE7\xAC\xA0", + "\xEF\xA7\xB9" => "\xE7\xB2\x92", + "\xEF\xA7\xBA" => "\xE7\x8B\x80", + "\xEF\xA7\xBB" => "\xE7\x82\x99", + "\xEF\xA7\xBC" => "\xE8\xAD\x98", + "\xEF\xA7\xBD" => "\xE4\xBB\x80", + "\xEF\xA7\xBE" => "\xE8\x8C\xB6", + "\xEF\xA7\xBF" => "\xE5\x88\xBA", + "\xEF\xA8\x80" => "\xE5\x88\x87", + "\xEF\xA8\x81" => "\xE5\xBA\xA6", + "\xEF\xA8\x82" => "\xE6\x8B\x93", + "\xEF\xA8\x83" => "\xE7\xB3\x96", + "\xEF\xA8\x84" => "\xE5\xAE\x85", + "\xEF\xA8\x85" => "\xE6\xB4\x9E", + "\xEF\xA8\x86" => "\xE6\x9A\xB4", + "\xEF\xA8\x87" => "\xE8\xBC\xBB", + "\xEF\xA8\x88" => "\xE8\xA1\x8C", + "\xEF\xA8\x89" => "\xE9\x99\x8D", + "\xEF\xA8\x8A" => "\xE8\xA6\x8B", + "\xEF\xA8\x8B" => "\xE5\xBB\x93", + "\xEF\xA8\x8C" => "\xE5\x85\x80", + "\xEF\xA8\x8D" => "\xE5\x97\x80", + "\xEF\xA8\x90" => "\xE5\xA1\x9A", + "\xEF\xA8\x92" => "\xE6\x99\xB4", + "\xEF\xA8\x95" => "\xE5\x87\x9E", + "\xEF\xA8\x96" => "\xE7\x8C\xAA", + "\xEF\xA8\x97" => "\xE7\x9B\x8A", + "\xEF\xA8\x98" => "\xE7\xA4\xBC", + "\xEF\xA8\x99" => "\xE7\xA5\x9E", + "\xEF\xA8\x9A" => "\xE7\xA5\xA5", + "\xEF\xA8\x9B" => "\xE7\xA6\x8F", + "\xEF\xA8\x9C" => "\xE9\x9D\x96", + "\xEF\xA8\x9D" => "\xE7\xB2\xBE", + "\xEF\xA8\x9E" => "\xE7\xBE\xBD", + "\xEF\xA8\xA0" => "\xE8\x98\x92", + "\xEF\xA8\xA2" => "\xE8\xAB\xB8", + "\xEF\xA8\xA5" => "\xE9\x80\xB8", + "\xEF\xA8\xA6" => "\xE9\x83\xBD", + "\xEF\xA8\xAA" => "\xE9\xA3\xAF", + "\xEF\xA8\xAB" => "\xE9\xA3\xBC", + "\xEF\xA8\xAC" => "\xE9\xA4\xA8", + "\xEF\xA8\xAD" => "\xE9\xB6\xB4", + "\xEF\xA8\xAE" => "\xE9\x83\x9E", + "\xEF\xA8\xAF" => "\xE9\x9A\xB7", + "\xEF\xA8\xB0" => "\xE4\xBE\xAE", + "\xEF\xA8\xB1" => "\xE5\x83\xA7", + "\xEF\xA8\xB2" => "\xE5\x85\x8D", + "\xEF\xA8\xB3" => "\xE5\x8B\x89", + "\xEF\xA8\xB4" => "\xE5\x8B\xA4", + "\xEF\xA8\xB5" => "\xE5\x8D\x91", + "\xEF\xA8\xB6" => "\xE5\x96\x9D", + "\xEF\xA8\xB7" => "\xE5\x98\x86", + "\xEF\xA8\xB8" => "\xE5\x99\xA8", + "\xEF\xA8\xB9" => "\xE5\xA1\x80", + "\xEF\xA8\xBA" => "\xE5\xA2\xA8", + "\xEF\xA8\xBB" => "\xE5\xB1\xA4", + "\xEF\xA8\xBC" => "\xE5\xB1\xAE", + "\xEF\xA8\xBD" => "\xE6\x82\x94", + "\xEF\xA8\xBE" => "\xE6\x85\xA8", + "\xEF\xA8\xBF" => "\xE6\x86\x8E", + "\xEF\xA9\x80" => "\xE6\x87\xB2", + "\xEF\xA9\x81" => "\xE6\x95\x8F", + "\xEF\xA9\x82" => "\xE6\x97\xA2", + "\xEF\xA9\x83" => "\xE6\x9A\x91", + "\xEF\xA9\x84" => "\xE6\xA2\x85", + "\xEF\xA9\x85" => "\xE6\xB5\xB7", + "\xEF\xA9\x86" => "\xE6\xB8\x9A", + "\xEF\xA9\x87" => "\xE6\xBC\xA2", + "\xEF\xA9\x88" => "\xE7\x85\xAE", + "\xEF\xA9\x89" => "\xE7\x88\xAB", + "\xEF\xA9\x8A" => "\xE7\x90\xA2", + "\xEF\xA9\x8B" => "\xE7\xA2\x91", + "\xEF\xA9\x8C" => "\xE7\xA4\xBE", + "\xEF\xA9\x8D" => "\xE7\xA5\x89", + "\xEF\xA9\x8E" => "\xE7\xA5\x88", + "\xEF\xA9\x8F" => "\xE7\xA5\x90", + "\xEF\xA9\x90" => "\xE7\xA5\x96", + "\xEF\xA9\x91" => "\xE7\xA5\x9D", + "\xEF\xA9\x92" => "\xE7\xA6\x8D", + "\xEF\xA9\x93" => "\xE7\xA6\x8E", + "\xEF\xA9\x94" => "\xE7\xA9\x80", + "\xEF\xA9\x95" => "\xE7\xAA\x81", + "\xEF\xA9\x96" => "\xE7\xAF\x80", + "\xEF\xA9\x97" => "\xE7\xB7\xB4", + "\xEF\xA9\x98" => "\xE7\xB8\x89", + "\xEF\xA9\x99" => "\xE7\xB9\x81", + "\xEF\xA9\x9A" => "\xE7\xBD\xB2", + "\xEF\xA9\x9B" => "\xE8\x80\x85", + "\xEF\xA9\x9C" => "\xE8\x87\xAD", + "\xEF\xA9\x9D" => "\xE8\x89\xB9", + "\xEF\xA9\x9E" => "\xE8\x89\xB9", + "\xEF\xA9\x9F" => "\xE8\x91\x97", + "\xEF\xA9\xA0" => "\xE8\xA4\x90", + "\xEF\xA9\xA1" => "\xE8\xA6\x96", + "\xEF\xA9\xA2" => "\xE8\xAC\x81", + "\xEF\xA9\xA3" => "\xE8\xAC\xB9", + "\xEF\xA9\xA4" => "\xE8\xB3\x93", + "\xEF\xA9\xA5" => "\xE8\xB4\x88", + "\xEF\xA9\xA6" => "\xE8\xBE\xB6", + "\xEF\xA9\xA7" => "\xE9\x80\xB8", + "\xEF\xA9\xA8" => "\xE9\x9B\xA3", + "\xEF\xA9\xA9" => "\xE9\x9F\xBF", + "\xEF\xA9\xAA" => "\xE9\xA0\xBB", + "\xEF\xA9\xAB" => "\xE6\x81\xB5", + "\xEF\xA9\xAC" => "\xF0\xA4\x8B\xAE", + "\xEF\xA9\xAD" => "\xE8\x88\x98", + "\xEF\xA9\xB0" => "\xE4\xB8\xA6", + "\xEF\xA9\xB1" => "\xE5\x86\xB5", + "\xEF\xA9\xB2" => "\xE5\x85\xA8", + "\xEF\xA9\xB3" => "\xE4\xBE\x80", + "\xEF\xA9\xB4" => "\xE5\x85\x85", + "\xEF\xA9\xB5" => "\xE5\x86\x80", + "\xEF\xA9\xB6" => "\xE5\x8B\x87", + "\xEF\xA9\xB7" => "\xE5\x8B\xBA", + "\xEF\xA9\xB8" => "\xE5\x96\x9D", + "\xEF\xA9\xB9" => "\xE5\x95\x95", + "\xEF\xA9\xBA" => "\xE5\x96\x99", + "\xEF\xA9\xBB" => "\xE5\x97\xA2", + "\xEF\xA9\xBC" => "\xE5\xA1\x9A", + "\xEF\xA9\xBD" => "\xE5\xA2\xB3", + "\xEF\xA9\xBE" => "\xE5\xA5\x84", + "\xEF\xA9\xBF" => "\xE5\xA5\x94", + "\xEF\xAA\x80" => "\xE5\xA9\xA2", + "\xEF\xAA\x81" => "\xE5\xAC\xA8", + "\xEF\xAA\x82" => "\xE5\xBB\x92", + "\xEF\xAA\x83" => "\xE5\xBB\x99", + "\xEF\xAA\x84" => "\xE5\xBD\xA9", + "\xEF\xAA\x85" => "\xE5\xBE\xAD", + "\xEF\xAA\x86" => "\xE6\x83\x98", + "\xEF\xAA\x87" => "\xE6\x85\x8E", + "\xEF\xAA\x88" => "\xE6\x84\x88", + "\xEF\xAA\x89" => "\xE6\x86\x8E", + "\xEF\xAA\x8A" => "\xE6\x85\xA0", + "\xEF\xAA\x8B" => "\xE6\x87\xB2", + "\xEF\xAA\x8C" => "\xE6\x88\xB4", + "\xEF\xAA\x8D" => "\xE6\x8F\x84", + "\xEF\xAA\x8E" => "\xE6\x90\x9C", + "\xEF\xAA\x8F" => "\xE6\x91\x92", + "\xEF\xAA\x90" => "\xE6\x95\x96", + "\xEF\xAA\x91" => "\xE6\x99\xB4", + "\xEF\xAA\x92" => "\xE6\x9C\x97", + "\xEF\xAA\x93" => "\xE6\x9C\x9B", + "\xEF\xAA\x94" => "\xE6\x9D\x96", + "\xEF\xAA\x95" => "\xE6\xAD\xB9", + "\xEF\xAA\x96" => "\xE6\xAE\xBA", + "\xEF\xAA\x97" => "\xE6\xB5\x81", + "\xEF\xAA\x98" => "\xE6\xBB\x9B", + "\xEF\xAA\x99" => "\xE6\xBB\x8B", + "\xEF\xAA\x9A" => "\xE6\xBC\xA2", + "\xEF\xAA\x9B" => "\xE7\x80\x9E", + "\xEF\xAA\x9C" => "\xE7\x85\xAE", + "\xEF\xAA\x9D" => "\xE7\x9E\xA7", + "\xEF\xAA\x9E" => "\xE7\x88\xB5", + "\xEF\xAA\x9F" => "\xE7\x8A\xAF", + "\xEF\xAA\xA0" => "\xE7\x8C\xAA", + "\xEF\xAA\xA1" => "\xE7\x91\xB1", + "\xEF\xAA\xA2" => "\xE7\x94\x86", + "\xEF\xAA\xA3" => "\xE7\x94\xBB", + "\xEF\xAA\xA4" => "\xE7\x98\x9D", + "\xEF\xAA\xA5" => "\xE7\x98\x9F", + "\xEF\xAA\xA6" => "\xE7\x9B\x8A", + "\xEF\xAA\xA7" => "\xE7\x9B\x9B", + "\xEF\xAA\xA8" => "\xE7\x9B\xB4", + "\xEF\xAA\xA9" => "\xE7\x9D\x8A", + "\xEF\xAA\xAA" => "\xE7\x9D\x80", + "\xEF\xAA\xAB" => "\xE7\xA3\x8C", + "\xEF\xAA\xAC" => "\xE7\xAA\xB1", + "\xEF\xAA\xAD" => "\xE7\xAF\x80", + "\xEF\xAA\xAE" => "\xE7\xB1\xBB", + "\xEF\xAA\xAF" => "\xE7\xB5\x9B", + "\xEF\xAA\xB0" => "\xE7\xB7\xB4", + "\xEF\xAA\xB1" => "\xE7\xBC\xBE", + "\xEF\xAA\xB2" => "\xE8\x80\x85", + "\xEF\xAA\xB3" => "\xE8\x8D\x92", + "\xEF\xAA\xB4" => "\xE8\x8F\xAF", + "\xEF\xAA\xB5" => "\xE8\x9D\xB9", + "\xEF\xAA\xB6" => "\xE8\xA5\x81", + "\xEF\xAA\xB7" => "\xE8\xA6\x86", + "\xEF\xAA\xB8" => "\xE8\xA6\x96", + "\xEF\xAA\xB9" => "\xE8\xAA\xBF", + "\xEF\xAA\xBA" => "\xE8\xAB\xB8", + "\xEF\xAA\xBB" => "\xE8\xAB\x8B", + "\xEF\xAA\xBC" => "\xE8\xAC\x81", + "\xEF\xAA\xBD" => "\xE8\xAB\xBE", + "\xEF\xAA\xBE" => "\xE8\xAB\xAD", + "\xEF\xAA\xBF" => "\xE8\xAC\xB9", + "\xEF\xAB\x80" => "\xE8\xAE\x8A", + "\xEF\xAB\x81" => "\xE8\xB4\x88", + "\xEF\xAB\x82" => "\xE8\xBC\xB8", + "\xEF\xAB\x83" => "\xE9\x81\xB2", + "\xEF\xAB\x84" => "\xE9\x86\x99", + "\xEF\xAB\x85" => "\xE9\x89\xB6", + "\xEF\xAB\x86" => "\xE9\x99\xBC", + "\xEF\xAB\x87" => "\xE9\x9B\xA3", + "\xEF\xAB\x88" => "\xE9\x9D\x96", + "\xEF\xAB\x89" => "\xE9\x9F\x9B", + "\xEF\xAB\x8A" => "\xE9\x9F\xBF", + "\xEF\xAB\x8B" => "\xE9\xA0\x8B", + "\xEF\xAB\x8C" => "\xE9\xA0\xBB", + "\xEF\xAB\x8D" => "\xE9\xAC\x92", + "\xEF\xAB\x8E" => "\xE9\xBE\x9C", + "\xEF\xAB\x8F" => "\xF0\xA2\xA1\x8A", + "\xEF\xAB\x90" => "\xF0\xA2\xA1\x84", + "\xEF\xAB\x91" => "\xF0\xA3\x8F\x95", + "\xEF\xAB\x92" => "\xE3\xAE\x9D", + "\xEF\xAB\x93" => "\xE4\x80\x98", + "\xEF\xAB\x94" => "\xE4\x80\xB9", + "\xEF\xAB\x95" => "\xF0\xA5\x89\x89", + "\xEF\xAB\x96" => "\xF0\xA5\xB3\x90", + "\xEF\xAB\x97" => "\xF0\xA7\xBB\x93", + "\xEF\xAB\x98" => "\xE9\xBD\x83", + "\xEF\xAB\x99" => "\xE9\xBE\x8E", + "\xEF\xAC\x9D" => "\xD7\x99\xD6\xB4", + "\xEF\xAC\x9F" => "\xD7\xB2\xD6\xB7", + "\xEF\xAC\xAA" => "\xD7\xA9\xD7\x81", + "\xEF\xAC\xAB" => "\xD7\xA9\xD7\x82", + "\xEF\xAC\xAC" => "\xD7\xA9\xD6\xBC\xD7\x81", + "\xEF\xAC\xAD" => "\xD7\xA9\xD6\xBC\xD7\x82", + "\xEF\xAC\xAE" => "\xD7\x90\xD6\xB7", + "\xEF\xAC\xAF" => "\xD7\x90\xD6\xB8", + "\xEF\xAC\xB0" => "\xD7\x90\xD6\xBC", + "\xEF\xAC\xB1" => "\xD7\x91\xD6\xBC", + "\xEF\xAC\xB2" => "\xD7\x92\xD6\xBC", + "\xEF\xAC\xB3" => "\xD7\x93\xD6\xBC", + "\xEF\xAC\xB4" => "\xD7\x94\xD6\xBC", + "\xEF\xAC\xB5" => "\xD7\x95\xD6\xBC", + "\xEF\xAC\xB6" => "\xD7\x96\xD6\xBC", + "\xEF\xAC\xB8" => "\xD7\x98\xD6\xBC", + "\xEF\xAC\xB9" => "\xD7\x99\xD6\xBC", + "\xEF\xAC\xBA" => "\xD7\x9A\xD6\xBC", + "\xEF\xAC\xBB" => "\xD7\x9B\xD6\xBC", + "\xEF\xAC\xBC" => "\xD7\x9C\xD6\xBC", + "\xEF\xAC\xBE" => "\xD7\x9E\xD6\xBC", + "\xEF\xAD\x80" => "\xD7\xA0\xD6\xBC", + "\xEF\xAD\x81" => "\xD7\xA1\xD6\xBC", + "\xEF\xAD\x83" => "\xD7\xA3\xD6\xBC", + "\xEF\xAD\x84" => "\xD7\xA4\xD6\xBC", + "\xEF\xAD\x86" => "\xD7\xA6\xD6\xBC", + "\xEF\xAD\x87" => "\xD7\xA7\xD6\xBC", + "\xEF\xAD\x88" => "\xD7\xA8\xD6\xBC", + "\xEF\xAD\x89" => "\xD7\xA9\xD6\xBC", + "\xEF\xAD\x8A" => "\xD7\xAA\xD6\xBC", + "\xEF\xAD\x8B" => "\xD7\x95\xD6\xB9", + "\xEF\xAD\x8C" => "\xD7\x91\xD6\xBF", + "\xEF\xAD\x8D" => "\xD7\x9B\xD6\xBF", + "\xEF\xAD\x8E" => "\xD7\xA4\xD6\xBF", + "\xF0\x91\x82\x9A" => "\xF0\x91\x82\x99\xF0\x91\x82\xBA", + "\xF0\x91\x82\x9C" => "\xF0\x91\x82\x9B\xF0\x91\x82\xBA", + "\xF0\x91\x82\xAB" => "\xF0\x91\x82\xA5\xF0\x91\x82\xBA", + "\xF0\x91\x84\xAE" => "\xF0\x91\x84\xB1\xF0\x91\x84\xA7", + "\xF0\x91\x84\xAF" => "\xF0\x91\x84\xB2\xF0\x91\x84\xA7", + "\xF0\x91\x8D\x8B" => "\xF0\x91\x8D\x87\xF0\x91\x8C\xBE", + "\xF0\x91\x8D\x8C" => "\xF0\x91\x8D\x87\xF0\x91\x8D\x97", + "\xF0\x91\x92\xBB" => "\xF0\x91\x92\xB9\xF0\x91\x92\xBA", + "\xF0\x91\x92\xBC" => "\xF0\x91\x92\xB9\xF0\x91\x92\xB0", + "\xF0\x91\x92\xBE" => "\xF0\x91\x92\xB9\xF0\x91\x92\xBD", + "\xF0\x91\x96\xBA" => "\xF0\x91\x96\xB8\xF0\x91\x96\xAF", + "\xF0\x91\x96\xBB" => "\xF0\x91\x96\xB9\xF0\x91\x96\xAF", + "\xF0\x91\xA4\xB8" => "\xF0\x91\xA4\xB5\xF0\x91\xA4\xB0", + "\xF0\x9D\x85\x9E" => "\xF0\x9D\x85\x97\xF0\x9D\x85\xA5", + "\xF0\x9D\x85\x9F" => "\xF0\x9D\x85\x98\xF0\x9D\x85\xA5", + "\xF0\x9D\x85\xA0" => "\xF0\x9D\x85\x98\xF0\x9D\x85\xA5\xF0\x9D\x85\xAE", + "\xF0\x9D\x85\xA1" => "\xF0\x9D\x85\x98\xF0\x9D\x85\xA5\xF0\x9D\x85\xAF", + "\xF0\x9D\x85\xA2" => "\xF0\x9D\x85\x98\xF0\x9D\x85\xA5\xF0\x9D\x85\xB0", + "\xF0\x9D\x85\xA3" => "\xF0\x9D\x85\x98\xF0\x9D\x85\xA5\xF0\x9D\x85\xB1", + "\xF0\x9D\x85\xA4" => "\xF0\x9D\x85\x98\xF0\x9D\x85\xA5\xF0\x9D\x85\xB2", + "\xF0\x9D\x86\xBB" => "\xF0\x9D\x86\xB9\xF0\x9D\x85\xA5", + "\xF0\x9D\x86\xBC" => "\xF0\x9D\x86\xBA\xF0\x9D\x85\xA5", + "\xF0\x9D\x86\xBD" => "\xF0\x9D\x86\xB9\xF0\x9D\x85\xA5\xF0\x9D\x85\xAE", + "\xF0\x9D\x86\xBE" => "\xF0\x9D\x86\xBA\xF0\x9D\x85\xA5\xF0\x9D\x85\xAE", + "\xF0\x9D\x86\xBF" => "\xF0\x9D\x86\xB9\xF0\x9D\x85\xA5\xF0\x9D\x85\xAF", + "\xF0\x9D\x87\x80" => "\xF0\x9D\x86\xBA\xF0\x9D\x85\xA5\xF0\x9D\x85\xAF", + "\xF0\xAF\xA0\x80" => "\xE4\xB8\xBD", + "\xF0\xAF\xA0\x81" => "\xE4\xB8\xB8", + "\xF0\xAF\xA0\x82" => "\xE4\xB9\x81", + "\xF0\xAF\xA0\x83" => "\xF0\xA0\x84\xA2", + "\xF0\xAF\xA0\x84" => "\xE4\xBD\xA0", + "\xF0\xAF\xA0\x85" => "\xE4\xBE\xAE", + "\xF0\xAF\xA0\x86" => "\xE4\xBE\xBB", + "\xF0\xAF\xA0\x87" => "\xE5\x80\x82", + "\xF0\xAF\xA0\x88" => "\xE5\x81\xBA", + "\xF0\xAF\xA0\x89" => "\xE5\x82\x99", + "\xF0\xAF\xA0\x8A" => "\xE5\x83\xA7", + "\xF0\xAF\xA0\x8B" => "\xE5\x83\x8F", + "\xF0\xAF\xA0\x8C" => "\xE3\x92\x9E", + "\xF0\xAF\xA0\x8D" => "\xF0\xA0\x98\xBA", + "\xF0\xAF\xA0\x8E" => "\xE5\x85\x8D", + "\xF0\xAF\xA0\x8F" => "\xE5\x85\x94", + "\xF0\xAF\xA0\x90" => "\xE5\x85\xA4", + "\xF0\xAF\xA0\x91" => "\xE5\x85\xB7", + "\xF0\xAF\xA0\x92" => "\xF0\xA0\x94\x9C", + "\xF0\xAF\xA0\x93" => "\xE3\x92\xB9", + "\xF0\xAF\xA0\x94" => "\xE5\x85\xA7", + "\xF0\xAF\xA0\x95" => "\xE5\x86\x8D", + "\xF0\xAF\xA0\x96" => "\xF0\xA0\x95\x8B", + "\xF0\xAF\xA0\x97" => "\xE5\x86\x97", + "\xF0\xAF\xA0\x98" => "\xE5\x86\xA4", + "\xF0\xAF\xA0\x99" => "\xE4\xBB\x8C", + "\xF0\xAF\xA0\x9A" => "\xE5\x86\xAC", + "\xF0\xAF\xA0\x9B" => "\xE5\x86\xB5", + "\xF0\xAF\xA0\x9C" => "\xF0\xA9\x87\x9F", + "\xF0\xAF\xA0\x9D" => "\xE5\x87\xB5", + "\xF0\xAF\xA0\x9E" => "\xE5\x88\x83", + "\xF0\xAF\xA0\x9F" => "\xE3\x93\x9F", + "\xF0\xAF\xA0\xA0" => "\xE5\x88\xBB", + "\xF0\xAF\xA0\xA1" => "\xE5\x89\x86", + "\xF0\xAF\xA0\xA2" => "\xE5\x89\xB2", + "\xF0\xAF\xA0\xA3" => "\xE5\x89\xB7", + "\xF0\xAF\xA0\xA4" => "\xE3\x94\x95", + "\xF0\xAF\xA0\xA5" => "\xE5\x8B\x87", + "\xF0\xAF\xA0\xA6" => "\xE5\x8B\x89", + "\xF0\xAF\xA0\xA7" => "\xE5\x8B\xA4", + "\xF0\xAF\xA0\xA8" => "\xE5\x8B\xBA", + "\xF0\xAF\xA0\xA9" => "\xE5\x8C\x85", + "\xF0\xAF\xA0\xAA" => "\xE5\x8C\x86", + "\xF0\xAF\xA0\xAB" => "\xE5\x8C\x97", + "\xF0\xAF\xA0\xAC" => "\xE5\x8D\x89", + "\xF0\xAF\xA0\xAD" => "\xE5\x8D\x91", + "\xF0\xAF\xA0\xAE" => "\xE5\x8D\x9A", + "\xF0\xAF\xA0\xAF" => "\xE5\x8D\xB3", + "\xF0\xAF\xA0\xB0" => "\xE5\x8D\xBD", + "\xF0\xAF\xA0\xB1" => "\xE5\x8D\xBF", + "\xF0\xAF\xA0\xB2" => "\xE5\x8D\xBF", + "\xF0\xAF\xA0\xB3" => "\xE5\x8D\xBF", + "\xF0\xAF\xA0\xB4" => "\xF0\xA0\xA8\xAC", + "\xF0\xAF\xA0\xB5" => "\xE7\x81\xB0", + "\xF0\xAF\xA0\xB6" => "\xE5\x8F\x8A", + "\xF0\xAF\xA0\xB7" => "\xE5\x8F\x9F", + "\xF0\xAF\xA0\xB8" => "\xF0\xA0\xAD\xA3", + "\xF0\xAF\xA0\xB9" => "\xE5\x8F\xAB", + "\xF0\xAF\xA0\xBA" => "\xE5\x8F\xB1", + "\xF0\xAF\xA0\xBB" => "\xE5\x90\x86", + "\xF0\xAF\xA0\xBC" => "\xE5\x92\x9E", + "\xF0\xAF\xA0\xBD" => "\xE5\x90\xB8", + "\xF0\xAF\xA0\xBE" => "\xE5\x91\x88", + "\xF0\xAF\xA0\xBF" => "\xE5\x91\xA8", + "\xF0\xAF\xA1\x80" => "\xE5\x92\xA2", + "\xF0\xAF\xA1\x81" => "\xE5\x93\xB6", + "\xF0\xAF\xA1\x82" => "\xE5\x94\x90", + "\xF0\xAF\xA1\x83" => "\xE5\x95\x93", + "\xF0\xAF\xA1\x84" => "\xE5\x95\xA3", + "\xF0\xAF\xA1\x85" => "\xE5\x96\x84", + "\xF0\xAF\xA1\x86" => "\xE5\x96\x84", + "\xF0\xAF\xA1\x87" => "\xE5\x96\x99", + "\xF0\xAF\xA1\x88" => "\xE5\x96\xAB", + "\xF0\xAF\xA1\x89" => "\xE5\x96\xB3", + "\xF0\xAF\xA1\x8A" => "\xE5\x97\x82", + "\xF0\xAF\xA1\x8B" => "\xE5\x9C\x96", + "\xF0\xAF\xA1\x8C" => "\xE5\x98\x86", + "\xF0\xAF\xA1\x8D" => "\xE5\x9C\x97", + "\xF0\xAF\xA1\x8E" => "\xE5\x99\x91", + "\xF0\xAF\xA1\x8F" => "\xE5\x99\xB4", + "\xF0\xAF\xA1\x90" => "\xE5\x88\x87", + "\xF0\xAF\xA1\x91" => "\xE5\xA3\xAE", + "\xF0\xAF\xA1\x92" => "\xE5\x9F\x8E", + "\xF0\xAF\xA1\x93" => "\xE5\x9F\xB4", + "\xF0\xAF\xA1\x94" => "\xE5\xA0\x8D", + "\xF0\xAF\xA1\x95" => "\xE5\x9E\x8B", + "\xF0\xAF\xA1\x96" => "\xE5\xA0\xB2", + "\xF0\xAF\xA1\x97" => "\xE5\xA0\xB1", + "\xF0\xAF\xA1\x98" => "\xE5\xA2\xAC", + "\xF0\xAF\xA1\x99" => "\xF0\xA1\x93\xA4", + "\xF0\xAF\xA1\x9A" => "\xE5\xA3\xB2", + "\xF0\xAF\xA1\x9B" => "\xE5\xA3\xB7", + "\xF0\xAF\xA1\x9C" => "\xE5\xA4\x86", + "\xF0\xAF\xA1\x9D" => "\xE5\xA4\x9A", + "\xF0\xAF\xA1\x9E" => "\xE5\xA4\xA2", + "\xF0\xAF\xA1\x9F" => "\xE5\xA5\xA2", + "\xF0\xAF\xA1\xA0" => "\xF0\xA1\x9A\xA8", + "\xF0\xAF\xA1\xA1" => "\xF0\xA1\x9B\xAA", + "\xF0\xAF\xA1\xA2" => "\xE5\xA7\xAC", + "\xF0\xAF\xA1\xA3" => "\xE5\xA8\x9B", + "\xF0\xAF\xA1\xA4" => "\xE5\xA8\xA7", + "\xF0\xAF\xA1\xA5" => "\xE5\xA7\x98", + "\xF0\xAF\xA1\xA6" => "\xE5\xA9\xA6", + "\xF0\xAF\xA1\xA7" => "\xE3\x9B\xAE", + "\xF0\xAF\xA1\xA8" => "\xE3\x9B\xBC", + "\xF0\xAF\xA1\xA9" => "\xE5\xAC\x88", + "\xF0\xAF\xA1\xAA" => "\xE5\xAC\xBE", + "\xF0\xAF\xA1\xAB" => "\xE5\xAC\xBE", + "\xF0\xAF\xA1\xAC" => "\xF0\xA1\xA7\x88", + "\xF0\xAF\xA1\xAD" => "\xE5\xAF\x83", + "\xF0\xAF\xA1\xAE" => "\xE5\xAF\x98", + "\xF0\xAF\xA1\xAF" => "\xE5\xAF\xA7", + "\xF0\xAF\xA1\xB0" => "\xE5\xAF\xB3", + "\xF0\xAF\xA1\xB1" => "\xF0\xA1\xAC\x98", + "\xF0\xAF\xA1\xB2" => "\xE5\xAF\xBF", + "\xF0\xAF\xA1\xB3" => "\xE5\xB0\x86", + "\xF0\xAF\xA1\xB4" => "\xE5\xBD\x93", + "\xF0\xAF\xA1\xB5" => "\xE5\xB0\xA2", + "\xF0\xAF\xA1\xB6" => "\xE3\x9E\x81", + "\xF0\xAF\xA1\xB7" => "\xE5\xB1\xA0", + "\xF0\xAF\xA1\xB8" => "\xE5\xB1\xAE", + "\xF0\xAF\xA1\xB9" => "\xE5\xB3\x80", + "\xF0\xAF\xA1\xBA" => "\xE5\xB2\x8D", + "\xF0\xAF\xA1\xBB" => "\xF0\xA1\xB7\xA4", + "\xF0\xAF\xA1\xBC" => "\xE5\xB5\x83", + "\xF0\xAF\xA1\xBD" => "\xF0\xA1\xB7\xA6", + "\xF0\xAF\xA1\xBE" => "\xE5\xB5\xAE", + "\xF0\xAF\xA1\xBF" => "\xE5\xB5\xAB", + "\xF0\xAF\xA2\x80" => "\xE5\xB5\xBC", + "\xF0\xAF\xA2\x81" => "\xE5\xB7\xA1", + "\xF0\xAF\xA2\x82" => "\xE5\xB7\xA2", + "\xF0\xAF\xA2\x83" => "\xE3\xA0\xAF", + "\xF0\xAF\xA2\x84" => "\xE5\xB7\xBD", + "\xF0\xAF\xA2\x85" => "\xE5\xB8\xA8", + "\xF0\xAF\xA2\x86" => "\xE5\xB8\xBD", + "\xF0\xAF\xA2\x87" => "\xE5\xB9\xA9", + "\xF0\xAF\xA2\x88" => "\xE3\xA1\xA2", + "\xF0\xAF\xA2\x89" => "\xF0\xA2\x86\x83", + "\xF0\xAF\xA2\x8A" => "\xE3\xA1\xBC", + "\xF0\xAF\xA2\x8B" => "\xE5\xBA\xB0", + "\xF0\xAF\xA2\x8C" => "\xE5\xBA\xB3", + "\xF0\xAF\xA2\x8D" => "\xE5\xBA\xB6", + "\xF0\xAF\xA2\x8E" => "\xE5\xBB\x8A", + "\xF0\xAF\xA2\x8F" => "\xF0\xAA\x8E\x92", + "\xF0\xAF\xA2\x90" => "\xE5\xBB\xBE", + "\xF0\xAF\xA2\x91" => "\xF0\xA2\x8C\xB1", + "\xF0\xAF\xA2\x92" => "\xF0\xA2\x8C\xB1", + "\xF0\xAF\xA2\x93" => "\xE8\x88\x81", + "\xF0\xAF\xA2\x94" => "\xE5\xBC\xA2", + "\xF0\xAF\xA2\x95" => "\xE5\xBC\xA2", + "\xF0\xAF\xA2\x96" => "\xE3\xA3\x87", + "\xF0\xAF\xA2\x97" => "\xF0\xA3\x8A\xB8", + "\xF0\xAF\xA2\x98" => "\xF0\xA6\x87\x9A", + "\xF0\xAF\xA2\x99" => "\xE5\xBD\xA2", + "\xF0\xAF\xA2\x9A" => "\xE5\xBD\xAB", + "\xF0\xAF\xA2\x9B" => "\xE3\xA3\xA3", + "\xF0\xAF\xA2\x9C" => "\xE5\xBE\x9A", + "\xF0\xAF\xA2\x9D" => "\xE5\xBF\x8D", + "\xF0\xAF\xA2\x9E" => "\xE5\xBF\x97", + "\xF0\xAF\xA2\x9F" => "\xE5\xBF\xB9", + "\xF0\xAF\xA2\xA0" => "\xE6\x82\x81", + "\xF0\xAF\xA2\xA1" => "\xE3\xA4\xBA", + "\xF0\xAF\xA2\xA2" => "\xE3\xA4\x9C", + "\xF0\xAF\xA2\xA3" => "\xE6\x82\x94", + "\xF0\xAF\xA2\xA4" => "\xF0\xA2\x9B\x94", + "\xF0\xAF\xA2\xA5" => "\xE6\x83\x87", + "\xF0\xAF\xA2\xA6" => "\xE6\x85\x88", + "\xF0\xAF\xA2\xA7" => "\xE6\x85\x8C", + "\xF0\xAF\xA2\xA8" => "\xE6\x85\x8E", + "\xF0\xAF\xA2\xA9" => "\xE6\x85\x8C", + "\xF0\xAF\xA2\xAA" => "\xE6\x85\xBA", + "\xF0\xAF\xA2\xAB" => "\xE6\x86\x8E", + "\xF0\xAF\xA2\xAC" => "\xE6\x86\xB2", + "\xF0\xAF\xA2\xAD" => "\xE6\x86\xA4", + "\xF0\xAF\xA2\xAE" => "\xE6\x86\xAF", + "\xF0\xAF\xA2\xAF" => "\xE6\x87\x9E", + "\xF0\xAF\xA2\xB0" => "\xE6\x87\xB2", + "\xF0\xAF\xA2\xB1" => "\xE6\x87\xB6", + "\xF0\xAF\xA2\xB2" => "\xE6\x88\x90", + "\xF0\xAF\xA2\xB3" => "\xE6\x88\x9B", + "\xF0\xAF\xA2\xB4" => "\xE6\x89\x9D", + "\xF0\xAF\xA2\xB5" => "\xE6\x8A\xB1", + "\xF0\xAF\xA2\xB6" => "\xE6\x8B\x94", + "\xF0\xAF\xA2\xB7" => "\xE6\x8D\x90", + "\xF0\xAF\xA2\xB8" => "\xF0\xA2\xAC\x8C", + "\xF0\xAF\xA2\xB9" => "\xE6\x8C\xBD", + "\xF0\xAF\xA2\xBA" => "\xE6\x8B\xBC", + "\xF0\xAF\xA2\xBB" => "\xE6\x8D\xA8", + "\xF0\xAF\xA2\xBC" => "\xE6\x8E\x83", + "\xF0\xAF\xA2\xBD" => "\xE6\x8F\xA4", + "\xF0\xAF\xA2\xBE" => "\xF0\xA2\xAF\xB1", + "\xF0\xAF\xA2\xBF" => "\xE6\x90\xA2", + "\xF0\xAF\xA3\x80" => "\xE6\x8F\x85", + "\xF0\xAF\xA3\x81" => "\xE6\x8E\xA9", + "\xF0\xAF\xA3\x82" => "\xE3\xA8\xAE", + "\xF0\xAF\xA3\x83" => "\xE6\x91\xA9", + "\xF0\xAF\xA3\x84" => "\xE6\x91\xBE", + "\xF0\xAF\xA3\x85" => "\xE6\x92\x9D", + "\xF0\xAF\xA3\x86" => "\xE6\x91\xB7", + "\xF0\xAF\xA3\x87" => "\xE3\xA9\xAC", + "\xF0\xAF\xA3\x88" => "\xE6\x95\x8F", + "\xF0\xAF\xA3\x89" => "\xE6\x95\xAC", + "\xF0\xAF\xA3\x8A" => "\xF0\xA3\x80\x8A", + "\xF0\xAF\xA3\x8B" => "\xE6\x97\xA3", + "\xF0\xAF\xA3\x8C" => "\xE6\x9B\xB8", + "\xF0\xAF\xA3\x8D" => "\xE6\x99\x89", + "\xF0\xAF\xA3\x8E" => "\xE3\xAC\x99", + "\xF0\xAF\xA3\x8F" => "\xE6\x9A\x91", + "\xF0\xAF\xA3\x90" => "\xE3\xAC\x88", + "\xF0\xAF\xA3\x91" => "\xE3\xAB\xA4", + "\xF0\xAF\xA3\x92" => "\xE5\x86\x92", + "\xF0\xAF\xA3\x93" => "\xE5\x86\x95", + "\xF0\xAF\xA3\x94" => "\xE6\x9C\x80", + "\xF0\xAF\xA3\x95" => "\xE6\x9A\x9C", + "\xF0\xAF\xA3\x96" => "\xE8\x82\xAD", + "\xF0\xAF\xA3\x97" => "\xE4\x8F\x99", + "\xF0\xAF\xA3\x98" => "\xE6\x9C\x97", + "\xF0\xAF\xA3\x99" => "\xE6\x9C\x9B", + "\xF0\xAF\xA3\x9A" => "\xE6\x9C\xA1", + "\xF0\xAF\xA3\x9B" => "\xE6\x9D\x9E", + "\xF0\xAF\xA3\x9C" => "\xE6\x9D\x93", + "\xF0\xAF\xA3\x9D" => "\xF0\xA3\x8F\x83", + "\xF0\xAF\xA3\x9E" => "\xE3\xAD\x89", + "\xF0\xAF\xA3\x9F" => "\xE6\x9F\xBA", + "\xF0\xAF\xA3\xA0" => "\xE6\x9E\x85", + "\xF0\xAF\xA3\xA1" => "\xE6\xA1\x92", + "\xF0\xAF\xA3\xA2" => "\xE6\xA2\x85", + "\xF0\xAF\xA3\xA3" => "\xF0\xA3\x91\xAD", + "\xF0\xAF\xA3\xA4" => "\xE6\xA2\x8E", + "\xF0\xAF\xA3\xA5" => "\xE6\xA0\x9F", + "\xF0\xAF\xA3\xA6" => "\xE6\xA4\x94", + "\xF0\xAF\xA3\xA7" => "\xE3\xAE\x9D", + "\xF0\xAF\xA3\xA8" => "\xE6\xA5\x82", + "\xF0\xAF\xA3\xA9" => "\xE6\xA6\xA3", + "\xF0\xAF\xA3\xAA" => "\xE6\xA7\xAA", + "\xF0\xAF\xA3\xAB" => "\xE6\xAA\xA8", + "\xF0\xAF\xA3\xAC" => "\xF0\xA3\x9A\xA3", + "\xF0\xAF\xA3\xAD" => "\xE6\xAB\x9B", + "\xF0\xAF\xA3\xAE" => "\xE3\xB0\x98", + "\xF0\xAF\xA3\xAF" => "\xE6\xAC\xA1", + "\xF0\xAF\xA3\xB0" => "\xF0\xA3\xA2\xA7", + "\xF0\xAF\xA3\xB1" => "\xE6\xAD\x94", + "\xF0\xAF\xA3\xB2" => "\xE3\xB1\x8E", + "\xF0\xAF\xA3\xB3" => "\xE6\xAD\xB2", + "\xF0\xAF\xA3\xB4" => "\xE6\xAE\x9F", + "\xF0\xAF\xA3\xB5" => "\xE6\xAE\xBA", + "\xF0\xAF\xA3\xB6" => "\xE6\xAE\xBB", + "\xF0\xAF\xA3\xB7" => "\xF0\xA3\xAA\x8D", + "\xF0\xAF\xA3\xB8" => "\xF0\xA1\xB4\x8B", + "\xF0\xAF\xA3\xB9" => "\xF0\xA3\xAB\xBA", + "\xF0\xAF\xA3\xBA" => "\xE6\xB1\x8E", + "\xF0\xAF\xA3\xBB" => "\xF0\xA3\xB2\xBC", + "\xF0\xAF\xA3\xBC" => "\xE6\xB2\xBF", + "\xF0\xAF\xA3\xBD" => "\xE6\xB3\x8D", + "\xF0\xAF\xA3\xBE" => "\xE6\xB1\xA7", + "\xF0\xAF\xA3\xBF" => "\xE6\xB4\x96", + "\xF0\xAF\xA4\x80" => "\xE6\xB4\xBE", + "\xF0\xAF\xA4\x81" => "\xE6\xB5\xB7", + "\xF0\xAF\xA4\x82" => "\xE6\xB5\x81", + "\xF0\xAF\xA4\x83" => "\xE6\xB5\xA9", + "\xF0\xAF\xA4\x84" => "\xE6\xB5\xB8", + "\xF0\xAF\xA4\x85" => "\xE6\xB6\x85", + "\xF0\xAF\xA4\x86" => "\xF0\xA3\xB4\x9E", + "\xF0\xAF\xA4\x87" => "\xE6\xB4\xB4", + "\xF0\xAF\xA4\x88" => "\xE6\xB8\xAF", + "\xF0\xAF\xA4\x89" => "\xE6\xB9\xAE", + "\xF0\xAF\xA4\x8A" => "\xE3\xB4\xB3", + "\xF0\xAF\xA4\x8B" => "\xE6\xBB\x8B", + "\xF0\xAF\xA4\x8C" => "\xE6\xBB\x87", + "\xF0\xAF\xA4\x8D" => "\xF0\xA3\xBB\x91", + "\xF0\xAF\xA4\x8E" => "\xE6\xB7\xB9", + "\xF0\xAF\xA4\x8F" => "\xE6\xBD\xAE", + "\xF0\xAF\xA4\x90" => "\xF0\xA3\xBD\x9E", + "\xF0\xAF\xA4\x91" => "\xF0\xA3\xBE\x8E", + "\xF0\xAF\xA4\x92" => "\xE6\xBF\x86", + "\xF0\xAF\xA4\x93" => "\xE7\x80\xB9", + "\xF0\xAF\xA4\x94" => "\xE7\x80\x9E", + "\xF0\xAF\xA4\x95" => "\xE7\x80\x9B", + "\xF0\xAF\xA4\x96" => "\xE3\xB6\x96", + "\xF0\xAF\xA4\x97" => "\xE7\x81\x8A", + "\xF0\xAF\xA4\x98" => "\xE7\x81\xBD", + "\xF0\xAF\xA4\x99" => "\xE7\x81\xB7", + "\xF0\xAF\xA4\x9A" => "\xE7\x82\xAD", + "\xF0\xAF\xA4\x9B" => "\xF0\xA0\x94\xA5", + "\xF0\xAF\xA4\x9C" => "\xE7\x85\x85", + "\xF0\xAF\xA4\x9D" => "\xF0\xA4\x89\xA3", + "\xF0\xAF\xA4\x9E" => "\xE7\x86\x9C", + "\xF0\xAF\xA4\x9F" => "\xF0\xA4\x8E\xAB", + "\xF0\xAF\xA4\xA0" => "\xE7\x88\xA8", + "\xF0\xAF\xA4\xA1" => "\xE7\x88\xB5", + "\xF0\xAF\xA4\xA2" => "\xE7\x89\x90", + "\xF0\xAF\xA4\xA3" => "\xF0\xA4\x98\x88", + "\xF0\xAF\xA4\xA4" => "\xE7\x8A\x80", + "\xF0\xAF\xA4\xA5" => "\xE7\x8A\x95", + "\xF0\xAF\xA4\xA6" => "\xF0\xA4\x9C\xB5", + "\xF0\xAF\xA4\xA7" => "\xF0\xA4\xA0\x94", + "\xF0\xAF\xA4\xA8" => "\xE7\x8D\xBA", + "\xF0\xAF\xA4\xA9" => "\xE7\x8E\x8B", + "\xF0\xAF\xA4\xAA" => "\xE3\xBA\xAC", + "\xF0\xAF\xA4\xAB" => "\xE7\x8E\xA5", + "\xF0\xAF\xA4\xAC" => "\xE3\xBA\xB8", + "\xF0\xAF\xA4\xAD" => "\xE3\xBA\xB8", + "\xF0\xAF\xA4\xAE" => "\xE7\x91\x87", + "\xF0\xAF\xA4\xAF" => "\xE7\x91\x9C", + "\xF0\xAF\xA4\xB0" => "\xE7\x91\xB1", + "\xF0\xAF\xA4\xB1" => "\xE7\x92\x85", + "\xF0\xAF\xA4\xB2" => "\xE7\x93\x8A", + "\xF0\xAF\xA4\xB3" => "\xE3\xBC\x9B", + "\xF0\xAF\xA4\xB4" => "\xE7\x94\xA4", + "\xF0\xAF\xA4\xB5" => "\xF0\xA4\xB0\xB6", + "\xF0\xAF\xA4\xB6" => "\xE7\x94\xBE", + "\xF0\xAF\xA4\xB7" => "\xF0\xA4\xB2\x92", + "\xF0\xAF\xA4\xB8" => "\xE7\x95\xB0", + "\xF0\xAF\xA4\xB9" => "\xF0\xA2\x86\x9F", + "\xF0\xAF\xA4\xBA" => "\xE7\x98\x90", + "\xF0\xAF\xA4\xBB" => "\xF0\xA4\xBE\xA1", + "\xF0\xAF\xA4\xBC" => "\xF0\xA4\xBE\xB8", + "\xF0\xAF\xA4\xBD" => "\xF0\xA5\x81\x84", + "\xF0\xAF\xA4\xBE" => "\xE3\xBF\xBC", + "\xF0\xAF\xA4\xBF" => "\xE4\x80\x88", + "\xF0\xAF\xA5\x80" => "\xE7\x9B\xB4", + "\xF0\xAF\xA5\x81" => "\xF0\xA5\x83\xB3", + "\xF0\xAF\xA5\x82" => "\xF0\xA5\x83\xB2", + "\xF0\xAF\xA5\x83" => "\xF0\xA5\x84\x99", + "\xF0\xAF\xA5\x84" => "\xF0\xA5\x84\xB3", + "\xF0\xAF\xA5\x85" => "\xE7\x9C\x9E", + "\xF0\xAF\xA5\x86" => "\xE7\x9C\x9F", + "\xF0\xAF\xA5\x87" => "\xE7\x9C\x9F", + "\xF0\xAF\xA5\x88" => "\xE7\x9D\x8A", + "\xF0\xAF\xA5\x89" => "\xE4\x80\xB9", + "\xF0\xAF\xA5\x8A" => "\xE7\x9E\x8B", + "\xF0\xAF\xA5\x8B" => "\xE4\x81\x86", + "\xF0\xAF\xA5\x8C" => "\xE4\x82\x96", + "\xF0\xAF\xA5\x8D" => "\xF0\xA5\x90\x9D", + "\xF0\xAF\xA5\x8E" => "\xE7\xA1\x8E", + "\xF0\xAF\xA5\x8F" => "\xE7\xA2\x8C", + "\xF0\xAF\xA5\x90" => "\xE7\xA3\x8C", + "\xF0\xAF\xA5\x91" => "\xE4\x83\xA3", + "\xF0\xAF\xA5\x92" => "\xF0\xA5\x98\xA6", + "\xF0\xAF\xA5\x93" => "\xE7\xA5\x96", + "\xF0\xAF\xA5\x94" => "\xF0\xA5\x9A\x9A", + "\xF0\xAF\xA5\x95" => "\xF0\xA5\x9B\x85", + "\xF0\xAF\xA5\x96" => "\xE7\xA6\x8F", + "\xF0\xAF\xA5\x97" => "\xE7\xA7\xAB", + "\xF0\xAF\xA5\x98" => "\xE4\x84\xAF", + "\xF0\xAF\xA5\x99" => "\xE7\xA9\x80", + "\xF0\xAF\xA5\x9A" => "\xE7\xA9\x8A", + "\xF0\xAF\xA5\x9B" => "\xE7\xA9\x8F", + "\xF0\xAF\xA5\x9C" => "\xF0\xA5\xA5\xBC", + "\xF0\xAF\xA5\x9D" => "\xF0\xA5\xAA\xA7", + "\xF0\xAF\xA5\x9E" => "\xF0\xA5\xAA\xA7", + "\xF0\xAF\xA5\x9F" => "\xE7\xAB\xAE", + "\xF0\xAF\xA5\xA0" => "\xE4\x88\x82", + "\xF0\xAF\xA5\xA1" => "\xF0\xA5\xAE\xAB", + "\xF0\xAF\xA5\xA2" => "\xE7\xAF\x86", + "\xF0\xAF\xA5\xA3" => "\xE7\xAF\x89", + "\xF0\xAF\xA5\xA4" => "\xE4\x88\xA7", + "\xF0\xAF\xA5\xA5" => "\xF0\xA5\xB2\x80", + "\xF0\xAF\xA5\xA6" => "\xE7\xB3\x92", + "\xF0\xAF\xA5\xA7" => "\xE4\x8A\xA0", + "\xF0\xAF\xA5\xA8" => "\xE7\xB3\xA8", + "\xF0\xAF\xA5\xA9" => "\xE7\xB3\xA3", + "\xF0\xAF\xA5\xAA" => "\xE7\xB4\x80", + "\xF0\xAF\xA5\xAB" => "\xF0\xA5\xBE\x86", + "\xF0\xAF\xA5\xAC" => "\xE7\xB5\xA3", + "\xF0\xAF\xA5\xAD" => "\xE4\x8C\x81", + "\xF0\xAF\xA5\xAE" => "\xE7\xB7\x87", + "\xF0\xAF\xA5\xAF" => "\xE7\xB8\x82", + "\xF0\xAF\xA5\xB0" => "\xE7\xB9\x85", + "\xF0\xAF\xA5\xB1" => "\xE4\x8C\xB4", + "\xF0\xAF\xA5\xB2" => "\xF0\xA6\x88\xA8", + "\xF0\xAF\xA5\xB3" => "\xF0\xA6\x89\x87", + "\xF0\xAF\xA5\xB4" => "\xE4\x8D\x99", + "\xF0\xAF\xA5\xB5" => "\xF0\xA6\x8B\x99", + "\xF0\xAF\xA5\xB6" => "\xE7\xBD\xBA", + "\xF0\xAF\xA5\xB7" => "\xF0\xA6\x8C\xBE", + "\xF0\xAF\xA5\xB8" => "\xE7\xBE\x95", + "\xF0\xAF\xA5\xB9" => "\xE7\xBF\xBA", + "\xF0\xAF\xA5\xBA" => "\xE8\x80\x85", + "\xF0\xAF\xA5\xBB" => "\xF0\xA6\x93\x9A", + "\xF0\xAF\xA5\xBC" => "\xF0\xA6\x94\xA3", + "\xF0\xAF\xA5\xBD" => "\xE8\x81\xA0", + "\xF0\xAF\xA5\xBE" => "\xF0\xA6\x96\xA8", + "\xF0\xAF\xA5\xBF" => "\xE8\x81\xB0", + "\xF0\xAF\xA6\x80" => "\xF0\xA3\x8D\x9F", + "\xF0\xAF\xA6\x81" => "\xE4\x8F\x95", + "\xF0\xAF\xA6\x82" => "\xE8\x82\xB2", + "\xF0\xAF\xA6\x83" => "\xE8\x84\x83", + "\xF0\xAF\xA6\x84" => "\xE4\x90\x8B", + "\xF0\xAF\xA6\x85" => "\xE8\x84\xBE", + "\xF0\xAF\xA6\x86" => "\xE5\xAA\xB5", + "\xF0\xAF\xA6\x87" => "\xF0\xA6\x9E\xA7", + "\xF0\xAF\xA6\x88" => "\xF0\xA6\x9E\xB5", + "\xF0\xAF\xA6\x89" => "\xF0\xA3\x8E\x93", + "\xF0\xAF\xA6\x8A" => "\xF0\xA3\x8E\x9C", + "\xF0\xAF\xA6\x8B" => "\xE8\x88\x81", + "\xF0\xAF\xA6\x8C" => "\xE8\x88\x84", + "\xF0\xAF\xA6\x8D" => "\xE8\xBE\x9E", + "\xF0\xAF\xA6\x8E" => "\xE4\x91\xAB", + "\xF0\xAF\xA6\x8F" => "\xE8\x8A\x91", + "\xF0\xAF\xA6\x90" => "\xE8\x8A\x8B", + "\xF0\xAF\xA6\x91" => "\xE8\x8A\x9D", + "\xF0\xAF\xA6\x92" => "\xE5\x8A\xB3", + "\xF0\xAF\xA6\x93" => "\xE8\x8A\xB1", + "\xF0\xAF\xA6\x94" => "\xE8\x8A\xB3", + "\xF0\xAF\xA6\x95" => "\xE8\x8A\xBD", + "\xF0\xAF\xA6\x96" => "\xE8\x8B\xA6", + "\xF0\xAF\xA6\x97" => "\xF0\xA6\xAC\xBC", + "\xF0\xAF\xA6\x98" => "\xE8\x8B\xA5", + "\xF0\xAF\xA6\x99" => "\xE8\x8C\x9D", + "\xF0\xAF\xA6\x9A" => "\xE8\x8D\xA3", + "\xF0\xAF\xA6\x9B" => "\xE8\x8E\xAD", + "\xF0\xAF\xA6\x9C" => "\xE8\x8C\xA3", + "\xF0\xAF\xA6\x9D" => "\xE8\x8E\xBD", + "\xF0\xAF\xA6\x9E" => "\xE8\x8F\xA7", + "\xF0\xAF\xA6\x9F" => "\xE8\x91\x97", + "\xF0\xAF\xA6\xA0" => "\xE8\x8D\x93", + "\xF0\xAF\xA6\xA1" => "\xE8\x8F\x8A", + "\xF0\xAF\xA6\xA2" => "\xE8\x8F\x8C", + "\xF0\xAF\xA6\xA3" => "\xE8\x8F\x9C", + "\xF0\xAF\xA6\xA4" => "\xF0\xA6\xB0\xB6", + "\xF0\xAF\xA6\xA5" => "\xF0\xA6\xB5\xAB", + "\xF0\xAF\xA6\xA6" => "\xF0\xA6\xB3\x95", + "\xF0\xAF\xA6\xA7" => "\xE4\x94\xAB", + "\xF0\xAF\xA6\xA8" => "\xE8\x93\xB1", + "\xF0\xAF\xA6\xA9" => "\xE8\x93\xB3", + "\xF0\xAF\xA6\xAA" => "\xE8\x94\x96", + "\xF0\xAF\xA6\xAB" => "\xF0\xA7\x8F\x8A", + "\xF0\xAF\xA6\xAC" => "\xE8\x95\xA4", + "\xF0\xAF\xA6\xAD" => "\xF0\xA6\xBC\xAC", + "\xF0\xAF\xA6\xAE" => "\xE4\x95\x9D", + "\xF0\xAF\xA6\xAF" => "\xE4\x95\xA1", + "\xF0\xAF\xA6\xB0" => "\xF0\xA6\xBE\xB1", + "\xF0\xAF\xA6\xB1" => "\xF0\xA7\x83\x92", + "\xF0\xAF\xA6\xB2" => "\xE4\x95\xAB", + "\xF0\xAF\xA6\xB3" => "\xE8\x99\x90", + "\xF0\xAF\xA6\xB4" => "\xE8\x99\x9C", + "\xF0\xAF\xA6\xB5" => "\xE8\x99\xA7", + "\xF0\xAF\xA6\xB6" => "\xE8\x99\xA9", + "\xF0\xAF\xA6\xB7" => "\xE8\x9A\xA9", + "\xF0\xAF\xA6\xB8" => "\xE8\x9A\x88", + "\xF0\xAF\xA6\xB9" => "\xE8\x9C\x8E", + "\xF0\xAF\xA6\xBA" => "\xE8\x9B\xA2", + "\xF0\xAF\xA6\xBB" => "\xE8\x9D\xB9", + "\xF0\xAF\xA6\xBC" => "\xE8\x9C\xA8", + "\xF0\xAF\xA6\xBD" => "\xE8\x9D\xAB", + "\xF0\xAF\xA6\xBE" => "\xE8\x9E\x86", + "\xF0\xAF\xA6\xBF" => "\xE4\x97\x97", + "\xF0\xAF\xA7\x80" => "\xE8\x9F\xA1", + "\xF0\xAF\xA7\x81" => "\xE8\xA0\x81", + "\xF0\xAF\xA7\x82" => "\xE4\x97\xB9", + "\xF0\xAF\xA7\x83" => "\xE8\xA1\xA0", + "\xF0\xAF\xA7\x84" => "\xE8\xA1\xA3", + "\xF0\xAF\xA7\x85" => "\xF0\xA7\x99\xA7", + "\xF0\xAF\xA7\x86" => "\xE8\xA3\x97", + "\xF0\xAF\xA7\x87" => "\xE8\xA3\x9E", + "\xF0\xAF\xA7\x88" => "\xE4\x98\xB5", + "\xF0\xAF\xA7\x89" => "\xE8\xA3\xBA", + "\xF0\xAF\xA7\x8A" => "\xE3\x92\xBB", + "\xF0\xAF\xA7\x8B" => "\xF0\xA7\xA2\xAE", + "\xF0\xAF\xA7\x8C" => "\xF0\xA7\xA5\xA6", + "\xF0\xAF\xA7\x8D" => "\xE4\x9A\xBE", + "\xF0\xAF\xA7\x8E" => "\xE4\x9B\x87", + "\xF0\xAF\xA7\x8F" => "\xE8\xAA\xA0", + "\xF0\xAF\xA7\x90" => "\xE8\xAB\xAD", + "\xF0\xAF\xA7\x91" => "\xE8\xAE\x8A", + "\xF0\xAF\xA7\x92" => "\xE8\xB1\x95", + "\xF0\xAF\xA7\x93" => "\xF0\xA7\xB2\xA8", + "\xF0\xAF\xA7\x94" => "\xE8\xB2\xAB", + "\xF0\xAF\xA7\x95" => "\xE8\xB3\x81", + "\xF0\xAF\xA7\x96" => "\xE8\xB4\x9B", + "\xF0\xAF\xA7\x97" => "\xE8\xB5\xB7", + "\xF0\xAF\xA7\x98" => "\xF0\xA7\xBC\xAF", + "\xF0\xAF\xA7\x99" => "\xF0\xA0\xA0\x84", + "\xF0\xAF\xA7\x9A" => "\xE8\xB7\x8B", + "\xF0\xAF\xA7\x9B" => "\xE8\xB6\xBC", + "\xF0\xAF\xA7\x9C" => "\xE8\xB7\xB0", + "\xF0\xAF\xA7\x9D" => "\xF0\xA0\xA3\x9E", + "\xF0\xAF\xA7\x9E" => "\xE8\xBB\x94", + "\xF0\xAF\xA7\x9F" => "\xE8\xBC\xB8", + "\xF0\xAF\xA7\xA0" => "\xF0\xA8\x97\x92", + "\xF0\xAF\xA7\xA1" => "\xF0\xA8\x97\xAD", + "\xF0\xAF\xA7\xA2" => "\xE9\x82\x94", + "\xF0\xAF\xA7\xA3" => "\xE9\x83\xB1", + "\xF0\xAF\xA7\xA4" => "\xE9\x84\x91", + "\xF0\xAF\xA7\xA5" => "\xF0\xA8\x9C\xAE", + "\xF0\xAF\xA7\xA6" => "\xE9\x84\x9B", + "\xF0\xAF\xA7\xA7" => "\xE9\x88\xB8", + "\xF0\xAF\xA7\xA8" => "\xE9\x8B\x97", + "\xF0\xAF\xA7\xA9" => "\xE9\x8B\x98", + "\xF0\xAF\xA7\xAA" => "\xE9\x89\xBC", + "\xF0\xAF\xA7\xAB" => "\xE9\x8F\xB9", + "\xF0\xAF\xA7\xAC" => "\xE9\x90\x95", + "\xF0\xAF\xA7\xAD" => "\xF0\xA8\xAF\xBA", + "\xF0\xAF\xA7\xAE" => "\xE9\x96\x8B", + "\xF0\xAF\xA7\xAF" => "\xE4\xA6\x95", + "\xF0\xAF\xA7\xB0" => "\xE9\x96\xB7", + "\xF0\xAF\xA7\xB1" => "\xF0\xA8\xB5\xB7", + "\xF0\xAF\xA7\xB2" => "\xE4\xA7\xA6", + "\xF0\xAF\xA7\xB3" => "\xE9\x9B\x83", + "\xF0\xAF\xA7\xB4" => "\xE5\xB6\xB2", + "\xF0\xAF\xA7\xB5" => "\xE9\x9C\xA3", + "\xF0\xAF\xA7\xB6" => "\xF0\xA9\x85\x85", + "\xF0\xAF\xA7\xB7" => "\xF0\xA9\x88\x9A", + "\xF0\xAF\xA7\xB8" => "\xE4\xA9\xAE", + "\xF0\xAF\xA7\xB9" => "\xE4\xA9\xB6", + "\xF0\xAF\xA7\xBA" => "\xE9\x9F\xA0", + "\xF0\xAF\xA7\xBB" => "\xF0\xA9\x90\x8A", + "\xF0\xAF\xA7\xBC" => "\xE4\xAA\xB2", + "\xF0\xAF\xA7\xBD" => "\xF0\xA9\x92\x96", + "\xF0\xAF\xA7\xBE" => "\xE9\xA0\x8B", + "\xF0\xAF\xA7\xBF" => "\xE9\xA0\x8B", + "\xF0\xAF\xA8\x80" => "\xE9\xA0\xA9", + "\xF0\xAF\xA8\x81" => "\xF0\xA9\x96\xB6", + "\xF0\xAF\xA8\x82" => "\xE9\xA3\xA2", + "\xF0\xAF\xA8\x83" => "\xE4\xAC\xB3", + "\xF0\xAF\xA8\x84" => "\xE9\xA4\xA9", + "\xF0\xAF\xA8\x85" => "\xE9\xA6\xA7", + "\xF0\xAF\xA8\x86" => "\xE9\xA7\x82", + "\xF0\xAF\xA8\x87" => "\xE9\xA7\xBE", + "\xF0\xAF\xA8\x88" => "\xE4\xAF\x8E", + "\xF0\xAF\xA8\x89" => "\xF0\xA9\xAC\xB0", + "\xF0\xAF\xA8\x8A" => "\xE9\xAC\x92", + "\xF0\xAF\xA8\x8B" => "\xE9\xB1\x80", + "\xF0\xAF\xA8\x8C" => "\xE9\xB3\xBD", + "\xF0\xAF\xA8\x8D" => "\xE4\xB3\x8E", + "\xF0\xAF\xA8\x8E" => "\xE4\xB3\xAD", + "\xF0\xAF\xA8\x8F" => "\xE9\xB5\xA7", + "\xF0\xAF\xA8\x90" => "\xF0\xAA\x83\x8E", + "\xF0\xAF\xA8\x91" => "\xE4\xB3\xB8", + "\xF0\xAF\xA8\x92" => "\xF0\xAA\x84\x85", + "\xF0\xAF\xA8\x93" => "\xF0\xAA\x88\x8E", + "\xF0\xAF\xA8\x94" => "\xF0\xAA\x8A\x91", + "\xF0\xAF\xA8\x95" => "\xE9\xBA\xBB", + "\xF0\xAF\xA8\x96" => "\xE4\xB5\x96", + "\xF0\xAF\xA8\x97" => "\xE9\xBB\xB9", + "\xF0\xAF\xA8\x98" => "\xE9\xBB\xBE", + "\xF0\xAF\xA8\x99" => "\xE9\xBC\x85", + "\xF0\xAF\xA8\x9A" => "\xE9\xBC\x8F", + "\xF0\xAF\xA8\x9B" => "\xE9\xBC\x96", + "\xF0\xAF\xA8\x9C" => "\xE9\xBC\xBB", + "\xF0\xAF\xA8\x9D" => "\xF0\xAA\x98\x80", + ); +} + +?> \ No newline at end of file diff --git a/Sources/Unicode/DecompositionCompatibility.php b/Sources/Unicode/DecompositionCompatibility.php new file mode 100644 index 0000000..ee4618a --- /dev/null +++ b/Sources/Unicode/DecompositionCompatibility.php @@ -0,0 +1,3843 @@ + "\x20", + "\xC2\xA8" => "\x20\xCC\x88", + "\xC2\xAA" => "\x61", + "\xC2\xAF" => "\x20\xCC\x84", + "\xC2\xB2" => "\x32", + "\xC2\xB3" => "\x33", + "\xC2\xB4" => "\x20\xCC\x81", + "\xC2\xB5" => "\xCE\xBC", + "\xC2\xB8" => "\x20\xCC\xA7", + "\xC2\xB9" => "\x31", + "\xC2\xBA" => "\x6F", + "\xC2\xBC" => "\x31\xE2\x81\x84\x34", + "\xC2\xBD" => "\x31\xE2\x81\x84\x32", + "\xC2\xBE" => "\x33\xE2\x81\x84\x34", + "\xC4\xB2" => "\x49\x4A", + "\xC4\xB3" => "\x69\x6A", + "\xC4\xBF" => "\x4C\xC2\xB7", + "\xC5\x80" => "\x6C\xC2\xB7", + "\xC5\x89" => "\xCA\xBC\x6E", + "\xC5\xBF" => "\x73", + "\xC7\x84" => "\x44\x5A\xCC\x8C", + "\xC7\x85" => "\x44\x7A\xCC\x8C", + "\xC7\x86" => "\x64\x7A\xCC\x8C", + "\xC7\x87" => "\x4C\x4A", + "\xC7\x88" => "\x4C\x6A", + "\xC7\x89" => "\x6C\x6A", + "\xC7\x8A" => "\x4E\x4A", + "\xC7\x8B" => "\x4E\x6A", + "\xC7\x8C" => "\x6E\x6A", + "\xC7\xB1" => "\x44\x5A", + "\xC7\xB2" => "\x44\x7A", + "\xC7\xB3" => "\x64\x7A", + "\xCA\xB0" => "\x68", + "\xCA\xB1" => "\xC9\xA6", + "\xCA\xB2" => "\x6A", + "\xCA\xB3" => "\x72", + "\xCA\xB4" => "\xC9\xB9", + "\xCA\xB5" => "\xC9\xBB", + "\xCA\xB6" => "\xCA\x81", + "\xCA\xB7" => "\x77", + "\xCA\xB8" => "\x79", + "\xCB\x98" => "\x20\xCC\x86", + "\xCB\x99" => "\x20\xCC\x87", + "\xCB\x9A" => "\x20\xCC\x8A", + "\xCB\x9B" => "\x20\xCC\xA8", + "\xCB\x9C" => "\x20\xCC\x83", + "\xCB\x9D" => "\x20\xCC\x8B", + "\xCB\xA0" => "\xC9\xA3", + "\xCB\xA1" => "\x6C", + "\xCB\xA2" => "\x73", + "\xCB\xA3" => "\x78", + "\xCB\xA4" => "\xCA\x95", + "\xCD\xBA" => "\x20\xCD\x85", + "\xCE\x84" => "\x20\xCC\x81", + "\xCE\x85" => "\x20\xCC\x88\xCC\x81", + "\xCF\x90" => "\xCE\xB2", + "\xCF\x91" => "\xCE\xB8", + "\xCF\x92" => "\xCE\xA5", + "\xCF\x93" => "\xCE\xA5\xCC\x81", + "\xCF\x94" => "\xCE\xA5\xCC\x88", + "\xCF\x95" => "\xCF\x86", + "\xCF\x96" => "\xCF\x80", + "\xCF\xB0" => "\xCE\xBA", + "\xCF\xB1" => "\xCF\x81", + "\xCF\xB2" => "\xCF\x82", + "\xCF\xB4" => "\xCE\x98", + "\xCF\xB5" => "\xCE\xB5", + "\xCF\xB9" => "\xCE\xA3", + "\xD6\x87" => "\xD5\xA5\xD6\x82", + "\xD9\xB5" => "\xD8\xA7\xD9\xB4", + "\xD9\xB6" => "\xD9\x88\xD9\xB4", + "\xD9\xB7" => "\xDB\x87\xD9\xB4", + "\xD9\xB8" => "\xD9\x8A\xD9\xB4", + "\xE0\xB8\xB3" => "\xE0\xB9\x8D\xE0\xB8\xB2", + "\xE0\xBA\xB3" => "\xE0\xBB\x8D\xE0\xBA\xB2", + "\xE0\xBB\x9C" => "\xE0\xBA\xAB\xE0\xBA\x99", + "\xE0\xBB\x9D" => "\xE0\xBA\xAB\xE0\xBA\xA1", + "\xE0\xBC\x8C" => "\xE0\xBC\x8B", + "\xE0\xBD\xB7" => "\xE0\xBE\xB2\xE0\xBD\xB1\xE0\xBE\x80", + "\xE0\xBD\xB9" => "\xE0\xBE\xB3\xE0\xBD\xB1\xE0\xBE\x80", + "\xE1\x83\xBC" => "\xE1\x83\x9C", + "\xE1\xB4\xAC" => "\x41", + "\xE1\xB4\xAD" => "\xC3\x86", + "\xE1\xB4\xAE" => "\x42", + "\xE1\xB4\xB0" => "\x44", + "\xE1\xB4\xB1" => "\x45", + "\xE1\xB4\xB2" => "\xC6\x8E", + "\xE1\xB4\xB3" => "\x47", + "\xE1\xB4\xB4" => "\x48", + "\xE1\xB4\xB5" => "\x49", + "\xE1\xB4\xB6" => "\x4A", + "\xE1\xB4\xB7" => "\x4B", + "\xE1\xB4\xB8" => "\x4C", + "\xE1\xB4\xB9" => "\x4D", + "\xE1\xB4\xBA" => "\x4E", + "\xE1\xB4\xBC" => "\x4F", + "\xE1\xB4\xBD" => "\xC8\xA2", + "\xE1\xB4\xBE" => "\x50", + "\xE1\xB4\xBF" => "\x52", + "\xE1\xB5\x80" => "\x54", + "\xE1\xB5\x81" => "\x55", + "\xE1\xB5\x82" => "\x57", + "\xE1\xB5\x83" => "\x61", + "\xE1\xB5\x84" => "\xC9\x90", + "\xE1\xB5\x85" => "\xC9\x91", + "\xE1\xB5\x86" => "\xE1\xB4\x82", + "\xE1\xB5\x87" => "\x62", + "\xE1\xB5\x88" => "\x64", + "\xE1\xB5\x89" => "\x65", + "\xE1\xB5\x8A" => "\xC9\x99", + "\xE1\xB5\x8B" => "\xC9\x9B", + "\xE1\xB5\x8C" => "\xC9\x9C", + "\xE1\xB5\x8D" => "\x67", + "\xE1\xB5\x8F" => "\x6B", + "\xE1\xB5\x90" => "\x6D", + "\xE1\xB5\x91" => "\xC5\x8B", + "\xE1\xB5\x92" => "\x6F", + "\xE1\xB5\x93" => "\xC9\x94", + "\xE1\xB5\x94" => "\xE1\xB4\x96", + "\xE1\xB5\x95" => "\xE1\xB4\x97", + "\xE1\xB5\x96" => "\x70", + "\xE1\xB5\x97" => "\x74", + "\xE1\xB5\x98" => "\x75", + "\xE1\xB5\x99" => "\xE1\xB4\x9D", + "\xE1\xB5\x9A" => "\xC9\xAF", + "\xE1\xB5\x9B" => "\x76", + "\xE1\xB5\x9C" => "\xE1\xB4\xA5", + "\xE1\xB5\x9D" => "\xCE\xB2", + "\xE1\xB5\x9E" => "\xCE\xB3", + "\xE1\xB5\x9F" => "\xCE\xB4", + "\xE1\xB5\xA0" => "\xCF\x86", + "\xE1\xB5\xA1" => "\xCF\x87", + "\xE1\xB5\xA2" => "\x69", + "\xE1\xB5\xA3" => "\x72", + "\xE1\xB5\xA4" => "\x75", + "\xE1\xB5\xA5" => "\x76", + "\xE1\xB5\xA6" => "\xCE\xB2", + "\xE1\xB5\xA7" => "\xCE\xB3", + "\xE1\xB5\xA8" => "\xCF\x81", + "\xE1\xB5\xA9" => "\xCF\x86", + "\xE1\xB5\xAA" => "\xCF\x87", + "\xE1\xB5\xB8" => "\xD0\xBD", + "\xE1\xB6\x9B" => "\xC9\x92", + "\xE1\xB6\x9C" => "\x63", + "\xE1\xB6\x9D" => "\xC9\x95", + "\xE1\xB6\x9E" => "\xC3\xB0", + "\xE1\xB6\x9F" => "\xC9\x9C", + "\xE1\xB6\xA0" => "\x66", + "\xE1\xB6\xA1" => "\xC9\x9F", + "\xE1\xB6\xA2" => "\xC9\xA1", + "\xE1\xB6\xA3" => "\xC9\xA5", + "\xE1\xB6\xA4" => "\xC9\xA8", + "\xE1\xB6\xA5" => "\xC9\xA9", + "\xE1\xB6\xA6" => "\xC9\xAA", + "\xE1\xB6\xA7" => "\xE1\xB5\xBB", + "\xE1\xB6\xA8" => "\xCA\x9D", + "\xE1\xB6\xA9" => "\xC9\xAD", + "\xE1\xB6\xAA" => "\xE1\xB6\x85", + "\xE1\xB6\xAB" => "\xCA\x9F", + "\xE1\xB6\xAC" => "\xC9\xB1", + "\xE1\xB6\xAD" => "\xC9\xB0", + "\xE1\xB6\xAE" => "\xC9\xB2", + "\xE1\xB6\xAF" => "\xC9\xB3", + "\xE1\xB6\xB0" => "\xC9\xB4", + "\xE1\xB6\xB1" => "\xC9\xB5", + "\xE1\xB6\xB2" => "\xC9\xB8", + "\xE1\xB6\xB3" => "\xCA\x82", + "\xE1\xB6\xB4" => "\xCA\x83", + "\xE1\xB6\xB5" => "\xC6\xAB", + "\xE1\xB6\xB6" => "\xCA\x89", + "\xE1\xB6\xB7" => "\xCA\x8A", + "\xE1\xB6\xB8" => "\xE1\xB4\x9C", + "\xE1\xB6\xB9" => "\xCA\x8B", + "\xE1\xB6\xBA" => "\xCA\x8C", + "\xE1\xB6\xBB" => "\x7A", + "\xE1\xB6\xBC" => "\xCA\x90", + "\xE1\xB6\xBD" => "\xCA\x91", + "\xE1\xB6\xBE" => "\xCA\x92", + "\xE1\xB6\xBF" => "\xCE\xB8", + "\xE1\xBA\x9A" => "\x61\xCA\xBE", + "\xE1\xBA\x9B" => "\x73\xCC\x87", + "\xE1\xBE\xBD" => "\x20\xCC\x93", + "\xE1\xBE\xBF" => "\x20\xCC\x93", + "\xE1\xBF\x80" => "\x20\xCD\x82", + "\xE1\xBF\x81" => "\x20\xCC\x88\xCD\x82", + "\xE1\xBF\x8D" => "\x20\xCC\x93\xCC\x80", + "\xE1\xBF\x8E" => "\x20\xCC\x93\xCC\x81", + "\xE1\xBF\x8F" => "\x20\xCC\x93\xCD\x82", + "\xE1\xBF\x9D" => "\x20\xCC\x94\xCC\x80", + "\xE1\xBF\x9E" => "\x20\xCC\x94\xCC\x81", + "\xE1\xBF\x9F" => "\x20\xCC\x94\xCD\x82", + "\xE1\xBF\xAD" => "\x20\xCC\x88\xCC\x80", + "\xE1\xBF\xAE" => "\x20\xCC\x88\xCC\x81", + "\xE1\xBF\xBD" => "\x20\xCC\x81", + "\xE1\xBF\xBE" => "\x20\xCC\x94", + "\xE2\x80\x80" => "\x20", + "\xE2\x80\x81" => "\x20", + "\xE2\x80\x82" => "\x20", + "\xE2\x80\x83" => "\x20", + "\xE2\x80\x84" => "\x20", + "\xE2\x80\x85" => "\x20", + "\xE2\x80\x86" => "\x20", + "\xE2\x80\x87" => "\x20", + "\xE2\x80\x88" => "\x20", + "\xE2\x80\x89" => "\x20", + "\xE2\x80\x8A" => "\x20", + "\xE2\x80\x91" => "\xE2\x80\x90", + "\xE2\x80\x97" => "\x20\xCC\xB3", + "\xE2\x80\xA4" => "\x2E", + "\xE2\x80\xA5" => "\x2E\x2E", + "\xE2\x80\xA6" => "\x2E\x2E\x2E", + "\xE2\x80\xAF" => "\x20", + "\xE2\x80\xB3" => "\xE2\x80\xB2\xE2\x80\xB2", + "\xE2\x80\xB4" => "\xE2\x80\xB2\xE2\x80\xB2\xE2\x80\xB2", + "\xE2\x80\xB6" => "\xE2\x80\xB5\xE2\x80\xB5", + "\xE2\x80\xB7" => "\xE2\x80\xB5\xE2\x80\xB5\xE2\x80\xB5", + "\xE2\x80\xBC" => "\x21\x21", + "\xE2\x80\xBE" => "\x20\xCC\x85", + "\xE2\x81\x87" => "\x3F\x3F", + "\xE2\x81\x88" => "\x3F\x21", + "\xE2\x81\x89" => "\x21\x3F", + "\xE2\x81\x97" => "\xE2\x80\xB2\xE2\x80\xB2\xE2\x80\xB2\xE2\x80\xB2", + "\xE2\x81\x9F" => "\x20", + "\xE2\x81\xB0" => "\x30", + "\xE2\x81\xB1" => "\x69", + "\xE2\x81\xB4" => "\x34", + "\xE2\x81\xB5" => "\x35", + "\xE2\x81\xB6" => "\x36", + "\xE2\x81\xB7" => "\x37", + "\xE2\x81\xB8" => "\x38", + "\xE2\x81\xB9" => "\x39", + "\xE2\x81\xBA" => "\x2B", + "\xE2\x81\xBB" => "\xE2\x88\x92", + "\xE2\x81\xBC" => "\x3D", + "\xE2\x81\xBD" => "\x28", + "\xE2\x81\xBE" => "\x29", + "\xE2\x81\xBF" => "\x6E", + "\xE2\x82\x80" => "\x30", + "\xE2\x82\x81" => "\x31", + "\xE2\x82\x82" => "\x32", + "\xE2\x82\x83" => "\x33", + "\xE2\x82\x84" => "\x34", + "\xE2\x82\x85" => "\x35", + "\xE2\x82\x86" => "\x36", + "\xE2\x82\x87" => "\x37", + "\xE2\x82\x88" => "\x38", + "\xE2\x82\x89" => "\x39", + "\xE2\x82\x8A" => "\x2B", + "\xE2\x82\x8B" => "\xE2\x88\x92", + "\xE2\x82\x8C" => "\x3D", + "\xE2\x82\x8D" => "\x28", + "\xE2\x82\x8E" => "\x29", + "\xE2\x82\x90" => "\x61", + "\xE2\x82\x91" => "\x65", + "\xE2\x82\x92" => "\x6F", + "\xE2\x82\x93" => "\x78", + "\xE2\x82\x94" => "\xC9\x99", + "\xE2\x82\x95" => "\x68", + "\xE2\x82\x96" => "\x6B", + "\xE2\x82\x97" => "\x6C", + "\xE2\x82\x98" => "\x6D", + "\xE2\x82\x99" => "\x6E", + "\xE2\x82\x9A" => "\x70", + "\xE2\x82\x9B" => "\x73", + "\xE2\x82\x9C" => "\x74", + "\xE2\x82\xA8" => "\x52\x73", + "\xE2\x84\x80" => "\x61\x2F\x63", + "\xE2\x84\x81" => "\x61\x2F\x73", + "\xE2\x84\x82" => "\x43", + "\xE2\x84\x83" => "\xC2\xB0\x43", + "\xE2\x84\x85" => "\x63\x2F\x6F", + "\xE2\x84\x86" => "\x63\x2F\x75", + "\xE2\x84\x87" => "\xC6\x90", + "\xE2\x84\x89" => "\xC2\xB0\x46", + "\xE2\x84\x8A" => "\x67", + "\xE2\x84\x8B" => "\x48", + "\xE2\x84\x8C" => "\x48", + "\xE2\x84\x8D" => "\x48", + "\xE2\x84\x8E" => "\x68", + "\xE2\x84\x8F" => "\xC4\xA7", + "\xE2\x84\x90" => "\x49", + "\xE2\x84\x91" => "\x49", + "\xE2\x84\x92" => "\x4C", + "\xE2\x84\x93" => "\x6C", + "\xE2\x84\x95" => "\x4E", + "\xE2\x84\x96" => "\x4E\x6F", + "\xE2\x84\x99" => "\x50", + "\xE2\x84\x9A" => "\x51", + "\xE2\x84\x9B" => "\x52", + "\xE2\x84\x9C" => "\x52", + "\xE2\x84\x9D" => "\x52", + "\xE2\x84\xA0" => "\x53\x4D", + "\xE2\x84\xA1" => "\x54\x45\x4C", + "\xE2\x84\xA2" => "\x54\x4D", + "\xE2\x84\xA4" => "\x5A", + "\xE2\x84\xA8" => "\x5A", + "\xE2\x84\xAC" => "\x42", + "\xE2\x84\xAD" => "\x43", + "\xE2\x84\xAF" => "\x65", + "\xE2\x84\xB0" => "\x45", + "\xE2\x84\xB1" => "\x46", + "\xE2\x84\xB3" => "\x4D", + "\xE2\x84\xB4" => "\x6F", + "\xE2\x84\xB5" => "\xD7\x90", + "\xE2\x84\xB6" => "\xD7\x91", + "\xE2\x84\xB7" => "\xD7\x92", + "\xE2\x84\xB8" => "\xD7\x93", + "\xE2\x84\xB9" => "\x69", + "\xE2\x84\xBB" => "\x46\x41\x58", + "\xE2\x84\xBC" => "\xCF\x80", + "\xE2\x84\xBD" => "\xCE\xB3", + "\xE2\x84\xBE" => "\xCE\x93", + "\xE2\x84\xBF" => "\xCE\xA0", + "\xE2\x85\x80" => "\xE2\x88\x91", + "\xE2\x85\x85" => "\x44", + "\xE2\x85\x86" => "\x64", + "\xE2\x85\x87" => "\x65", + "\xE2\x85\x88" => "\x69", + "\xE2\x85\x89" => "\x6A", + "\xE2\x85\x90" => "\x31\xE2\x81\x84\x37", + "\xE2\x85\x91" => "\x31\xE2\x81\x84\x39", + "\xE2\x85\x92" => "\x31\xE2\x81\x84\x31\x30", + "\xE2\x85\x93" => "\x31\xE2\x81\x84\x33", + "\xE2\x85\x94" => "\x32\xE2\x81\x84\x33", + "\xE2\x85\x95" => "\x31\xE2\x81\x84\x35", + "\xE2\x85\x96" => "\x32\xE2\x81\x84\x35", + "\xE2\x85\x97" => "\x33\xE2\x81\x84\x35", + "\xE2\x85\x98" => "\x34\xE2\x81\x84\x35", + "\xE2\x85\x99" => "\x31\xE2\x81\x84\x36", + "\xE2\x85\x9A" => "\x35\xE2\x81\x84\x36", + "\xE2\x85\x9B" => "\x31\xE2\x81\x84\x38", + "\xE2\x85\x9C" => "\x33\xE2\x81\x84\x38", + "\xE2\x85\x9D" => "\x35\xE2\x81\x84\x38", + "\xE2\x85\x9E" => "\x37\xE2\x81\x84\x38", + "\xE2\x85\x9F" => "\x31\xE2\x81\x84", + "\xE2\x85\xA0" => "\x49", + "\xE2\x85\xA1" => "\x49\x49", + "\xE2\x85\xA2" => "\x49\x49\x49", + "\xE2\x85\xA3" => "\x49\x56", + "\xE2\x85\xA4" => "\x56", + "\xE2\x85\xA5" => "\x56\x49", + "\xE2\x85\xA6" => "\x56\x49\x49", + "\xE2\x85\xA7" => "\x56\x49\x49\x49", + "\xE2\x85\xA8" => "\x49\x58", + "\xE2\x85\xA9" => "\x58", + "\xE2\x85\xAA" => "\x58\x49", + "\xE2\x85\xAB" => "\x58\x49\x49", + "\xE2\x85\xAC" => "\x4C", + "\xE2\x85\xAD" => "\x43", + "\xE2\x85\xAE" => "\x44", + "\xE2\x85\xAF" => "\x4D", + "\xE2\x85\xB0" => "\x69", + "\xE2\x85\xB1" => "\x69\x69", + "\xE2\x85\xB2" => "\x69\x69\x69", + "\xE2\x85\xB3" => "\x69\x76", + "\xE2\x85\xB4" => "\x76", + "\xE2\x85\xB5" => "\x76\x69", + "\xE2\x85\xB6" => "\x76\x69\x69", + "\xE2\x85\xB7" => "\x76\x69\x69\x69", + "\xE2\x85\xB8" => "\x69\x78", + "\xE2\x85\xB9" => "\x78", + "\xE2\x85\xBA" => "\x78\x69", + "\xE2\x85\xBB" => "\x78\x69\x69", + "\xE2\x85\xBC" => "\x6C", + "\xE2\x85\xBD" => "\x63", + "\xE2\x85\xBE" => "\x64", + "\xE2\x85\xBF" => "\x6D", + "\xE2\x86\x89" => "\x30\xE2\x81\x84\x33", + "\xE2\x88\xAC" => "\xE2\x88\xAB\xE2\x88\xAB", + "\xE2\x88\xAD" => "\xE2\x88\xAB\xE2\x88\xAB\xE2\x88\xAB", + "\xE2\x88\xAF" => "\xE2\x88\xAE\xE2\x88\xAE", + "\xE2\x88\xB0" => "\xE2\x88\xAE\xE2\x88\xAE\xE2\x88\xAE", + "\xE2\x91\xA0" => "\x31", + "\xE2\x91\xA1" => "\x32", + "\xE2\x91\xA2" => "\x33", + "\xE2\x91\xA3" => "\x34", + "\xE2\x91\xA4" => "\x35", + "\xE2\x91\xA5" => "\x36", + "\xE2\x91\xA6" => "\x37", + "\xE2\x91\xA7" => "\x38", + "\xE2\x91\xA8" => "\x39", + "\xE2\x91\xA9" => "\x31\x30", + "\xE2\x91\xAA" => "\x31\x31", + "\xE2\x91\xAB" => "\x31\x32", + "\xE2\x91\xAC" => "\x31\x33", + "\xE2\x91\xAD" => "\x31\x34", + "\xE2\x91\xAE" => "\x31\x35", + "\xE2\x91\xAF" => "\x31\x36", + "\xE2\x91\xB0" => "\x31\x37", + "\xE2\x91\xB1" => "\x31\x38", + "\xE2\x91\xB2" => "\x31\x39", + "\xE2\x91\xB3" => "\x32\x30", + "\xE2\x91\xB4" => "\x28\x31\x29", + "\xE2\x91\xB5" => "\x28\x32\x29", + "\xE2\x91\xB6" => "\x28\x33\x29", + "\xE2\x91\xB7" => "\x28\x34\x29", + "\xE2\x91\xB8" => "\x28\x35\x29", + "\xE2\x91\xB9" => "\x28\x36\x29", + "\xE2\x91\xBA" => "\x28\x37\x29", + "\xE2\x91\xBB" => "\x28\x38\x29", + "\xE2\x91\xBC" => "\x28\x39\x29", + "\xE2\x91\xBD" => "\x28\x31\x30\x29", + "\xE2\x91\xBE" => "\x28\x31\x31\x29", + "\xE2\x91\xBF" => "\x28\x31\x32\x29", + "\xE2\x92\x80" => "\x28\x31\x33\x29", + "\xE2\x92\x81" => "\x28\x31\x34\x29", + "\xE2\x92\x82" => "\x28\x31\x35\x29", + "\xE2\x92\x83" => "\x28\x31\x36\x29", + "\xE2\x92\x84" => "\x28\x31\x37\x29", + "\xE2\x92\x85" => "\x28\x31\x38\x29", + "\xE2\x92\x86" => "\x28\x31\x39\x29", + "\xE2\x92\x87" => "\x28\x32\x30\x29", + "\xE2\x92\x88" => "\x31\x2E", + "\xE2\x92\x89" => "\x32\x2E", + "\xE2\x92\x8A" => "\x33\x2E", + "\xE2\x92\x8B" => "\x34\x2E", + "\xE2\x92\x8C" => "\x35\x2E", + "\xE2\x92\x8D" => "\x36\x2E", + "\xE2\x92\x8E" => "\x37\x2E", + "\xE2\x92\x8F" => "\x38\x2E", + "\xE2\x92\x90" => "\x39\x2E", + "\xE2\x92\x91" => "\x31\x30\x2E", + "\xE2\x92\x92" => "\x31\x31\x2E", + "\xE2\x92\x93" => "\x31\x32\x2E", + "\xE2\x92\x94" => "\x31\x33\x2E", + "\xE2\x92\x95" => "\x31\x34\x2E", + "\xE2\x92\x96" => "\x31\x35\x2E", + "\xE2\x92\x97" => "\x31\x36\x2E", + "\xE2\x92\x98" => "\x31\x37\x2E", + "\xE2\x92\x99" => "\x31\x38\x2E", + "\xE2\x92\x9A" => "\x31\x39\x2E", + "\xE2\x92\x9B" => "\x32\x30\x2E", + "\xE2\x92\x9C" => "\x28\x61\x29", + "\xE2\x92\x9D" => "\x28\x62\x29", + "\xE2\x92\x9E" => "\x28\x63\x29", + "\xE2\x92\x9F" => "\x28\x64\x29", + "\xE2\x92\xA0" => "\x28\x65\x29", + "\xE2\x92\xA1" => "\x28\x66\x29", + "\xE2\x92\xA2" => "\x28\x67\x29", + "\xE2\x92\xA3" => "\x28\x68\x29", + "\xE2\x92\xA4" => "\x28\x69\x29", + "\xE2\x92\xA5" => "\x28\x6A\x29", + "\xE2\x92\xA6" => "\x28\x6B\x29", + "\xE2\x92\xA7" => "\x28\x6C\x29", + "\xE2\x92\xA8" => "\x28\x6D\x29", + "\xE2\x92\xA9" => "\x28\x6E\x29", + "\xE2\x92\xAA" => "\x28\x6F\x29", + "\xE2\x92\xAB" => "\x28\x70\x29", + "\xE2\x92\xAC" => "\x28\x71\x29", + "\xE2\x92\xAD" => "\x28\x72\x29", + "\xE2\x92\xAE" => "\x28\x73\x29", + "\xE2\x92\xAF" => "\x28\x74\x29", + "\xE2\x92\xB0" => "\x28\x75\x29", + "\xE2\x92\xB1" => "\x28\x76\x29", + "\xE2\x92\xB2" => "\x28\x77\x29", + "\xE2\x92\xB3" => "\x28\x78\x29", + "\xE2\x92\xB4" => "\x28\x79\x29", + "\xE2\x92\xB5" => "\x28\x7A\x29", + "\xE2\x92\xB6" => "\x41", + "\xE2\x92\xB7" => "\x42", + "\xE2\x92\xB8" => "\x43", + "\xE2\x92\xB9" => "\x44", + "\xE2\x92\xBA" => "\x45", + "\xE2\x92\xBB" => "\x46", + "\xE2\x92\xBC" => "\x47", + "\xE2\x92\xBD" => "\x48", + "\xE2\x92\xBE" => "\x49", + "\xE2\x92\xBF" => "\x4A", + "\xE2\x93\x80" => "\x4B", + "\xE2\x93\x81" => "\x4C", + "\xE2\x93\x82" => "\x4D", + "\xE2\x93\x83" => "\x4E", + "\xE2\x93\x84" => "\x4F", + "\xE2\x93\x85" => "\x50", + "\xE2\x93\x86" => "\x51", + "\xE2\x93\x87" => "\x52", + "\xE2\x93\x88" => "\x53", + "\xE2\x93\x89" => "\x54", + "\xE2\x93\x8A" => "\x55", + "\xE2\x93\x8B" => "\x56", + "\xE2\x93\x8C" => "\x57", + "\xE2\x93\x8D" => "\x58", + "\xE2\x93\x8E" => "\x59", + "\xE2\x93\x8F" => "\x5A", + "\xE2\x93\x90" => "\x61", + "\xE2\x93\x91" => "\x62", + "\xE2\x93\x92" => "\x63", + "\xE2\x93\x93" => "\x64", + "\xE2\x93\x94" => "\x65", + "\xE2\x93\x95" => "\x66", + "\xE2\x93\x96" => "\x67", + "\xE2\x93\x97" => "\x68", + "\xE2\x93\x98" => "\x69", + "\xE2\x93\x99" => "\x6A", + "\xE2\x93\x9A" => "\x6B", + "\xE2\x93\x9B" => "\x6C", + "\xE2\x93\x9C" => "\x6D", + "\xE2\x93\x9D" => "\x6E", + "\xE2\x93\x9E" => "\x6F", + "\xE2\x93\x9F" => "\x70", + "\xE2\x93\xA0" => "\x71", + "\xE2\x93\xA1" => "\x72", + "\xE2\x93\xA2" => "\x73", + "\xE2\x93\xA3" => "\x74", + "\xE2\x93\xA4" => "\x75", + "\xE2\x93\xA5" => "\x76", + "\xE2\x93\xA6" => "\x77", + "\xE2\x93\xA7" => "\x78", + "\xE2\x93\xA8" => "\x79", + "\xE2\x93\xA9" => "\x7A", + "\xE2\x93\xAA" => "\x30", + "\xE2\xA8\x8C" => "\xE2\x88\xAB\xE2\x88\xAB\xE2\x88\xAB\xE2\x88\xAB", + "\xE2\xA9\xB4" => "\x3A\x3A\x3D", + "\xE2\xA9\xB5" => "\x3D\x3D", + "\xE2\xA9\xB6" => "\x3D\x3D\x3D", + "\xE2\xB1\xBC" => "\x6A", + "\xE2\xB1\xBD" => "\x56", + "\xE2\xB5\xAF" => "\xE2\xB5\xA1", + "\xE2\xBA\x9F" => "\xE6\xAF\x8D", + "\xE2\xBB\xB3" => "\xE9\xBE\x9F", + "\xE2\xBC\x80" => "\xE4\xB8\x80", + "\xE2\xBC\x81" => "\xE4\xB8\xA8", + "\xE2\xBC\x82" => "\xE4\xB8\xB6", + "\xE2\xBC\x83" => "\xE4\xB8\xBF", + "\xE2\xBC\x84" => "\xE4\xB9\x99", + "\xE2\xBC\x85" => "\xE4\xBA\x85", + "\xE2\xBC\x86" => "\xE4\xBA\x8C", + "\xE2\xBC\x87" => "\xE4\xBA\xA0", + "\xE2\xBC\x88" => "\xE4\xBA\xBA", + "\xE2\xBC\x89" => "\xE5\x84\xBF", + "\xE2\xBC\x8A" => "\xE5\x85\xA5", + "\xE2\xBC\x8B" => "\xE5\x85\xAB", + "\xE2\xBC\x8C" => "\xE5\x86\x82", + "\xE2\xBC\x8D" => "\xE5\x86\x96", + "\xE2\xBC\x8E" => "\xE5\x86\xAB", + "\xE2\xBC\x8F" => "\xE5\x87\xA0", + "\xE2\xBC\x90" => "\xE5\x87\xB5", + "\xE2\xBC\x91" => "\xE5\x88\x80", + "\xE2\xBC\x92" => "\xE5\x8A\x9B", + "\xE2\xBC\x93" => "\xE5\x8B\xB9", + "\xE2\xBC\x94" => "\xE5\x8C\x95", + "\xE2\xBC\x95" => "\xE5\x8C\x9A", + "\xE2\xBC\x96" => "\xE5\x8C\xB8", + "\xE2\xBC\x97" => "\xE5\x8D\x81", + "\xE2\xBC\x98" => "\xE5\x8D\x9C", + "\xE2\xBC\x99" => "\xE5\x8D\xA9", + "\xE2\xBC\x9A" => "\xE5\x8E\x82", + "\xE2\xBC\x9B" => "\xE5\x8E\xB6", + "\xE2\xBC\x9C" => "\xE5\x8F\x88", + "\xE2\xBC\x9D" => "\xE5\x8F\xA3", + "\xE2\xBC\x9E" => "\xE5\x9B\x97", + "\xE2\xBC\x9F" => "\xE5\x9C\x9F", + "\xE2\xBC\xA0" => "\xE5\xA3\xAB", + "\xE2\xBC\xA1" => "\xE5\xA4\x82", + "\xE2\xBC\xA2" => "\xE5\xA4\x8A", + "\xE2\xBC\xA3" => "\xE5\xA4\x95", + "\xE2\xBC\xA4" => "\xE5\xA4\xA7", + "\xE2\xBC\xA5" => "\xE5\xA5\xB3", + "\xE2\xBC\xA6" => "\xE5\xAD\x90", + "\xE2\xBC\xA7" => "\xE5\xAE\x80", + "\xE2\xBC\xA8" => "\xE5\xAF\xB8", + "\xE2\xBC\xA9" => "\xE5\xB0\x8F", + "\xE2\xBC\xAA" => "\xE5\xB0\xA2", + "\xE2\xBC\xAB" => "\xE5\xB0\xB8", + "\xE2\xBC\xAC" => "\xE5\xB1\xAE", + "\xE2\xBC\xAD" => "\xE5\xB1\xB1", + "\xE2\xBC\xAE" => "\xE5\xB7\x9B", + "\xE2\xBC\xAF" => "\xE5\xB7\xA5", + "\xE2\xBC\xB0" => "\xE5\xB7\xB1", + "\xE2\xBC\xB1" => "\xE5\xB7\xBE", + "\xE2\xBC\xB2" => "\xE5\xB9\xB2", + "\xE2\xBC\xB3" => "\xE5\xB9\xBA", + "\xE2\xBC\xB4" => "\xE5\xB9\xBF", + "\xE2\xBC\xB5" => "\xE5\xBB\xB4", + "\xE2\xBC\xB6" => "\xE5\xBB\xBE", + "\xE2\xBC\xB7" => "\xE5\xBC\x8B", + "\xE2\xBC\xB8" => "\xE5\xBC\x93", + "\xE2\xBC\xB9" => "\xE5\xBD\x90", + "\xE2\xBC\xBA" => "\xE5\xBD\xA1", + "\xE2\xBC\xBB" => "\xE5\xBD\xB3", + "\xE2\xBC\xBC" => "\xE5\xBF\x83", + "\xE2\xBC\xBD" => "\xE6\x88\x88", + "\xE2\xBC\xBE" => "\xE6\x88\xB6", + "\xE2\xBC\xBF" => "\xE6\x89\x8B", + "\xE2\xBD\x80" => "\xE6\x94\xAF", + "\xE2\xBD\x81" => "\xE6\x94\xB4", + "\xE2\xBD\x82" => "\xE6\x96\x87", + "\xE2\xBD\x83" => "\xE6\x96\x97", + "\xE2\xBD\x84" => "\xE6\x96\xA4", + "\xE2\xBD\x85" => "\xE6\x96\xB9", + "\xE2\xBD\x86" => "\xE6\x97\xA0", + "\xE2\xBD\x87" => "\xE6\x97\xA5", + "\xE2\xBD\x88" => "\xE6\x9B\xB0", + "\xE2\xBD\x89" => "\xE6\x9C\x88", + "\xE2\xBD\x8A" => "\xE6\x9C\xA8", + "\xE2\xBD\x8B" => "\xE6\xAC\xA0", + "\xE2\xBD\x8C" => "\xE6\xAD\xA2", + "\xE2\xBD\x8D" => "\xE6\xAD\xB9", + "\xE2\xBD\x8E" => "\xE6\xAE\xB3", + "\xE2\xBD\x8F" => "\xE6\xAF\x8B", + "\xE2\xBD\x90" => "\xE6\xAF\x94", + "\xE2\xBD\x91" => "\xE6\xAF\x9B", + "\xE2\xBD\x92" => "\xE6\xB0\x8F", + "\xE2\xBD\x93" => "\xE6\xB0\x94", + "\xE2\xBD\x94" => "\xE6\xB0\xB4", + "\xE2\xBD\x95" => "\xE7\x81\xAB", + "\xE2\xBD\x96" => "\xE7\x88\xAA", + "\xE2\xBD\x97" => "\xE7\x88\xB6", + "\xE2\xBD\x98" => "\xE7\x88\xBB", + "\xE2\xBD\x99" => "\xE7\x88\xBF", + "\xE2\xBD\x9A" => "\xE7\x89\x87", + "\xE2\xBD\x9B" => "\xE7\x89\x99", + "\xE2\xBD\x9C" => "\xE7\x89\x9B", + "\xE2\xBD\x9D" => "\xE7\x8A\xAC", + "\xE2\xBD\x9E" => "\xE7\x8E\x84", + "\xE2\xBD\x9F" => "\xE7\x8E\x89", + "\xE2\xBD\xA0" => "\xE7\x93\x9C", + "\xE2\xBD\xA1" => "\xE7\x93\xA6", + "\xE2\xBD\xA2" => "\xE7\x94\x98", + "\xE2\xBD\xA3" => "\xE7\x94\x9F", + "\xE2\xBD\xA4" => "\xE7\x94\xA8", + "\xE2\xBD\xA5" => "\xE7\x94\xB0", + "\xE2\xBD\xA6" => "\xE7\x96\x8B", + "\xE2\xBD\xA7" => "\xE7\x96\x92", + "\xE2\xBD\xA8" => "\xE7\x99\xB6", + "\xE2\xBD\xA9" => "\xE7\x99\xBD", + "\xE2\xBD\xAA" => "\xE7\x9A\xAE", + "\xE2\xBD\xAB" => "\xE7\x9A\xBF", + "\xE2\xBD\xAC" => "\xE7\x9B\xAE", + "\xE2\xBD\xAD" => "\xE7\x9F\x9B", + "\xE2\xBD\xAE" => "\xE7\x9F\xA2", + "\xE2\xBD\xAF" => "\xE7\x9F\xB3", + "\xE2\xBD\xB0" => "\xE7\xA4\xBA", + "\xE2\xBD\xB1" => "\xE7\xA6\xB8", + "\xE2\xBD\xB2" => "\xE7\xA6\xBE", + "\xE2\xBD\xB3" => "\xE7\xA9\xB4", + "\xE2\xBD\xB4" => "\xE7\xAB\x8B", + "\xE2\xBD\xB5" => "\xE7\xAB\xB9", + "\xE2\xBD\xB6" => "\xE7\xB1\xB3", + "\xE2\xBD\xB7" => "\xE7\xB3\xB8", + "\xE2\xBD\xB8" => "\xE7\xBC\xB6", + "\xE2\xBD\xB9" => "\xE7\xBD\x91", + "\xE2\xBD\xBA" => "\xE7\xBE\x8A", + "\xE2\xBD\xBB" => "\xE7\xBE\xBD", + "\xE2\xBD\xBC" => "\xE8\x80\x81", + "\xE2\xBD\xBD" => "\xE8\x80\x8C", + "\xE2\xBD\xBE" => "\xE8\x80\x92", + "\xE2\xBD\xBF" => "\xE8\x80\xB3", + "\xE2\xBE\x80" => "\xE8\x81\xBF", + "\xE2\xBE\x81" => "\xE8\x82\x89", + "\xE2\xBE\x82" => "\xE8\x87\xA3", + "\xE2\xBE\x83" => "\xE8\x87\xAA", + "\xE2\xBE\x84" => "\xE8\x87\xB3", + "\xE2\xBE\x85" => "\xE8\x87\xBC", + "\xE2\xBE\x86" => "\xE8\x88\x8C", + "\xE2\xBE\x87" => "\xE8\x88\x9B", + "\xE2\xBE\x88" => "\xE8\x88\x9F", + "\xE2\xBE\x89" => "\xE8\x89\xAE", + "\xE2\xBE\x8A" => "\xE8\x89\xB2", + "\xE2\xBE\x8B" => "\xE8\x89\xB8", + "\xE2\xBE\x8C" => "\xE8\x99\x8D", + "\xE2\xBE\x8D" => "\xE8\x99\xAB", + "\xE2\xBE\x8E" => "\xE8\xA1\x80", + "\xE2\xBE\x8F" => "\xE8\xA1\x8C", + "\xE2\xBE\x90" => "\xE8\xA1\xA3", + "\xE2\xBE\x91" => "\xE8\xA5\xBE", + "\xE2\xBE\x92" => "\xE8\xA6\x8B", + "\xE2\xBE\x93" => "\xE8\xA7\x92", + "\xE2\xBE\x94" => "\xE8\xA8\x80", + "\xE2\xBE\x95" => "\xE8\xB0\xB7", + "\xE2\xBE\x96" => "\xE8\xB1\x86", + "\xE2\xBE\x97" => "\xE8\xB1\x95", + "\xE2\xBE\x98" => "\xE8\xB1\xB8", + "\xE2\xBE\x99" => "\xE8\xB2\x9D", + "\xE2\xBE\x9A" => "\xE8\xB5\xA4", + "\xE2\xBE\x9B" => "\xE8\xB5\xB0", + "\xE2\xBE\x9C" => "\xE8\xB6\xB3", + "\xE2\xBE\x9D" => "\xE8\xBA\xAB", + "\xE2\xBE\x9E" => "\xE8\xBB\x8A", + "\xE2\xBE\x9F" => "\xE8\xBE\x9B", + "\xE2\xBE\xA0" => "\xE8\xBE\xB0", + "\xE2\xBE\xA1" => "\xE8\xBE\xB5", + "\xE2\xBE\xA2" => "\xE9\x82\x91", + "\xE2\xBE\xA3" => "\xE9\x85\x89", + "\xE2\xBE\xA4" => "\xE9\x87\x86", + "\xE2\xBE\xA5" => "\xE9\x87\x8C", + "\xE2\xBE\xA6" => "\xE9\x87\x91", + "\xE2\xBE\xA7" => "\xE9\x95\xB7", + "\xE2\xBE\xA8" => "\xE9\x96\x80", + "\xE2\xBE\xA9" => "\xE9\x98\x9C", + "\xE2\xBE\xAA" => "\xE9\x9A\xB6", + "\xE2\xBE\xAB" => "\xE9\x9A\xB9", + "\xE2\xBE\xAC" => "\xE9\x9B\xA8", + "\xE2\xBE\xAD" => "\xE9\x9D\x91", + "\xE2\xBE\xAE" => "\xE9\x9D\x9E", + "\xE2\xBE\xAF" => "\xE9\x9D\xA2", + "\xE2\xBE\xB0" => "\xE9\x9D\xA9", + "\xE2\xBE\xB1" => "\xE9\x9F\x8B", + "\xE2\xBE\xB2" => "\xE9\x9F\xAD", + "\xE2\xBE\xB3" => "\xE9\x9F\xB3", + "\xE2\xBE\xB4" => "\xE9\xA0\x81", + "\xE2\xBE\xB5" => "\xE9\xA2\xA8", + "\xE2\xBE\xB6" => "\xE9\xA3\x9B", + "\xE2\xBE\xB7" => "\xE9\xA3\x9F", + "\xE2\xBE\xB8" => "\xE9\xA6\x96", + "\xE2\xBE\xB9" => "\xE9\xA6\x99", + "\xE2\xBE\xBA" => "\xE9\xA6\xAC", + "\xE2\xBE\xBB" => "\xE9\xAA\xA8", + "\xE2\xBE\xBC" => "\xE9\xAB\x98", + "\xE2\xBE\xBD" => "\xE9\xAB\x9F", + "\xE2\xBE\xBE" => "\xE9\xAC\xA5", + "\xE2\xBE\xBF" => "\xE9\xAC\xAF", + "\xE2\xBF\x80" => "\xE9\xAC\xB2", + "\xE2\xBF\x81" => "\xE9\xAC\xBC", + "\xE2\xBF\x82" => "\xE9\xAD\x9A", + "\xE2\xBF\x83" => "\xE9\xB3\xA5", + "\xE2\xBF\x84" => "\xE9\xB9\xB5", + "\xE2\xBF\x85" => "\xE9\xB9\xBF", + "\xE2\xBF\x86" => "\xE9\xBA\xA5", + "\xE2\xBF\x87" => "\xE9\xBA\xBB", + "\xE2\xBF\x88" => "\xE9\xBB\x83", + "\xE2\xBF\x89" => "\xE9\xBB\x8D", + "\xE2\xBF\x8A" => "\xE9\xBB\x91", + "\xE2\xBF\x8B" => "\xE9\xBB\xB9", + "\xE2\xBF\x8C" => "\xE9\xBB\xBD", + "\xE2\xBF\x8D" => "\xE9\xBC\x8E", + "\xE2\xBF\x8E" => "\xE9\xBC\x93", + "\xE2\xBF\x8F" => "\xE9\xBC\xA0", + "\xE2\xBF\x90" => "\xE9\xBC\xBB", + "\xE2\xBF\x91" => "\xE9\xBD\x8A", + "\xE2\xBF\x92" => "\xE9\xBD\x92", + "\xE2\xBF\x93" => "\xE9\xBE\x8D", + "\xE2\xBF\x94" => "\xE9\xBE\x9C", + "\xE2\xBF\x95" => "\xE9\xBE\xA0", + "\xE3\x80\x80" => "\x20", + "\xE3\x80\xB6" => "\xE3\x80\x92", + "\xE3\x80\xB8" => "\xE5\x8D\x81", + "\xE3\x80\xB9" => "\xE5\x8D\x84", + "\xE3\x80\xBA" => "\xE5\x8D\x85", + "\xE3\x82\x9B" => "\x20\xE3\x82\x99", + "\xE3\x82\x9C" => "\x20\xE3\x82\x9A", + "\xE3\x82\x9F" => "\xE3\x82\x88\xE3\x82\x8A", + "\xE3\x83\xBF" => "\xE3\x82\xB3\xE3\x83\x88", + "\xE3\x84\xB1" => "\xE1\x84\x80", + "\xE3\x84\xB2" => "\xE1\x84\x81", + "\xE3\x84\xB3" => "\xE1\x86\xAA", + "\xE3\x84\xB4" => "\xE1\x84\x82", + "\xE3\x84\xB5" => "\xE1\x86\xAC", + "\xE3\x84\xB6" => "\xE1\x86\xAD", + "\xE3\x84\xB7" => "\xE1\x84\x83", + "\xE3\x84\xB8" => "\xE1\x84\x84", + "\xE3\x84\xB9" => "\xE1\x84\x85", + "\xE3\x84\xBA" => "\xE1\x86\xB0", + "\xE3\x84\xBB" => "\xE1\x86\xB1", + "\xE3\x84\xBC" => "\xE1\x86\xB2", + "\xE3\x84\xBD" => "\xE1\x86\xB3", + "\xE3\x84\xBE" => "\xE1\x86\xB4", + "\xE3\x84\xBF" => "\xE1\x86\xB5", + "\xE3\x85\x80" => "\xE1\x84\x9A", + "\xE3\x85\x81" => "\xE1\x84\x86", + "\xE3\x85\x82" => "\xE1\x84\x87", + "\xE3\x85\x83" => "\xE1\x84\x88", + "\xE3\x85\x84" => "\xE1\x84\xA1", + "\xE3\x85\x85" => "\xE1\x84\x89", + "\xE3\x85\x86" => "\xE1\x84\x8A", + "\xE3\x85\x87" => "\xE1\x84\x8B", + "\xE3\x85\x88" => "\xE1\x84\x8C", + "\xE3\x85\x89" => "\xE1\x84\x8D", + "\xE3\x85\x8A" => "\xE1\x84\x8E", + "\xE3\x85\x8B" => "\xE1\x84\x8F", + "\xE3\x85\x8C" => "\xE1\x84\x90", + "\xE3\x85\x8D" => "\xE1\x84\x91", + "\xE3\x85\x8E" => "\xE1\x84\x92", + "\xE3\x85\x8F" => "\xE1\x85\xA1", + "\xE3\x85\x90" => "\xE1\x85\xA2", + "\xE3\x85\x91" => "\xE1\x85\xA3", + "\xE3\x85\x92" => "\xE1\x85\xA4", + "\xE3\x85\x93" => "\xE1\x85\xA5", + "\xE3\x85\x94" => "\xE1\x85\xA6", + "\xE3\x85\x95" => "\xE1\x85\xA7", + "\xE3\x85\x96" => "\xE1\x85\xA8", + "\xE3\x85\x97" => "\xE1\x85\xA9", + "\xE3\x85\x98" => "\xE1\x85\xAA", + "\xE3\x85\x99" => "\xE1\x85\xAB", + "\xE3\x85\x9A" => "\xE1\x85\xAC", + "\xE3\x85\x9B" => "\xE1\x85\xAD", + "\xE3\x85\x9C" => "\xE1\x85\xAE", + "\xE3\x85\x9D" => "\xE1\x85\xAF", + "\xE3\x85\x9E" => "\xE1\x85\xB0", + "\xE3\x85\x9F" => "\xE1\x85\xB1", + "\xE3\x85\xA0" => "\xE1\x85\xB2", + "\xE3\x85\xA1" => "\xE1\x85\xB3", + "\xE3\x85\xA2" => "\xE1\x85\xB4", + "\xE3\x85\xA3" => "\xE1\x85\xB5", + "\xE3\x85\xA4" => "\xE1\x85\xA0", + "\xE3\x85\xA5" => "\xE1\x84\x94", + "\xE3\x85\xA6" => "\xE1\x84\x95", + "\xE3\x85\xA7" => "\xE1\x87\x87", + "\xE3\x85\xA8" => "\xE1\x87\x88", + "\xE3\x85\xA9" => "\xE1\x87\x8C", + "\xE3\x85\xAA" => "\xE1\x87\x8E", + "\xE3\x85\xAB" => "\xE1\x87\x93", + "\xE3\x85\xAC" => "\xE1\x87\x97", + "\xE3\x85\xAD" => "\xE1\x87\x99", + "\xE3\x85\xAE" => "\xE1\x84\x9C", + "\xE3\x85\xAF" => "\xE1\x87\x9D", + "\xE3\x85\xB0" => "\xE1\x87\x9F", + "\xE3\x85\xB1" => "\xE1\x84\x9D", + "\xE3\x85\xB2" => "\xE1\x84\x9E", + "\xE3\x85\xB3" => "\xE1\x84\xA0", + "\xE3\x85\xB4" => "\xE1\x84\xA2", + "\xE3\x85\xB5" => "\xE1\x84\xA3", + "\xE3\x85\xB6" => "\xE1\x84\xA7", + "\xE3\x85\xB7" => "\xE1\x84\xA9", + "\xE3\x85\xB8" => "\xE1\x84\xAB", + "\xE3\x85\xB9" => "\xE1\x84\xAC", + "\xE3\x85\xBA" => "\xE1\x84\xAD", + "\xE3\x85\xBB" => "\xE1\x84\xAE", + "\xE3\x85\xBC" => "\xE1\x84\xAF", + "\xE3\x85\xBD" => "\xE1\x84\xB2", + "\xE3\x85\xBE" => "\xE1\x84\xB6", + "\xE3\x85\xBF" => "\xE1\x85\x80", + "\xE3\x86\x80" => "\xE1\x85\x87", + "\xE3\x86\x81" => "\xE1\x85\x8C", + "\xE3\x86\x82" => "\xE1\x87\xB1", + "\xE3\x86\x83" => "\xE1\x87\xB2", + "\xE3\x86\x84" => "\xE1\x85\x97", + "\xE3\x86\x85" => "\xE1\x85\x98", + "\xE3\x86\x86" => "\xE1\x85\x99", + "\xE3\x86\x87" => "\xE1\x86\x84", + "\xE3\x86\x88" => "\xE1\x86\x85", + "\xE3\x86\x89" => "\xE1\x86\x88", + "\xE3\x86\x8A" => "\xE1\x86\x91", + "\xE3\x86\x8B" => "\xE1\x86\x92", + "\xE3\x86\x8C" => "\xE1\x86\x94", + "\xE3\x86\x8D" => "\xE1\x86\x9E", + "\xE3\x86\x8E" => "\xE1\x86\xA1", + "\xE3\x86\x92" => "\xE4\xB8\x80", + "\xE3\x86\x93" => "\xE4\xBA\x8C", + "\xE3\x86\x94" => "\xE4\xB8\x89", + "\xE3\x86\x95" => "\xE5\x9B\x9B", + "\xE3\x86\x96" => "\xE4\xB8\x8A", + "\xE3\x86\x97" => "\xE4\xB8\xAD", + "\xE3\x86\x98" => "\xE4\xB8\x8B", + "\xE3\x86\x99" => "\xE7\x94\xB2", + "\xE3\x86\x9A" => "\xE4\xB9\x99", + "\xE3\x86\x9B" => "\xE4\xB8\x99", + "\xE3\x86\x9C" => "\xE4\xB8\x81", + "\xE3\x86\x9D" => "\xE5\xA4\xA9", + "\xE3\x86\x9E" => "\xE5\x9C\xB0", + "\xE3\x86\x9F" => "\xE4\xBA\xBA", + "\xE3\x88\x80" => "\x28\xE1\x84\x80\x29", + "\xE3\x88\x81" => "\x28\xE1\x84\x82\x29", + "\xE3\x88\x82" => "\x28\xE1\x84\x83\x29", + "\xE3\x88\x83" => "\x28\xE1\x84\x85\x29", + "\xE3\x88\x84" => "\x28\xE1\x84\x86\x29", + "\xE3\x88\x85" => "\x28\xE1\x84\x87\x29", + "\xE3\x88\x86" => "\x28\xE1\x84\x89\x29", + "\xE3\x88\x87" => "\x28\xE1\x84\x8B\x29", + "\xE3\x88\x88" => "\x28\xE1\x84\x8C\x29", + "\xE3\x88\x89" => "\x28\xE1\x84\x8E\x29", + "\xE3\x88\x8A" => "\x28\xE1\x84\x8F\x29", + "\xE3\x88\x8B" => "\x28\xE1\x84\x90\x29", + "\xE3\x88\x8C" => "\x28\xE1\x84\x91\x29", + "\xE3\x88\x8D" => "\x28\xE1\x84\x92\x29", + "\xE3\x88\x8E" => "\x28\xE1\x84\x80\xE1\x85\xA1\x29", + "\xE3\x88\x8F" => "\x28\xE1\x84\x82\xE1\x85\xA1\x29", + "\xE3\x88\x90" => "\x28\xE1\x84\x83\xE1\x85\xA1\x29", + "\xE3\x88\x91" => "\x28\xE1\x84\x85\xE1\x85\xA1\x29", + "\xE3\x88\x92" => "\x28\xE1\x84\x86\xE1\x85\xA1\x29", + "\xE3\x88\x93" => "\x28\xE1\x84\x87\xE1\x85\xA1\x29", + "\xE3\x88\x94" => "\x28\xE1\x84\x89\xE1\x85\xA1\x29", + "\xE3\x88\x95" => "\x28\xE1\x84\x8B\xE1\x85\xA1\x29", + "\xE3\x88\x96" => "\x28\xE1\x84\x8C\xE1\x85\xA1\x29", + "\xE3\x88\x97" => "\x28\xE1\x84\x8E\xE1\x85\xA1\x29", + "\xE3\x88\x98" => "\x28\xE1\x84\x8F\xE1\x85\xA1\x29", + "\xE3\x88\x99" => "\x28\xE1\x84\x90\xE1\x85\xA1\x29", + "\xE3\x88\x9A" => "\x28\xE1\x84\x91\xE1\x85\xA1\x29", + "\xE3\x88\x9B" => "\x28\xE1\x84\x92\xE1\x85\xA1\x29", + "\xE3\x88\x9C" => "\x28\xE1\x84\x8C\xE1\x85\xAE\x29", + "\xE3\x88\x9D" => "\x28\xE1\x84\x8B\xE1\x85\xA9\xE1\x84\x8C\xE1\x85\xA5\xE1\x86\xAB\x29", + "\xE3\x88\x9E" => "\x28\xE1\x84\x8B\xE1\x85\xA9\xE1\x84\x92\xE1\x85\xAE\x29", + "\xE3\x88\xA0" => "\x28\xE4\xB8\x80\x29", + "\xE3\x88\xA1" => "\x28\xE4\xBA\x8C\x29", + "\xE3\x88\xA2" => "\x28\xE4\xB8\x89\x29", + "\xE3\x88\xA3" => "\x28\xE5\x9B\x9B\x29", + "\xE3\x88\xA4" => "\x28\xE4\xBA\x94\x29", + "\xE3\x88\xA5" => "\x28\xE5\x85\xAD\x29", + "\xE3\x88\xA6" => "\x28\xE4\xB8\x83\x29", + "\xE3\x88\xA7" => "\x28\xE5\x85\xAB\x29", + "\xE3\x88\xA8" => "\x28\xE4\xB9\x9D\x29", + "\xE3\x88\xA9" => "\x28\xE5\x8D\x81\x29", + "\xE3\x88\xAA" => "\x28\xE6\x9C\x88\x29", + "\xE3\x88\xAB" => "\x28\xE7\x81\xAB\x29", + "\xE3\x88\xAC" => "\x28\xE6\xB0\xB4\x29", + "\xE3\x88\xAD" => "\x28\xE6\x9C\xA8\x29", + "\xE3\x88\xAE" => "\x28\xE9\x87\x91\x29", + "\xE3\x88\xAF" => "\x28\xE5\x9C\x9F\x29", + "\xE3\x88\xB0" => "\x28\xE6\x97\xA5\x29", + "\xE3\x88\xB1" => "\x28\xE6\xA0\xAA\x29", + "\xE3\x88\xB2" => "\x28\xE6\x9C\x89\x29", + "\xE3\x88\xB3" => "\x28\xE7\xA4\xBE\x29", + "\xE3\x88\xB4" => "\x28\xE5\x90\x8D\x29", + "\xE3\x88\xB5" => "\x28\xE7\x89\xB9\x29", + "\xE3\x88\xB6" => "\x28\xE8\xB2\xA1\x29", + "\xE3\x88\xB7" => "\x28\xE7\xA5\x9D\x29", + "\xE3\x88\xB8" => "\x28\xE5\x8A\xB4\x29", + "\xE3\x88\xB9" => "\x28\xE4\xBB\xA3\x29", + "\xE3\x88\xBA" => "\x28\xE5\x91\xBC\x29", + "\xE3\x88\xBB" => "\x28\xE5\xAD\xA6\x29", + "\xE3\x88\xBC" => "\x28\xE7\x9B\xA3\x29", + "\xE3\x88\xBD" => "\x28\xE4\xBC\x81\x29", + "\xE3\x88\xBE" => "\x28\xE8\xB3\x87\x29", + "\xE3\x88\xBF" => "\x28\xE5\x8D\x94\x29", + "\xE3\x89\x80" => "\x28\xE7\xA5\xAD\x29", + "\xE3\x89\x81" => "\x28\xE4\xBC\x91\x29", + "\xE3\x89\x82" => "\x28\xE8\x87\xAA\x29", + "\xE3\x89\x83" => "\x28\xE8\x87\xB3\x29", + "\xE3\x89\x84" => "\xE5\x95\x8F", + "\xE3\x89\x85" => "\xE5\xB9\xBC", + "\xE3\x89\x86" => "\xE6\x96\x87", + "\xE3\x89\x87" => "\xE7\xAE\x8F", + "\xE3\x89\x90" => "\x50\x54\x45", + "\xE3\x89\x91" => "\x32\x31", + "\xE3\x89\x92" => "\x32\x32", + "\xE3\x89\x93" => "\x32\x33", + "\xE3\x89\x94" => "\x32\x34", + "\xE3\x89\x95" => "\x32\x35", + "\xE3\x89\x96" => "\x32\x36", + "\xE3\x89\x97" => "\x32\x37", + "\xE3\x89\x98" => "\x32\x38", + "\xE3\x89\x99" => "\x32\x39", + "\xE3\x89\x9A" => "\x33\x30", + "\xE3\x89\x9B" => "\x33\x31", + "\xE3\x89\x9C" => "\x33\x32", + "\xE3\x89\x9D" => "\x33\x33", + "\xE3\x89\x9E" => "\x33\x34", + "\xE3\x89\x9F" => "\x33\x35", + "\xE3\x89\xA0" => "\xE1\x84\x80", + "\xE3\x89\xA1" => "\xE1\x84\x82", + "\xE3\x89\xA2" => "\xE1\x84\x83", + "\xE3\x89\xA3" => "\xE1\x84\x85", + "\xE3\x89\xA4" => "\xE1\x84\x86", + "\xE3\x89\xA5" => "\xE1\x84\x87", + "\xE3\x89\xA6" => "\xE1\x84\x89", + "\xE3\x89\xA7" => "\xE1\x84\x8B", + "\xE3\x89\xA8" => "\xE1\x84\x8C", + "\xE3\x89\xA9" => "\xE1\x84\x8E", + "\xE3\x89\xAA" => "\xE1\x84\x8F", + "\xE3\x89\xAB" => "\xE1\x84\x90", + "\xE3\x89\xAC" => "\xE1\x84\x91", + "\xE3\x89\xAD" => "\xE1\x84\x92", + "\xE3\x89\xAE" => "\xE1\x84\x80\xE1\x85\xA1", + "\xE3\x89\xAF" => "\xE1\x84\x82\xE1\x85\xA1", + "\xE3\x89\xB0" => "\xE1\x84\x83\xE1\x85\xA1", + "\xE3\x89\xB1" => "\xE1\x84\x85\xE1\x85\xA1", + "\xE3\x89\xB2" => "\xE1\x84\x86\xE1\x85\xA1", + "\xE3\x89\xB3" => "\xE1\x84\x87\xE1\x85\xA1", + "\xE3\x89\xB4" => "\xE1\x84\x89\xE1\x85\xA1", + "\xE3\x89\xB5" => "\xE1\x84\x8B\xE1\x85\xA1", + "\xE3\x89\xB6" => "\xE1\x84\x8C\xE1\x85\xA1", + "\xE3\x89\xB7" => "\xE1\x84\x8E\xE1\x85\xA1", + "\xE3\x89\xB8" => "\xE1\x84\x8F\xE1\x85\xA1", + "\xE3\x89\xB9" => "\xE1\x84\x90\xE1\x85\xA1", + "\xE3\x89\xBA" => "\xE1\x84\x91\xE1\x85\xA1", + "\xE3\x89\xBB" => "\xE1\x84\x92\xE1\x85\xA1", + "\xE3\x89\xBC" => "\xE1\x84\x8E\xE1\x85\xA1\xE1\x86\xB7\xE1\x84\x80\xE1\x85\xA9", + "\xE3\x89\xBD" => "\xE1\x84\x8C\xE1\x85\xAE\xE1\x84\x8B\xE1\x85\xB4", + "\xE3\x89\xBE" => "\xE1\x84\x8B\xE1\x85\xAE", + "\xE3\x8A\x80" => "\xE4\xB8\x80", + "\xE3\x8A\x81" => "\xE4\xBA\x8C", + "\xE3\x8A\x82" => "\xE4\xB8\x89", + "\xE3\x8A\x83" => "\xE5\x9B\x9B", + "\xE3\x8A\x84" => "\xE4\xBA\x94", + "\xE3\x8A\x85" => "\xE5\x85\xAD", + "\xE3\x8A\x86" => "\xE4\xB8\x83", + "\xE3\x8A\x87" => "\xE5\x85\xAB", + "\xE3\x8A\x88" => "\xE4\xB9\x9D", + "\xE3\x8A\x89" => "\xE5\x8D\x81", + "\xE3\x8A\x8A" => "\xE6\x9C\x88", + "\xE3\x8A\x8B" => "\xE7\x81\xAB", + "\xE3\x8A\x8C" => "\xE6\xB0\xB4", + "\xE3\x8A\x8D" => "\xE6\x9C\xA8", + "\xE3\x8A\x8E" => "\xE9\x87\x91", + "\xE3\x8A\x8F" => "\xE5\x9C\x9F", + "\xE3\x8A\x90" => "\xE6\x97\xA5", + "\xE3\x8A\x91" => "\xE6\xA0\xAA", + "\xE3\x8A\x92" => "\xE6\x9C\x89", + "\xE3\x8A\x93" => "\xE7\xA4\xBE", + "\xE3\x8A\x94" => "\xE5\x90\x8D", + "\xE3\x8A\x95" => "\xE7\x89\xB9", + "\xE3\x8A\x96" => "\xE8\xB2\xA1", + "\xE3\x8A\x97" => "\xE7\xA5\x9D", + "\xE3\x8A\x98" => "\xE5\x8A\xB4", + "\xE3\x8A\x99" => "\xE7\xA7\x98", + "\xE3\x8A\x9A" => "\xE7\x94\xB7", + "\xE3\x8A\x9B" => "\xE5\xA5\xB3", + "\xE3\x8A\x9C" => "\xE9\x81\xA9", + "\xE3\x8A\x9D" => "\xE5\x84\xAA", + "\xE3\x8A\x9E" => "\xE5\x8D\xB0", + "\xE3\x8A\x9F" => "\xE6\xB3\xA8", + "\xE3\x8A\xA0" => "\xE9\xA0\x85", + "\xE3\x8A\xA1" => "\xE4\xBC\x91", + "\xE3\x8A\xA2" => "\xE5\x86\x99", + "\xE3\x8A\xA3" => "\xE6\xAD\xA3", + "\xE3\x8A\xA4" => "\xE4\xB8\x8A", + "\xE3\x8A\xA5" => "\xE4\xB8\xAD", + "\xE3\x8A\xA6" => "\xE4\xB8\x8B", + "\xE3\x8A\xA7" => "\xE5\xB7\xA6", + "\xE3\x8A\xA8" => "\xE5\x8F\xB3", + "\xE3\x8A\xA9" => "\xE5\x8C\xBB", + "\xE3\x8A\xAA" => "\xE5\xAE\x97", + "\xE3\x8A\xAB" => "\xE5\xAD\xA6", + "\xE3\x8A\xAC" => "\xE7\x9B\xA3", + "\xE3\x8A\xAD" => "\xE4\xBC\x81", + "\xE3\x8A\xAE" => "\xE8\xB3\x87", + "\xE3\x8A\xAF" => "\xE5\x8D\x94", + "\xE3\x8A\xB0" => "\xE5\xA4\x9C", + "\xE3\x8A\xB1" => "\x33\x36", + "\xE3\x8A\xB2" => "\x33\x37", + "\xE3\x8A\xB3" => "\x33\x38", + "\xE3\x8A\xB4" => "\x33\x39", + "\xE3\x8A\xB5" => "\x34\x30", + "\xE3\x8A\xB6" => "\x34\x31", + "\xE3\x8A\xB7" => "\x34\x32", + "\xE3\x8A\xB8" => "\x34\x33", + "\xE3\x8A\xB9" => "\x34\x34", + "\xE3\x8A\xBA" => "\x34\x35", + "\xE3\x8A\xBB" => "\x34\x36", + "\xE3\x8A\xBC" => "\x34\x37", + "\xE3\x8A\xBD" => "\x34\x38", + "\xE3\x8A\xBE" => "\x34\x39", + "\xE3\x8A\xBF" => "\x35\x30", + "\xE3\x8B\x80" => "\x31\xE6\x9C\x88", + "\xE3\x8B\x81" => "\x32\xE6\x9C\x88", + "\xE3\x8B\x82" => "\x33\xE6\x9C\x88", + "\xE3\x8B\x83" => "\x34\xE6\x9C\x88", + "\xE3\x8B\x84" => "\x35\xE6\x9C\x88", + "\xE3\x8B\x85" => "\x36\xE6\x9C\x88", + "\xE3\x8B\x86" => "\x37\xE6\x9C\x88", + "\xE3\x8B\x87" => "\x38\xE6\x9C\x88", + "\xE3\x8B\x88" => "\x39\xE6\x9C\x88", + "\xE3\x8B\x89" => "\x31\x30\xE6\x9C\x88", + "\xE3\x8B\x8A" => "\x31\x31\xE6\x9C\x88", + "\xE3\x8B\x8B" => "\x31\x32\xE6\x9C\x88", + "\xE3\x8B\x8C" => "\x48\x67", + "\xE3\x8B\x8D" => "\x65\x72\x67", + "\xE3\x8B\x8E" => "\x65\x56", + "\xE3\x8B\x8F" => "\x4C\x54\x44", + "\xE3\x8B\x90" => "\xE3\x82\xA2", + "\xE3\x8B\x91" => "\xE3\x82\xA4", + "\xE3\x8B\x92" => "\xE3\x82\xA6", + "\xE3\x8B\x93" => "\xE3\x82\xA8", + "\xE3\x8B\x94" => "\xE3\x82\xAA", + "\xE3\x8B\x95" => "\xE3\x82\xAB", + "\xE3\x8B\x96" => "\xE3\x82\xAD", + "\xE3\x8B\x97" => "\xE3\x82\xAF", + "\xE3\x8B\x98" => "\xE3\x82\xB1", + "\xE3\x8B\x99" => "\xE3\x82\xB3", + "\xE3\x8B\x9A" => "\xE3\x82\xB5", + "\xE3\x8B\x9B" => "\xE3\x82\xB7", + "\xE3\x8B\x9C" => "\xE3\x82\xB9", + "\xE3\x8B\x9D" => "\xE3\x82\xBB", + "\xE3\x8B\x9E" => "\xE3\x82\xBD", + "\xE3\x8B\x9F" => "\xE3\x82\xBF", + "\xE3\x8B\xA0" => "\xE3\x83\x81", + "\xE3\x8B\xA1" => "\xE3\x83\x84", + "\xE3\x8B\xA2" => "\xE3\x83\x86", + "\xE3\x8B\xA3" => "\xE3\x83\x88", + "\xE3\x8B\xA4" => "\xE3\x83\x8A", + "\xE3\x8B\xA5" => "\xE3\x83\x8B", + "\xE3\x8B\xA6" => "\xE3\x83\x8C", + "\xE3\x8B\xA7" => "\xE3\x83\x8D", + "\xE3\x8B\xA8" => "\xE3\x83\x8E", + "\xE3\x8B\xA9" => "\xE3\x83\x8F", + "\xE3\x8B\xAA" => "\xE3\x83\x92", + "\xE3\x8B\xAB" => "\xE3\x83\x95", + "\xE3\x8B\xAC" => "\xE3\x83\x98", + "\xE3\x8B\xAD" => "\xE3\x83\x9B", + "\xE3\x8B\xAE" => "\xE3\x83\x9E", + "\xE3\x8B\xAF" => "\xE3\x83\x9F", + "\xE3\x8B\xB0" => "\xE3\x83\xA0", + "\xE3\x8B\xB1" => "\xE3\x83\xA1", + "\xE3\x8B\xB2" => "\xE3\x83\xA2", + "\xE3\x8B\xB3" => "\xE3\x83\xA4", + "\xE3\x8B\xB4" => "\xE3\x83\xA6", + "\xE3\x8B\xB5" => "\xE3\x83\xA8", + "\xE3\x8B\xB6" => "\xE3\x83\xA9", + "\xE3\x8B\xB7" => "\xE3\x83\xAA", + "\xE3\x8B\xB8" => "\xE3\x83\xAB", + "\xE3\x8B\xB9" => "\xE3\x83\xAC", + "\xE3\x8B\xBA" => "\xE3\x83\xAD", + "\xE3\x8B\xBB" => "\xE3\x83\xAF", + "\xE3\x8B\xBC" => "\xE3\x83\xB0", + "\xE3\x8B\xBD" => "\xE3\x83\xB1", + "\xE3\x8B\xBE" => "\xE3\x83\xB2", + "\xE3\x8B\xBF" => "\xE4\xBB\xA4\xE5\x92\x8C", + "\xE3\x8C\x80" => "\xE3\x82\xA2\xE3\x83\x8F\xE3\x82\x9A\xE3\x83\xBC\xE3\x83\x88", + "\xE3\x8C\x81" => "\xE3\x82\xA2\xE3\x83\xAB\xE3\x83\x95\xE3\x82\xA1", + "\xE3\x8C\x82" => "\xE3\x82\xA2\xE3\x83\xB3\xE3\x83\x98\xE3\x82\x9A\xE3\x82\xA2", + "\xE3\x8C\x83" => "\xE3\x82\xA2\xE3\x83\xBC\xE3\x83\xAB", + "\xE3\x8C\x84" => "\xE3\x82\xA4\xE3\x83\x8B\xE3\x83\xB3\xE3\x82\xAF\xE3\x82\x99", + "\xE3\x8C\x85" => "\xE3\x82\xA4\xE3\x83\xB3\xE3\x83\x81", + "\xE3\x8C\x86" => "\xE3\x82\xA6\xE3\x82\xA9\xE3\x83\xB3", + "\xE3\x8C\x87" => "\xE3\x82\xA8\xE3\x82\xB9\xE3\x82\xAF\xE3\x83\xBC\xE3\x83\x88\xE3\x82\x99", + "\xE3\x8C\x88" => "\xE3\x82\xA8\xE3\x83\xBC\xE3\x82\xAB\xE3\x83\xBC", + "\xE3\x8C\x89" => "\xE3\x82\xAA\xE3\x83\xB3\xE3\x82\xB9", + "\xE3\x8C\x8A" => "\xE3\x82\xAA\xE3\x83\xBC\xE3\x83\xA0", + "\xE3\x8C\x8B" => "\xE3\x82\xAB\xE3\x82\xA4\xE3\x83\xAA", + "\xE3\x8C\x8C" => "\xE3\x82\xAB\xE3\x83\xA9\xE3\x83\x83\xE3\x83\x88", + "\xE3\x8C\x8D" => "\xE3\x82\xAB\xE3\x83\xAD\xE3\x83\xAA\xE3\x83\xBC", + "\xE3\x8C\x8E" => "\xE3\x82\xAB\xE3\x82\x99\xE3\x83\xAD\xE3\x83\xB3", + "\xE3\x8C\x8F" => "\xE3\x82\xAB\xE3\x82\x99\xE3\x83\xB3\xE3\x83\x9E", + "\xE3\x8C\x90" => "\xE3\x82\xAD\xE3\x82\x99\xE3\x82\xAB\xE3\x82\x99", + "\xE3\x8C\x91" => "\xE3\x82\xAD\xE3\x82\x99\xE3\x83\x8B\xE3\x83\xBC", + "\xE3\x8C\x92" => "\xE3\x82\xAD\xE3\x83\xA5\xE3\x83\xAA\xE3\x83\xBC", + "\xE3\x8C\x93" => "\xE3\x82\xAD\xE3\x82\x99\xE3\x83\xAB\xE3\x82\xBF\xE3\x82\x99\xE3\x83\xBC", + "\xE3\x8C\x94" => "\xE3\x82\xAD\xE3\x83\xAD", + "\xE3\x8C\x95" => "\xE3\x82\xAD\xE3\x83\xAD\xE3\x82\xAF\xE3\x82\x99\xE3\x83\xA9\xE3\x83\xA0", + "\xE3\x8C\x96" => "\xE3\x82\xAD\xE3\x83\xAD\xE3\x83\xA1\xE3\x83\xBC\xE3\x83\x88\xE3\x83\xAB", + "\xE3\x8C\x97" => "\xE3\x82\xAD\xE3\x83\xAD\xE3\x83\xAF\xE3\x83\x83\xE3\x83\x88", + "\xE3\x8C\x98" => "\xE3\x82\xAF\xE3\x82\x99\xE3\x83\xA9\xE3\x83\xA0", + "\xE3\x8C\x99" => "\xE3\x82\xAF\xE3\x82\x99\xE3\x83\xA9\xE3\x83\xA0\xE3\x83\x88\xE3\x83\xB3", + "\xE3\x8C\x9A" => "\xE3\x82\xAF\xE3\x83\xAB\xE3\x82\xBB\xE3\x82\x99\xE3\x82\xA4\xE3\x83\xAD", + "\xE3\x8C\x9B" => "\xE3\x82\xAF\xE3\x83\xAD\xE3\x83\xBC\xE3\x83\x8D", + "\xE3\x8C\x9C" => "\xE3\x82\xB1\xE3\x83\xBC\xE3\x82\xB9", + "\xE3\x8C\x9D" => "\xE3\x82\xB3\xE3\x83\xAB\xE3\x83\x8A", + "\xE3\x8C\x9E" => "\xE3\x82\xB3\xE3\x83\xBC\xE3\x83\x9B\xE3\x82\x9A", + "\xE3\x8C\x9F" => "\xE3\x82\xB5\xE3\x82\xA4\xE3\x82\xAF\xE3\x83\xAB", + "\xE3\x8C\xA0" => "\xE3\x82\xB5\xE3\x83\xB3\xE3\x83\x81\xE3\x83\xBC\xE3\x83\xA0", + "\xE3\x8C\xA1" => "\xE3\x82\xB7\xE3\x83\xAA\xE3\x83\xB3\xE3\x82\xAF\xE3\x82\x99", + "\xE3\x8C\xA2" => "\xE3\x82\xBB\xE3\x83\xB3\xE3\x83\x81", + "\xE3\x8C\xA3" => "\xE3\x82\xBB\xE3\x83\xB3\xE3\x83\x88", + "\xE3\x8C\xA4" => "\xE3\x82\xBF\xE3\x82\x99\xE3\x83\xBC\xE3\x82\xB9", + "\xE3\x8C\xA5" => "\xE3\x83\x86\xE3\x82\x99\xE3\x82\xB7", + "\xE3\x8C\xA6" => "\xE3\x83\x88\xE3\x82\x99\xE3\x83\xAB", + "\xE3\x8C\xA7" => "\xE3\x83\x88\xE3\x83\xB3", + "\xE3\x8C\xA8" => "\xE3\x83\x8A\xE3\x83\x8E", + "\xE3\x8C\xA9" => "\xE3\x83\x8E\xE3\x83\x83\xE3\x83\x88", + "\xE3\x8C\xAA" => "\xE3\x83\x8F\xE3\x82\xA4\xE3\x83\x84", + "\xE3\x8C\xAB" => "\xE3\x83\x8F\xE3\x82\x9A\xE3\x83\xBC\xE3\x82\xBB\xE3\x83\xB3\xE3\x83\x88", + "\xE3\x8C\xAC" => "\xE3\x83\x8F\xE3\x82\x9A\xE3\x83\xBC\xE3\x83\x84", + "\xE3\x8C\xAD" => "\xE3\x83\x8F\xE3\x82\x99\xE3\x83\xBC\xE3\x83\xAC\xE3\x83\xAB", + "\xE3\x8C\xAE" => "\xE3\x83\x92\xE3\x82\x9A\xE3\x82\xA2\xE3\x82\xB9\xE3\x83\x88\xE3\x83\xAB", + "\xE3\x8C\xAF" => "\xE3\x83\x92\xE3\x82\x9A\xE3\x82\xAF\xE3\x83\xAB", + "\xE3\x8C\xB0" => "\xE3\x83\x92\xE3\x82\x9A\xE3\x82\xB3", + "\xE3\x8C\xB1" => "\xE3\x83\x92\xE3\x82\x99\xE3\x83\xAB", + "\xE3\x8C\xB2" => "\xE3\x83\x95\xE3\x82\xA1\xE3\x83\xA9\xE3\x83\x83\xE3\x83\x88\xE3\x82\x99", + "\xE3\x8C\xB3" => "\xE3\x83\x95\xE3\x82\xA3\xE3\x83\xBC\xE3\x83\x88", + "\xE3\x8C\xB4" => "\xE3\x83\x95\xE3\x82\x99\xE3\x83\x83\xE3\x82\xB7\xE3\x82\xA7\xE3\x83\xAB", + "\xE3\x8C\xB5" => "\xE3\x83\x95\xE3\x83\xA9\xE3\x83\xB3", + "\xE3\x8C\xB6" => "\xE3\x83\x98\xE3\x82\xAF\xE3\x82\xBF\xE3\x83\xBC\xE3\x83\xAB", + "\xE3\x8C\xB7" => "\xE3\x83\x98\xE3\x82\x9A\xE3\x82\xBD", + "\xE3\x8C\xB8" => "\xE3\x83\x98\xE3\x82\x9A\xE3\x83\x8B\xE3\x83\x92", + "\xE3\x8C\xB9" => "\xE3\x83\x98\xE3\x83\xAB\xE3\x83\x84", + "\xE3\x8C\xBA" => "\xE3\x83\x98\xE3\x82\x9A\xE3\x83\xB3\xE3\x82\xB9", + "\xE3\x8C\xBB" => "\xE3\x83\x98\xE3\x82\x9A\xE3\x83\xBC\xE3\x82\xB7\xE3\x82\x99", + "\xE3\x8C\xBC" => "\xE3\x83\x98\xE3\x82\x99\xE3\x83\xBC\xE3\x82\xBF", + "\xE3\x8C\xBD" => "\xE3\x83\x9B\xE3\x82\x9A\xE3\x82\xA4\xE3\x83\xB3\xE3\x83\x88", + "\xE3\x8C\xBE" => "\xE3\x83\x9B\xE3\x82\x99\xE3\x83\xAB\xE3\x83\x88", + "\xE3\x8C\xBF" => "\xE3\x83\x9B\xE3\x83\xB3", + "\xE3\x8D\x80" => "\xE3\x83\x9B\xE3\x82\x9A\xE3\x83\xB3\xE3\x83\x88\xE3\x82\x99", + "\xE3\x8D\x81" => "\xE3\x83\x9B\xE3\x83\xBC\xE3\x83\xAB", + "\xE3\x8D\x82" => "\xE3\x83\x9B\xE3\x83\xBC\xE3\x83\xB3", + "\xE3\x8D\x83" => "\xE3\x83\x9E\xE3\x82\xA4\xE3\x82\xAF\xE3\x83\xAD", + "\xE3\x8D\x84" => "\xE3\x83\x9E\xE3\x82\xA4\xE3\x83\xAB", + "\xE3\x8D\x85" => "\xE3\x83\x9E\xE3\x83\x83\xE3\x83\x8F", + "\xE3\x8D\x86" => "\xE3\x83\x9E\xE3\x83\xAB\xE3\x82\xAF", + "\xE3\x8D\x87" => "\xE3\x83\x9E\xE3\x83\xB3\xE3\x82\xB7\xE3\x83\xA7\xE3\x83\xB3", + "\xE3\x8D\x88" => "\xE3\x83\x9F\xE3\x82\xAF\xE3\x83\xAD\xE3\x83\xB3", + "\xE3\x8D\x89" => "\xE3\x83\x9F\xE3\x83\xAA", + "\xE3\x8D\x8A" => "\xE3\x83\x9F\xE3\x83\xAA\xE3\x83\x8F\xE3\x82\x99\xE3\x83\xBC\xE3\x83\xAB", + "\xE3\x8D\x8B" => "\xE3\x83\xA1\xE3\x82\xAB\xE3\x82\x99", + "\xE3\x8D\x8C" => "\xE3\x83\xA1\xE3\x82\xAB\xE3\x82\x99\xE3\x83\x88\xE3\x83\xB3", + "\xE3\x8D\x8D" => "\xE3\x83\xA1\xE3\x83\xBC\xE3\x83\x88\xE3\x83\xAB", + "\xE3\x8D\x8E" => "\xE3\x83\xA4\xE3\x83\xBC\xE3\x83\x88\xE3\x82\x99", + "\xE3\x8D\x8F" => "\xE3\x83\xA4\xE3\x83\xBC\xE3\x83\xAB", + "\xE3\x8D\x90" => "\xE3\x83\xA6\xE3\x82\xA2\xE3\x83\xB3", + "\xE3\x8D\x91" => "\xE3\x83\xAA\xE3\x83\x83\xE3\x83\x88\xE3\x83\xAB", + "\xE3\x8D\x92" => "\xE3\x83\xAA\xE3\x83\xA9", + "\xE3\x8D\x93" => "\xE3\x83\xAB\xE3\x83\x92\xE3\x82\x9A\xE3\x83\xBC", + "\xE3\x8D\x94" => "\xE3\x83\xAB\xE3\x83\xBC\xE3\x83\x95\xE3\x82\x99\xE3\x83\xAB", + "\xE3\x8D\x95" => "\xE3\x83\xAC\xE3\x83\xA0", + "\xE3\x8D\x96" => "\xE3\x83\xAC\xE3\x83\xB3\xE3\x83\x88\xE3\x82\xB1\xE3\x82\x99\xE3\x83\xB3", + "\xE3\x8D\x97" => "\xE3\x83\xAF\xE3\x83\x83\xE3\x83\x88", + "\xE3\x8D\x98" => "\x30\xE7\x82\xB9", + "\xE3\x8D\x99" => "\x31\xE7\x82\xB9", + "\xE3\x8D\x9A" => "\x32\xE7\x82\xB9", + "\xE3\x8D\x9B" => "\x33\xE7\x82\xB9", + "\xE3\x8D\x9C" => "\x34\xE7\x82\xB9", + "\xE3\x8D\x9D" => "\x35\xE7\x82\xB9", + "\xE3\x8D\x9E" => "\x36\xE7\x82\xB9", + "\xE3\x8D\x9F" => "\x37\xE7\x82\xB9", + "\xE3\x8D\xA0" => "\x38\xE7\x82\xB9", + "\xE3\x8D\xA1" => "\x39\xE7\x82\xB9", + "\xE3\x8D\xA2" => "\x31\x30\xE7\x82\xB9", + "\xE3\x8D\xA3" => "\x31\x31\xE7\x82\xB9", + "\xE3\x8D\xA4" => "\x31\x32\xE7\x82\xB9", + "\xE3\x8D\xA5" => "\x31\x33\xE7\x82\xB9", + "\xE3\x8D\xA6" => "\x31\x34\xE7\x82\xB9", + "\xE3\x8D\xA7" => "\x31\x35\xE7\x82\xB9", + "\xE3\x8D\xA8" => "\x31\x36\xE7\x82\xB9", + "\xE3\x8D\xA9" => "\x31\x37\xE7\x82\xB9", + "\xE3\x8D\xAA" => "\x31\x38\xE7\x82\xB9", + "\xE3\x8D\xAB" => "\x31\x39\xE7\x82\xB9", + "\xE3\x8D\xAC" => "\x32\x30\xE7\x82\xB9", + "\xE3\x8D\xAD" => "\x32\x31\xE7\x82\xB9", + "\xE3\x8D\xAE" => "\x32\x32\xE7\x82\xB9", + "\xE3\x8D\xAF" => "\x32\x33\xE7\x82\xB9", + "\xE3\x8D\xB0" => "\x32\x34\xE7\x82\xB9", + "\xE3\x8D\xB1" => "\x68\x50\x61", + "\xE3\x8D\xB2" => "\x64\x61", + "\xE3\x8D\xB3" => "\x41\x55", + "\xE3\x8D\xB4" => "\x62\x61\x72", + "\xE3\x8D\xB5" => "\x6F\x56", + "\xE3\x8D\xB6" => "\x70\x63", + "\xE3\x8D\xB7" => "\x64\x6D", + "\xE3\x8D\xB8" => "\x64\x6D\x32", + "\xE3\x8D\xB9" => "\x64\x6D\x33", + "\xE3\x8D\xBA" => "\x49\x55", + "\xE3\x8D\xBB" => "\xE5\xB9\xB3\xE6\x88\x90", + "\xE3\x8D\xBC" => "\xE6\x98\xAD\xE5\x92\x8C", + "\xE3\x8D\xBD" => "\xE5\xA4\xA7\xE6\xAD\xA3", + "\xE3\x8D\xBE" => "\xE6\x98\x8E\xE6\xB2\xBB", + "\xE3\x8D\xBF" => "\xE6\xA0\xAA\xE5\xBC\x8F\xE4\xBC\x9A\xE7\xA4\xBE", + "\xE3\x8E\x80" => "\x70\x41", + "\xE3\x8E\x81" => "\x6E\x41", + "\xE3\x8E\x82" => "\xCE\xBC\x41", + "\xE3\x8E\x83" => "\x6D\x41", + "\xE3\x8E\x84" => "\x6B\x41", + "\xE3\x8E\x85" => "\x4B\x42", + "\xE3\x8E\x86" => "\x4D\x42", + "\xE3\x8E\x87" => "\x47\x42", + "\xE3\x8E\x88" => "\x63\x61\x6C", + "\xE3\x8E\x89" => "\x6B\x63\x61\x6C", + "\xE3\x8E\x8A" => "\x70\x46", + "\xE3\x8E\x8B" => "\x6E\x46", + "\xE3\x8E\x8C" => "\xCE\xBC\x46", + "\xE3\x8E\x8D" => "\xCE\xBC\x67", + "\xE3\x8E\x8E" => "\x6D\x67", + "\xE3\x8E\x8F" => "\x6B\x67", + "\xE3\x8E\x90" => "\x48\x7A", + "\xE3\x8E\x91" => "\x6B\x48\x7A", + "\xE3\x8E\x92" => "\x4D\x48\x7A", + "\xE3\x8E\x93" => "\x47\x48\x7A", + "\xE3\x8E\x94" => "\x54\x48\x7A", + "\xE3\x8E\x95" => "\xCE\xBC\x6C", + "\xE3\x8E\x96" => "\x6D\x6C", + "\xE3\x8E\x97" => "\x64\x6C", + "\xE3\x8E\x98" => "\x6B\x6C", + "\xE3\x8E\x99" => "\x66\x6D", + "\xE3\x8E\x9A" => "\x6E\x6D", + "\xE3\x8E\x9B" => "\xCE\xBC\x6D", + "\xE3\x8E\x9C" => "\x6D\x6D", + "\xE3\x8E\x9D" => "\x63\x6D", + "\xE3\x8E\x9E" => "\x6B\x6D", + "\xE3\x8E\x9F" => "\x6D\x6D\x32", + "\xE3\x8E\xA0" => "\x63\x6D\x32", + "\xE3\x8E\xA1" => "\x6D\x32", + "\xE3\x8E\xA2" => "\x6B\x6D\x32", + "\xE3\x8E\xA3" => "\x6D\x6D\x33", + "\xE3\x8E\xA4" => "\x63\x6D\x33", + "\xE3\x8E\xA5" => "\x6D\x33", + "\xE3\x8E\xA6" => "\x6B\x6D\x33", + "\xE3\x8E\xA7" => "\x6D\xE2\x88\x95\x73", + "\xE3\x8E\xA8" => "\x6D\xE2\x88\x95\x73\x32", + "\xE3\x8E\xA9" => "\x50\x61", + "\xE3\x8E\xAA" => "\x6B\x50\x61", + "\xE3\x8E\xAB" => "\x4D\x50\x61", + "\xE3\x8E\xAC" => "\x47\x50\x61", + "\xE3\x8E\xAD" => "\x72\x61\x64", + "\xE3\x8E\xAE" => "\x72\x61\x64\xE2\x88\x95\x73", + "\xE3\x8E\xAF" => "\x72\x61\x64\xE2\x88\x95\x73\x32", + "\xE3\x8E\xB0" => "\x70\x73", + "\xE3\x8E\xB1" => "\x6E\x73", + "\xE3\x8E\xB2" => "\xCE\xBC\x73", + "\xE3\x8E\xB3" => "\x6D\x73", + "\xE3\x8E\xB4" => "\x70\x56", + "\xE3\x8E\xB5" => "\x6E\x56", + "\xE3\x8E\xB6" => "\xCE\xBC\x56", + "\xE3\x8E\xB7" => "\x6D\x56", + "\xE3\x8E\xB8" => "\x6B\x56", + "\xE3\x8E\xB9" => "\x4D\x56", + "\xE3\x8E\xBA" => "\x70\x57", + "\xE3\x8E\xBB" => "\x6E\x57", + "\xE3\x8E\xBC" => "\xCE\xBC\x57", + "\xE3\x8E\xBD" => "\x6D\x57", + "\xE3\x8E\xBE" => "\x6B\x57", + "\xE3\x8E\xBF" => "\x4D\x57", + "\xE3\x8F\x80" => "\x6B\xCE\xA9", + "\xE3\x8F\x81" => "\x4D\xCE\xA9", + "\xE3\x8F\x82" => "\x61\x2E\x6D\x2E", + "\xE3\x8F\x83" => "\x42\x71", + "\xE3\x8F\x84" => "\x63\x63", + "\xE3\x8F\x85" => "\x63\x64", + "\xE3\x8F\x86" => "\x43\xE2\x88\x95\x6B\x67", + "\xE3\x8F\x87" => "\x43\x6F\x2E", + "\xE3\x8F\x88" => "\x64\x42", + "\xE3\x8F\x89" => "\x47\x79", + "\xE3\x8F\x8A" => "\x68\x61", + "\xE3\x8F\x8B" => "\x48\x50", + "\xE3\x8F\x8C" => "\x69\x6E", + "\xE3\x8F\x8D" => "\x4B\x4B", + "\xE3\x8F\x8E" => "\x4B\x4D", + "\xE3\x8F\x8F" => "\x6B\x74", + "\xE3\x8F\x90" => "\x6C\x6D", + "\xE3\x8F\x91" => "\x6C\x6E", + "\xE3\x8F\x92" => "\x6C\x6F\x67", + "\xE3\x8F\x93" => "\x6C\x78", + "\xE3\x8F\x94" => "\x6D\x62", + "\xE3\x8F\x95" => "\x6D\x69\x6C", + "\xE3\x8F\x96" => "\x6D\x6F\x6C", + "\xE3\x8F\x97" => "\x50\x48", + "\xE3\x8F\x98" => "\x70\x2E\x6D\x2E", + "\xE3\x8F\x99" => "\x50\x50\x4D", + "\xE3\x8F\x9A" => "\x50\x52", + "\xE3\x8F\x9B" => "\x73\x72", + "\xE3\x8F\x9C" => "\x53\x76", + "\xE3\x8F\x9D" => "\x57\x62", + "\xE3\x8F\x9E" => "\x56\xE2\x88\x95\x6D", + "\xE3\x8F\x9F" => "\x41\xE2\x88\x95\x6D", + "\xE3\x8F\xA0" => "\x31\xE6\x97\xA5", + "\xE3\x8F\xA1" => "\x32\xE6\x97\xA5", + "\xE3\x8F\xA2" => "\x33\xE6\x97\xA5", + "\xE3\x8F\xA3" => "\x34\xE6\x97\xA5", + "\xE3\x8F\xA4" => "\x35\xE6\x97\xA5", + "\xE3\x8F\xA5" => "\x36\xE6\x97\xA5", + "\xE3\x8F\xA6" => "\x37\xE6\x97\xA5", + "\xE3\x8F\xA7" => "\x38\xE6\x97\xA5", + "\xE3\x8F\xA8" => "\x39\xE6\x97\xA5", + "\xE3\x8F\xA9" => "\x31\x30\xE6\x97\xA5", + "\xE3\x8F\xAA" => "\x31\x31\xE6\x97\xA5", + "\xE3\x8F\xAB" => "\x31\x32\xE6\x97\xA5", + "\xE3\x8F\xAC" => "\x31\x33\xE6\x97\xA5", + "\xE3\x8F\xAD" => "\x31\x34\xE6\x97\xA5", + "\xE3\x8F\xAE" => "\x31\x35\xE6\x97\xA5", + "\xE3\x8F\xAF" => "\x31\x36\xE6\x97\xA5", + "\xE3\x8F\xB0" => "\x31\x37\xE6\x97\xA5", + "\xE3\x8F\xB1" => "\x31\x38\xE6\x97\xA5", + "\xE3\x8F\xB2" => "\x31\x39\xE6\x97\xA5", + "\xE3\x8F\xB3" => "\x32\x30\xE6\x97\xA5", + "\xE3\x8F\xB4" => "\x32\x31\xE6\x97\xA5", + "\xE3\x8F\xB5" => "\x32\x32\xE6\x97\xA5", + "\xE3\x8F\xB6" => "\x32\x33\xE6\x97\xA5", + "\xE3\x8F\xB7" => "\x32\x34\xE6\x97\xA5", + "\xE3\x8F\xB8" => "\x32\x35\xE6\x97\xA5", + "\xE3\x8F\xB9" => "\x32\x36\xE6\x97\xA5", + "\xE3\x8F\xBA" => "\x32\x37\xE6\x97\xA5", + "\xE3\x8F\xBB" => "\x32\x38\xE6\x97\xA5", + "\xE3\x8F\xBC" => "\x32\x39\xE6\x97\xA5", + "\xE3\x8F\xBD" => "\x33\x30\xE6\x97\xA5", + "\xE3\x8F\xBE" => "\x33\x31\xE6\x97\xA5", + "\xE3\x8F\xBF" => "\x67\x61\x6C", + "\xEA\x9A\x9C" => "\xD1\x8A", + "\xEA\x9A\x9D" => "\xD1\x8C", + "\xEA\x9D\xB0" => "\xEA\x9D\xAF", + "\xEA\x9F\xB2" => "\x43", + "\xEA\x9F\xB3" => "\x46", + "\xEA\x9F\xB4" => "\x51", + "\xEA\x9F\xB8" => "\xC4\xA6", + "\xEA\x9F\xB9" => "\xC5\x93", + "\xEA\xAD\x9C" => "\xEA\x9C\xA7", + "\xEA\xAD\x9D" => "\xEA\xAC\xB7", + "\xEA\xAD\x9E" => "\xC9\xAB", + "\xEA\xAD\x9F" => "\xEA\xAD\x92", + "\xEA\xAD\xA9" => "\xCA\x8D", + "\xEF\xAC\x80" => "\x66\x66", + "\xEF\xAC\x81" => "\x66\x69", + "\xEF\xAC\x82" => "\x66\x6C", + "\xEF\xAC\x83" => "\x66\x66\x69", + "\xEF\xAC\x84" => "\x66\x66\x6C", + "\xEF\xAC\x85" => "\x73\x74", + "\xEF\xAC\x86" => "\x73\x74", + "\xEF\xAC\x93" => "\xD5\xB4\xD5\xB6", + "\xEF\xAC\x94" => "\xD5\xB4\xD5\xA5", + "\xEF\xAC\x95" => "\xD5\xB4\xD5\xAB", + "\xEF\xAC\x96" => "\xD5\xBE\xD5\xB6", + "\xEF\xAC\x97" => "\xD5\xB4\xD5\xAD", + "\xEF\xAC\xA0" => "\xD7\xA2", + "\xEF\xAC\xA1" => "\xD7\x90", + "\xEF\xAC\xA2" => "\xD7\x93", + "\xEF\xAC\xA3" => "\xD7\x94", + "\xEF\xAC\xA4" => "\xD7\x9B", + "\xEF\xAC\xA5" => "\xD7\x9C", + "\xEF\xAC\xA6" => "\xD7\x9D", + "\xEF\xAC\xA7" => "\xD7\xA8", + "\xEF\xAC\xA8" => "\xD7\xAA", + "\xEF\xAC\xA9" => "\x2B", + "\xEF\xAD\x8F" => "\xD7\x90\xD7\x9C", + "\xEF\xAD\x90" => "\xD9\xB1", + "\xEF\xAD\x91" => "\xD9\xB1", + "\xEF\xAD\x92" => "\xD9\xBB", + "\xEF\xAD\x93" => "\xD9\xBB", + "\xEF\xAD\x94" => "\xD9\xBB", + "\xEF\xAD\x95" => "\xD9\xBB", + "\xEF\xAD\x96" => "\xD9\xBE", + "\xEF\xAD\x97" => "\xD9\xBE", + "\xEF\xAD\x98" => "\xD9\xBE", + "\xEF\xAD\x99" => "\xD9\xBE", + "\xEF\xAD\x9A" => "\xDA\x80", + "\xEF\xAD\x9B" => "\xDA\x80", + "\xEF\xAD\x9C" => "\xDA\x80", + "\xEF\xAD\x9D" => "\xDA\x80", + "\xEF\xAD\x9E" => "\xD9\xBA", + "\xEF\xAD\x9F" => "\xD9\xBA", + "\xEF\xAD\xA0" => "\xD9\xBA", + "\xEF\xAD\xA1" => "\xD9\xBA", + "\xEF\xAD\xA2" => "\xD9\xBF", + "\xEF\xAD\xA3" => "\xD9\xBF", + "\xEF\xAD\xA4" => "\xD9\xBF", + "\xEF\xAD\xA5" => "\xD9\xBF", + "\xEF\xAD\xA6" => "\xD9\xB9", + "\xEF\xAD\xA7" => "\xD9\xB9", + "\xEF\xAD\xA8" => "\xD9\xB9", + "\xEF\xAD\xA9" => "\xD9\xB9", + "\xEF\xAD\xAA" => "\xDA\xA4", + "\xEF\xAD\xAB" => "\xDA\xA4", + "\xEF\xAD\xAC" => "\xDA\xA4", + "\xEF\xAD\xAD" => "\xDA\xA4", + "\xEF\xAD\xAE" => "\xDA\xA6", + "\xEF\xAD\xAF" => "\xDA\xA6", + "\xEF\xAD\xB0" => "\xDA\xA6", + "\xEF\xAD\xB1" => "\xDA\xA6", + "\xEF\xAD\xB2" => "\xDA\x84", + "\xEF\xAD\xB3" => "\xDA\x84", + "\xEF\xAD\xB4" => "\xDA\x84", + "\xEF\xAD\xB5" => "\xDA\x84", + "\xEF\xAD\xB6" => "\xDA\x83", + "\xEF\xAD\xB7" => "\xDA\x83", + "\xEF\xAD\xB8" => "\xDA\x83", + "\xEF\xAD\xB9" => "\xDA\x83", + "\xEF\xAD\xBA" => "\xDA\x86", + "\xEF\xAD\xBB" => "\xDA\x86", + "\xEF\xAD\xBC" => "\xDA\x86", + "\xEF\xAD\xBD" => "\xDA\x86", + "\xEF\xAD\xBE" => "\xDA\x87", + "\xEF\xAD\xBF" => "\xDA\x87", + "\xEF\xAE\x80" => "\xDA\x87", + "\xEF\xAE\x81" => "\xDA\x87", + "\xEF\xAE\x82" => "\xDA\x8D", + "\xEF\xAE\x83" => "\xDA\x8D", + "\xEF\xAE\x84" => "\xDA\x8C", + "\xEF\xAE\x85" => "\xDA\x8C", + "\xEF\xAE\x86" => "\xDA\x8E", + "\xEF\xAE\x87" => "\xDA\x8E", + "\xEF\xAE\x88" => "\xDA\x88", + "\xEF\xAE\x89" => "\xDA\x88", + "\xEF\xAE\x8A" => "\xDA\x98", + "\xEF\xAE\x8B" => "\xDA\x98", + "\xEF\xAE\x8C" => "\xDA\x91", + "\xEF\xAE\x8D" => "\xDA\x91", + "\xEF\xAE\x8E" => "\xDA\xA9", + "\xEF\xAE\x8F" => "\xDA\xA9", + "\xEF\xAE\x90" => "\xDA\xA9", + "\xEF\xAE\x91" => "\xDA\xA9", + "\xEF\xAE\x92" => "\xDA\xAF", + "\xEF\xAE\x93" => "\xDA\xAF", + "\xEF\xAE\x94" => "\xDA\xAF", + "\xEF\xAE\x95" => "\xDA\xAF", + "\xEF\xAE\x96" => "\xDA\xB3", + "\xEF\xAE\x97" => "\xDA\xB3", + "\xEF\xAE\x98" => "\xDA\xB3", + "\xEF\xAE\x99" => "\xDA\xB3", + "\xEF\xAE\x9A" => "\xDA\xB1", + "\xEF\xAE\x9B" => "\xDA\xB1", + "\xEF\xAE\x9C" => "\xDA\xB1", + "\xEF\xAE\x9D" => "\xDA\xB1", + "\xEF\xAE\x9E" => "\xDA\xBA", + "\xEF\xAE\x9F" => "\xDA\xBA", + "\xEF\xAE\xA0" => "\xDA\xBB", + "\xEF\xAE\xA1" => "\xDA\xBB", + "\xEF\xAE\xA2" => "\xDA\xBB", + "\xEF\xAE\xA3" => "\xDA\xBB", + "\xEF\xAE\xA4" => "\xDB\x95\xD9\x94", + "\xEF\xAE\xA5" => "\xDB\x95\xD9\x94", + "\xEF\xAE\xA6" => "\xDB\x81", + "\xEF\xAE\xA7" => "\xDB\x81", + "\xEF\xAE\xA8" => "\xDB\x81", + "\xEF\xAE\xA9" => "\xDB\x81", + "\xEF\xAE\xAA" => "\xDA\xBE", + "\xEF\xAE\xAB" => "\xDA\xBE", + "\xEF\xAE\xAC" => "\xDA\xBE", + "\xEF\xAE\xAD" => "\xDA\xBE", + "\xEF\xAE\xAE" => "\xDB\x92", + "\xEF\xAE\xAF" => "\xDB\x92", + "\xEF\xAE\xB0" => "\xDB\x92\xD9\x94", + "\xEF\xAE\xB1" => "\xDB\x92\xD9\x94", + "\xEF\xAF\x93" => "\xDA\xAD", + "\xEF\xAF\x94" => "\xDA\xAD", + "\xEF\xAF\x95" => "\xDA\xAD", + "\xEF\xAF\x96" => "\xDA\xAD", + "\xEF\xAF\x97" => "\xDB\x87", + "\xEF\xAF\x98" => "\xDB\x87", + "\xEF\xAF\x99" => "\xDB\x86", + "\xEF\xAF\x9A" => "\xDB\x86", + "\xEF\xAF\x9B" => "\xDB\x88", + "\xEF\xAF\x9C" => "\xDB\x88", + "\xEF\xAF\x9D" => "\xDB\x87\xD9\xB4", + "\xEF\xAF\x9E" => "\xDB\x8B", + "\xEF\xAF\x9F" => "\xDB\x8B", + "\xEF\xAF\xA0" => "\xDB\x85", + "\xEF\xAF\xA1" => "\xDB\x85", + "\xEF\xAF\xA2" => "\xDB\x89", + "\xEF\xAF\xA3" => "\xDB\x89", + "\xEF\xAF\xA4" => "\xDB\x90", + "\xEF\xAF\xA5" => "\xDB\x90", + "\xEF\xAF\xA6" => "\xDB\x90", + "\xEF\xAF\xA7" => "\xDB\x90", + "\xEF\xAF\xA8" => "\xD9\x89", + "\xEF\xAF\xA9" => "\xD9\x89", + "\xEF\xAF\xAA" => "\xD9\x8A\xD9\x94\xD8\xA7", + "\xEF\xAF\xAB" => "\xD9\x8A\xD9\x94\xD8\xA7", + "\xEF\xAF\xAC" => "\xD9\x8A\xD9\x94\xDB\x95", + "\xEF\xAF\xAD" => "\xD9\x8A\xD9\x94\xDB\x95", + "\xEF\xAF\xAE" => "\xD9\x8A\xD9\x94\xD9\x88", + "\xEF\xAF\xAF" => "\xD9\x8A\xD9\x94\xD9\x88", + "\xEF\xAF\xB0" => "\xD9\x8A\xD9\x94\xDB\x87", + "\xEF\xAF\xB1" => "\xD9\x8A\xD9\x94\xDB\x87", + "\xEF\xAF\xB2" => "\xD9\x8A\xD9\x94\xDB\x86", + "\xEF\xAF\xB3" => "\xD9\x8A\xD9\x94\xDB\x86", + "\xEF\xAF\xB4" => "\xD9\x8A\xD9\x94\xDB\x88", + "\xEF\xAF\xB5" => "\xD9\x8A\xD9\x94\xDB\x88", + "\xEF\xAF\xB6" => "\xD9\x8A\xD9\x94\xDB\x90", + "\xEF\xAF\xB7" => "\xD9\x8A\xD9\x94\xDB\x90", + "\xEF\xAF\xB8" => "\xD9\x8A\xD9\x94\xDB\x90", + "\xEF\xAF\xB9" => "\xD9\x8A\xD9\x94\xD9\x89", + "\xEF\xAF\xBA" => "\xD9\x8A\xD9\x94\xD9\x89", + "\xEF\xAF\xBB" => "\xD9\x8A\xD9\x94\xD9\x89", + "\xEF\xAF\xBC" => "\xDB\x8C", + "\xEF\xAF\xBD" => "\xDB\x8C", + "\xEF\xAF\xBE" => "\xDB\x8C", + "\xEF\xAF\xBF" => "\xDB\x8C", + "\xEF\xB0\x80" => "\xD9\x8A\xD9\x94\xD8\xAC", + "\xEF\xB0\x81" => "\xD9\x8A\xD9\x94\xD8\xAD", + "\xEF\xB0\x82" => "\xD9\x8A\xD9\x94\xD9\x85", + "\xEF\xB0\x83" => "\xD9\x8A\xD9\x94\xD9\x89", + "\xEF\xB0\x84" => "\xD9\x8A\xD9\x94\xD9\x8A", + "\xEF\xB0\x85" => "\xD8\xA8\xD8\xAC", + "\xEF\xB0\x86" => "\xD8\xA8\xD8\xAD", + "\xEF\xB0\x87" => "\xD8\xA8\xD8\xAE", + "\xEF\xB0\x88" => "\xD8\xA8\xD9\x85", + "\xEF\xB0\x89" => "\xD8\xA8\xD9\x89", + "\xEF\xB0\x8A" => "\xD8\xA8\xD9\x8A", + "\xEF\xB0\x8B" => "\xD8\xAA\xD8\xAC", + "\xEF\xB0\x8C" => "\xD8\xAA\xD8\xAD", + "\xEF\xB0\x8D" => "\xD8\xAA\xD8\xAE", + "\xEF\xB0\x8E" => "\xD8\xAA\xD9\x85", + "\xEF\xB0\x8F" => "\xD8\xAA\xD9\x89", + "\xEF\xB0\x90" => "\xD8\xAA\xD9\x8A", + "\xEF\xB0\x91" => "\xD8\xAB\xD8\xAC", + "\xEF\xB0\x92" => "\xD8\xAB\xD9\x85", + "\xEF\xB0\x93" => "\xD8\xAB\xD9\x89", + "\xEF\xB0\x94" => "\xD8\xAB\xD9\x8A", + "\xEF\xB0\x95" => "\xD8\xAC\xD8\xAD", + "\xEF\xB0\x96" => "\xD8\xAC\xD9\x85", + "\xEF\xB0\x97" => "\xD8\xAD\xD8\xAC", + "\xEF\xB0\x98" => "\xD8\xAD\xD9\x85", + "\xEF\xB0\x99" => "\xD8\xAE\xD8\xAC", + "\xEF\xB0\x9A" => "\xD8\xAE\xD8\xAD", + "\xEF\xB0\x9B" => "\xD8\xAE\xD9\x85", + "\xEF\xB0\x9C" => "\xD8\xB3\xD8\xAC", + "\xEF\xB0\x9D" => "\xD8\xB3\xD8\xAD", + "\xEF\xB0\x9E" => "\xD8\xB3\xD8\xAE", + "\xEF\xB0\x9F" => "\xD8\xB3\xD9\x85", + "\xEF\xB0\xA0" => "\xD8\xB5\xD8\xAD", + "\xEF\xB0\xA1" => "\xD8\xB5\xD9\x85", + "\xEF\xB0\xA2" => "\xD8\xB6\xD8\xAC", + "\xEF\xB0\xA3" => "\xD8\xB6\xD8\xAD", + "\xEF\xB0\xA4" => "\xD8\xB6\xD8\xAE", + "\xEF\xB0\xA5" => "\xD8\xB6\xD9\x85", + "\xEF\xB0\xA6" => "\xD8\xB7\xD8\xAD", + "\xEF\xB0\xA7" => "\xD8\xB7\xD9\x85", + "\xEF\xB0\xA8" => "\xD8\xB8\xD9\x85", + "\xEF\xB0\xA9" => "\xD8\xB9\xD8\xAC", + "\xEF\xB0\xAA" => "\xD8\xB9\xD9\x85", + "\xEF\xB0\xAB" => "\xD8\xBA\xD8\xAC", + "\xEF\xB0\xAC" => "\xD8\xBA\xD9\x85", + "\xEF\xB0\xAD" => "\xD9\x81\xD8\xAC", + "\xEF\xB0\xAE" => "\xD9\x81\xD8\xAD", + "\xEF\xB0\xAF" => "\xD9\x81\xD8\xAE", + "\xEF\xB0\xB0" => "\xD9\x81\xD9\x85", + "\xEF\xB0\xB1" => "\xD9\x81\xD9\x89", + "\xEF\xB0\xB2" => "\xD9\x81\xD9\x8A", + "\xEF\xB0\xB3" => "\xD9\x82\xD8\xAD", + "\xEF\xB0\xB4" => "\xD9\x82\xD9\x85", + "\xEF\xB0\xB5" => "\xD9\x82\xD9\x89", + "\xEF\xB0\xB6" => "\xD9\x82\xD9\x8A", + "\xEF\xB0\xB7" => "\xD9\x83\xD8\xA7", + "\xEF\xB0\xB8" => "\xD9\x83\xD8\xAC", + "\xEF\xB0\xB9" => "\xD9\x83\xD8\xAD", + "\xEF\xB0\xBA" => "\xD9\x83\xD8\xAE", + "\xEF\xB0\xBB" => "\xD9\x83\xD9\x84", + "\xEF\xB0\xBC" => "\xD9\x83\xD9\x85", + "\xEF\xB0\xBD" => "\xD9\x83\xD9\x89", + "\xEF\xB0\xBE" => "\xD9\x83\xD9\x8A", + "\xEF\xB0\xBF" => "\xD9\x84\xD8\xAC", + "\xEF\xB1\x80" => "\xD9\x84\xD8\xAD", + "\xEF\xB1\x81" => "\xD9\x84\xD8\xAE", + "\xEF\xB1\x82" => "\xD9\x84\xD9\x85", + "\xEF\xB1\x83" => "\xD9\x84\xD9\x89", + "\xEF\xB1\x84" => "\xD9\x84\xD9\x8A", + "\xEF\xB1\x85" => "\xD9\x85\xD8\xAC", + "\xEF\xB1\x86" => "\xD9\x85\xD8\xAD", + "\xEF\xB1\x87" => "\xD9\x85\xD8\xAE", + "\xEF\xB1\x88" => "\xD9\x85\xD9\x85", + "\xEF\xB1\x89" => "\xD9\x85\xD9\x89", + "\xEF\xB1\x8A" => "\xD9\x85\xD9\x8A", + "\xEF\xB1\x8B" => "\xD9\x86\xD8\xAC", + "\xEF\xB1\x8C" => "\xD9\x86\xD8\xAD", + "\xEF\xB1\x8D" => "\xD9\x86\xD8\xAE", + "\xEF\xB1\x8E" => "\xD9\x86\xD9\x85", + "\xEF\xB1\x8F" => "\xD9\x86\xD9\x89", + "\xEF\xB1\x90" => "\xD9\x86\xD9\x8A", + "\xEF\xB1\x91" => "\xD9\x87\xD8\xAC", + "\xEF\xB1\x92" => "\xD9\x87\xD9\x85", + "\xEF\xB1\x93" => "\xD9\x87\xD9\x89", + "\xEF\xB1\x94" => "\xD9\x87\xD9\x8A", + "\xEF\xB1\x95" => "\xD9\x8A\xD8\xAC", + "\xEF\xB1\x96" => "\xD9\x8A\xD8\xAD", + "\xEF\xB1\x97" => "\xD9\x8A\xD8\xAE", + "\xEF\xB1\x98" => "\xD9\x8A\xD9\x85", + "\xEF\xB1\x99" => "\xD9\x8A\xD9\x89", + "\xEF\xB1\x9A" => "\xD9\x8A\xD9\x8A", + "\xEF\xB1\x9B" => "\xD8\xB0\xD9\xB0", + "\xEF\xB1\x9C" => "\xD8\xB1\xD9\xB0", + "\xEF\xB1\x9D" => "\xD9\x89\xD9\xB0", + "\xEF\xB1\x9E" => "\x20\xD9\x8C\xD9\x91", + "\xEF\xB1\x9F" => "\x20\xD9\x8D\xD9\x91", + "\xEF\xB1\xA0" => "\x20\xD9\x8E\xD9\x91", + "\xEF\xB1\xA1" => "\x20\xD9\x8F\xD9\x91", + "\xEF\xB1\xA2" => "\x20\xD9\x90\xD9\x91", + "\xEF\xB1\xA3" => "\x20\xD9\x91\xD9\xB0", + "\xEF\xB1\xA4" => "\xD9\x8A\xD9\x94\xD8\xB1", + "\xEF\xB1\xA5" => "\xD9\x8A\xD9\x94\xD8\xB2", + "\xEF\xB1\xA6" => "\xD9\x8A\xD9\x94\xD9\x85", + "\xEF\xB1\xA7" => "\xD9\x8A\xD9\x94\xD9\x86", + "\xEF\xB1\xA8" => "\xD9\x8A\xD9\x94\xD9\x89", + "\xEF\xB1\xA9" => "\xD9\x8A\xD9\x94\xD9\x8A", + "\xEF\xB1\xAA" => "\xD8\xA8\xD8\xB1", + "\xEF\xB1\xAB" => "\xD8\xA8\xD8\xB2", + "\xEF\xB1\xAC" => "\xD8\xA8\xD9\x85", + "\xEF\xB1\xAD" => "\xD8\xA8\xD9\x86", + "\xEF\xB1\xAE" => "\xD8\xA8\xD9\x89", + "\xEF\xB1\xAF" => "\xD8\xA8\xD9\x8A", + "\xEF\xB1\xB0" => "\xD8\xAA\xD8\xB1", + "\xEF\xB1\xB1" => "\xD8\xAA\xD8\xB2", + "\xEF\xB1\xB2" => "\xD8\xAA\xD9\x85", + "\xEF\xB1\xB3" => "\xD8\xAA\xD9\x86", + "\xEF\xB1\xB4" => "\xD8\xAA\xD9\x89", + "\xEF\xB1\xB5" => "\xD8\xAA\xD9\x8A", + "\xEF\xB1\xB6" => "\xD8\xAB\xD8\xB1", + "\xEF\xB1\xB7" => "\xD8\xAB\xD8\xB2", + "\xEF\xB1\xB8" => "\xD8\xAB\xD9\x85", + "\xEF\xB1\xB9" => "\xD8\xAB\xD9\x86", + "\xEF\xB1\xBA" => "\xD8\xAB\xD9\x89", + "\xEF\xB1\xBB" => "\xD8\xAB\xD9\x8A", + "\xEF\xB1\xBC" => "\xD9\x81\xD9\x89", + "\xEF\xB1\xBD" => "\xD9\x81\xD9\x8A", + "\xEF\xB1\xBE" => "\xD9\x82\xD9\x89", + "\xEF\xB1\xBF" => "\xD9\x82\xD9\x8A", + "\xEF\xB2\x80" => "\xD9\x83\xD8\xA7", + "\xEF\xB2\x81" => "\xD9\x83\xD9\x84", + "\xEF\xB2\x82" => "\xD9\x83\xD9\x85", + "\xEF\xB2\x83" => "\xD9\x83\xD9\x89", + "\xEF\xB2\x84" => "\xD9\x83\xD9\x8A", + "\xEF\xB2\x85" => "\xD9\x84\xD9\x85", + "\xEF\xB2\x86" => "\xD9\x84\xD9\x89", + "\xEF\xB2\x87" => "\xD9\x84\xD9\x8A", + "\xEF\xB2\x88" => "\xD9\x85\xD8\xA7", + "\xEF\xB2\x89" => "\xD9\x85\xD9\x85", + "\xEF\xB2\x8A" => "\xD9\x86\xD8\xB1", + "\xEF\xB2\x8B" => "\xD9\x86\xD8\xB2", + "\xEF\xB2\x8C" => "\xD9\x86\xD9\x85", + "\xEF\xB2\x8D" => "\xD9\x86\xD9\x86", + "\xEF\xB2\x8E" => "\xD9\x86\xD9\x89", + "\xEF\xB2\x8F" => "\xD9\x86\xD9\x8A", + "\xEF\xB2\x90" => "\xD9\x89\xD9\xB0", + "\xEF\xB2\x91" => "\xD9\x8A\xD8\xB1", + "\xEF\xB2\x92" => "\xD9\x8A\xD8\xB2", + "\xEF\xB2\x93" => "\xD9\x8A\xD9\x85", + "\xEF\xB2\x94" => "\xD9\x8A\xD9\x86", + "\xEF\xB2\x95" => "\xD9\x8A\xD9\x89", + "\xEF\xB2\x96" => "\xD9\x8A\xD9\x8A", + "\xEF\xB2\x97" => "\xD9\x8A\xD9\x94\xD8\xAC", + "\xEF\xB2\x98" => "\xD9\x8A\xD9\x94\xD8\xAD", + "\xEF\xB2\x99" => "\xD9\x8A\xD9\x94\xD8\xAE", + "\xEF\xB2\x9A" => "\xD9\x8A\xD9\x94\xD9\x85", + "\xEF\xB2\x9B" => "\xD9\x8A\xD9\x94\xD9\x87", + "\xEF\xB2\x9C" => "\xD8\xA8\xD8\xAC", + "\xEF\xB2\x9D" => "\xD8\xA8\xD8\xAD", + "\xEF\xB2\x9E" => "\xD8\xA8\xD8\xAE", + "\xEF\xB2\x9F" => "\xD8\xA8\xD9\x85", + "\xEF\xB2\xA0" => "\xD8\xA8\xD9\x87", + "\xEF\xB2\xA1" => "\xD8\xAA\xD8\xAC", + "\xEF\xB2\xA2" => "\xD8\xAA\xD8\xAD", + "\xEF\xB2\xA3" => "\xD8\xAA\xD8\xAE", + "\xEF\xB2\xA4" => "\xD8\xAA\xD9\x85", + "\xEF\xB2\xA5" => "\xD8\xAA\xD9\x87", + "\xEF\xB2\xA6" => "\xD8\xAB\xD9\x85", + "\xEF\xB2\xA7" => "\xD8\xAC\xD8\xAD", + "\xEF\xB2\xA8" => "\xD8\xAC\xD9\x85", + "\xEF\xB2\xA9" => "\xD8\xAD\xD8\xAC", + "\xEF\xB2\xAA" => "\xD8\xAD\xD9\x85", + "\xEF\xB2\xAB" => "\xD8\xAE\xD8\xAC", + "\xEF\xB2\xAC" => "\xD8\xAE\xD9\x85", + "\xEF\xB2\xAD" => "\xD8\xB3\xD8\xAC", + "\xEF\xB2\xAE" => "\xD8\xB3\xD8\xAD", + "\xEF\xB2\xAF" => "\xD8\xB3\xD8\xAE", + "\xEF\xB2\xB0" => "\xD8\xB3\xD9\x85", + "\xEF\xB2\xB1" => "\xD8\xB5\xD8\xAD", + "\xEF\xB2\xB2" => "\xD8\xB5\xD8\xAE", + "\xEF\xB2\xB3" => "\xD8\xB5\xD9\x85", + "\xEF\xB2\xB4" => "\xD8\xB6\xD8\xAC", + "\xEF\xB2\xB5" => "\xD8\xB6\xD8\xAD", + "\xEF\xB2\xB6" => "\xD8\xB6\xD8\xAE", + "\xEF\xB2\xB7" => "\xD8\xB6\xD9\x85", + "\xEF\xB2\xB8" => "\xD8\xB7\xD8\xAD", + "\xEF\xB2\xB9" => "\xD8\xB8\xD9\x85", + "\xEF\xB2\xBA" => "\xD8\xB9\xD8\xAC", + "\xEF\xB2\xBB" => "\xD8\xB9\xD9\x85", + "\xEF\xB2\xBC" => "\xD8\xBA\xD8\xAC", + "\xEF\xB2\xBD" => "\xD8\xBA\xD9\x85", + "\xEF\xB2\xBE" => "\xD9\x81\xD8\xAC", + "\xEF\xB2\xBF" => "\xD9\x81\xD8\xAD", + "\xEF\xB3\x80" => "\xD9\x81\xD8\xAE", + "\xEF\xB3\x81" => "\xD9\x81\xD9\x85", + "\xEF\xB3\x82" => "\xD9\x82\xD8\xAD", + "\xEF\xB3\x83" => "\xD9\x82\xD9\x85", + "\xEF\xB3\x84" => "\xD9\x83\xD8\xAC", + "\xEF\xB3\x85" => "\xD9\x83\xD8\xAD", + "\xEF\xB3\x86" => "\xD9\x83\xD8\xAE", + "\xEF\xB3\x87" => "\xD9\x83\xD9\x84", + "\xEF\xB3\x88" => "\xD9\x83\xD9\x85", + "\xEF\xB3\x89" => "\xD9\x84\xD8\xAC", + "\xEF\xB3\x8A" => "\xD9\x84\xD8\xAD", + "\xEF\xB3\x8B" => "\xD9\x84\xD8\xAE", + "\xEF\xB3\x8C" => "\xD9\x84\xD9\x85", + "\xEF\xB3\x8D" => "\xD9\x84\xD9\x87", + "\xEF\xB3\x8E" => "\xD9\x85\xD8\xAC", + "\xEF\xB3\x8F" => "\xD9\x85\xD8\xAD", + "\xEF\xB3\x90" => "\xD9\x85\xD8\xAE", + "\xEF\xB3\x91" => "\xD9\x85\xD9\x85", + "\xEF\xB3\x92" => "\xD9\x86\xD8\xAC", + "\xEF\xB3\x93" => "\xD9\x86\xD8\xAD", + "\xEF\xB3\x94" => "\xD9\x86\xD8\xAE", + "\xEF\xB3\x95" => "\xD9\x86\xD9\x85", + "\xEF\xB3\x96" => "\xD9\x86\xD9\x87", + "\xEF\xB3\x97" => "\xD9\x87\xD8\xAC", + "\xEF\xB3\x98" => "\xD9\x87\xD9\x85", + "\xEF\xB3\x99" => "\xD9\x87\xD9\xB0", + "\xEF\xB3\x9A" => "\xD9\x8A\xD8\xAC", + "\xEF\xB3\x9B" => "\xD9\x8A\xD8\xAD", + "\xEF\xB3\x9C" => "\xD9\x8A\xD8\xAE", + "\xEF\xB3\x9D" => "\xD9\x8A\xD9\x85", + "\xEF\xB3\x9E" => "\xD9\x8A\xD9\x87", + "\xEF\xB3\x9F" => "\xD9\x8A\xD9\x94\xD9\x85", + "\xEF\xB3\xA0" => "\xD9\x8A\xD9\x94\xD9\x87", + "\xEF\xB3\xA1" => "\xD8\xA8\xD9\x85", + "\xEF\xB3\xA2" => "\xD8\xA8\xD9\x87", + "\xEF\xB3\xA3" => "\xD8\xAA\xD9\x85", + "\xEF\xB3\xA4" => "\xD8\xAA\xD9\x87", + "\xEF\xB3\xA5" => "\xD8\xAB\xD9\x85", + "\xEF\xB3\xA6" => "\xD8\xAB\xD9\x87", + "\xEF\xB3\xA7" => "\xD8\xB3\xD9\x85", + "\xEF\xB3\xA8" => "\xD8\xB3\xD9\x87", + "\xEF\xB3\xA9" => "\xD8\xB4\xD9\x85", + "\xEF\xB3\xAA" => "\xD8\xB4\xD9\x87", + "\xEF\xB3\xAB" => "\xD9\x83\xD9\x84", + "\xEF\xB3\xAC" => "\xD9\x83\xD9\x85", + "\xEF\xB3\xAD" => "\xD9\x84\xD9\x85", + "\xEF\xB3\xAE" => "\xD9\x86\xD9\x85", + "\xEF\xB3\xAF" => "\xD9\x86\xD9\x87", + "\xEF\xB3\xB0" => "\xD9\x8A\xD9\x85", + "\xEF\xB3\xB1" => "\xD9\x8A\xD9\x87", + "\xEF\xB3\xB2" => "\xD9\x80\xD9\x8E\xD9\x91", + "\xEF\xB3\xB3" => "\xD9\x80\xD9\x8F\xD9\x91", + "\xEF\xB3\xB4" => "\xD9\x80\xD9\x90\xD9\x91", + "\xEF\xB3\xB5" => "\xD8\xB7\xD9\x89", + "\xEF\xB3\xB6" => "\xD8\xB7\xD9\x8A", + "\xEF\xB3\xB7" => "\xD8\xB9\xD9\x89", + "\xEF\xB3\xB8" => "\xD8\xB9\xD9\x8A", + "\xEF\xB3\xB9" => "\xD8\xBA\xD9\x89", + "\xEF\xB3\xBA" => "\xD8\xBA\xD9\x8A", + "\xEF\xB3\xBB" => "\xD8\xB3\xD9\x89", + "\xEF\xB3\xBC" => "\xD8\xB3\xD9\x8A", + "\xEF\xB3\xBD" => "\xD8\xB4\xD9\x89", + "\xEF\xB3\xBE" => "\xD8\xB4\xD9\x8A", + "\xEF\xB3\xBF" => "\xD8\xAD\xD9\x89", + "\xEF\xB4\x80" => "\xD8\xAD\xD9\x8A", + "\xEF\xB4\x81" => "\xD8\xAC\xD9\x89", + "\xEF\xB4\x82" => "\xD8\xAC\xD9\x8A", + "\xEF\xB4\x83" => "\xD8\xAE\xD9\x89", + "\xEF\xB4\x84" => "\xD8\xAE\xD9\x8A", + "\xEF\xB4\x85" => "\xD8\xB5\xD9\x89", + "\xEF\xB4\x86" => "\xD8\xB5\xD9\x8A", + "\xEF\xB4\x87" => "\xD8\xB6\xD9\x89", + "\xEF\xB4\x88" => "\xD8\xB6\xD9\x8A", + "\xEF\xB4\x89" => "\xD8\xB4\xD8\xAC", + "\xEF\xB4\x8A" => "\xD8\xB4\xD8\xAD", + "\xEF\xB4\x8B" => "\xD8\xB4\xD8\xAE", + "\xEF\xB4\x8C" => "\xD8\xB4\xD9\x85", + "\xEF\xB4\x8D" => "\xD8\xB4\xD8\xB1", + "\xEF\xB4\x8E" => "\xD8\xB3\xD8\xB1", + "\xEF\xB4\x8F" => "\xD8\xB5\xD8\xB1", + "\xEF\xB4\x90" => "\xD8\xB6\xD8\xB1", + "\xEF\xB4\x91" => "\xD8\xB7\xD9\x89", + "\xEF\xB4\x92" => "\xD8\xB7\xD9\x8A", + "\xEF\xB4\x93" => "\xD8\xB9\xD9\x89", + "\xEF\xB4\x94" => "\xD8\xB9\xD9\x8A", + "\xEF\xB4\x95" => "\xD8\xBA\xD9\x89", + "\xEF\xB4\x96" => "\xD8\xBA\xD9\x8A", + "\xEF\xB4\x97" => "\xD8\xB3\xD9\x89", + "\xEF\xB4\x98" => "\xD8\xB3\xD9\x8A", + "\xEF\xB4\x99" => "\xD8\xB4\xD9\x89", + "\xEF\xB4\x9A" => "\xD8\xB4\xD9\x8A", + "\xEF\xB4\x9B" => "\xD8\xAD\xD9\x89", + "\xEF\xB4\x9C" => "\xD8\xAD\xD9\x8A", + "\xEF\xB4\x9D" => "\xD8\xAC\xD9\x89", + "\xEF\xB4\x9E" => "\xD8\xAC\xD9\x8A", + "\xEF\xB4\x9F" => "\xD8\xAE\xD9\x89", + "\xEF\xB4\xA0" => "\xD8\xAE\xD9\x8A", + "\xEF\xB4\xA1" => "\xD8\xB5\xD9\x89", + "\xEF\xB4\xA2" => "\xD8\xB5\xD9\x8A", + "\xEF\xB4\xA3" => "\xD8\xB6\xD9\x89", + "\xEF\xB4\xA4" => "\xD8\xB6\xD9\x8A", + "\xEF\xB4\xA5" => "\xD8\xB4\xD8\xAC", + "\xEF\xB4\xA6" => "\xD8\xB4\xD8\xAD", + "\xEF\xB4\xA7" => "\xD8\xB4\xD8\xAE", + "\xEF\xB4\xA8" => "\xD8\xB4\xD9\x85", + "\xEF\xB4\xA9" => "\xD8\xB4\xD8\xB1", + "\xEF\xB4\xAA" => "\xD8\xB3\xD8\xB1", + "\xEF\xB4\xAB" => "\xD8\xB5\xD8\xB1", + "\xEF\xB4\xAC" => "\xD8\xB6\xD8\xB1", + "\xEF\xB4\xAD" => "\xD8\xB4\xD8\xAC", + "\xEF\xB4\xAE" => "\xD8\xB4\xD8\xAD", + "\xEF\xB4\xAF" => "\xD8\xB4\xD8\xAE", + "\xEF\xB4\xB0" => "\xD8\xB4\xD9\x85", + "\xEF\xB4\xB1" => "\xD8\xB3\xD9\x87", + "\xEF\xB4\xB2" => "\xD8\xB4\xD9\x87", + "\xEF\xB4\xB3" => "\xD8\xB7\xD9\x85", + "\xEF\xB4\xB4" => "\xD8\xB3\xD8\xAC", + "\xEF\xB4\xB5" => "\xD8\xB3\xD8\xAD", + "\xEF\xB4\xB6" => "\xD8\xB3\xD8\xAE", + "\xEF\xB4\xB7" => "\xD8\xB4\xD8\xAC", + "\xEF\xB4\xB8" => "\xD8\xB4\xD8\xAD", + "\xEF\xB4\xB9" => "\xD8\xB4\xD8\xAE", + "\xEF\xB4\xBA" => "\xD8\xB7\xD9\x85", + "\xEF\xB4\xBB" => "\xD8\xB8\xD9\x85", + "\xEF\xB4\xBC" => "\xD8\xA7\xD9\x8B", + "\xEF\xB4\xBD" => "\xD8\xA7\xD9\x8B", + "\xEF\xB5\x90" => "\xD8\xAA\xD8\xAC\xD9\x85", + "\xEF\xB5\x91" => "\xD8\xAA\xD8\xAD\xD8\xAC", + "\xEF\xB5\x92" => "\xD8\xAA\xD8\xAD\xD8\xAC", + "\xEF\xB5\x93" => "\xD8\xAA\xD8\xAD\xD9\x85", + "\xEF\xB5\x94" => "\xD8\xAA\xD8\xAE\xD9\x85", + "\xEF\xB5\x95" => "\xD8\xAA\xD9\x85\xD8\xAC", + "\xEF\xB5\x96" => "\xD8\xAA\xD9\x85\xD8\xAD", + "\xEF\xB5\x97" => "\xD8\xAA\xD9\x85\xD8\xAE", + "\xEF\xB5\x98" => "\xD8\xAC\xD9\x85\xD8\xAD", + "\xEF\xB5\x99" => "\xD8\xAC\xD9\x85\xD8\xAD", + "\xEF\xB5\x9A" => "\xD8\xAD\xD9\x85\xD9\x8A", + "\xEF\xB5\x9B" => "\xD8\xAD\xD9\x85\xD9\x89", + "\xEF\xB5\x9C" => "\xD8\xB3\xD8\xAD\xD8\xAC", + "\xEF\xB5\x9D" => "\xD8\xB3\xD8\xAC\xD8\xAD", + "\xEF\xB5\x9E" => "\xD8\xB3\xD8\xAC\xD9\x89", + "\xEF\xB5\x9F" => "\xD8\xB3\xD9\x85\xD8\xAD", + "\xEF\xB5\xA0" => "\xD8\xB3\xD9\x85\xD8\xAD", + "\xEF\xB5\xA1" => "\xD8\xB3\xD9\x85\xD8\xAC", + "\xEF\xB5\xA2" => "\xD8\xB3\xD9\x85\xD9\x85", + "\xEF\xB5\xA3" => "\xD8\xB3\xD9\x85\xD9\x85", + "\xEF\xB5\xA4" => "\xD8\xB5\xD8\xAD\xD8\xAD", + "\xEF\xB5\xA5" => "\xD8\xB5\xD8\xAD\xD8\xAD", + "\xEF\xB5\xA6" => "\xD8\xB5\xD9\x85\xD9\x85", + "\xEF\xB5\xA7" => "\xD8\xB4\xD8\xAD\xD9\x85", + "\xEF\xB5\xA8" => "\xD8\xB4\xD8\xAD\xD9\x85", + "\xEF\xB5\xA9" => "\xD8\xB4\xD8\xAC\xD9\x8A", + "\xEF\xB5\xAA" => "\xD8\xB4\xD9\x85\xD8\xAE", + "\xEF\xB5\xAB" => "\xD8\xB4\xD9\x85\xD8\xAE", + "\xEF\xB5\xAC" => "\xD8\xB4\xD9\x85\xD9\x85", + "\xEF\xB5\xAD" => "\xD8\xB4\xD9\x85\xD9\x85", + "\xEF\xB5\xAE" => "\xD8\xB6\xD8\xAD\xD9\x89", + "\xEF\xB5\xAF" => "\xD8\xB6\xD8\xAE\xD9\x85", + "\xEF\xB5\xB0" => "\xD8\xB6\xD8\xAE\xD9\x85", + "\xEF\xB5\xB1" => "\xD8\xB7\xD9\x85\xD8\xAD", + "\xEF\xB5\xB2" => "\xD8\xB7\xD9\x85\xD8\xAD", + "\xEF\xB5\xB3" => "\xD8\xB7\xD9\x85\xD9\x85", + "\xEF\xB5\xB4" => "\xD8\xB7\xD9\x85\xD9\x8A", + "\xEF\xB5\xB5" => "\xD8\xB9\xD8\xAC\xD9\x85", + "\xEF\xB5\xB6" => "\xD8\xB9\xD9\x85\xD9\x85", + "\xEF\xB5\xB7" => "\xD8\xB9\xD9\x85\xD9\x85", + "\xEF\xB5\xB8" => "\xD8\xB9\xD9\x85\xD9\x89", + "\xEF\xB5\xB9" => "\xD8\xBA\xD9\x85\xD9\x85", + "\xEF\xB5\xBA" => "\xD8\xBA\xD9\x85\xD9\x8A", + "\xEF\xB5\xBB" => "\xD8\xBA\xD9\x85\xD9\x89", + "\xEF\xB5\xBC" => "\xD9\x81\xD8\xAE\xD9\x85", + "\xEF\xB5\xBD" => "\xD9\x81\xD8\xAE\xD9\x85", + "\xEF\xB5\xBE" => "\xD9\x82\xD9\x85\xD8\xAD", + "\xEF\xB5\xBF" => "\xD9\x82\xD9\x85\xD9\x85", + "\xEF\xB6\x80" => "\xD9\x84\xD8\xAD\xD9\x85", + "\xEF\xB6\x81" => "\xD9\x84\xD8\xAD\xD9\x8A", + "\xEF\xB6\x82" => "\xD9\x84\xD8\xAD\xD9\x89", + "\xEF\xB6\x83" => "\xD9\x84\xD8\xAC\xD8\xAC", + "\xEF\xB6\x84" => "\xD9\x84\xD8\xAC\xD8\xAC", + "\xEF\xB6\x85" => "\xD9\x84\xD8\xAE\xD9\x85", + "\xEF\xB6\x86" => "\xD9\x84\xD8\xAE\xD9\x85", + "\xEF\xB6\x87" => "\xD9\x84\xD9\x85\xD8\xAD", + "\xEF\xB6\x88" => "\xD9\x84\xD9\x85\xD8\xAD", + "\xEF\xB6\x89" => "\xD9\x85\xD8\xAD\xD8\xAC", + "\xEF\xB6\x8A" => "\xD9\x85\xD8\xAD\xD9\x85", + "\xEF\xB6\x8B" => "\xD9\x85\xD8\xAD\xD9\x8A", + "\xEF\xB6\x8C" => "\xD9\x85\xD8\xAC\xD8\xAD", + "\xEF\xB6\x8D" => "\xD9\x85\xD8\xAC\xD9\x85", + "\xEF\xB6\x8E" => "\xD9\x85\xD8\xAE\xD8\xAC", + "\xEF\xB6\x8F" => "\xD9\x85\xD8\xAE\xD9\x85", + "\xEF\xB6\x92" => "\xD9\x85\xD8\xAC\xD8\xAE", + "\xEF\xB6\x93" => "\xD9\x87\xD9\x85\xD8\xAC", + "\xEF\xB6\x94" => "\xD9\x87\xD9\x85\xD9\x85", + "\xEF\xB6\x95" => "\xD9\x86\xD8\xAD\xD9\x85", + "\xEF\xB6\x96" => "\xD9\x86\xD8\xAD\xD9\x89", + "\xEF\xB6\x97" => "\xD9\x86\xD8\xAC\xD9\x85", + "\xEF\xB6\x98" => "\xD9\x86\xD8\xAC\xD9\x85", + "\xEF\xB6\x99" => "\xD9\x86\xD8\xAC\xD9\x89", + "\xEF\xB6\x9A" => "\xD9\x86\xD9\x85\xD9\x8A", + "\xEF\xB6\x9B" => "\xD9\x86\xD9\x85\xD9\x89", + "\xEF\xB6\x9C" => "\xD9\x8A\xD9\x85\xD9\x85", + "\xEF\xB6\x9D" => "\xD9\x8A\xD9\x85\xD9\x85", + "\xEF\xB6\x9E" => "\xD8\xA8\xD8\xAE\xD9\x8A", + "\xEF\xB6\x9F" => "\xD8\xAA\xD8\xAC\xD9\x8A", + "\xEF\xB6\xA0" => "\xD8\xAA\xD8\xAC\xD9\x89", + "\xEF\xB6\xA1" => "\xD8\xAA\xD8\xAE\xD9\x8A", + "\xEF\xB6\xA2" => "\xD8\xAA\xD8\xAE\xD9\x89", + "\xEF\xB6\xA3" => "\xD8\xAA\xD9\x85\xD9\x8A", + "\xEF\xB6\xA4" => "\xD8\xAA\xD9\x85\xD9\x89", + "\xEF\xB6\xA5" => "\xD8\xAC\xD9\x85\xD9\x8A", + "\xEF\xB6\xA6" => "\xD8\xAC\xD8\xAD\xD9\x89", + "\xEF\xB6\xA7" => "\xD8\xAC\xD9\x85\xD9\x89", + "\xEF\xB6\xA8" => "\xD8\xB3\xD8\xAE\xD9\x89", + "\xEF\xB6\xA9" => "\xD8\xB5\xD8\xAD\xD9\x8A", + "\xEF\xB6\xAA" => "\xD8\xB4\xD8\xAD\xD9\x8A", + "\xEF\xB6\xAB" => "\xD8\xB6\xD8\xAD\xD9\x8A", + "\xEF\xB6\xAC" => "\xD9\x84\xD8\xAC\xD9\x8A", + "\xEF\xB6\xAD" => "\xD9\x84\xD9\x85\xD9\x8A", + "\xEF\xB6\xAE" => "\xD9\x8A\xD8\xAD\xD9\x8A", + "\xEF\xB6\xAF" => "\xD9\x8A\xD8\xAC\xD9\x8A", + "\xEF\xB6\xB0" => "\xD9\x8A\xD9\x85\xD9\x8A", + "\xEF\xB6\xB1" => "\xD9\x85\xD9\x85\xD9\x8A", + "\xEF\xB6\xB2" => "\xD9\x82\xD9\x85\xD9\x8A", + "\xEF\xB6\xB3" => "\xD9\x86\xD8\xAD\xD9\x8A", + "\xEF\xB6\xB4" => "\xD9\x82\xD9\x85\xD8\xAD", + "\xEF\xB6\xB5" => "\xD9\x84\xD8\xAD\xD9\x85", + "\xEF\xB6\xB6" => "\xD8\xB9\xD9\x85\xD9\x8A", + "\xEF\xB6\xB7" => "\xD9\x83\xD9\x85\xD9\x8A", + "\xEF\xB6\xB8" => "\xD9\x86\xD8\xAC\xD8\xAD", + "\xEF\xB6\xB9" => "\xD9\x85\xD8\xAE\xD9\x8A", + "\xEF\xB6\xBA" => "\xD9\x84\xD8\xAC\xD9\x85", + "\xEF\xB6\xBB" => "\xD9\x83\xD9\x85\xD9\x85", + "\xEF\xB6\xBC" => "\xD9\x84\xD8\xAC\xD9\x85", + "\xEF\xB6\xBD" => "\xD9\x86\xD8\xAC\xD8\xAD", + "\xEF\xB6\xBE" => "\xD8\xAC\xD8\xAD\xD9\x8A", + "\xEF\xB6\xBF" => "\xD8\xAD\xD8\xAC\xD9\x8A", + "\xEF\xB7\x80" => "\xD9\x85\xD8\xAC\xD9\x8A", + "\xEF\xB7\x81" => "\xD9\x81\xD9\x85\xD9\x8A", + "\xEF\xB7\x82" => "\xD8\xA8\xD8\xAD\xD9\x8A", + "\xEF\xB7\x83" => "\xD9\x83\xD9\x85\xD9\x85", + "\xEF\xB7\x84" => "\xD8\xB9\xD8\xAC\xD9\x85", + "\xEF\xB7\x85" => "\xD8\xB5\xD9\x85\xD9\x85", + "\xEF\xB7\x86" => "\xD8\xB3\xD8\xAE\xD9\x8A", + "\xEF\xB7\x87" => "\xD9\x86\xD8\xAC\xD9\x8A", + "\xEF\xB7\xB0" => "\xD8\xB5\xD9\x84\xDB\x92", + "\xEF\xB7\xB1" => "\xD9\x82\xD9\x84\xDB\x92", + "\xEF\xB7\xB2" => "\xD8\xA7\xD9\x84\xD9\x84\xD9\x87", + "\xEF\xB7\xB3" => "\xD8\xA7\xD9\x83\xD8\xA8\xD8\xB1", + "\xEF\xB7\xB4" => "\xD9\x85\xD8\xAD\xD9\x85\xD8\xAF", + "\xEF\xB7\xB5" => "\xD8\xB5\xD9\x84\xD8\xB9\xD9\x85", + "\xEF\xB7\xB6" => "\xD8\xB1\xD8\xB3\xD9\x88\xD9\x84", + "\xEF\xB7\xB7" => "\xD8\xB9\xD9\x84\xD9\x8A\xD9\x87", + "\xEF\xB7\xB8" => "\xD9\x88\xD8\xB3\xD9\x84\xD9\x85", + "\xEF\xB7\xB9" => "\xD8\xB5\xD9\x84\xD9\x89", + "\xEF\xB7\xBA" => "\xD8\xB5\xD9\x84\xD9\x89\x20\xD8\xA7\xD9\x84\xD9\x84\xD9\x87\x20\xD8\xB9\xD9\x84\xD9\x8A\xD9\x87\x20\xD9\x88\xD8\xB3\xD9\x84\xD9\x85", + "\xEF\xB7\xBB" => "\xD8\xAC\xD9\x84\x20\xD8\xAC\xD9\x84\xD8\xA7\xD9\x84\xD9\x87", + "\xEF\xB7\xBC" => "\xD8\xB1\xDB\x8C\xD8\xA7\xD9\x84", + "\xEF\xB8\x90" => "\x2C", + "\xEF\xB8\x91" => "\xE3\x80\x81", + "\xEF\xB8\x92" => "\xE3\x80\x82", + "\xEF\xB8\x93" => "\x3A", + "\xEF\xB8\x94" => "\x3B", + "\xEF\xB8\x95" => "\x21", + "\xEF\xB8\x96" => "\x3F", + "\xEF\xB8\x97" => "\xE3\x80\x96", + "\xEF\xB8\x98" => "\xE3\x80\x97", + "\xEF\xB8\x99" => "\x2E\x2E\x2E", + "\xEF\xB8\xB0" => "\x2E\x2E", + "\xEF\xB8\xB1" => "\xE2\x80\x94", + "\xEF\xB8\xB2" => "\xE2\x80\x93", + "\xEF\xB8\xB3" => "\x5F", + "\xEF\xB8\xB4" => "\x5F", + "\xEF\xB8\xB5" => "\x28", + "\xEF\xB8\xB6" => "\x29", + "\xEF\xB8\xB7" => "\x7B", + "\xEF\xB8\xB8" => "\x7D", + "\xEF\xB8\xB9" => "\xE3\x80\x94", + "\xEF\xB8\xBA" => "\xE3\x80\x95", + "\xEF\xB8\xBB" => "\xE3\x80\x90", + "\xEF\xB8\xBC" => "\xE3\x80\x91", + "\xEF\xB8\xBD" => "\xE3\x80\x8A", + "\xEF\xB8\xBE" => "\xE3\x80\x8B", + "\xEF\xB8\xBF" => "\xE3\x80\x88", + "\xEF\xB9\x80" => "\xE3\x80\x89", + "\xEF\xB9\x81" => "\xE3\x80\x8C", + "\xEF\xB9\x82" => "\xE3\x80\x8D", + "\xEF\xB9\x83" => "\xE3\x80\x8E", + "\xEF\xB9\x84" => "\xE3\x80\x8F", + "\xEF\xB9\x87" => "\x5B", + "\xEF\xB9\x88" => "\x5D", + "\xEF\xB9\x89" => "\x20\xCC\x85", + "\xEF\xB9\x8A" => "\x20\xCC\x85", + "\xEF\xB9\x8B" => "\x20\xCC\x85", + "\xEF\xB9\x8C" => "\x20\xCC\x85", + "\xEF\xB9\x8D" => "\x5F", + "\xEF\xB9\x8E" => "\x5F", + "\xEF\xB9\x8F" => "\x5F", + "\xEF\xB9\x90" => "\x2C", + "\xEF\xB9\x91" => "\xE3\x80\x81", + "\xEF\xB9\x92" => "\x2E", + "\xEF\xB9\x94" => "\x3B", + "\xEF\xB9\x95" => "\x3A", + "\xEF\xB9\x96" => "\x3F", + "\xEF\xB9\x97" => "\x21", + "\xEF\xB9\x98" => "\xE2\x80\x94", + "\xEF\xB9\x99" => "\x28", + "\xEF\xB9\x9A" => "\x29", + "\xEF\xB9\x9B" => "\x7B", + "\xEF\xB9\x9C" => "\x7D", + "\xEF\xB9\x9D" => "\xE3\x80\x94", + "\xEF\xB9\x9E" => "\xE3\x80\x95", + "\xEF\xB9\x9F" => "\x23", + "\xEF\xB9\xA0" => "\x26", + "\xEF\xB9\xA1" => "\x2A", + "\xEF\xB9\xA2" => "\x2B", + "\xEF\xB9\xA3" => "\x2D", + "\xEF\xB9\xA4" => "\x3C", + "\xEF\xB9\xA5" => "\x3E", + "\xEF\xB9\xA6" => "\x3D", + "\xEF\xB9\xA8" => "\x5C", + "\xEF\xB9\xA9" => "\x24", + "\xEF\xB9\xAA" => "\x25", + "\xEF\xB9\xAB" => "\x40", + "\xEF\xB9\xB0" => "\x20\xD9\x8B", + "\xEF\xB9\xB1" => "\xD9\x80\xD9\x8B", + "\xEF\xB9\xB2" => "\x20\xD9\x8C", + "\xEF\xB9\xB4" => "\x20\xD9\x8D", + "\xEF\xB9\xB6" => "\x20\xD9\x8E", + "\xEF\xB9\xB7" => "\xD9\x80\xD9\x8E", + "\xEF\xB9\xB8" => "\x20\xD9\x8F", + "\xEF\xB9\xB9" => "\xD9\x80\xD9\x8F", + "\xEF\xB9\xBA" => "\x20\xD9\x90", + "\xEF\xB9\xBB" => "\xD9\x80\xD9\x90", + "\xEF\xB9\xBC" => "\x20\xD9\x91", + "\xEF\xB9\xBD" => "\xD9\x80\xD9\x91", + "\xEF\xB9\xBE" => "\x20\xD9\x92", + "\xEF\xB9\xBF" => "\xD9\x80\xD9\x92", + "\xEF\xBA\x80" => "\xD8\xA1", + "\xEF\xBA\x81" => "\xD8\xA7\xD9\x93", + "\xEF\xBA\x82" => "\xD8\xA7\xD9\x93", + "\xEF\xBA\x83" => "\xD8\xA7\xD9\x94", + "\xEF\xBA\x84" => "\xD8\xA7\xD9\x94", + "\xEF\xBA\x85" => "\xD9\x88\xD9\x94", + "\xEF\xBA\x86" => "\xD9\x88\xD9\x94", + "\xEF\xBA\x87" => "\xD8\xA7\xD9\x95", + "\xEF\xBA\x88" => "\xD8\xA7\xD9\x95", + "\xEF\xBA\x89" => "\xD9\x8A\xD9\x94", + "\xEF\xBA\x8A" => "\xD9\x8A\xD9\x94", + "\xEF\xBA\x8B" => "\xD9\x8A\xD9\x94", + "\xEF\xBA\x8C" => "\xD9\x8A\xD9\x94", + "\xEF\xBA\x8D" => "\xD8\xA7", + "\xEF\xBA\x8E" => "\xD8\xA7", + "\xEF\xBA\x8F" => "\xD8\xA8", + "\xEF\xBA\x90" => "\xD8\xA8", + "\xEF\xBA\x91" => "\xD8\xA8", + "\xEF\xBA\x92" => "\xD8\xA8", + "\xEF\xBA\x93" => "\xD8\xA9", + "\xEF\xBA\x94" => "\xD8\xA9", + "\xEF\xBA\x95" => "\xD8\xAA", + "\xEF\xBA\x96" => "\xD8\xAA", + "\xEF\xBA\x97" => "\xD8\xAA", + "\xEF\xBA\x98" => "\xD8\xAA", + "\xEF\xBA\x99" => "\xD8\xAB", + "\xEF\xBA\x9A" => "\xD8\xAB", + "\xEF\xBA\x9B" => "\xD8\xAB", + "\xEF\xBA\x9C" => "\xD8\xAB", + "\xEF\xBA\x9D" => "\xD8\xAC", + "\xEF\xBA\x9E" => "\xD8\xAC", + "\xEF\xBA\x9F" => "\xD8\xAC", + "\xEF\xBA\xA0" => "\xD8\xAC", + "\xEF\xBA\xA1" => "\xD8\xAD", + "\xEF\xBA\xA2" => "\xD8\xAD", + "\xEF\xBA\xA3" => "\xD8\xAD", + "\xEF\xBA\xA4" => "\xD8\xAD", + "\xEF\xBA\xA5" => "\xD8\xAE", + "\xEF\xBA\xA6" => "\xD8\xAE", + "\xEF\xBA\xA7" => "\xD8\xAE", + "\xEF\xBA\xA8" => "\xD8\xAE", + "\xEF\xBA\xA9" => "\xD8\xAF", + "\xEF\xBA\xAA" => "\xD8\xAF", + "\xEF\xBA\xAB" => "\xD8\xB0", + "\xEF\xBA\xAC" => "\xD8\xB0", + "\xEF\xBA\xAD" => "\xD8\xB1", + "\xEF\xBA\xAE" => "\xD8\xB1", + "\xEF\xBA\xAF" => "\xD8\xB2", + "\xEF\xBA\xB0" => "\xD8\xB2", + "\xEF\xBA\xB1" => "\xD8\xB3", + "\xEF\xBA\xB2" => "\xD8\xB3", + "\xEF\xBA\xB3" => "\xD8\xB3", + "\xEF\xBA\xB4" => "\xD8\xB3", + "\xEF\xBA\xB5" => "\xD8\xB4", + "\xEF\xBA\xB6" => "\xD8\xB4", + "\xEF\xBA\xB7" => "\xD8\xB4", + "\xEF\xBA\xB8" => "\xD8\xB4", + "\xEF\xBA\xB9" => "\xD8\xB5", + "\xEF\xBA\xBA" => "\xD8\xB5", + "\xEF\xBA\xBB" => "\xD8\xB5", + "\xEF\xBA\xBC" => "\xD8\xB5", + "\xEF\xBA\xBD" => "\xD8\xB6", + "\xEF\xBA\xBE" => "\xD8\xB6", + "\xEF\xBA\xBF" => "\xD8\xB6", + "\xEF\xBB\x80" => "\xD8\xB6", + "\xEF\xBB\x81" => "\xD8\xB7", + "\xEF\xBB\x82" => "\xD8\xB7", + "\xEF\xBB\x83" => "\xD8\xB7", + "\xEF\xBB\x84" => "\xD8\xB7", + "\xEF\xBB\x85" => "\xD8\xB8", + "\xEF\xBB\x86" => "\xD8\xB8", + "\xEF\xBB\x87" => "\xD8\xB8", + "\xEF\xBB\x88" => "\xD8\xB8", + "\xEF\xBB\x89" => "\xD8\xB9", + "\xEF\xBB\x8A" => "\xD8\xB9", + "\xEF\xBB\x8B" => "\xD8\xB9", + "\xEF\xBB\x8C" => "\xD8\xB9", + "\xEF\xBB\x8D" => "\xD8\xBA", + "\xEF\xBB\x8E" => "\xD8\xBA", + "\xEF\xBB\x8F" => "\xD8\xBA", + "\xEF\xBB\x90" => "\xD8\xBA", + "\xEF\xBB\x91" => "\xD9\x81", + "\xEF\xBB\x92" => "\xD9\x81", + "\xEF\xBB\x93" => "\xD9\x81", + "\xEF\xBB\x94" => "\xD9\x81", + "\xEF\xBB\x95" => "\xD9\x82", + "\xEF\xBB\x96" => "\xD9\x82", + "\xEF\xBB\x97" => "\xD9\x82", + "\xEF\xBB\x98" => "\xD9\x82", + "\xEF\xBB\x99" => "\xD9\x83", + "\xEF\xBB\x9A" => "\xD9\x83", + "\xEF\xBB\x9B" => "\xD9\x83", + "\xEF\xBB\x9C" => "\xD9\x83", + "\xEF\xBB\x9D" => "\xD9\x84", + "\xEF\xBB\x9E" => "\xD9\x84", + "\xEF\xBB\x9F" => "\xD9\x84", + "\xEF\xBB\xA0" => "\xD9\x84", + "\xEF\xBB\xA1" => "\xD9\x85", + "\xEF\xBB\xA2" => "\xD9\x85", + "\xEF\xBB\xA3" => "\xD9\x85", + "\xEF\xBB\xA4" => "\xD9\x85", + "\xEF\xBB\xA5" => "\xD9\x86", + "\xEF\xBB\xA6" => "\xD9\x86", + "\xEF\xBB\xA7" => "\xD9\x86", + "\xEF\xBB\xA8" => "\xD9\x86", + "\xEF\xBB\xA9" => "\xD9\x87", + "\xEF\xBB\xAA" => "\xD9\x87", + "\xEF\xBB\xAB" => "\xD9\x87", + "\xEF\xBB\xAC" => "\xD9\x87", + "\xEF\xBB\xAD" => "\xD9\x88", + "\xEF\xBB\xAE" => "\xD9\x88", + "\xEF\xBB\xAF" => "\xD9\x89", + "\xEF\xBB\xB0" => "\xD9\x89", + "\xEF\xBB\xB1" => "\xD9\x8A", + "\xEF\xBB\xB2" => "\xD9\x8A", + "\xEF\xBB\xB3" => "\xD9\x8A", + "\xEF\xBB\xB4" => "\xD9\x8A", + "\xEF\xBB\xB5" => "\xD9\x84\xD8\xA7\xD9\x93", + "\xEF\xBB\xB6" => "\xD9\x84\xD8\xA7\xD9\x93", + "\xEF\xBB\xB7" => "\xD9\x84\xD8\xA7\xD9\x94", + "\xEF\xBB\xB8" => "\xD9\x84\xD8\xA7\xD9\x94", + "\xEF\xBB\xB9" => "\xD9\x84\xD8\xA7\xD9\x95", + "\xEF\xBB\xBA" => "\xD9\x84\xD8\xA7\xD9\x95", + "\xEF\xBB\xBB" => "\xD9\x84\xD8\xA7", + "\xEF\xBB\xBC" => "\xD9\x84\xD8\xA7", + "\xEF\xBC\x81" => "\x21", + "\xEF\xBC\x82" => "\x22", + "\xEF\xBC\x83" => "\x23", + "\xEF\xBC\x84" => "\x24", + "\xEF\xBC\x85" => "\x25", + "\xEF\xBC\x86" => "\x26", + "\xEF\xBC\x87" => "\x27", + "\xEF\xBC\x88" => "\x28", + "\xEF\xBC\x89" => "\x29", + "\xEF\xBC\x8A" => "\x2A", + "\xEF\xBC\x8B" => "\x2B", + "\xEF\xBC\x8C" => "\x2C", + "\xEF\xBC\x8D" => "\x2D", + "\xEF\xBC\x8E" => "\x2E", + "\xEF\xBC\x8F" => "\x2F", + "\xEF\xBC\x90" => "\x30", + "\xEF\xBC\x91" => "\x31", + "\xEF\xBC\x92" => "\x32", + "\xEF\xBC\x93" => "\x33", + "\xEF\xBC\x94" => "\x34", + "\xEF\xBC\x95" => "\x35", + "\xEF\xBC\x96" => "\x36", + "\xEF\xBC\x97" => "\x37", + "\xEF\xBC\x98" => "\x38", + "\xEF\xBC\x99" => "\x39", + "\xEF\xBC\x9A" => "\x3A", + "\xEF\xBC\x9B" => "\x3B", + "\xEF\xBC\x9C" => "\x3C", + "\xEF\xBC\x9D" => "\x3D", + "\xEF\xBC\x9E" => "\x3E", + "\xEF\xBC\x9F" => "\x3F", + "\xEF\xBC\xA0" => "\x40", + "\xEF\xBC\xA1" => "\x41", + "\xEF\xBC\xA2" => "\x42", + "\xEF\xBC\xA3" => "\x43", + "\xEF\xBC\xA4" => "\x44", + "\xEF\xBC\xA5" => "\x45", + "\xEF\xBC\xA6" => "\x46", + "\xEF\xBC\xA7" => "\x47", + "\xEF\xBC\xA8" => "\x48", + "\xEF\xBC\xA9" => "\x49", + "\xEF\xBC\xAA" => "\x4A", + "\xEF\xBC\xAB" => "\x4B", + "\xEF\xBC\xAC" => "\x4C", + "\xEF\xBC\xAD" => "\x4D", + "\xEF\xBC\xAE" => "\x4E", + "\xEF\xBC\xAF" => "\x4F", + "\xEF\xBC\xB0" => "\x50", + "\xEF\xBC\xB1" => "\x51", + "\xEF\xBC\xB2" => "\x52", + "\xEF\xBC\xB3" => "\x53", + "\xEF\xBC\xB4" => "\x54", + "\xEF\xBC\xB5" => "\x55", + "\xEF\xBC\xB6" => "\x56", + "\xEF\xBC\xB7" => "\x57", + "\xEF\xBC\xB8" => "\x58", + "\xEF\xBC\xB9" => "\x59", + "\xEF\xBC\xBA" => "\x5A", + "\xEF\xBC\xBB" => "\x5B", + "\xEF\xBC\xBC" => "\x5C", + "\xEF\xBC\xBD" => "\x5D", + "\xEF\xBC\xBE" => "\x5E", + "\xEF\xBC\xBF" => "\x5F", + "\xEF\xBD\x80" => "\x60", + "\xEF\xBD\x81" => "\x61", + "\xEF\xBD\x82" => "\x62", + "\xEF\xBD\x83" => "\x63", + "\xEF\xBD\x84" => "\x64", + "\xEF\xBD\x85" => "\x65", + "\xEF\xBD\x86" => "\x66", + "\xEF\xBD\x87" => "\x67", + "\xEF\xBD\x88" => "\x68", + "\xEF\xBD\x89" => "\x69", + "\xEF\xBD\x8A" => "\x6A", + "\xEF\xBD\x8B" => "\x6B", + "\xEF\xBD\x8C" => "\x6C", + "\xEF\xBD\x8D" => "\x6D", + "\xEF\xBD\x8E" => "\x6E", + "\xEF\xBD\x8F" => "\x6F", + "\xEF\xBD\x90" => "\x70", + "\xEF\xBD\x91" => "\x71", + "\xEF\xBD\x92" => "\x72", + "\xEF\xBD\x93" => "\x73", + "\xEF\xBD\x94" => "\x74", + "\xEF\xBD\x95" => "\x75", + "\xEF\xBD\x96" => "\x76", + "\xEF\xBD\x97" => "\x77", + "\xEF\xBD\x98" => "\x78", + "\xEF\xBD\x99" => "\x79", + "\xEF\xBD\x9A" => "\x7A", + "\xEF\xBD\x9B" => "\x7B", + "\xEF\xBD\x9C" => "\x7C", + "\xEF\xBD\x9D" => "\x7D", + "\xEF\xBD\x9E" => "\x7E", + "\xEF\xBD\x9F" => "\xE2\xA6\x85", + "\xEF\xBD\xA0" => "\xE2\xA6\x86", + "\xEF\xBD\xA1" => "\xE3\x80\x82", + "\xEF\xBD\xA2" => "\xE3\x80\x8C", + "\xEF\xBD\xA3" => "\xE3\x80\x8D", + "\xEF\xBD\xA4" => "\xE3\x80\x81", + "\xEF\xBD\xA5" => "\xE3\x83\xBB", + "\xEF\xBD\xA6" => "\xE3\x83\xB2", + "\xEF\xBD\xA7" => "\xE3\x82\xA1", + "\xEF\xBD\xA8" => "\xE3\x82\xA3", + "\xEF\xBD\xA9" => "\xE3\x82\xA5", + "\xEF\xBD\xAA" => "\xE3\x82\xA7", + "\xEF\xBD\xAB" => "\xE3\x82\xA9", + "\xEF\xBD\xAC" => "\xE3\x83\xA3", + "\xEF\xBD\xAD" => "\xE3\x83\xA5", + "\xEF\xBD\xAE" => "\xE3\x83\xA7", + "\xEF\xBD\xAF" => "\xE3\x83\x83", + "\xEF\xBD\xB0" => "\xE3\x83\xBC", + "\xEF\xBD\xB1" => "\xE3\x82\xA2", + "\xEF\xBD\xB2" => "\xE3\x82\xA4", + "\xEF\xBD\xB3" => "\xE3\x82\xA6", + "\xEF\xBD\xB4" => "\xE3\x82\xA8", + "\xEF\xBD\xB5" => "\xE3\x82\xAA", + "\xEF\xBD\xB6" => "\xE3\x82\xAB", + "\xEF\xBD\xB7" => "\xE3\x82\xAD", + "\xEF\xBD\xB8" => "\xE3\x82\xAF", + "\xEF\xBD\xB9" => "\xE3\x82\xB1", + "\xEF\xBD\xBA" => "\xE3\x82\xB3", + "\xEF\xBD\xBB" => "\xE3\x82\xB5", + "\xEF\xBD\xBC" => "\xE3\x82\xB7", + "\xEF\xBD\xBD" => "\xE3\x82\xB9", + "\xEF\xBD\xBE" => "\xE3\x82\xBB", + "\xEF\xBD\xBF" => "\xE3\x82\xBD", + "\xEF\xBE\x80" => "\xE3\x82\xBF", + "\xEF\xBE\x81" => "\xE3\x83\x81", + "\xEF\xBE\x82" => "\xE3\x83\x84", + "\xEF\xBE\x83" => "\xE3\x83\x86", + "\xEF\xBE\x84" => "\xE3\x83\x88", + "\xEF\xBE\x85" => "\xE3\x83\x8A", + "\xEF\xBE\x86" => "\xE3\x83\x8B", + "\xEF\xBE\x87" => "\xE3\x83\x8C", + "\xEF\xBE\x88" => "\xE3\x83\x8D", + "\xEF\xBE\x89" => "\xE3\x83\x8E", + "\xEF\xBE\x8A" => "\xE3\x83\x8F", + "\xEF\xBE\x8B" => "\xE3\x83\x92", + "\xEF\xBE\x8C" => "\xE3\x83\x95", + "\xEF\xBE\x8D" => "\xE3\x83\x98", + "\xEF\xBE\x8E" => "\xE3\x83\x9B", + "\xEF\xBE\x8F" => "\xE3\x83\x9E", + "\xEF\xBE\x90" => "\xE3\x83\x9F", + "\xEF\xBE\x91" => "\xE3\x83\xA0", + "\xEF\xBE\x92" => "\xE3\x83\xA1", + "\xEF\xBE\x93" => "\xE3\x83\xA2", + "\xEF\xBE\x94" => "\xE3\x83\xA4", + "\xEF\xBE\x95" => "\xE3\x83\xA6", + "\xEF\xBE\x96" => "\xE3\x83\xA8", + "\xEF\xBE\x97" => "\xE3\x83\xA9", + "\xEF\xBE\x98" => "\xE3\x83\xAA", + "\xEF\xBE\x99" => "\xE3\x83\xAB", + "\xEF\xBE\x9A" => "\xE3\x83\xAC", + "\xEF\xBE\x9B" => "\xE3\x83\xAD", + "\xEF\xBE\x9C" => "\xE3\x83\xAF", + "\xEF\xBE\x9D" => "\xE3\x83\xB3", + "\xEF\xBE\x9E" => "\xE3\x82\x99", + "\xEF\xBE\x9F" => "\xE3\x82\x9A", + "\xEF\xBE\xA0" => "\xE1\x85\xA0", + "\xEF\xBE\xA1" => "\xE1\x84\x80", + "\xEF\xBE\xA2" => "\xE1\x84\x81", + "\xEF\xBE\xA3" => "\xE1\x86\xAA", + "\xEF\xBE\xA4" => "\xE1\x84\x82", + "\xEF\xBE\xA5" => "\xE1\x86\xAC", + "\xEF\xBE\xA6" => "\xE1\x86\xAD", + "\xEF\xBE\xA7" => "\xE1\x84\x83", + "\xEF\xBE\xA8" => "\xE1\x84\x84", + "\xEF\xBE\xA9" => "\xE1\x84\x85", + "\xEF\xBE\xAA" => "\xE1\x86\xB0", + "\xEF\xBE\xAB" => "\xE1\x86\xB1", + "\xEF\xBE\xAC" => "\xE1\x86\xB2", + "\xEF\xBE\xAD" => "\xE1\x86\xB3", + "\xEF\xBE\xAE" => "\xE1\x86\xB4", + "\xEF\xBE\xAF" => "\xE1\x86\xB5", + "\xEF\xBE\xB0" => "\xE1\x84\x9A", + "\xEF\xBE\xB1" => "\xE1\x84\x86", + "\xEF\xBE\xB2" => "\xE1\x84\x87", + "\xEF\xBE\xB3" => "\xE1\x84\x88", + "\xEF\xBE\xB4" => "\xE1\x84\xA1", + "\xEF\xBE\xB5" => "\xE1\x84\x89", + "\xEF\xBE\xB6" => "\xE1\x84\x8A", + "\xEF\xBE\xB7" => "\xE1\x84\x8B", + "\xEF\xBE\xB8" => "\xE1\x84\x8C", + "\xEF\xBE\xB9" => "\xE1\x84\x8D", + "\xEF\xBE\xBA" => "\xE1\x84\x8E", + "\xEF\xBE\xBB" => "\xE1\x84\x8F", + "\xEF\xBE\xBC" => "\xE1\x84\x90", + "\xEF\xBE\xBD" => "\xE1\x84\x91", + "\xEF\xBE\xBE" => "\xE1\x84\x92", + "\xEF\xBF\x82" => "\xE1\x85\xA1", + "\xEF\xBF\x83" => "\xE1\x85\xA2", + "\xEF\xBF\x84" => "\xE1\x85\xA3", + "\xEF\xBF\x85" => "\xE1\x85\xA4", + "\xEF\xBF\x86" => "\xE1\x85\xA5", + "\xEF\xBF\x87" => "\xE1\x85\xA6", + "\xEF\xBF\x8A" => "\xE1\x85\xA7", + "\xEF\xBF\x8B" => "\xE1\x85\xA8", + "\xEF\xBF\x8C" => "\xE1\x85\xA9", + "\xEF\xBF\x8D" => "\xE1\x85\xAA", + "\xEF\xBF\x8E" => "\xE1\x85\xAB", + "\xEF\xBF\x8F" => "\xE1\x85\xAC", + "\xEF\xBF\x92" => "\xE1\x85\xAD", + "\xEF\xBF\x93" => "\xE1\x85\xAE", + "\xEF\xBF\x94" => "\xE1\x85\xAF", + "\xEF\xBF\x95" => "\xE1\x85\xB0", + "\xEF\xBF\x96" => "\xE1\x85\xB1", + "\xEF\xBF\x97" => "\xE1\x85\xB2", + "\xEF\xBF\x9A" => "\xE1\x85\xB3", + "\xEF\xBF\x9B" => "\xE1\x85\xB4", + "\xEF\xBF\x9C" => "\xE1\x85\xB5", + "\xEF\xBF\xA0" => "\xC2\xA2", + "\xEF\xBF\xA1" => "\xC2\xA3", + "\xEF\xBF\xA2" => "\xC2\xAC", + "\xEF\xBF\xA3" => "\x20\xCC\x84", + "\xEF\xBF\xA4" => "\xC2\xA6", + "\xEF\xBF\xA5" => "\xC2\xA5", + "\xEF\xBF\xA6" => "\xE2\x82\xA9", + "\xEF\xBF\xA8" => "\xE2\x94\x82", + "\xEF\xBF\xA9" => "\xE2\x86\x90", + "\xEF\xBF\xAA" => "\xE2\x86\x91", + "\xEF\xBF\xAB" => "\xE2\x86\x92", + "\xEF\xBF\xAC" => "\xE2\x86\x93", + "\xEF\xBF\xAD" => "\xE2\x96\xA0", + "\xEF\xBF\xAE" => "\xE2\x97\x8B", + "\xF0\x90\x9E\x81" => "\xCB\x90", + "\xF0\x90\x9E\x82" => "\xCB\x91", + "\xF0\x90\x9E\x83" => "\xC3\xA6", + "\xF0\x90\x9E\x84" => "\xCA\x99", + "\xF0\x90\x9E\x85" => "\xC9\x93", + "\xF0\x90\x9E\x87" => "\xCA\xA3", + "\xF0\x90\x9E\x88" => "\xEA\xAD\xA6", + "\xF0\x90\x9E\x89" => "\xCA\xA5", + "\xF0\x90\x9E\x8A" => "\xCA\xA4", + "\xF0\x90\x9E\x8B" => "\xC9\x96", + "\xF0\x90\x9E\x8C" => "\xC9\x97", + "\xF0\x90\x9E\x8D" => "\xE1\xB6\x91", + "\xF0\x90\x9E\x8E" => "\xC9\x98", + "\xF0\x90\x9E\x8F" => "\xC9\x9E", + "\xF0\x90\x9E\x90" => "\xCA\xA9", + "\xF0\x90\x9E\x91" => "\xC9\xA4", + "\xF0\x90\x9E\x92" => "\xC9\xA2", + "\xF0\x90\x9E\x93" => "\xC9\xA0", + "\xF0\x90\x9E\x94" => "\xCA\x9B", + "\xF0\x90\x9E\x95" => "\xC4\xA7", + "\xF0\x90\x9E\x96" => "\xCA\x9C", + "\xF0\x90\x9E\x97" => "\xC9\xA7", + "\xF0\x90\x9E\x98" => "\xCA\x84", + "\xF0\x90\x9E\x99" => "\xCA\xAA", + "\xF0\x90\x9E\x9A" => "\xCA\xAB", + "\xF0\x90\x9E\x9B" => "\xC9\xAC", + "\xF0\x90\x9E\x9C" => "\xF0\x9D\xBC\x84", + "\xF0\x90\x9E\x9D" => "\xEA\x9E\x8E", + "\xF0\x90\x9E\x9E" => "\xC9\xAE", + "\xF0\x90\x9E\x9F" => "\xF0\x9D\xBC\x85", + "\xF0\x90\x9E\xA0" => "\xCA\x8E", + "\xF0\x90\x9E\xA1" => "\xF0\x9D\xBC\x86", + "\xF0\x90\x9E\xA2" => "\xC3\xB8", + "\xF0\x90\x9E\xA3" => "\xC9\xB6", + "\xF0\x90\x9E\xA4" => "\xC9\xB7", + "\xF0\x90\x9E\xA5" => "\x71", + "\xF0\x90\x9E\xA6" => "\xC9\xBA", + "\xF0\x90\x9E\xA7" => "\xF0\x9D\xBC\x88", + "\xF0\x90\x9E\xA8" => "\xC9\xBD", + "\xF0\x90\x9E\xA9" => "\xC9\xBE", + "\xF0\x90\x9E\xAA" => "\xCA\x80", + "\xF0\x90\x9E\xAB" => "\xCA\xA8", + "\xF0\x90\x9E\xAC" => "\xCA\xA6", + "\xF0\x90\x9E\xAD" => "\xEA\xAD\xA7", + "\xF0\x90\x9E\xAE" => "\xCA\xA7", + "\xF0\x90\x9E\xAF" => "\xCA\x88", + "\xF0\x90\x9E\xB0" => "\xE2\xB1\xB1", + "\xF0\x90\x9E\xB2" => "\xCA\x8F", + "\xF0\x90\x9E\xB3" => "\xCA\xA1", + "\xF0\x90\x9E\xB4" => "\xCA\xA2", + "\xF0\x90\x9E\xB5" => "\xCA\x98", + "\xF0\x90\x9E\xB6" => "\xC7\x80", + "\xF0\x90\x9E\xB7" => "\xC7\x81", + "\xF0\x90\x9E\xB8" => "\xC7\x82", + "\xF0\x90\x9E\xB9" => "\xF0\x9D\xBC\x8A", + "\xF0\x90\x9E\xBA" => "\xF0\x9D\xBC\x9E", + "\xF0\x9D\x90\x80" => "\x41", + "\xF0\x9D\x90\x81" => "\x42", + "\xF0\x9D\x90\x82" => "\x43", + "\xF0\x9D\x90\x83" => "\x44", + "\xF0\x9D\x90\x84" => "\x45", + "\xF0\x9D\x90\x85" => "\x46", + "\xF0\x9D\x90\x86" => "\x47", + "\xF0\x9D\x90\x87" => "\x48", + "\xF0\x9D\x90\x88" => "\x49", + "\xF0\x9D\x90\x89" => "\x4A", + "\xF0\x9D\x90\x8A" => "\x4B", + "\xF0\x9D\x90\x8B" => "\x4C", + "\xF0\x9D\x90\x8C" => "\x4D", + "\xF0\x9D\x90\x8D" => "\x4E", + "\xF0\x9D\x90\x8E" => "\x4F", + "\xF0\x9D\x90\x8F" => "\x50", + "\xF0\x9D\x90\x90" => "\x51", + "\xF0\x9D\x90\x91" => "\x52", + "\xF0\x9D\x90\x92" => "\x53", + "\xF0\x9D\x90\x93" => "\x54", + "\xF0\x9D\x90\x94" => "\x55", + "\xF0\x9D\x90\x95" => "\x56", + "\xF0\x9D\x90\x96" => "\x57", + "\xF0\x9D\x90\x97" => "\x58", + "\xF0\x9D\x90\x98" => "\x59", + "\xF0\x9D\x90\x99" => "\x5A", + "\xF0\x9D\x90\x9A" => "\x61", + "\xF0\x9D\x90\x9B" => "\x62", + "\xF0\x9D\x90\x9C" => "\x63", + "\xF0\x9D\x90\x9D" => "\x64", + "\xF0\x9D\x90\x9E" => "\x65", + "\xF0\x9D\x90\x9F" => "\x66", + "\xF0\x9D\x90\xA0" => "\x67", + "\xF0\x9D\x90\xA1" => "\x68", + "\xF0\x9D\x90\xA2" => "\x69", + "\xF0\x9D\x90\xA3" => "\x6A", + "\xF0\x9D\x90\xA4" => "\x6B", + "\xF0\x9D\x90\xA5" => "\x6C", + "\xF0\x9D\x90\xA6" => "\x6D", + "\xF0\x9D\x90\xA7" => "\x6E", + "\xF0\x9D\x90\xA8" => "\x6F", + "\xF0\x9D\x90\xA9" => "\x70", + "\xF0\x9D\x90\xAA" => "\x71", + "\xF0\x9D\x90\xAB" => "\x72", + "\xF0\x9D\x90\xAC" => "\x73", + "\xF0\x9D\x90\xAD" => "\x74", + "\xF0\x9D\x90\xAE" => "\x75", + "\xF0\x9D\x90\xAF" => "\x76", + "\xF0\x9D\x90\xB0" => "\x77", + "\xF0\x9D\x90\xB1" => "\x78", + "\xF0\x9D\x90\xB2" => "\x79", + "\xF0\x9D\x90\xB3" => "\x7A", + "\xF0\x9D\x90\xB4" => "\x41", + "\xF0\x9D\x90\xB5" => "\x42", + "\xF0\x9D\x90\xB6" => "\x43", + "\xF0\x9D\x90\xB7" => "\x44", + "\xF0\x9D\x90\xB8" => "\x45", + "\xF0\x9D\x90\xB9" => "\x46", + "\xF0\x9D\x90\xBA" => "\x47", + "\xF0\x9D\x90\xBB" => "\x48", + "\xF0\x9D\x90\xBC" => "\x49", + "\xF0\x9D\x90\xBD" => "\x4A", + "\xF0\x9D\x90\xBE" => "\x4B", + "\xF0\x9D\x90\xBF" => "\x4C", + "\xF0\x9D\x91\x80" => "\x4D", + "\xF0\x9D\x91\x81" => "\x4E", + "\xF0\x9D\x91\x82" => "\x4F", + "\xF0\x9D\x91\x83" => "\x50", + "\xF0\x9D\x91\x84" => "\x51", + "\xF0\x9D\x91\x85" => "\x52", + "\xF0\x9D\x91\x86" => "\x53", + "\xF0\x9D\x91\x87" => "\x54", + "\xF0\x9D\x91\x88" => "\x55", + "\xF0\x9D\x91\x89" => "\x56", + "\xF0\x9D\x91\x8A" => "\x57", + "\xF0\x9D\x91\x8B" => "\x58", + "\xF0\x9D\x91\x8C" => "\x59", + "\xF0\x9D\x91\x8D" => "\x5A", + "\xF0\x9D\x91\x8E" => "\x61", + "\xF0\x9D\x91\x8F" => "\x62", + "\xF0\x9D\x91\x90" => "\x63", + "\xF0\x9D\x91\x91" => "\x64", + "\xF0\x9D\x91\x92" => "\x65", + "\xF0\x9D\x91\x93" => "\x66", + "\xF0\x9D\x91\x94" => "\x67", + "\xF0\x9D\x91\x96" => "\x69", + "\xF0\x9D\x91\x97" => "\x6A", + "\xF0\x9D\x91\x98" => "\x6B", + "\xF0\x9D\x91\x99" => "\x6C", + "\xF0\x9D\x91\x9A" => "\x6D", + "\xF0\x9D\x91\x9B" => "\x6E", + "\xF0\x9D\x91\x9C" => "\x6F", + "\xF0\x9D\x91\x9D" => "\x70", + "\xF0\x9D\x91\x9E" => "\x71", + "\xF0\x9D\x91\x9F" => "\x72", + "\xF0\x9D\x91\xA0" => "\x73", + "\xF0\x9D\x91\xA1" => "\x74", + "\xF0\x9D\x91\xA2" => "\x75", + "\xF0\x9D\x91\xA3" => "\x76", + "\xF0\x9D\x91\xA4" => "\x77", + "\xF0\x9D\x91\xA5" => "\x78", + "\xF0\x9D\x91\xA6" => "\x79", + "\xF0\x9D\x91\xA7" => "\x7A", + "\xF0\x9D\x91\xA8" => "\x41", + "\xF0\x9D\x91\xA9" => "\x42", + "\xF0\x9D\x91\xAA" => "\x43", + "\xF0\x9D\x91\xAB" => "\x44", + "\xF0\x9D\x91\xAC" => "\x45", + "\xF0\x9D\x91\xAD" => "\x46", + "\xF0\x9D\x91\xAE" => "\x47", + "\xF0\x9D\x91\xAF" => "\x48", + "\xF0\x9D\x91\xB0" => "\x49", + "\xF0\x9D\x91\xB1" => "\x4A", + "\xF0\x9D\x91\xB2" => "\x4B", + "\xF0\x9D\x91\xB3" => "\x4C", + "\xF0\x9D\x91\xB4" => "\x4D", + "\xF0\x9D\x91\xB5" => "\x4E", + "\xF0\x9D\x91\xB6" => "\x4F", + "\xF0\x9D\x91\xB7" => "\x50", + "\xF0\x9D\x91\xB8" => "\x51", + "\xF0\x9D\x91\xB9" => "\x52", + "\xF0\x9D\x91\xBA" => "\x53", + "\xF0\x9D\x91\xBB" => "\x54", + "\xF0\x9D\x91\xBC" => "\x55", + "\xF0\x9D\x91\xBD" => "\x56", + "\xF0\x9D\x91\xBE" => "\x57", + "\xF0\x9D\x91\xBF" => "\x58", + "\xF0\x9D\x92\x80" => "\x59", + "\xF0\x9D\x92\x81" => "\x5A", + "\xF0\x9D\x92\x82" => "\x61", + "\xF0\x9D\x92\x83" => "\x62", + "\xF0\x9D\x92\x84" => "\x63", + "\xF0\x9D\x92\x85" => "\x64", + "\xF0\x9D\x92\x86" => "\x65", + "\xF0\x9D\x92\x87" => "\x66", + "\xF0\x9D\x92\x88" => "\x67", + "\xF0\x9D\x92\x89" => "\x68", + "\xF0\x9D\x92\x8A" => "\x69", + "\xF0\x9D\x92\x8B" => "\x6A", + "\xF0\x9D\x92\x8C" => "\x6B", + "\xF0\x9D\x92\x8D" => "\x6C", + "\xF0\x9D\x92\x8E" => "\x6D", + "\xF0\x9D\x92\x8F" => "\x6E", + "\xF0\x9D\x92\x90" => "\x6F", + "\xF0\x9D\x92\x91" => "\x70", + "\xF0\x9D\x92\x92" => "\x71", + "\xF0\x9D\x92\x93" => "\x72", + "\xF0\x9D\x92\x94" => "\x73", + "\xF0\x9D\x92\x95" => "\x74", + "\xF0\x9D\x92\x96" => "\x75", + "\xF0\x9D\x92\x97" => "\x76", + "\xF0\x9D\x92\x98" => "\x77", + "\xF0\x9D\x92\x99" => "\x78", + "\xF0\x9D\x92\x9A" => "\x79", + "\xF0\x9D\x92\x9B" => "\x7A", + "\xF0\x9D\x92\x9C" => "\x41", + "\xF0\x9D\x92\x9E" => "\x43", + "\xF0\x9D\x92\x9F" => "\x44", + "\xF0\x9D\x92\xA2" => "\x47", + "\xF0\x9D\x92\xA5" => "\x4A", + "\xF0\x9D\x92\xA6" => "\x4B", + "\xF0\x9D\x92\xA9" => "\x4E", + "\xF0\x9D\x92\xAA" => "\x4F", + "\xF0\x9D\x92\xAB" => "\x50", + "\xF0\x9D\x92\xAC" => "\x51", + "\xF0\x9D\x92\xAE" => "\x53", + "\xF0\x9D\x92\xAF" => "\x54", + "\xF0\x9D\x92\xB0" => "\x55", + "\xF0\x9D\x92\xB1" => "\x56", + "\xF0\x9D\x92\xB2" => "\x57", + "\xF0\x9D\x92\xB3" => "\x58", + "\xF0\x9D\x92\xB4" => "\x59", + "\xF0\x9D\x92\xB5" => "\x5A", + "\xF0\x9D\x92\xB6" => "\x61", + "\xF0\x9D\x92\xB7" => "\x62", + "\xF0\x9D\x92\xB8" => "\x63", + "\xF0\x9D\x92\xB9" => "\x64", + "\xF0\x9D\x92\xBB" => "\x66", + "\xF0\x9D\x92\xBD" => "\x68", + "\xF0\x9D\x92\xBE" => "\x69", + "\xF0\x9D\x92\xBF" => "\x6A", + "\xF0\x9D\x93\x80" => "\x6B", + "\xF0\x9D\x93\x81" => "\x6C", + "\xF0\x9D\x93\x82" => "\x6D", + "\xF0\x9D\x93\x83" => "\x6E", + "\xF0\x9D\x93\x85" => "\x70", + "\xF0\x9D\x93\x86" => "\x71", + "\xF0\x9D\x93\x87" => "\x72", + "\xF0\x9D\x93\x88" => "\x73", + "\xF0\x9D\x93\x89" => "\x74", + "\xF0\x9D\x93\x8A" => "\x75", + "\xF0\x9D\x93\x8B" => "\x76", + "\xF0\x9D\x93\x8C" => "\x77", + "\xF0\x9D\x93\x8D" => "\x78", + "\xF0\x9D\x93\x8E" => "\x79", + "\xF0\x9D\x93\x8F" => "\x7A", + "\xF0\x9D\x93\x90" => "\x41", + "\xF0\x9D\x93\x91" => "\x42", + "\xF0\x9D\x93\x92" => "\x43", + "\xF0\x9D\x93\x93" => "\x44", + "\xF0\x9D\x93\x94" => "\x45", + "\xF0\x9D\x93\x95" => "\x46", + "\xF0\x9D\x93\x96" => "\x47", + "\xF0\x9D\x93\x97" => "\x48", + "\xF0\x9D\x93\x98" => "\x49", + "\xF0\x9D\x93\x99" => "\x4A", + "\xF0\x9D\x93\x9A" => "\x4B", + "\xF0\x9D\x93\x9B" => "\x4C", + "\xF0\x9D\x93\x9C" => "\x4D", + "\xF0\x9D\x93\x9D" => "\x4E", + "\xF0\x9D\x93\x9E" => "\x4F", + "\xF0\x9D\x93\x9F" => "\x50", + "\xF0\x9D\x93\xA0" => "\x51", + "\xF0\x9D\x93\xA1" => "\x52", + "\xF0\x9D\x93\xA2" => "\x53", + "\xF0\x9D\x93\xA3" => "\x54", + "\xF0\x9D\x93\xA4" => "\x55", + "\xF0\x9D\x93\xA5" => "\x56", + "\xF0\x9D\x93\xA6" => "\x57", + "\xF0\x9D\x93\xA7" => "\x58", + "\xF0\x9D\x93\xA8" => "\x59", + "\xF0\x9D\x93\xA9" => "\x5A", + "\xF0\x9D\x93\xAA" => "\x61", + "\xF0\x9D\x93\xAB" => "\x62", + "\xF0\x9D\x93\xAC" => "\x63", + "\xF0\x9D\x93\xAD" => "\x64", + "\xF0\x9D\x93\xAE" => "\x65", + "\xF0\x9D\x93\xAF" => "\x66", + "\xF0\x9D\x93\xB0" => "\x67", + "\xF0\x9D\x93\xB1" => "\x68", + "\xF0\x9D\x93\xB2" => "\x69", + "\xF0\x9D\x93\xB3" => "\x6A", + "\xF0\x9D\x93\xB4" => "\x6B", + "\xF0\x9D\x93\xB5" => "\x6C", + "\xF0\x9D\x93\xB6" => "\x6D", + "\xF0\x9D\x93\xB7" => "\x6E", + "\xF0\x9D\x93\xB8" => "\x6F", + "\xF0\x9D\x93\xB9" => "\x70", + "\xF0\x9D\x93\xBA" => "\x71", + "\xF0\x9D\x93\xBB" => "\x72", + "\xF0\x9D\x93\xBC" => "\x73", + "\xF0\x9D\x93\xBD" => "\x74", + "\xF0\x9D\x93\xBE" => "\x75", + "\xF0\x9D\x93\xBF" => "\x76", + "\xF0\x9D\x94\x80" => "\x77", + "\xF0\x9D\x94\x81" => "\x78", + "\xF0\x9D\x94\x82" => "\x79", + "\xF0\x9D\x94\x83" => "\x7A", + "\xF0\x9D\x94\x84" => "\x41", + "\xF0\x9D\x94\x85" => "\x42", + "\xF0\x9D\x94\x87" => "\x44", + "\xF0\x9D\x94\x88" => "\x45", + "\xF0\x9D\x94\x89" => "\x46", + "\xF0\x9D\x94\x8A" => "\x47", + "\xF0\x9D\x94\x8D" => "\x4A", + "\xF0\x9D\x94\x8E" => "\x4B", + "\xF0\x9D\x94\x8F" => "\x4C", + "\xF0\x9D\x94\x90" => "\x4D", + "\xF0\x9D\x94\x91" => "\x4E", + "\xF0\x9D\x94\x92" => "\x4F", + "\xF0\x9D\x94\x93" => "\x50", + "\xF0\x9D\x94\x94" => "\x51", + "\xF0\x9D\x94\x96" => "\x53", + "\xF0\x9D\x94\x97" => "\x54", + "\xF0\x9D\x94\x98" => "\x55", + "\xF0\x9D\x94\x99" => "\x56", + "\xF0\x9D\x94\x9A" => "\x57", + "\xF0\x9D\x94\x9B" => "\x58", + "\xF0\x9D\x94\x9C" => "\x59", + "\xF0\x9D\x94\x9E" => "\x61", + "\xF0\x9D\x94\x9F" => "\x62", + "\xF0\x9D\x94\xA0" => "\x63", + "\xF0\x9D\x94\xA1" => "\x64", + "\xF0\x9D\x94\xA2" => "\x65", + "\xF0\x9D\x94\xA3" => "\x66", + "\xF0\x9D\x94\xA4" => "\x67", + "\xF0\x9D\x94\xA5" => "\x68", + "\xF0\x9D\x94\xA6" => "\x69", + "\xF0\x9D\x94\xA7" => "\x6A", + "\xF0\x9D\x94\xA8" => "\x6B", + "\xF0\x9D\x94\xA9" => "\x6C", + "\xF0\x9D\x94\xAA" => "\x6D", + "\xF0\x9D\x94\xAB" => "\x6E", + "\xF0\x9D\x94\xAC" => "\x6F", + "\xF0\x9D\x94\xAD" => "\x70", + "\xF0\x9D\x94\xAE" => "\x71", + "\xF0\x9D\x94\xAF" => "\x72", + "\xF0\x9D\x94\xB0" => "\x73", + "\xF0\x9D\x94\xB1" => "\x74", + "\xF0\x9D\x94\xB2" => "\x75", + "\xF0\x9D\x94\xB3" => "\x76", + "\xF0\x9D\x94\xB4" => "\x77", + "\xF0\x9D\x94\xB5" => "\x78", + "\xF0\x9D\x94\xB6" => "\x79", + "\xF0\x9D\x94\xB7" => "\x7A", + "\xF0\x9D\x94\xB8" => "\x41", + "\xF0\x9D\x94\xB9" => "\x42", + "\xF0\x9D\x94\xBB" => "\x44", + "\xF0\x9D\x94\xBC" => "\x45", + "\xF0\x9D\x94\xBD" => "\x46", + "\xF0\x9D\x94\xBE" => "\x47", + "\xF0\x9D\x95\x80" => "\x49", + "\xF0\x9D\x95\x81" => "\x4A", + "\xF0\x9D\x95\x82" => "\x4B", + "\xF0\x9D\x95\x83" => "\x4C", + "\xF0\x9D\x95\x84" => "\x4D", + "\xF0\x9D\x95\x86" => "\x4F", + "\xF0\x9D\x95\x8A" => "\x53", + "\xF0\x9D\x95\x8B" => "\x54", + "\xF0\x9D\x95\x8C" => "\x55", + "\xF0\x9D\x95\x8D" => "\x56", + "\xF0\x9D\x95\x8E" => "\x57", + "\xF0\x9D\x95\x8F" => "\x58", + "\xF0\x9D\x95\x90" => "\x59", + "\xF0\x9D\x95\x92" => "\x61", + "\xF0\x9D\x95\x93" => "\x62", + "\xF0\x9D\x95\x94" => "\x63", + "\xF0\x9D\x95\x95" => "\x64", + "\xF0\x9D\x95\x96" => "\x65", + "\xF0\x9D\x95\x97" => "\x66", + "\xF0\x9D\x95\x98" => "\x67", + "\xF0\x9D\x95\x99" => "\x68", + "\xF0\x9D\x95\x9A" => "\x69", + "\xF0\x9D\x95\x9B" => "\x6A", + "\xF0\x9D\x95\x9C" => "\x6B", + "\xF0\x9D\x95\x9D" => "\x6C", + "\xF0\x9D\x95\x9E" => "\x6D", + "\xF0\x9D\x95\x9F" => "\x6E", + "\xF0\x9D\x95\xA0" => "\x6F", + "\xF0\x9D\x95\xA1" => "\x70", + "\xF0\x9D\x95\xA2" => "\x71", + "\xF0\x9D\x95\xA3" => "\x72", + "\xF0\x9D\x95\xA4" => "\x73", + "\xF0\x9D\x95\xA5" => "\x74", + "\xF0\x9D\x95\xA6" => "\x75", + "\xF0\x9D\x95\xA7" => "\x76", + "\xF0\x9D\x95\xA8" => "\x77", + "\xF0\x9D\x95\xA9" => "\x78", + "\xF0\x9D\x95\xAA" => "\x79", + "\xF0\x9D\x95\xAB" => "\x7A", + "\xF0\x9D\x95\xAC" => "\x41", + "\xF0\x9D\x95\xAD" => "\x42", + "\xF0\x9D\x95\xAE" => "\x43", + "\xF0\x9D\x95\xAF" => "\x44", + "\xF0\x9D\x95\xB0" => "\x45", + "\xF0\x9D\x95\xB1" => "\x46", + "\xF0\x9D\x95\xB2" => "\x47", + "\xF0\x9D\x95\xB3" => "\x48", + "\xF0\x9D\x95\xB4" => "\x49", + "\xF0\x9D\x95\xB5" => "\x4A", + "\xF0\x9D\x95\xB6" => "\x4B", + "\xF0\x9D\x95\xB7" => "\x4C", + "\xF0\x9D\x95\xB8" => "\x4D", + "\xF0\x9D\x95\xB9" => "\x4E", + "\xF0\x9D\x95\xBA" => "\x4F", + "\xF0\x9D\x95\xBB" => "\x50", + "\xF0\x9D\x95\xBC" => "\x51", + "\xF0\x9D\x95\xBD" => "\x52", + "\xF0\x9D\x95\xBE" => "\x53", + "\xF0\x9D\x95\xBF" => "\x54", + "\xF0\x9D\x96\x80" => "\x55", + "\xF0\x9D\x96\x81" => "\x56", + "\xF0\x9D\x96\x82" => "\x57", + "\xF0\x9D\x96\x83" => "\x58", + "\xF0\x9D\x96\x84" => "\x59", + "\xF0\x9D\x96\x85" => "\x5A", + "\xF0\x9D\x96\x86" => "\x61", + "\xF0\x9D\x96\x87" => "\x62", + "\xF0\x9D\x96\x88" => "\x63", + "\xF0\x9D\x96\x89" => "\x64", + "\xF0\x9D\x96\x8A" => "\x65", + "\xF0\x9D\x96\x8B" => "\x66", + "\xF0\x9D\x96\x8C" => "\x67", + "\xF0\x9D\x96\x8D" => "\x68", + "\xF0\x9D\x96\x8E" => "\x69", + "\xF0\x9D\x96\x8F" => "\x6A", + "\xF0\x9D\x96\x90" => "\x6B", + "\xF0\x9D\x96\x91" => "\x6C", + "\xF0\x9D\x96\x92" => "\x6D", + "\xF0\x9D\x96\x93" => "\x6E", + "\xF0\x9D\x96\x94" => "\x6F", + "\xF0\x9D\x96\x95" => "\x70", + "\xF0\x9D\x96\x96" => "\x71", + "\xF0\x9D\x96\x97" => "\x72", + "\xF0\x9D\x96\x98" => "\x73", + "\xF0\x9D\x96\x99" => "\x74", + "\xF0\x9D\x96\x9A" => "\x75", + "\xF0\x9D\x96\x9B" => "\x76", + "\xF0\x9D\x96\x9C" => "\x77", + "\xF0\x9D\x96\x9D" => "\x78", + "\xF0\x9D\x96\x9E" => "\x79", + "\xF0\x9D\x96\x9F" => "\x7A", + "\xF0\x9D\x96\xA0" => "\x41", + "\xF0\x9D\x96\xA1" => "\x42", + "\xF0\x9D\x96\xA2" => "\x43", + "\xF0\x9D\x96\xA3" => "\x44", + "\xF0\x9D\x96\xA4" => "\x45", + "\xF0\x9D\x96\xA5" => "\x46", + "\xF0\x9D\x96\xA6" => "\x47", + "\xF0\x9D\x96\xA7" => "\x48", + "\xF0\x9D\x96\xA8" => "\x49", + "\xF0\x9D\x96\xA9" => "\x4A", + "\xF0\x9D\x96\xAA" => "\x4B", + "\xF0\x9D\x96\xAB" => "\x4C", + "\xF0\x9D\x96\xAC" => "\x4D", + "\xF0\x9D\x96\xAD" => "\x4E", + "\xF0\x9D\x96\xAE" => "\x4F", + "\xF0\x9D\x96\xAF" => "\x50", + "\xF0\x9D\x96\xB0" => "\x51", + "\xF0\x9D\x96\xB1" => "\x52", + "\xF0\x9D\x96\xB2" => "\x53", + "\xF0\x9D\x96\xB3" => "\x54", + "\xF0\x9D\x96\xB4" => "\x55", + "\xF0\x9D\x96\xB5" => "\x56", + "\xF0\x9D\x96\xB6" => "\x57", + "\xF0\x9D\x96\xB7" => "\x58", + "\xF0\x9D\x96\xB8" => "\x59", + "\xF0\x9D\x96\xB9" => "\x5A", + "\xF0\x9D\x96\xBA" => "\x61", + "\xF0\x9D\x96\xBB" => "\x62", + "\xF0\x9D\x96\xBC" => "\x63", + "\xF0\x9D\x96\xBD" => "\x64", + "\xF0\x9D\x96\xBE" => "\x65", + "\xF0\x9D\x96\xBF" => "\x66", + "\xF0\x9D\x97\x80" => "\x67", + "\xF0\x9D\x97\x81" => "\x68", + "\xF0\x9D\x97\x82" => "\x69", + "\xF0\x9D\x97\x83" => "\x6A", + "\xF0\x9D\x97\x84" => "\x6B", + "\xF0\x9D\x97\x85" => "\x6C", + "\xF0\x9D\x97\x86" => "\x6D", + "\xF0\x9D\x97\x87" => "\x6E", + "\xF0\x9D\x97\x88" => "\x6F", + "\xF0\x9D\x97\x89" => "\x70", + "\xF0\x9D\x97\x8A" => "\x71", + "\xF0\x9D\x97\x8B" => "\x72", + "\xF0\x9D\x97\x8C" => "\x73", + "\xF0\x9D\x97\x8D" => "\x74", + "\xF0\x9D\x97\x8E" => "\x75", + "\xF0\x9D\x97\x8F" => "\x76", + "\xF0\x9D\x97\x90" => "\x77", + "\xF0\x9D\x97\x91" => "\x78", + "\xF0\x9D\x97\x92" => "\x79", + "\xF0\x9D\x97\x93" => "\x7A", + "\xF0\x9D\x97\x94" => "\x41", + "\xF0\x9D\x97\x95" => "\x42", + "\xF0\x9D\x97\x96" => "\x43", + "\xF0\x9D\x97\x97" => "\x44", + "\xF0\x9D\x97\x98" => "\x45", + "\xF0\x9D\x97\x99" => "\x46", + "\xF0\x9D\x97\x9A" => "\x47", + "\xF0\x9D\x97\x9B" => "\x48", + "\xF0\x9D\x97\x9C" => "\x49", + "\xF0\x9D\x97\x9D" => "\x4A", + "\xF0\x9D\x97\x9E" => "\x4B", + "\xF0\x9D\x97\x9F" => "\x4C", + "\xF0\x9D\x97\xA0" => "\x4D", + "\xF0\x9D\x97\xA1" => "\x4E", + "\xF0\x9D\x97\xA2" => "\x4F", + "\xF0\x9D\x97\xA3" => "\x50", + "\xF0\x9D\x97\xA4" => "\x51", + "\xF0\x9D\x97\xA5" => "\x52", + "\xF0\x9D\x97\xA6" => "\x53", + "\xF0\x9D\x97\xA7" => "\x54", + "\xF0\x9D\x97\xA8" => "\x55", + "\xF0\x9D\x97\xA9" => "\x56", + "\xF0\x9D\x97\xAA" => "\x57", + "\xF0\x9D\x97\xAB" => "\x58", + "\xF0\x9D\x97\xAC" => "\x59", + "\xF0\x9D\x97\xAD" => "\x5A", + "\xF0\x9D\x97\xAE" => "\x61", + "\xF0\x9D\x97\xAF" => "\x62", + "\xF0\x9D\x97\xB0" => "\x63", + "\xF0\x9D\x97\xB1" => "\x64", + "\xF0\x9D\x97\xB2" => "\x65", + "\xF0\x9D\x97\xB3" => "\x66", + "\xF0\x9D\x97\xB4" => "\x67", + "\xF0\x9D\x97\xB5" => "\x68", + "\xF0\x9D\x97\xB6" => "\x69", + "\xF0\x9D\x97\xB7" => "\x6A", + "\xF0\x9D\x97\xB8" => "\x6B", + "\xF0\x9D\x97\xB9" => "\x6C", + "\xF0\x9D\x97\xBA" => "\x6D", + "\xF0\x9D\x97\xBB" => "\x6E", + "\xF0\x9D\x97\xBC" => "\x6F", + "\xF0\x9D\x97\xBD" => "\x70", + "\xF0\x9D\x97\xBE" => "\x71", + "\xF0\x9D\x97\xBF" => "\x72", + "\xF0\x9D\x98\x80" => "\x73", + "\xF0\x9D\x98\x81" => "\x74", + "\xF0\x9D\x98\x82" => "\x75", + "\xF0\x9D\x98\x83" => "\x76", + "\xF0\x9D\x98\x84" => "\x77", + "\xF0\x9D\x98\x85" => "\x78", + "\xF0\x9D\x98\x86" => "\x79", + "\xF0\x9D\x98\x87" => "\x7A", + "\xF0\x9D\x98\x88" => "\x41", + "\xF0\x9D\x98\x89" => "\x42", + "\xF0\x9D\x98\x8A" => "\x43", + "\xF0\x9D\x98\x8B" => "\x44", + "\xF0\x9D\x98\x8C" => "\x45", + "\xF0\x9D\x98\x8D" => "\x46", + "\xF0\x9D\x98\x8E" => "\x47", + "\xF0\x9D\x98\x8F" => "\x48", + "\xF0\x9D\x98\x90" => "\x49", + "\xF0\x9D\x98\x91" => "\x4A", + "\xF0\x9D\x98\x92" => "\x4B", + "\xF0\x9D\x98\x93" => "\x4C", + "\xF0\x9D\x98\x94" => "\x4D", + "\xF0\x9D\x98\x95" => "\x4E", + "\xF0\x9D\x98\x96" => "\x4F", + "\xF0\x9D\x98\x97" => "\x50", + "\xF0\x9D\x98\x98" => "\x51", + "\xF0\x9D\x98\x99" => "\x52", + "\xF0\x9D\x98\x9A" => "\x53", + "\xF0\x9D\x98\x9B" => "\x54", + "\xF0\x9D\x98\x9C" => "\x55", + "\xF0\x9D\x98\x9D" => "\x56", + "\xF0\x9D\x98\x9E" => "\x57", + "\xF0\x9D\x98\x9F" => "\x58", + "\xF0\x9D\x98\xA0" => "\x59", + "\xF0\x9D\x98\xA1" => "\x5A", + "\xF0\x9D\x98\xA2" => "\x61", + "\xF0\x9D\x98\xA3" => "\x62", + "\xF0\x9D\x98\xA4" => "\x63", + "\xF0\x9D\x98\xA5" => "\x64", + "\xF0\x9D\x98\xA6" => "\x65", + "\xF0\x9D\x98\xA7" => "\x66", + "\xF0\x9D\x98\xA8" => "\x67", + "\xF0\x9D\x98\xA9" => "\x68", + "\xF0\x9D\x98\xAA" => "\x69", + "\xF0\x9D\x98\xAB" => "\x6A", + "\xF0\x9D\x98\xAC" => "\x6B", + "\xF0\x9D\x98\xAD" => "\x6C", + "\xF0\x9D\x98\xAE" => "\x6D", + "\xF0\x9D\x98\xAF" => "\x6E", + "\xF0\x9D\x98\xB0" => "\x6F", + "\xF0\x9D\x98\xB1" => "\x70", + "\xF0\x9D\x98\xB2" => "\x71", + "\xF0\x9D\x98\xB3" => "\x72", + "\xF0\x9D\x98\xB4" => "\x73", + "\xF0\x9D\x98\xB5" => "\x74", + "\xF0\x9D\x98\xB6" => "\x75", + "\xF0\x9D\x98\xB7" => "\x76", + "\xF0\x9D\x98\xB8" => "\x77", + "\xF0\x9D\x98\xB9" => "\x78", + "\xF0\x9D\x98\xBA" => "\x79", + "\xF0\x9D\x98\xBB" => "\x7A", + "\xF0\x9D\x98\xBC" => "\x41", + "\xF0\x9D\x98\xBD" => "\x42", + "\xF0\x9D\x98\xBE" => "\x43", + "\xF0\x9D\x98\xBF" => "\x44", + "\xF0\x9D\x99\x80" => "\x45", + "\xF0\x9D\x99\x81" => "\x46", + "\xF0\x9D\x99\x82" => "\x47", + "\xF0\x9D\x99\x83" => "\x48", + "\xF0\x9D\x99\x84" => "\x49", + "\xF0\x9D\x99\x85" => "\x4A", + "\xF0\x9D\x99\x86" => "\x4B", + "\xF0\x9D\x99\x87" => "\x4C", + "\xF0\x9D\x99\x88" => "\x4D", + "\xF0\x9D\x99\x89" => "\x4E", + "\xF0\x9D\x99\x8A" => "\x4F", + "\xF0\x9D\x99\x8B" => "\x50", + "\xF0\x9D\x99\x8C" => "\x51", + "\xF0\x9D\x99\x8D" => "\x52", + "\xF0\x9D\x99\x8E" => "\x53", + "\xF0\x9D\x99\x8F" => "\x54", + "\xF0\x9D\x99\x90" => "\x55", + "\xF0\x9D\x99\x91" => "\x56", + "\xF0\x9D\x99\x92" => "\x57", + "\xF0\x9D\x99\x93" => "\x58", + "\xF0\x9D\x99\x94" => "\x59", + "\xF0\x9D\x99\x95" => "\x5A", + "\xF0\x9D\x99\x96" => "\x61", + "\xF0\x9D\x99\x97" => "\x62", + "\xF0\x9D\x99\x98" => "\x63", + "\xF0\x9D\x99\x99" => "\x64", + "\xF0\x9D\x99\x9A" => "\x65", + "\xF0\x9D\x99\x9B" => "\x66", + "\xF0\x9D\x99\x9C" => "\x67", + "\xF0\x9D\x99\x9D" => "\x68", + "\xF0\x9D\x99\x9E" => "\x69", + "\xF0\x9D\x99\x9F" => "\x6A", + "\xF0\x9D\x99\xA0" => "\x6B", + "\xF0\x9D\x99\xA1" => "\x6C", + "\xF0\x9D\x99\xA2" => "\x6D", + "\xF0\x9D\x99\xA3" => "\x6E", + "\xF0\x9D\x99\xA4" => "\x6F", + "\xF0\x9D\x99\xA5" => "\x70", + "\xF0\x9D\x99\xA6" => "\x71", + "\xF0\x9D\x99\xA7" => "\x72", + "\xF0\x9D\x99\xA8" => "\x73", + "\xF0\x9D\x99\xA9" => "\x74", + "\xF0\x9D\x99\xAA" => "\x75", + "\xF0\x9D\x99\xAB" => "\x76", + "\xF0\x9D\x99\xAC" => "\x77", + "\xF0\x9D\x99\xAD" => "\x78", + "\xF0\x9D\x99\xAE" => "\x79", + "\xF0\x9D\x99\xAF" => "\x7A", + "\xF0\x9D\x99\xB0" => "\x41", + "\xF0\x9D\x99\xB1" => "\x42", + "\xF0\x9D\x99\xB2" => "\x43", + "\xF0\x9D\x99\xB3" => "\x44", + "\xF0\x9D\x99\xB4" => "\x45", + "\xF0\x9D\x99\xB5" => "\x46", + "\xF0\x9D\x99\xB6" => "\x47", + "\xF0\x9D\x99\xB7" => "\x48", + "\xF0\x9D\x99\xB8" => "\x49", + "\xF0\x9D\x99\xB9" => "\x4A", + "\xF0\x9D\x99\xBA" => "\x4B", + "\xF0\x9D\x99\xBB" => "\x4C", + "\xF0\x9D\x99\xBC" => "\x4D", + "\xF0\x9D\x99\xBD" => "\x4E", + "\xF0\x9D\x99\xBE" => "\x4F", + "\xF0\x9D\x99\xBF" => "\x50", + "\xF0\x9D\x9A\x80" => "\x51", + "\xF0\x9D\x9A\x81" => "\x52", + "\xF0\x9D\x9A\x82" => "\x53", + "\xF0\x9D\x9A\x83" => "\x54", + "\xF0\x9D\x9A\x84" => "\x55", + "\xF0\x9D\x9A\x85" => "\x56", + "\xF0\x9D\x9A\x86" => "\x57", + "\xF0\x9D\x9A\x87" => "\x58", + "\xF0\x9D\x9A\x88" => "\x59", + "\xF0\x9D\x9A\x89" => "\x5A", + "\xF0\x9D\x9A\x8A" => "\x61", + "\xF0\x9D\x9A\x8B" => "\x62", + "\xF0\x9D\x9A\x8C" => "\x63", + "\xF0\x9D\x9A\x8D" => "\x64", + "\xF0\x9D\x9A\x8E" => "\x65", + "\xF0\x9D\x9A\x8F" => "\x66", + "\xF0\x9D\x9A\x90" => "\x67", + "\xF0\x9D\x9A\x91" => "\x68", + "\xF0\x9D\x9A\x92" => "\x69", + "\xF0\x9D\x9A\x93" => "\x6A", + "\xF0\x9D\x9A\x94" => "\x6B", + "\xF0\x9D\x9A\x95" => "\x6C", + "\xF0\x9D\x9A\x96" => "\x6D", + "\xF0\x9D\x9A\x97" => "\x6E", + "\xF0\x9D\x9A\x98" => "\x6F", + "\xF0\x9D\x9A\x99" => "\x70", + "\xF0\x9D\x9A\x9A" => "\x71", + "\xF0\x9D\x9A\x9B" => "\x72", + "\xF0\x9D\x9A\x9C" => "\x73", + "\xF0\x9D\x9A\x9D" => "\x74", + "\xF0\x9D\x9A\x9E" => "\x75", + "\xF0\x9D\x9A\x9F" => "\x76", + "\xF0\x9D\x9A\xA0" => "\x77", + "\xF0\x9D\x9A\xA1" => "\x78", + "\xF0\x9D\x9A\xA2" => "\x79", + "\xF0\x9D\x9A\xA3" => "\x7A", + "\xF0\x9D\x9A\xA4" => "\xC4\xB1", + "\xF0\x9D\x9A\xA5" => "\xC8\xB7", + "\xF0\x9D\x9A\xA8" => "\xCE\x91", + "\xF0\x9D\x9A\xA9" => "\xCE\x92", + "\xF0\x9D\x9A\xAA" => "\xCE\x93", + "\xF0\x9D\x9A\xAB" => "\xCE\x94", + "\xF0\x9D\x9A\xAC" => "\xCE\x95", + "\xF0\x9D\x9A\xAD" => "\xCE\x96", + "\xF0\x9D\x9A\xAE" => "\xCE\x97", + "\xF0\x9D\x9A\xAF" => "\xCE\x98", + "\xF0\x9D\x9A\xB0" => "\xCE\x99", + "\xF0\x9D\x9A\xB1" => "\xCE\x9A", + "\xF0\x9D\x9A\xB2" => "\xCE\x9B", + "\xF0\x9D\x9A\xB3" => "\xCE\x9C", + "\xF0\x9D\x9A\xB4" => "\xCE\x9D", + "\xF0\x9D\x9A\xB5" => "\xCE\x9E", + "\xF0\x9D\x9A\xB6" => "\xCE\x9F", + "\xF0\x9D\x9A\xB7" => "\xCE\xA0", + "\xF0\x9D\x9A\xB8" => "\xCE\xA1", + "\xF0\x9D\x9A\xB9" => "\xCE\x98", + "\xF0\x9D\x9A\xBA" => "\xCE\xA3", + "\xF0\x9D\x9A\xBB" => "\xCE\xA4", + "\xF0\x9D\x9A\xBC" => "\xCE\xA5", + "\xF0\x9D\x9A\xBD" => "\xCE\xA6", + "\xF0\x9D\x9A\xBE" => "\xCE\xA7", + "\xF0\x9D\x9A\xBF" => "\xCE\xA8", + "\xF0\x9D\x9B\x80" => "\xCE\xA9", + "\xF0\x9D\x9B\x81" => "\xE2\x88\x87", + "\xF0\x9D\x9B\x82" => "\xCE\xB1", + "\xF0\x9D\x9B\x83" => "\xCE\xB2", + "\xF0\x9D\x9B\x84" => "\xCE\xB3", + "\xF0\x9D\x9B\x85" => "\xCE\xB4", + "\xF0\x9D\x9B\x86" => "\xCE\xB5", + "\xF0\x9D\x9B\x87" => "\xCE\xB6", + "\xF0\x9D\x9B\x88" => "\xCE\xB7", + "\xF0\x9D\x9B\x89" => "\xCE\xB8", + "\xF0\x9D\x9B\x8A" => "\xCE\xB9", + "\xF0\x9D\x9B\x8B" => "\xCE\xBA", + "\xF0\x9D\x9B\x8C" => "\xCE\xBB", + "\xF0\x9D\x9B\x8D" => "\xCE\xBC", + "\xF0\x9D\x9B\x8E" => "\xCE\xBD", + "\xF0\x9D\x9B\x8F" => "\xCE\xBE", + "\xF0\x9D\x9B\x90" => "\xCE\xBF", + "\xF0\x9D\x9B\x91" => "\xCF\x80", + "\xF0\x9D\x9B\x92" => "\xCF\x81", + "\xF0\x9D\x9B\x93" => "\xCF\x82", + "\xF0\x9D\x9B\x94" => "\xCF\x83", + "\xF0\x9D\x9B\x95" => "\xCF\x84", + "\xF0\x9D\x9B\x96" => "\xCF\x85", + "\xF0\x9D\x9B\x97" => "\xCF\x86", + "\xF0\x9D\x9B\x98" => "\xCF\x87", + "\xF0\x9D\x9B\x99" => "\xCF\x88", + "\xF0\x9D\x9B\x9A" => "\xCF\x89", + "\xF0\x9D\x9B\x9B" => "\xE2\x88\x82", + "\xF0\x9D\x9B\x9C" => "\xCE\xB5", + "\xF0\x9D\x9B\x9D" => "\xCE\xB8", + "\xF0\x9D\x9B\x9E" => "\xCE\xBA", + "\xF0\x9D\x9B\x9F" => "\xCF\x86", + "\xF0\x9D\x9B\xA0" => "\xCF\x81", + "\xF0\x9D\x9B\xA1" => "\xCF\x80", + "\xF0\x9D\x9B\xA2" => "\xCE\x91", + "\xF0\x9D\x9B\xA3" => "\xCE\x92", + "\xF0\x9D\x9B\xA4" => "\xCE\x93", + "\xF0\x9D\x9B\xA5" => "\xCE\x94", + "\xF0\x9D\x9B\xA6" => "\xCE\x95", + "\xF0\x9D\x9B\xA7" => "\xCE\x96", + "\xF0\x9D\x9B\xA8" => "\xCE\x97", + "\xF0\x9D\x9B\xA9" => "\xCE\x98", + "\xF0\x9D\x9B\xAA" => "\xCE\x99", + "\xF0\x9D\x9B\xAB" => "\xCE\x9A", + "\xF0\x9D\x9B\xAC" => "\xCE\x9B", + "\xF0\x9D\x9B\xAD" => "\xCE\x9C", + "\xF0\x9D\x9B\xAE" => "\xCE\x9D", + "\xF0\x9D\x9B\xAF" => "\xCE\x9E", + "\xF0\x9D\x9B\xB0" => "\xCE\x9F", + "\xF0\x9D\x9B\xB1" => "\xCE\xA0", + "\xF0\x9D\x9B\xB2" => "\xCE\xA1", + "\xF0\x9D\x9B\xB3" => "\xCE\x98", + "\xF0\x9D\x9B\xB4" => "\xCE\xA3", + "\xF0\x9D\x9B\xB5" => "\xCE\xA4", + "\xF0\x9D\x9B\xB6" => "\xCE\xA5", + "\xF0\x9D\x9B\xB7" => "\xCE\xA6", + "\xF0\x9D\x9B\xB8" => "\xCE\xA7", + "\xF0\x9D\x9B\xB9" => "\xCE\xA8", + "\xF0\x9D\x9B\xBA" => "\xCE\xA9", + "\xF0\x9D\x9B\xBB" => "\xE2\x88\x87", + "\xF0\x9D\x9B\xBC" => "\xCE\xB1", + "\xF0\x9D\x9B\xBD" => "\xCE\xB2", + "\xF0\x9D\x9B\xBE" => "\xCE\xB3", + "\xF0\x9D\x9B\xBF" => "\xCE\xB4", + "\xF0\x9D\x9C\x80" => "\xCE\xB5", + "\xF0\x9D\x9C\x81" => "\xCE\xB6", + "\xF0\x9D\x9C\x82" => "\xCE\xB7", + "\xF0\x9D\x9C\x83" => "\xCE\xB8", + "\xF0\x9D\x9C\x84" => "\xCE\xB9", + "\xF0\x9D\x9C\x85" => "\xCE\xBA", + "\xF0\x9D\x9C\x86" => "\xCE\xBB", + "\xF0\x9D\x9C\x87" => "\xCE\xBC", + "\xF0\x9D\x9C\x88" => "\xCE\xBD", + "\xF0\x9D\x9C\x89" => "\xCE\xBE", + "\xF0\x9D\x9C\x8A" => "\xCE\xBF", + "\xF0\x9D\x9C\x8B" => "\xCF\x80", + "\xF0\x9D\x9C\x8C" => "\xCF\x81", + "\xF0\x9D\x9C\x8D" => "\xCF\x82", + "\xF0\x9D\x9C\x8E" => "\xCF\x83", + "\xF0\x9D\x9C\x8F" => "\xCF\x84", + "\xF0\x9D\x9C\x90" => "\xCF\x85", + "\xF0\x9D\x9C\x91" => "\xCF\x86", + "\xF0\x9D\x9C\x92" => "\xCF\x87", + "\xF0\x9D\x9C\x93" => "\xCF\x88", + "\xF0\x9D\x9C\x94" => "\xCF\x89", + "\xF0\x9D\x9C\x95" => "\xE2\x88\x82", + "\xF0\x9D\x9C\x96" => "\xCE\xB5", + "\xF0\x9D\x9C\x97" => "\xCE\xB8", + "\xF0\x9D\x9C\x98" => "\xCE\xBA", + "\xF0\x9D\x9C\x99" => "\xCF\x86", + "\xF0\x9D\x9C\x9A" => "\xCF\x81", + "\xF0\x9D\x9C\x9B" => "\xCF\x80", + "\xF0\x9D\x9C\x9C" => "\xCE\x91", + "\xF0\x9D\x9C\x9D" => "\xCE\x92", + "\xF0\x9D\x9C\x9E" => "\xCE\x93", + "\xF0\x9D\x9C\x9F" => "\xCE\x94", + "\xF0\x9D\x9C\xA0" => "\xCE\x95", + "\xF0\x9D\x9C\xA1" => "\xCE\x96", + "\xF0\x9D\x9C\xA2" => "\xCE\x97", + "\xF0\x9D\x9C\xA3" => "\xCE\x98", + "\xF0\x9D\x9C\xA4" => "\xCE\x99", + "\xF0\x9D\x9C\xA5" => "\xCE\x9A", + "\xF0\x9D\x9C\xA6" => "\xCE\x9B", + "\xF0\x9D\x9C\xA7" => "\xCE\x9C", + "\xF0\x9D\x9C\xA8" => "\xCE\x9D", + "\xF0\x9D\x9C\xA9" => "\xCE\x9E", + "\xF0\x9D\x9C\xAA" => "\xCE\x9F", + "\xF0\x9D\x9C\xAB" => "\xCE\xA0", + "\xF0\x9D\x9C\xAC" => "\xCE\xA1", + "\xF0\x9D\x9C\xAD" => "\xCE\x98", + "\xF0\x9D\x9C\xAE" => "\xCE\xA3", + "\xF0\x9D\x9C\xAF" => "\xCE\xA4", + "\xF0\x9D\x9C\xB0" => "\xCE\xA5", + "\xF0\x9D\x9C\xB1" => "\xCE\xA6", + "\xF0\x9D\x9C\xB2" => "\xCE\xA7", + "\xF0\x9D\x9C\xB3" => "\xCE\xA8", + "\xF0\x9D\x9C\xB4" => "\xCE\xA9", + "\xF0\x9D\x9C\xB5" => "\xE2\x88\x87", + "\xF0\x9D\x9C\xB6" => "\xCE\xB1", + "\xF0\x9D\x9C\xB7" => "\xCE\xB2", + "\xF0\x9D\x9C\xB8" => "\xCE\xB3", + "\xF0\x9D\x9C\xB9" => "\xCE\xB4", + "\xF0\x9D\x9C\xBA" => "\xCE\xB5", + "\xF0\x9D\x9C\xBB" => "\xCE\xB6", + "\xF0\x9D\x9C\xBC" => "\xCE\xB7", + "\xF0\x9D\x9C\xBD" => "\xCE\xB8", + "\xF0\x9D\x9C\xBE" => "\xCE\xB9", + "\xF0\x9D\x9C\xBF" => "\xCE\xBA", + "\xF0\x9D\x9D\x80" => "\xCE\xBB", + "\xF0\x9D\x9D\x81" => "\xCE\xBC", + "\xF0\x9D\x9D\x82" => "\xCE\xBD", + "\xF0\x9D\x9D\x83" => "\xCE\xBE", + "\xF0\x9D\x9D\x84" => "\xCE\xBF", + "\xF0\x9D\x9D\x85" => "\xCF\x80", + "\xF0\x9D\x9D\x86" => "\xCF\x81", + "\xF0\x9D\x9D\x87" => "\xCF\x82", + "\xF0\x9D\x9D\x88" => "\xCF\x83", + "\xF0\x9D\x9D\x89" => "\xCF\x84", + "\xF0\x9D\x9D\x8A" => "\xCF\x85", + "\xF0\x9D\x9D\x8B" => "\xCF\x86", + "\xF0\x9D\x9D\x8C" => "\xCF\x87", + "\xF0\x9D\x9D\x8D" => "\xCF\x88", + "\xF0\x9D\x9D\x8E" => "\xCF\x89", + "\xF0\x9D\x9D\x8F" => "\xE2\x88\x82", + "\xF0\x9D\x9D\x90" => "\xCE\xB5", + "\xF0\x9D\x9D\x91" => "\xCE\xB8", + "\xF0\x9D\x9D\x92" => "\xCE\xBA", + "\xF0\x9D\x9D\x93" => "\xCF\x86", + "\xF0\x9D\x9D\x94" => "\xCF\x81", + "\xF0\x9D\x9D\x95" => "\xCF\x80", + "\xF0\x9D\x9D\x96" => "\xCE\x91", + "\xF0\x9D\x9D\x97" => "\xCE\x92", + "\xF0\x9D\x9D\x98" => "\xCE\x93", + "\xF0\x9D\x9D\x99" => "\xCE\x94", + "\xF0\x9D\x9D\x9A" => "\xCE\x95", + "\xF0\x9D\x9D\x9B" => "\xCE\x96", + "\xF0\x9D\x9D\x9C" => "\xCE\x97", + "\xF0\x9D\x9D\x9D" => "\xCE\x98", + "\xF0\x9D\x9D\x9E" => "\xCE\x99", + "\xF0\x9D\x9D\x9F" => "\xCE\x9A", + "\xF0\x9D\x9D\xA0" => "\xCE\x9B", + "\xF0\x9D\x9D\xA1" => "\xCE\x9C", + "\xF0\x9D\x9D\xA2" => "\xCE\x9D", + "\xF0\x9D\x9D\xA3" => "\xCE\x9E", + "\xF0\x9D\x9D\xA4" => "\xCE\x9F", + "\xF0\x9D\x9D\xA5" => "\xCE\xA0", + "\xF0\x9D\x9D\xA6" => "\xCE\xA1", + "\xF0\x9D\x9D\xA7" => "\xCE\x98", + "\xF0\x9D\x9D\xA8" => "\xCE\xA3", + "\xF0\x9D\x9D\xA9" => "\xCE\xA4", + "\xF0\x9D\x9D\xAA" => "\xCE\xA5", + "\xF0\x9D\x9D\xAB" => "\xCE\xA6", + "\xF0\x9D\x9D\xAC" => "\xCE\xA7", + "\xF0\x9D\x9D\xAD" => "\xCE\xA8", + "\xF0\x9D\x9D\xAE" => "\xCE\xA9", + "\xF0\x9D\x9D\xAF" => "\xE2\x88\x87", + "\xF0\x9D\x9D\xB0" => "\xCE\xB1", + "\xF0\x9D\x9D\xB1" => "\xCE\xB2", + "\xF0\x9D\x9D\xB2" => "\xCE\xB3", + "\xF0\x9D\x9D\xB3" => "\xCE\xB4", + "\xF0\x9D\x9D\xB4" => "\xCE\xB5", + "\xF0\x9D\x9D\xB5" => "\xCE\xB6", + "\xF0\x9D\x9D\xB6" => "\xCE\xB7", + "\xF0\x9D\x9D\xB7" => "\xCE\xB8", + "\xF0\x9D\x9D\xB8" => "\xCE\xB9", + "\xF0\x9D\x9D\xB9" => "\xCE\xBA", + "\xF0\x9D\x9D\xBA" => "\xCE\xBB", + "\xF0\x9D\x9D\xBB" => "\xCE\xBC", + "\xF0\x9D\x9D\xBC" => "\xCE\xBD", + "\xF0\x9D\x9D\xBD" => "\xCE\xBE", + "\xF0\x9D\x9D\xBE" => "\xCE\xBF", + "\xF0\x9D\x9D\xBF" => "\xCF\x80", + "\xF0\x9D\x9E\x80" => "\xCF\x81", + "\xF0\x9D\x9E\x81" => "\xCF\x82", + "\xF0\x9D\x9E\x82" => "\xCF\x83", + "\xF0\x9D\x9E\x83" => "\xCF\x84", + "\xF0\x9D\x9E\x84" => "\xCF\x85", + "\xF0\x9D\x9E\x85" => "\xCF\x86", + "\xF0\x9D\x9E\x86" => "\xCF\x87", + "\xF0\x9D\x9E\x87" => "\xCF\x88", + "\xF0\x9D\x9E\x88" => "\xCF\x89", + "\xF0\x9D\x9E\x89" => "\xE2\x88\x82", + "\xF0\x9D\x9E\x8A" => "\xCE\xB5", + "\xF0\x9D\x9E\x8B" => "\xCE\xB8", + "\xF0\x9D\x9E\x8C" => "\xCE\xBA", + "\xF0\x9D\x9E\x8D" => "\xCF\x86", + "\xF0\x9D\x9E\x8E" => "\xCF\x81", + "\xF0\x9D\x9E\x8F" => "\xCF\x80", + "\xF0\x9D\x9E\x90" => "\xCE\x91", + "\xF0\x9D\x9E\x91" => "\xCE\x92", + "\xF0\x9D\x9E\x92" => "\xCE\x93", + "\xF0\x9D\x9E\x93" => "\xCE\x94", + "\xF0\x9D\x9E\x94" => "\xCE\x95", + "\xF0\x9D\x9E\x95" => "\xCE\x96", + "\xF0\x9D\x9E\x96" => "\xCE\x97", + "\xF0\x9D\x9E\x97" => "\xCE\x98", + "\xF0\x9D\x9E\x98" => "\xCE\x99", + "\xF0\x9D\x9E\x99" => "\xCE\x9A", + "\xF0\x9D\x9E\x9A" => "\xCE\x9B", + "\xF0\x9D\x9E\x9B" => "\xCE\x9C", + "\xF0\x9D\x9E\x9C" => "\xCE\x9D", + "\xF0\x9D\x9E\x9D" => "\xCE\x9E", + "\xF0\x9D\x9E\x9E" => "\xCE\x9F", + "\xF0\x9D\x9E\x9F" => "\xCE\xA0", + "\xF0\x9D\x9E\xA0" => "\xCE\xA1", + "\xF0\x9D\x9E\xA1" => "\xCE\x98", + "\xF0\x9D\x9E\xA2" => "\xCE\xA3", + "\xF0\x9D\x9E\xA3" => "\xCE\xA4", + "\xF0\x9D\x9E\xA4" => "\xCE\xA5", + "\xF0\x9D\x9E\xA5" => "\xCE\xA6", + "\xF0\x9D\x9E\xA6" => "\xCE\xA7", + "\xF0\x9D\x9E\xA7" => "\xCE\xA8", + "\xF0\x9D\x9E\xA8" => "\xCE\xA9", + "\xF0\x9D\x9E\xA9" => "\xE2\x88\x87", + "\xF0\x9D\x9E\xAA" => "\xCE\xB1", + "\xF0\x9D\x9E\xAB" => "\xCE\xB2", + "\xF0\x9D\x9E\xAC" => "\xCE\xB3", + "\xF0\x9D\x9E\xAD" => "\xCE\xB4", + "\xF0\x9D\x9E\xAE" => "\xCE\xB5", + "\xF0\x9D\x9E\xAF" => "\xCE\xB6", + "\xF0\x9D\x9E\xB0" => "\xCE\xB7", + "\xF0\x9D\x9E\xB1" => "\xCE\xB8", + "\xF0\x9D\x9E\xB2" => "\xCE\xB9", + "\xF0\x9D\x9E\xB3" => "\xCE\xBA", + "\xF0\x9D\x9E\xB4" => "\xCE\xBB", + "\xF0\x9D\x9E\xB5" => "\xCE\xBC", + "\xF0\x9D\x9E\xB6" => "\xCE\xBD", + "\xF0\x9D\x9E\xB7" => "\xCE\xBE", + "\xF0\x9D\x9E\xB8" => "\xCE\xBF", + "\xF0\x9D\x9E\xB9" => "\xCF\x80", + "\xF0\x9D\x9E\xBA" => "\xCF\x81", + "\xF0\x9D\x9E\xBB" => "\xCF\x82", + "\xF0\x9D\x9E\xBC" => "\xCF\x83", + "\xF0\x9D\x9E\xBD" => "\xCF\x84", + "\xF0\x9D\x9E\xBE" => "\xCF\x85", + "\xF0\x9D\x9E\xBF" => "\xCF\x86", + "\xF0\x9D\x9F\x80" => "\xCF\x87", + "\xF0\x9D\x9F\x81" => "\xCF\x88", + "\xF0\x9D\x9F\x82" => "\xCF\x89", + "\xF0\x9D\x9F\x83" => "\xE2\x88\x82", + "\xF0\x9D\x9F\x84" => "\xCE\xB5", + "\xF0\x9D\x9F\x85" => "\xCE\xB8", + "\xF0\x9D\x9F\x86" => "\xCE\xBA", + "\xF0\x9D\x9F\x87" => "\xCF\x86", + "\xF0\x9D\x9F\x88" => "\xCF\x81", + "\xF0\x9D\x9F\x89" => "\xCF\x80", + "\xF0\x9D\x9F\x8A" => "\xCF\x9C", + "\xF0\x9D\x9F\x8B" => "\xCF\x9D", + "\xF0\x9D\x9F\x8E" => "\x30", + "\xF0\x9D\x9F\x8F" => "\x31", + "\xF0\x9D\x9F\x90" => "\x32", + "\xF0\x9D\x9F\x91" => "\x33", + "\xF0\x9D\x9F\x92" => "\x34", + "\xF0\x9D\x9F\x93" => "\x35", + "\xF0\x9D\x9F\x94" => "\x36", + "\xF0\x9D\x9F\x95" => "\x37", + "\xF0\x9D\x9F\x96" => "\x38", + "\xF0\x9D\x9F\x97" => "\x39", + "\xF0\x9D\x9F\x98" => "\x30", + "\xF0\x9D\x9F\x99" => "\x31", + "\xF0\x9D\x9F\x9A" => "\x32", + "\xF0\x9D\x9F\x9B" => "\x33", + "\xF0\x9D\x9F\x9C" => "\x34", + "\xF0\x9D\x9F\x9D" => "\x35", + "\xF0\x9D\x9F\x9E" => "\x36", + "\xF0\x9D\x9F\x9F" => "\x37", + "\xF0\x9D\x9F\xA0" => "\x38", + "\xF0\x9D\x9F\xA1" => "\x39", + "\xF0\x9D\x9F\xA2" => "\x30", + "\xF0\x9D\x9F\xA3" => "\x31", + "\xF0\x9D\x9F\xA4" => "\x32", + "\xF0\x9D\x9F\xA5" => "\x33", + "\xF0\x9D\x9F\xA6" => "\x34", + "\xF0\x9D\x9F\xA7" => "\x35", + "\xF0\x9D\x9F\xA8" => "\x36", + "\xF0\x9D\x9F\xA9" => "\x37", + "\xF0\x9D\x9F\xAA" => "\x38", + "\xF0\x9D\x9F\xAB" => "\x39", + "\xF0\x9D\x9F\xAC" => "\x30", + "\xF0\x9D\x9F\xAD" => "\x31", + "\xF0\x9D\x9F\xAE" => "\x32", + "\xF0\x9D\x9F\xAF" => "\x33", + "\xF0\x9D\x9F\xB0" => "\x34", + "\xF0\x9D\x9F\xB1" => "\x35", + "\xF0\x9D\x9F\xB2" => "\x36", + "\xF0\x9D\x9F\xB3" => "\x37", + "\xF0\x9D\x9F\xB4" => "\x38", + "\xF0\x9D\x9F\xB5" => "\x39", + "\xF0\x9D\x9F\xB6" => "\x30", + "\xF0\x9D\x9F\xB7" => "\x31", + "\xF0\x9D\x9F\xB8" => "\x32", + "\xF0\x9D\x9F\xB9" => "\x33", + "\xF0\x9D\x9F\xBA" => "\x34", + "\xF0\x9D\x9F\xBB" => "\x35", + "\xF0\x9D\x9F\xBC" => "\x36", + "\xF0\x9D\x9F\xBD" => "\x37", + "\xF0\x9D\x9F\xBE" => "\x38", + "\xF0\x9D\x9F\xBF" => "\x39", + "\xF0\x9E\x80\xB0" => "\xD0\xB0", + "\xF0\x9E\x80\xB1" => "\xD0\xB1", + "\xF0\x9E\x80\xB2" => "\xD0\xB2", + "\xF0\x9E\x80\xB3" => "\xD0\xB3", + "\xF0\x9E\x80\xB4" => "\xD0\xB4", + "\xF0\x9E\x80\xB5" => "\xD0\xB5", + "\xF0\x9E\x80\xB6" => "\xD0\xB6", + "\xF0\x9E\x80\xB7" => "\xD0\xB7", + "\xF0\x9E\x80\xB8" => "\xD0\xB8", + "\xF0\x9E\x80\xB9" => "\xD0\xBA", + "\xF0\x9E\x80\xBA" => "\xD0\xBB", + "\xF0\x9E\x80\xBB" => "\xD0\xBC", + "\xF0\x9E\x80\xBC" => "\xD0\xBE", + "\xF0\x9E\x80\xBD" => "\xD0\xBF", + "\xF0\x9E\x80\xBE" => "\xD1\x80", + "\xF0\x9E\x80\xBF" => "\xD1\x81", + "\xF0\x9E\x81\x80" => "\xD1\x82", + "\xF0\x9E\x81\x81" => "\xD1\x83", + "\xF0\x9E\x81\x82" => "\xD1\x84", + "\xF0\x9E\x81\x83" => "\xD1\x85", + "\xF0\x9E\x81\x84" => "\xD1\x86", + "\xF0\x9E\x81\x85" => "\xD1\x87", + "\xF0\x9E\x81\x86" => "\xD1\x88", + "\xF0\x9E\x81\x87" => "\xD1\x8B", + "\xF0\x9E\x81\x88" => "\xD1\x8D", + "\xF0\x9E\x81\x89" => "\xD1\x8E", + "\xF0\x9E\x81\x8A" => "\xEA\x9A\x89", + "\xF0\x9E\x81\x8B" => "\xD3\x99", + "\xF0\x9E\x81\x8C" => "\xD1\x96", + "\xF0\x9E\x81\x8D" => "\xD1\x98", + "\xF0\x9E\x81\x8E" => "\xD3\xA9", + "\xF0\x9E\x81\x8F" => "\xD2\xAF", + "\xF0\x9E\x81\x90" => "\xD3\x8F", + "\xF0\x9E\x81\x91" => "\xD0\xB0", + "\xF0\x9E\x81\x92" => "\xD0\xB1", + "\xF0\x9E\x81\x93" => "\xD0\xB2", + "\xF0\x9E\x81\x94" => "\xD0\xB3", + "\xF0\x9E\x81\x95" => "\xD0\xB4", + "\xF0\x9E\x81\x96" => "\xD0\xB5", + "\xF0\x9E\x81\x97" => "\xD0\xB6", + "\xF0\x9E\x81\x98" => "\xD0\xB7", + "\xF0\x9E\x81\x99" => "\xD0\xB8", + "\xF0\x9E\x81\x9A" => "\xD0\xBA", + "\xF0\x9E\x81\x9B" => "\xD0\xBB", + "\xF0\x9E\x81\x9C" => "\xD0\xBE", + "\xF0\x9E\x81\x9D" => "\xD0\xBF", + "\xF0\x9E\x81\x9E" => "\xD1\x81", + "\xF0\x9E\x81\x9F" => "\xD1\x83", + "\xF0\x9E\x81\xA0" => "\xD1\x84", + "\xF0\x9E\x81\xA1" => "\xD1\x85", + "\xF0\x9E\x81\xA2" => "\xD1\x86", + "\xF0\x9E\x81\xA3" => "\xD1\x87", + "\xF0\x9E\x81\xA4" => "\xD1\x88", + "\xF0\x9E\x81\xA5" => "\xD1\x8A", + "\xF0\x9E\x81\xA6" => "\xD1\x8B", + "\xF0\x9E\x81\xA7" => "\xD2\x91", + "\xF0\x9E\x81\xA8" => "\xD1\x96", + "\xF0\x9E\x81\xA9" => "\xD1\x95", + "\xF0\x9E\x81\xAA" => "\xD1\x9F", + "\xF0\x9E\x81\xAB" => "\xD2\xAB", + "\xF0\x9E\x81\xAC" => "\xEA\x99\x91", + "\xF0\x9E\x81\xAD" => "\xD2\xB1", + "\xF0\x9E\xB8\x80" => "\xD8\xA7", + "\xF0\x9E\xB8\x81" => "\xD8\xA8", + "\xF0\x9E\xB8\x82" => "\xD8\xAC", + "\xF0\x9E\xB8\x83" => "\xD8\xAF", + "\xF0\x9E\xB8\x85" => "\xD9\x88", + "\xF0\x9E\xB8\x86" => "\xD8\xB2", + "\xF0\x9E\xB8\x87" => "\xD8\xAD", + "\xF0\x9E\xB8\x88" => "\xD8\xB7", + "\xF0\x9E\xB8\x89" => "\xD9\x8A", + "\xF0\x9E\xB8\x8A" => "\xD9\x83", + "\xF0\x9E\xB8\x8B" => "\xD9\x84", + "\xF0\x9E\xB8\x8C" => "\xD9\x85", + "\xF0\x9E\xB8\x8D" => "\xD9\x86", + "\xF0\x9E\xB8\x8E" => "\xD8\xB3", + "\xF0\x9E\xB8\x8F" => "\xD8\xB9", + "\xF0\x9E\xB8\x90" => "\xD9\x81", + "\xF0\x9E\xB8\x91" => "\xD8\xB5", + "\xF0\x9E\xB8\x92" => "\xD9\x82", + "\xF0\x9E\xB8\x93" => "\xD8\xB1", + "\xF0\x9E\xB8\x94" => "\xD8\xB4", + "\xF0\x9E\xB8\x95" => "\xD8\xAA", + "\xF0\x9E\xB8\x96" => "\xD8\xAB", + "\xF0\x9E\xB8\x97" => "\xD8\xAE", + "\xF0\x9E\xB8\x98" => "\xD8\xB0", + "\xF0\x9E\xB8\x99" => "\xD8\xB6", + "\xF0\x9E\xB8\x9A" => "\xD8\xB8", + "\xF0\x9E\xB8\x9B" => "\xD8\xBA", + "\xF0\x9E\xB8\x9C" => "\xD9\xAE", + "\xF0\x9E\xB8\x9D" => "\xDA\xBA", + "\xF0\x9E\xB8\x9E" => "\xDA\xA1", + "\xF0\x9E\xB8\x9F" => "\xD9\xAF", + "\xF0\x9E\xB8\xA1" => "\xD8\xA8", + "\xF0\x9E\xB8\xA2" => "\xD8\xAC", + "\xF0\x9E\xB8\xA4" => "\xD9\x87", + "\xF0\x9E\xB8\xA7" => "\xD8\xAD", + "\xF0\x9E\xB8\xA9" => "\xD9\x8A", + "\xF0\x9E\xB8\xAA" => "\xD9\x83", + "\xF0\x9E\xB8\xAB" => "\xD9\x84", + "\xF0\x9E\xB8\xAC" => "\xD9\x85", + "\xF0\x9E\xB8\xAD" => "\xD9\x86", + "\xF0\x9E\xB8\xAE" => "\xD8\xB3", + "\xF0\x9E\xB8\xAF" => "\xD8\xB9", + "\xF0\x9E\xB8\xB0" => "\xD9\x81", + "\xF0\x9E\xB8\xB1" => "\xD8\xB5", + "\xF0\x9E\xB8\xB2" => "\xD9\x82", + "\xF0\x9E\xB8\xB4" => "\xD8\xB4", + "\xF0\x9E\xB8\xB5" => "\xD8\xAA", + "\xF0\x9E\xB8\xB6" => "\xD8\xAB", + "\xF0\x9E\xB8\xB7" => "\xD8\xAE", + "\xF0\x9E\xB8\xB9" => "\xD8\xB6", + "\xF0\x9E\xB8\xBB" => "\xD8\xBA", + "\xF0\x9E\xB9\x82" => "\xD8\xAC", + "\xF0\x9E\xB9\x87" => "\xD8\xAD", + "\xF0\x9E\xB9\x89" => "\xD9\x8A", + "\xF0\x9E\xB9\x8B" => "\xD9\x84", + "\xF0\x9E\xB9\x8D" => "\xD9\x86", + "\xF0\x9E\xB9\x8E" => "\xD8\xB3", + "\xF0\x9E\xB9\x8F" => "\xD8\xB9", + "\xF0\x9E\xB9\x91" => "\xD8\xB5", + "\xF0\x9E\xB9\x92" => "\xD9\x82", + "\xF0\x9E\xB9\x94" => "\xD8\xB4", + "\xF0\x9E\xB9\x97" => "\xD8\xAE", + "\xF0\x9E\xB9\x99" => "\xD8\xB6", + "\xF0\x9E\xB9\x9B" => "\xD8\xBA", + "\xF0\x9E\xB9\x9D" => "\xDA\xBA", + "\xF0\x9E\xB9\x9F" => "\xD9\xAF", + "\xF0\x9E\xB9\xA1" => "\xD8\xA8", + "\xF0\x9E\xB9\xA2" => "\xD8\xAC", + "\xF0\x9E\xB9\xA4" => "\xD9\x87", + "\xF0\x9E\xB9\xA7" => "\xD8\xAD", + "\xF0\x9E\xB9\xA8" => "\xD8\xB7", + "\xF0\x9E\xB9\xA9" => "\xD9\x8A", + "\xF0\x9E\xB9\xAA" => "\xD9\x83", + "\xF0\x9E\xB9\xAC" => "\xD9\x85", + "\xF0\x9E\xB9\xAD" => "\xD9\x86", + "\xF0\x9E\xB9\xAE" => "\xD8\xB3", + "\xF0\x9E\xB9\xAF" => "\xD8\xB9", + "\xF0\x9E\xB9\xB0" => "\xD9\x81", + "\xF0\x9E\xB9\xB1" => "\xD8\xB5", + "\xF0\x9E\xB9\xB2" => "\xD9\x82", + "\xF0\x9E\xB9\xB4" => "\xD8\xB4", + "\xF0\x9E\xB9\xB5" => "\xD8\xAA", + "\xF0\x9E\xB9\xB6" => "\xD8\xAB", + "\xF0\x9E\xB9\xB7" => "\xD8\xAE", + "\xF0\x9E\xB9\xB9" => "\xD8\xB6", + "\xF0\x9E\xB9\xBA" => "\xD8\xB8", + "\xF0\x9E\xB9\xBB" => "\xD8\xBA", + "\xF0\x9E\xB9\xBC" => "\xD9\xAE", + "\xF0\x9E\xB9\xBE" => "\xDA\xA1", + "\xF0\x9E\xBA\x80" => "\xD8\xA7", + "\xF0\x9E\xBA\x81" => "\xD8\xA8", + "\xF0\x9E\xBA\x82" => "\xD8\xAC", + "\xF0\x9E\xBA\x83" => "\xD8\xAF", + "\xF0\x9E\xBA\x84" => "\xD9\x87", + "\xF0\x9E\xBA\x85" => "\xD9\x88", + "\xF0\x9E\xBA\x86" => "\xD8\xB2", + "\xF0\x9E\xBA\x87" => "\xD8\xAD", + "\xF0\x9E\xBA\x88" => "\xD8\xB7", + "\xF0\x9E\xBA\x89" => "\xD9\x8A", + "\xF0\x9E\xBA\x8B" => "\xD9\x84", + "\xF0\x9E\xBA\x8C" => "\xD9\x85", + "\xF0\x9E\xBA\x8D" => "\xD9\x86", + "\xF0\x9E\xBA\x8E" => "\xD8\xB3", + "\xF0\x9E\xBA\x8F" => "\xD8\xB9", + "\xF0\x9E\xBA\x90" => "\xD9\x81", + "\xF0\x9E\xBA\x91" => "\xD8\xB5", + "\xF0\x9E\xBA\x92" => "\xD9\x82", + "\xF0\x9E\xBA\x93" => "\xD8\xB1", + "\xF0\x9E\xBA\x94" => "\xD8\xB4", + "\xF0\x9E\xBA\x95" => "\xD8\xAA", + "\xF0\x9E\xBA\x96" => "\xD8\xAB", + "\xF0\x9E\xBA\x97" => "\xD8\xAE", + "\xF0\x9E\xBA\x98" => "\xD8\xB0", + "\xF0\x9E\xBA\x99" => "\xD8\xB6", + "\xF0\x9E\xBA\x9A" => "\xD8\xB8", + "\xF0\x9E\xBA\x9B" => "\xD8\xBA", + "\xF0\x9E\xBA\xA1" => "\xD8\xA8", + "\xF0\x9E\xBA\xA2" => "\xD8\xAC", + "\xF0\x9E\xBA\xA3" => "\xD8\xAF", + "\xF0\x9E\xBA\xA5" => "\xD9\x88", + "\xF0\x9E\xBA\xA6" => "\xD8\xB2", + "\xF0\x9E\xBA\xA7" => "\xD8\xAD", + "\xF0\x9E\xBA\xA8" => "\xD8\xB7", + "\xF0\x9E\xBA\xA9" => "\xD9\x8A", + "\xF0\x9E\xBA\xAB" => "\xD9\x84", + "\xF0\x9E\xBA\xAC" => "\xD9\x85", + "\xF0\x9E\xBA\xAD" => "\xD9\x86", + "\xF0\x9E\xBA\xAE" => "\xD8\xB3", + "\xF0\x9E\xBA\xAF" => "\xD8\xB9", + "\xF0\x9E\xBA\xB0" => "\xD9\x81", + "\xF0\x9E\xBA\xB1" => "\xD8\xB5", + "\xF0\x9E\xBA\xB2" => "\xD9\x82", + "\xF0\x9E\xBA\xB3" => "\xD8\xB1", + "\xF0\x9E\xBA\xB4" => "\xD8\xB4", + "\xF0\x9E\xBA\xB5" => "\xD8\xAA", + "\xF0\x9E\xBA\xB6" => "\xD8\xAB", + "\xF0\x9E\xBA\xB7" => "\xD8\xAE", + "\xF0\x9E\xBA\xB8" => "\xD8\xB0", + "\xF0\x9E\xBA\xB9" => "\xD8\xB6", + "\xF0\x9E\xBA\xBA" => "\xD8\xB8", + "\xF0\x9E\xBA\xBB" => "\xD8\xBA", + "\xF0\x9F\x84\x80" => "\x30\x2E", + "\xF0\x9F\x84\x81" => "\x30\x2C", + "\xF0\x9F\x84\x82" => "\x31\x2C", + "\xF0\x9F\x84\x83" => "\x32\x2C", + "\xF0\x9F\x84\x84" => "\x33\x2C", + "\xF0\x9F\x84\x85" => "\x34\x2C", + "\xF0\x9F\x84\x86" => "\x35\x2C", + "\xF0\x9F\x84\x87" => "\x36\x2C", + "\xF0\x9F\x84\x88" => "\x37\x2C", + "\xF0\x9F\x84\x89" => "\x38\x2C", + "\xF0\x9F\x84\x8A" => "\x39\x2C", + "\xF0\x9F\x84\x90" => "\x28\x41\x29", + "\xF0\x9F\x84\x91" => "\x28\x42\x29", + "\xF0\x9F\x84\x92" => "\x28\x43\x29", + "\xF0\x9F\x84\x93" => "\x28\x44\x29", + "\xF0\x9F\x84\x94" => "\x28\x45\x29", + "\xF0\x9F\x84\x95" => "\x28\x46\x29", + "\xF0\x9F\x84\x96" => "\x28\x47\x29", + "\xF0\x9F\x84\x97" => "\x28\x48\x29", + "\xF0\x9F\x84\x98" => "\x28\x49\x29", + "\xF0\x9F\x84\x99" => "\x28\x4A\x29", + "\xF0\x9F\x84\x9A" => "\x28\x4B\x29", + "\xF0\x9F\x84\x9B" => "\x28\x4C\x29", + "\xF0\x9F\x84\x9C" => "\x28\x4D\x29", + "\xF0\x9F\x84\x9D" => "\x28\x4E\x29", + "\xF0\x9F\x84\x9E" => "\x28\x4F\x29", + "\xF0\x9F\x84\x9F" => "\x28\x50\x29", + "\xF0\x9F\x84\xA0" => "\x28\x51\x29", + "\xF0\x9F\x84\xA1" => "\x28\x52\x29", + "\xF0\x9F\x84\xA2" => "\x28\x53\x29", + "\xF0\x9F\x84\xA3" => "\x28\x54\x29", + "\xF0\x9F\x84\xA4" => "\x28\x55\x29", + "\xF0\x9F\x84\xA5" => "\x28\x56\x29", + "\xF0\x9F\x84\xA6" => "\x28\x57\x29", + "\xF0\x9F\x84\xA7" => "\x28\x58\x29", + "\xF0\x9F\x84\xA8" => "\x28\x59\x29", + "\xF0\x9F\x84\xA9" => "\x28\x5A\x29", + "\xF0\x9F\x84\xAA" => "\xE3\x80\x94\x53\xE3\x80\x95", + "\xF0\x9F\x84\xAB" => "\x43", + "\xF0\x9F\x84\xAC" => "\x52", + "\xF0\x9F\x84\xAD" => "\x43\x44", + "\xF0\x9F\x84\xAE" => "\x57\x5A", + "\xF0\x9F\x84\xB0" => "\x41", + "\xF0\x9F\x84\xB1" => "\x42", + "\xF0\x9F\x84\xB2" => "\x43", + "\xF0\x9F\x84\xB3" => "\x44", + "\xF0\x9F\x84\xB4" => "\x45", + "\xF0\x9F\x84\xB5" => "\x46", + "\xF0\x9F\x84\xB6" => "\x47", + "\xF0\x9F\x84\xB7" => "\x48", + "\xF0\x9F\x84\xB8" => "\x49", + "\xF0\x9F\x84\xB9" => "\x4A", + "\xF0\x9F\x84\xBA" => "\x4B", + "\xF0\x9F\x84\xBB" => "\x4C", + "\xF0\x9F\x84\xBC" => "\x4D", + "\xF0\x9F\x84\xBD" => "\x4E", + "\xF0\x9F\x84\xBE" => "\x4F", + "\xF0\x9F\x84\xBF" => "\x50", + "\xF0\x9F\x85\x80" => "\x51", + "\xF0\x9F\x85\x81" => "\x52", + "\xF0\x9F\x85\x82" => "\x53", + "\xF0\x9F\x85\x83" => "\x54", + "\xF0\x9F\x85\x84" => "\x55", + "\xF0\x9F\x85\x85" => "\x56", + "\xF0\x9F\x85\x86" => "\x57", + "\xF0\x9F\x85\x87" => "\x58", + "\xF0\x9F\x85\x88" => "\x59", + "\xF0\x9F\x85\x89" => "\x5A", + "\xF0\x9F\x85\x8A" => "\x48\x56", + "\xF0\x9F\x85\x8B" => "\x4D\x56", + "\xF0\x9F\x85\x8C" => "\x53\x44", + "\xF0\x9F\x85\x8D" => "\x53\x53", + "\xF0\x9F\x85\x8E" => "\x50\x50\x56", + "\xF0\x9F\x85\x8F" => "\x57\x43", + "\xF0\x9F\x85\xAA" => "\x4D\x43", + "\xF0\x9F\x85\xAB" => "\x4D\x44", + "\xF0\x9F\x85\xAC" => "\x4D\x52", + "\xF0\x9F\x86\x90" => "\x44\x4A", + "\xF0\x9F\x88\x80" => "\xE3\x81\xBB\xE3\x81\x8B", + "\xF0\x9F\x88\x81" => "\xE3\x82\xB3\xE3\x82\xB3", + "\xF0\x9F\x88\x82" => "\xE3\x82\xB5", + "\xF0\x9F\x88\x90" => "\xE6\x89\x8B", + "\xF0\x9F\x88\x91" => "\xE5\xAD\x97", + "\xF0\x9F\x88\x92" => "\xE5\x8F\x8C", + "\xF0\x9F\x88\x93" => "\xE3\x83\x86\xE3\x82\x99", + "\xF0\x9F\x88\x94" => "\xE4\xBA\x8C", + "\xF0\x9F\x88\x95" => "\xE5\xA4\x9A", + "\xF0\x9F\x88\x96" => "\xE8\xA7\xA3", + "\xF0\x9F\x88\x97" => "\xE5\xA4\xA9", + "\xF0\x9F\x88\x98" => "\xE4\xBA\xA4", + "\xF0\x9F\x88\x99" => "\xE6\x98\xA0", + "\xF0\x9F\x88\x9A" => "\xE7\x84\xA1", + "\xF0\x9F\x88\x9B" => "\xE6\x96\x99", + "\xF0\x9F\x88\x9C" => "\xE5\x89\x8D", + "\xF0\x9F\x88\x9D" => "\xE5\xBE\x8C", + "\xF0\x9F\x88\x9E" => "\xE5\x86\x8D", + "\xF0\x9F\x88\x9F" => "\xE6\x96\xB0", + "\xF0\x9F\x88\xA0" => "\xE5\x88\x9D", + "\xF0\x9F\x88\xA1" => "\xE7\xB5\x82", + "\xF0\x9F\x88\xA2" => "\xE7\x94\x9F", + "\xF0\x9F\x88\xA3" => "\xE8\xB2\xA9", + "\xF0\x9F\x88\xA4" => "\xE5\xA3\xB0", + "\xF0\x9F\x88\xA5" => "\xE5\x90\xB9", + "\xF0\x9F\x88\xA6" => "\xE6\xBC\x94", + "\xF0\x9F\x88\xA7" => "\xE6\x8A\x95", + "\xF0\x9F\x88\xA8" => "\xE6\x8D\x95", + "\xF0\x9F\x88\xA9" => "\xE4\xB8\x80", + "\xF0\x9F\x88\xAA" => "\xE4\xB8\x89", + "\xF0\x9F\x88\xAB" => "\xE9\x81\x8A", + "\xF0\x9F\x88\xAC" => "\xE5\xB7\xA6", + "\xF0\x9F\x88\xAD" => "\xE4\xB8\xAD", + "\xF0\x9F\x88\xAE" => "\xE5\x8F\xB3", + "\xF0\x9F\x88\xAF" => "\xE6\x8C\x87", + "\xF0\x9F\x88\xB0" => "\xE8\xB5\xB0", + "\xF0\x9F\x88\xB1" => "\xE6\x89\x93", + "\xF0\x9F\x88\xB2" => "\xE7\xA6\x81", + "\xF0\x9F\x88\xB3" => "\xE7\xA9\xBA", + "\xF0\x9F\x88\xB4" => "\xE5\x90\x88", + "\xF0\x9F\x88\xB5" => "\xE6\xBA\x80", + "\xF0\x9F\x88\xB6" => "\xE6\x9C\x89", + "\xF0\x9F\x88\xB7" => "\xE6\x9C\x88", + "\xF0\x9F\x88\xB8" => "\xE7\x94\xB3", + "\xF0\x9F\x88\xB9" => "\xE5\x89\xB2", + "\xF0\x9F\x88\xBA" => "\xE5\x96\xB6", + "\xF0\x9F\x88\xBB" => "\xE9\x85\x8D", + "\xF0\x9F\x89\x80" => "\xE3\x80\x94\xE6\x9C\xAC\xE3\x80\x95", + "\xF0\x9F\x89\x81" => "\xE3\x80\x94\xE4\xB8\x89\xE3\x80\x95", + "\xF0\x9F\x89\x82" => "\xE3\x80\x94\xE4\xBA\x8C\xE3\x80\x95", + "\xF0\x9F\x89\x83" => "\xE3\x80\x94\xE5\xAE\x89\xE3\x80\x95", + "\xF0\x9F\x89\x84" => "\xE3\x80\x94\xE7\x82\xB9\xE3\x80\x95", + "\xF0\x9F\x89\x85" => "\xE3\x80\x94\xE6\x89\x93\xE3\x80\x95", + "\xF0\x9F\x89\x86" => "\xE3\x80\x94\xE7\x9B\x97\xE3\x80\x95", + "\xF0\x9F\x89\x87" => "\xE3\x80\x94\xE5\x8B\x9D\xE3\x80\x95", + "\xF0\x9F\x89\x88" => "\xE3\x80\x94\xE6\x95\x97\xE3\x80\x95", + "\xF0\x9F\x89\x90" => "\xE5\xBE\x97", + "\xF0\x9F\x89\x91" => "\xE5\x8F\xAF", + "\xF0\x9F\xAF\xB0" => "\x30", + "\xF0\x9F\xAF\xB1" => "\x31", + "\xF0\x9F\xAF\xB2" => "\x32", + "\xF0\x9F\xAF\xB3" => "\x33", + "\xF0\x9F\xAF\xB4" => "\x34", + "\xF0\x9F\xAF\xB5" => "\x35", + "\xF0\x9F\xAF\xB6" => "\x36", + "\xF0\x9F\xAF\xB7" => "\x37", + "\xF0\x9F\xAF\xB8" => "\x38", + "\xF0\x9F\xAF\xB9" => "\x39", + ); +} + +?> \ No newline at end of file diff --git a/Sources/Unicode/DefaultIgnorables.php b/Sources/Unicode/DefaultIgnorables.php new file mode 100644 index 0000000..1597338 --- /dev/null +++ b/Sources/Unicode/DefaultIgnorables.php @@ -0,0 +1,4205 @@ + \ No newline at end of file diff --git a/Sources/Unicode/Idna.php b/Sources/Unicode/Idna.php new file mode 100644 index 0000000..24c9eb8 --- /dev/null +++ b/Sources/Unicode/Idna.php @@ -0,0 +1,7415 @@ + "\x61", + "\x42" => "\x62", + "\x43" => "\x63", + "\x44" => "\x64", + "\x45" => "\x65", + "\x46" => "\x66", + "\x47" => "\x67", + "\x48" => "\x68", + "\x49" => "\x69", + "\x4A" => "\x6A", + "\x4B" => "\x6B", + "\x4C" => "\x6C", + "\x4D" => "\x6D", + "\x4E" => "\x6E", + "\x4F" => "\x6F", + "\x50" => "\x70", + "\x51" => "\x71", + "\x52" => "\x72", + "\x53" => "\x73", + "\x54" => "\x74", + "\x55" => "\x75", + "\x56" => "\x76", + "\x57" => "\x77", + "\x58" => "\x78", + "\x59" => "\x79", + "\x5A" => "\x7A", + "\xC2\xAA" => "\x61", + "\xC2\xB2" => "\x32", + "\xC2\xB3" => "\x33", + "\xC2\xB5" => "\xCE\xBC", + "\xC2\xB9" => "\x31", + "\xC2\xBA" => "\x6F", + "\xC2\xBC" => "\x31\xE2\x81\x84\x34", + "\xC2\xBD" => "\x31\xE2\x81\x84\x32", + "\xC2\xBE" => "\x33\xE2\x81\x84\x34", + "\xC3\x80" => "\xC3\xA0", + "\xC3\x81" => "\xC3\xA1", + "\xC3\x82" => "\xC3\xA2", + "\xC3\x83" => "\xC3\xA3", + "\xC3\x84" => "\xC3\xA4", + "\xC3\x85" => "\xC3\xA5", + "\xC3\x86" => "\xC3\xA6", + "\xC3\x87" => "\xC3\xA7", + "\xC3\x88" => "\xC3\xA8", + "\xC3\x89" => "\xC3\xA9", + "\xC3\x8A" => "\xC3\xAA", + "\xC3\x8B" => "\xC3\xAB", + "\xC3\x8C" => "\xC3\xAC", + "\xC3\x8D" => "\xC3\xAD", + "\xC3\x8E" => "\xC3\xAE", + "\xC3\x8F" => "\xC3\xAF", + "\xC3\x90" => "\xC3\xB0", + "\xC3\x91" => "\xC3\xB1", + "\xC3\x92" => "\xC3\xB2", + "\xC3\x93" => "\xC3\xB3", + "\xC3\x94" => "\xC3\xB4", + "\xC3\x95" => "\xC3\xB5", + "\xC3\x96" => "\xC3\xB6", + "\xC3\x98" => "\xC3\xB8", + "\xC3\x99" => "\xC3\xB9", + "\xC3\x9A" => "\xC3\xBA", + "\xC3\x9B" => "\xC3\xBB", + "\xC3\x9C" => "\xC3\xBC", + "\xC3\x9D" => "\xC3\xBD", + "\xC3\x9E" => "\xC3\xBE", + "\xC4\x80" => "\xC4\x81", + "\xC4\x82" => "\xC4\x83", + "\xC4\x84" => "\xC4\x85", + "\xC4\x86" => "\xC4\x87", + "\xC4\x88" => "\xC4\x89", + "\xC4\x8A" => "\xC4\x8B", + "\xC4\x8C" => "\xC4\x8D", + "\xC4\x8E" => "\xC4\x8F", + "\xC4\x90" => "\xC4\x91", + "\xC4\x92" => "\xC4\x93", + "\xC4\x94" => "\xC4\x95", + "\xC4\x96" => "\xC4\x97", + "\xC4\x98" => "\xC4\x99", + "\xC4\x9A" => "\xC4\x9B", + "\xC4\x9C" => "\xC4\x9D", + "\xC4\x9E" => "\xC4\x9F", + "\xC4\xA0" => "\xC4\xA1", + "\xC4\xA2" => "\xC4\xA3", + "\xC4\xA4" => "\xC4\xA5", + "\xC4\xA6" => "\xC4\xA7", + "\xC4\xA8" => "\xC4\xA9", + "\xC4\xAA" => "\xC4\xAB", + "\xC4\xAC" => "\xC4\xAD", + "\xC4\xAE" => "\xC4\xAF", + "\xC4\xB0" => "\x69\xCC\x87", + "\xC4\xB2" => "\x69\x6A", + "\xC4\xB3" => "\x69\x6A", + "\xC4\xB4" => "\xC4\xB5", + "\xC4\xB6" => "\xC4\xB7", + "\xC4\xB9" => "\xC4\xBA", + "\xC4\xBB" => "\xC4\xBC", + "\xC4\xBD" => "\xC4\xBE", + "\xC4\xBF" => "\x6C\xC2\xB7", + "\xC5\x80" => "\x6C\xC2\xB7", + "\xC5\x81" => "\xC5\x82", + "\xC5\x83" => "\xC5\x84", + "\xC5\x85" => "\xC5\x86", + "\xC5\x87" => "\xC5\x88", + "\xC5\x89" => "\xCA\xBC\x6E", + "\xC5\x8A" => "\xC5\x8B", + "\xC5\x8C" => "\xC5\x8D", + "\xC5\x8E" => "\xC5\x8F", + "\xC5\x90" => "\xC5\x91", + "\xC5\x92" => "\xC5\x93", + "\xC5\x94" => "\xC5\x95", + "\xC5\x96" => "\xC5\x97", + "\xC5\x98" => "\xC5\x99", + "\xC5\x9A" => "\xC5\x9B", + "\xC5\x9C" => "\xC5\x9D", + "\xC5\x9E" => "\xC5\x9F", + "\xC5\xA0" => "\xC5\xA1", + "\xC5\xA2" => "\xC5\xA3", + "\xC5\xA4" => "\xC5\xA5", + "\xC5\xA6" => "\xC5\xA7", + "\xC5\xA8" => "\xC5\xA9", + "\xC5\xAA" => "\xC5\xAB", + "\xC5\xAC" => "\xC5\xAD", + "\xC5\xAE" => "\xC5\xAF", + "\xC5\xB0" => "\xC5\xB1", + "\xC5\xB2" => "\xC5\xB3", + "\xC5\xB4" => "\xC5\xB5", + "\xC5\xB6" => "\xC5\xB7", + "\xC5\xB8" => "\xC3\xBF", + "\xC5\xB9" => "\xC5\xBA", + "\xC5\xBB" => "\xC5\xBC", + "\xC5\xBD" => "\xC5\xBE", + "\xC5\xBF" => "\x73", + "\xC6\x81" => "\xC9\x93", + "\xC6\x82" => "\xC6\x83", + "\xC6\x84" => "\xC6\x85", + "\xC6\x86" => "\xC9\x94", + "\xC6\x87" => "\xC6\x88", + "\xC6\x89" => "\xC9\x96", + "\xC6\x8A" => "\xC9\x97", + "\xC6\x8B" => "\xC6\x8C", + "\xC6\x8E" => "\xC7\x9D", + "\xC6\x8F" => "\xC9\x99", + "\xC6\x90" => "\xC9\x9B", + "\xC6\x91" => "\xC6\x92", + "\xC6\x93" => "\xC9\xA0", + "\xC6\x94" => "\xC9\xA3", + "\xC6\x96" => "\xC9\xA9", + "\xC6\x97" => "\xC9\xA8", + "\xC6\x98" => "\xC6\x99", + "\xC6\x9C" => "\xC9\xAF", + "\xC6\x9D" => "\xC9\xB2", + "\xC6\x9F" => "\xC9\xB5", + "\xC6\xA0" => "\xC6\xA1", + "\xC6\xA2" => "\xC6\xA3", + "\xC6\xA4" => "\xC6\xA5", + "\xC6\xA6" => "\xCA\x80", + "\xC6\xA7" => "\xC6\xA8", + "\xC6\xA9" => "\xCA\x83", + "\xC6\xAC" => "\xC6\xAD", + "\xC6\xAE" => "\xCA\x88", + "\xC6\xAF" => "\xC6\xB0", + "\xC6\xB1" => "\xCA\x8A", + "\xC6\xB2" => "\xCA\x8B", + "\xC6\xB3" => "\xC6\xB4", + "\xC6\xB5" => "\xC6\xB6", + "\xC6\xB7" => "\xCA\x92", + "\xC6\xB8" => "\xC6\xB9", + "\xC6\xBC" => "\xC6\xBD", + "\xC7\x84" => "\x64\xC5\xBE", + "\xC7\x85" => "\x64\xC5\xBE", + "\xC7\x86" => "\x64\xC5\xBE", + "\xC7\x87" => "\x6C\x6A", + "\xC7\x88" => "\x6C\x6A", + "\xC7\x89" => "\x6C\x6A", + "\xC7\x8A" => "\x6E\x6A", + "\xC7\x8B" => "\x6E\x6A", + "\xC7\x8C" => "\x6E\x6A", + "\xC7\x8D" => "\xC7\x8E", + "\xC7\x8F" => "\xC7\x90", + "\xC7\x91" => "\xC7\x92", + "\xC7\x93" => "\xC7\x94", + "\xC7\x95" => "\xC7\x96", + "\xC7\x97" => "\xC7\x98", + "\xC7\x99" => "\xC7\x9A", + "\xC7\x9B" => "\xC7\x9C", + "\xC7\x9E" => "\xC7\x9F", + "\xC7\xA0" => "\xC7\xA1", + "\xC7\xA2" => "\xC7\xA3", + "\xC7\xA4" => "\xC7\xA5", + "\xC7\xA6" => "\xC7\xA7", + "\xC7\xA8" => "\xC7\xA9", + "\xC7\xAA" => "\xC7\xAB", + "\xC7\xAC" => "\xC7\xAD", + "\xC7\xAE" => "\xC7\xAF", + "\xC7\xB1" => "\x64\x7A", + "\xC7\xB2" => "\x64\x7A", + "\xC7\xB3" => "\x64\x7A", + "\xC7\xB4" => "\xC7\xB5", + "\xC7\xB6" => "\xC6\x95", + "\xC7\xB7" => "\xC6\xBF", + "\xC7\xB8" => "\xC7\xB9", + "\xC7\xBA" => "\xC7\xBB", + "\xC7\xBC" => "\xC7\xBD", + "\xC7\xBE" => "\xC7\xBF", + "\xC8\x80" => "\xC8\x81", + "\xC8\x82" => "\xC8\x83", + "\xC8\x84" => "\xC8\x85", + "\xC8\x86" => "\xC8\x87", + "\xC8\x88" => "\xC8\x89", + "\xC8\x8A" => "\xC8\x8B", + "\xC8\x8C" => "\xC8\x8D", + "\xC8\x8E" => "\xC8\x8F", + "\xC8\x90" => "\xC8\x91", + "\xC8\x92" => "\xC8\x93", + "\xC8\x94" => "\xC8\x95", + "\xC8\x96" => "\xC8\x97", + "\xC8\x98" => "\xC8\x99", + "\xC8\x9A" => "\xC8\x9B", + "\xC8\x9C" => "\xC8\x9D", + "\xC8\x9E" => "\xC8\x9F", + "\xC8\xA0" => "\xC6\x9E", + "\xC8\xA2" => "\xC8\xA3", + "\xC8\xA4" => "\xC8\xA5", + "\xC8\xA6" => "\xC8\xA7", + "\xC8\xA8" => "\xC8\xA9", + "\xC8\xAA" => "\xC8\xAB", + "\xC8\xAC" => "\xC8\xAD", + "\xC8\xAE" => "\xC8\xAF", + "\xC8\xB0" => "\xC8\xB1", + "\xC8\xB2" => "\xC8\xB3", + "\xC8\xBA" => "\xE2\xB1\xA5", + "\xC8\xBB" => "\xC8\xBC", + "\xC8\xBD" => "\xC6\x9A", + "\xC8\xBE" => "\xE2\xB1\xA6", + "\xC9\x81" => "\xC9\x82", + "\xC9\x83" => "\xC6\x80", + "\xC9\x84" => "\xCA\x89", + "\xC9\x85" => "\xCA\x8C", + "\xC9\x86" => "\xC9\x87", + "\xC9\x88" => "\xC9\x89", + "\xC9\x8A" => "\xC9\x8B", + "\xC9\x8C" => "\xC9\x8D", + "\xC9\x8E" => "\xC9\x8F", + "\xCA\xB0" => "\x68", + "\xCA\xB1" => "\xC9\xA6", + "\xCA\xB2" => "\x6A", + "\xCA\xB3" => "\x72", + "\xCA\xB4" => "\xC9\xB9", + "\xCA\xB5" => "\xC9\xBB", + "\xCA\xB6" => "\xCA\x81", + "\xCA\xB7" => "\x77", + "\xCA\xB8" => "\x79", + "\xCB\xA0" => "\xC9\xA3", + "\xCB\xA1" => "\x6C", + "\xCB\xA2" => "\x73", + "\xCB\xA3" => "\x78", + "\xCB\xA4" => "\xCA\x95", + "\xCD\x80" => "\xCC\x80", + "\xCD\x81" => "\xCC\x81", + "\xCD\x83" => "\xCC\x93", + "\xCD\x84" => "\xCC\x88\xCC\x81", + "\xCD\x85" => "\xCE\xB9", + "\xCD\xB0" => "\xCD\xB1", + "\xCD\xB2" => "\xCD\xB3", + "\xCD\xB4" => "\xCA\xB9", + "\xCD\xB6" => "\xCD\xB7", + "\xCD\xBF" => "\xCF\xB3", + "\xCE\x86" => "\xCE\xAC", + "\xCE\x87" => "\xC2\xB7", + "\xCE\x88" => "\xCE\xAD", + "\xCE\x89" => "\xCE\xAE", + "\xCE\x8A" => "\xCE\xAF", + "\xCE\x8C" => "\xCF\x8C", + "\xCE\x8E" => "\xCF\x8D", + "\xCE\x8F" => "\xCF\x8E", + "\xCE\x91" => "\xCE\xB1", + "\xCE\x92" => "\xCE\xB2", + "\xCE\x93" => "\xCE\xB3", + "\xCE\x94" => "\xCE\xB4", + "\xCE\x95" => "\xCE\xB5", + "\xCE\x96" => "\xCE\xB6", + "\xCE\x97" => "\xCE\xB7", + "\xCE\x98" => "\xCE\xB8", + "\xCE\x99" => "\xCE\xB9", + "\xCE\x9A" => "\xCE\xBA", + "\xCE\x9B" => "\xCE\xBB", + "\xCE\x9C" => "\xCE\xBC", + "\xCE\x9D" => "\xCE\xBD", + "\xCE\x9E" => "\xCE\xBE", + "\xCE\x9F" => "\xCE\xBF", + "\xCE\xA0" => "\xCF\x80", + "\xCE\xA1" => "\xCF\x81", + "\xCE\xA3" => "\xCF\x83", + "\xCE\xA4" => "\xCF\x84", + "\xCE\xA5" => "\xCF\x85", + "\xCE\xA6" => "\xCF\x86", + "\xCE\xA7" => "\xCF\x87", + "\xCE\xA8" => "\xCF\x88", + "\xCE\xA9" => "\xCF\x89", + "\xCE\xAA" => "\xCF\x8A", + "\xCE\xAB" => "\xCF\x8B", + "\xCF\x8F" => "\xCF\x97", + "\xCF\x90" => "\xCE\xB2", + "\xCF\x91" => "\xCE\xB8", + "\xCF\x92" => "\xCF\x85", + "\xCF\x93" => "\xCF\x8D", + "\xCF\x94" => "\xCF\x8B", + "\xCF\x95" => "\xCF\x86", + "\xCF\x96" => "\xCF\x80", + "\xCF\x98" => "\xCF\x99", + "\xCF\x9A" => "\xCF\x9B", + "\xCF\x9C" => "\xCF\x9D", + "\xCF\x9E" => "\xCF\x9F", + "\xCF\xA0" => "\xCF\xA1", + "\xCF\xA2" => "\xCF\xA3", + "\xCF\xA4" => "\xCF\xA5", + "\xCF\xA6" => "\xCF\xA7", + "\xCF\xA8" => "\xCF\xA9", + "\xCF\xAA" => "\xCF\xAB", + "\xCF\xAC" => "\xCF\xAD", + "\xCF\xAE" => "\xCF\xAF", + "\xCF\xB0" => "\xCE\xBA", + "\xCF\xB1" => "\xCF\x81", + "\xCF\xB2" => "\xCF\x83", + "\xCF\xB4" => "\xCE\xB8", + "\xCF\xB5" => "\xCE\xB5", + "\xCF\xB7" => "\xCF\xB8", + "\xCF\xB9" => "\xCF\x83", + "\xCF\xBA" => "\xCF\xBB", + "\xCF\xBD" => "\xCD\xBB", + "\xCF\xBE" => "\xCD\xBC", + "\xCF\xBF" => "\xCD\xBD", + "\xD0\x80" => "\xD1\x90", + "\xD0\x81" => "\xD1\x91", + "\xD0\x82" => "\xD1\x92", + "\xD0\x83" => "\xD1\x93", + "\xD0\x84" => "\xD1\x94", + "\xD0\x85" => "\xD1\x95", + "\xD0\x86" => "\xD1\x96", + "\xD0\x87" => "\xD1\x97", + "\xD0\x88" => "\xD1\x98", + "\xD0\x89" => "\xD1\x99", + "\xD0\x8A" => "\xD1\x9A", + "\xD0\x8B" => "\xD1\x9B", + "\xD0\x8C" => "\xD1\x9C", + "\xD0\x8D" => "\xD1\x9D", + "\xD0\x8E" => "\xD1\x9E", + "\xD0\x8F" => "\xD1\x9F", + "\xD0\x90" => "\xD0\xB0", + "\xD0\x91" => "\xD0\xB1", + "\xD0\x92" => "\xD0\xB2", + "\xD0\x93" => "\xD0\xB3", + "\xD0\x94" => "\xD0\xB4", + "\xD0\x95" => "\xD0\xB5", + "\xD0\x96" => "\xD0\xB6", + "\xD0\x97" => "\xD0\xB7", + "\xD0\x98" => "\xD0\xB8", + "\xD0\x99" => "\xD0\xB9", + "\xD0\x9A" => "\xD0\xBA", + "\xD0\x9B" => "\xD0\xBB", + "\xD0\x9C" => "\xD0\xBC", + "\xD0\x9D" => "\xD0\xBD", + "\xD0\x9E" => "\xD0\xBE", + "\xD0\x9F" => "\xD0\xBF", + "\xD0\xA0" => "\xD1\x80", + "\xD0\xA1" => "\xD1\x81", + "\xD0\xA2" => "\xD1\x82", + "\xD0\xA3" => "\xD1\x83", + "\xD0\xA4" => "\xD1\x84", + "\xD0\xA5" => "\xD1\x85", + "\xD0\xA6" => "\xD1\x86", + "\xD0\xA7" => "\xD1\x87", + "\xD0\xA8" => "\xD1\x88", + "\xD0\xA9" => "\xD1\x89", + "\xD0\xAA" => "\xD1\x8A", + "\xD0\xAB" => "\xD1\x8B", + "\xD0\xAC" => "\xD1\x8C", + "\xD0\xAD" => "\xD1\x8D", + "\xD0\xAE" => "\xD1\x8E", + "\xD0\xAF" => "\xD1\x8F", + "\xD1\xA0" => "\xD1\xA1", + "\xD1\xA2" => "\xD1\xA3", + "\xD1\xA4" => "\xD1\xA5", + "\xD1\xA6" => "\xD1\xA7", + "\xD1\xA8" => "\xD1\xA9", + "\xD1\xAA" => "\xD1\xAB", + "\xD1\xAC" => "\xD1\xAD", + "\xD1\xAE" => "\xD1\xAF", + "\xD1\xB0" => "\xD1\xB1", + "\xD1\xB2" => "\xD1\xB3", + "\xD1\xB4" => "\xD1\xB5", + "\xD1\xB6" => "\xD1\xB7", + "\xD1\xB8" => "\xD1\xB9", + "\xD1\xBA" => "\xD1\xBB", + "\xD1\xBC" => "\xD1\xBD", + "\xD1\xBE" => "\xD1\xBF", + "\xD2\x80" => "\xD2\x81", + "\xD2\x8A" => "\xD2\x8B", + "\xD2\x8C" => "\xD2\x8D", + "\xD2\x8E" => "\xD2\x8F", + "\xD2\x90" => "\xD2\x91", + "\xD2\x92" => "\xD2\x93", + "\xD2\x94" => "\xD2\x95", + "\xD2\x96" => "\xD2\x97", + "\xD2\x98" => "\xD2\x99", + "\xD2\x9A" => "\xD2\x9B", + "\xD2\x9C" => "\xD2\x9D", + "\xD2\x9E" => "\xD2\x9F", + "\xD2\xA0" => "\xD2\xA1", + "\xD2\xA2" => "\xD2\xA3", + "\xD2\xA4" => "\xD2\xA5", + "\xD2\xA6" => "\xD2\xA7", + "\xD2\xA8" => "\xD2\xA9", + "\xD2\xAA" => "\xD2\xAB", + "\xD2\xAC" => "\xD2\xAD", + "\xD2\xAE" => "\xD2\xAF", + "\xD2\xB0" => "\xD2\xB1", + "\xD2\xB2" => "\xD2\xB3", + "\xD2\xB4" => "\xD2\xB5", + "\xD2\xB6" => "\xD2\xB7", + "\xD2\xB8" => "\xD2\xB9", + "\xD2\xBA" => "\xD2\xBB", + "\xD2\xBC" => "\xD2\xBD", + "\xD2\xBE" => "\xD2\xBF", + "\xD3\x81" => "\xD3\x82", + "\xD3\x83" => "\xD3\x84", + "\xD3\x85" => "\xD3\x86", + "\xD3\x87" => "\xD3\x88", + "\xD3\x89" => "\xD3\x8A", + "\xD3\x8B" => "\xD3\x8C", + "\xD3\x8D" => "\xD3\x8E", + "\xD3\x90" => "\xD3\x91", + "\xD3\x92" => "\xD3\x93", + "\xD3\x94" => "\xD3\x95", + "\xD3\x96" => "\xD3\x97", + "\xD3\x98" => "\xD3\x99", + "\xD3\x9A" => "\xD3\x9B", + "\xD3\x9C" => "\xD3\x9D", + "\xD3\x9E" => "\xD3\x9F", + "\xD3\xA0" => "\xD3\xA1", + "\xD3\xA2" => "\xD3\xA3", + "\xD3\xA4" => "\xD3\xA5", + "\xD3\xA6" => "\xD3\xA7", + "\xD3\xA8" => "\xD3\xA9", + "\xD3\xAA" => "\xD3\xAB", + "\xD3\xAC" => "\xD3\xAD", + "\xD3\xAE" => "\xD3\xAF", + "\xD3\xB0" => "\xD3\xB1", + "\xD3\xB2" => "\xD3\xB3", + "\xD3\xB4" => "\xD3\xB5", + "\xD3\xB6" => "\xD3\xB7", + "\xD3\xB8" => "\xD3\xB9", + "\xD3\xBA" => "\xD3\xBB", + "\xD3\xBC" => "\xD3\xBD", + "\xD3\xBE" => "\xD3\xBF", + "\xD4\x80" => "\xD4\x81", + "\xD4\x82" => "\xD4\x83", + "\xD4\x84" => "\xD4\x85", + "\xD4\x86" => "\xD4\x87", + "\xD4\x88" => "\xD4\x89", + "\xD4\x8A" => "\xD4\x8B", + "\xD4\x8C" => "\xD4\x8D", + "\xD4\x8E" => "\xD4\x8F", + "\xD4\x90" => "\xD4\x91", + "\xD4\x92" => "\xD4\x93", + "\xD4\x94" => "\xD4\x95", + "\xD4\x96" => "\xD4\x97", + "\xD4\x98" => "\xD4\x99", + "\xD4\x9A" => "\xD4\x9B", + "\xD4\x9C" => "\xD4\x9D", + "\xD4\x9E" => "\xD4\x9F", + "\xD4\xA0" => "\xD4\xA1", + "\xD4\xA2" => "\xD4\xA3", + "\xD4\xA4" => "\xD4\xA5", + "\xD4\xA6" => "\xD4\xA7", + "\xD4\xA8" => "\xD4\xA9", + "\xD4\xAA" => "\xD4\xAB", + "\xD4\xAC" => "\xD4\xAD", + "\xD4\xAE" => "\xD4\xAF", + "\xD4\xB1" => "\xD5\xA1", + "\xD4\xB2" => "\xD5\xA2", + "\xD4\xB3" => "\xD5\xA3", + "\xD4\xB4" => "\xD5\xA4", + "\xD4\xB5" => "\xD5\xA5", + "\xD4\xB6" => "\xD5\xA6", + "\xD4\xB7" => "\xD5\xA7", + "\xD4\xB8" => "\xD5\xA8", + "\xD4\xB9" => "\xD5\xA9", + "\xD4\xBA" => "\xD5\xAA", + "\xD4\xBB" => "\xD5\xAB", + "\xD4\xBC" => "\xD5\xAC", + "\xD4\xBD" => "\xD5\xAD", + "\xD4\xBE" => "\xD5\xAE", + "\xD4\xBF" => "\xD5\xAF", + "\xD5\x80" => "\xD5\xB0", + "\xD5\x81" => "\xD5\xB1", + "\xD5\x82" => "\xD5\xB2", + "\xD5\x83" => "\xD5\xB3", + "\xD5\x84" => "\xD5\xB4", + "\xD5\x85" => "\xD5\xB5", + "\xD5\x86" => "\xD5\xB6", + "\xD5\x87" => "\xD5\xB7", + "\xD5\x88" => "\xD5\xB8", + "\xD5\x89" => "\xD5\xB9", + "\xD5\x8A" => "\xD5\xBA", + "\xD5\x8B" => "\xD5\xBB", + "\xD5\x8C" => "\xD5\xBC", + "\xD5\x8D" => "\xD5\xBD", + "\xD5\x8E" => "\xD5\xBE", + "\xD5\x8F" => "\xD5\xBF", + "\xD5\x90" => "\xD6\x80", + "\xD5\x91" => "\xD6\x81", + "\xD5\x92" => "\xD6\x82", + "\xD5\x93" => "\xD6\x83", + "\xD5\x94" => "\xD6\x84", + "\xD5\x95" => "\xD6\x85", + "\xD5\x96" => "\xD6\x86", + "\xD6\x87" => "\xD5\xA5\xD6\x82", + "\xD9\xB5" => "\xD8\xA7\xD9\xB4", + "\xD9\xB6" => "\xD9\x88\xD9\xB4", + "\xD9\xB7" => "\xDB\x87\xD9\xB4", + "\xD9\xB8" => "\xD9\x8A\xD9\xB4", + "\xE0\xA5\x98" => "\xE0\xA4\x95\xE0\xA4\xBC", + "\xE0\xA5\x99" => "\xE0\xA4\x96\xE0\xA4\xBC", + "\xE0\xA5\x9A" => "\xE0\xA4\x97\xE0\xA4\xBC", + "\xE0\xA5\x9B" => "\xE0\xA4\x9C\xE0\xA4\xBC", + "\xE0\xA5\x9C" => "\xE0\xA4\xA1\xE0\xA4\xBC", + "\xE0\xA5\x9D" => "\xE0\xA4\xA2\xE0\xA4\xBC", + "\xE0\xA5\x9E" => "\xE0\xA4\xAB\xE0\xA4\xBC", + "\xE0\xA5\x9F" => "\xE0\xA4\xAF\xE0\xA4\xBC", + "\xE0\xA7\x9C" => "\xE0\xA6\xA1\xE0\xA6\xBC", + "\xE0\xA7\x9D" => "\xE0\xA6\xA2\xE0\xA6\xBC", + "\xE0\xA7\x9F" => "\xE0\xA6\xAF\xE0\xA6\xBC", + "\xE0\xA8\xB3" => "\xE0\xA8\xB2\xE0\xA8\xBC", + "\xE0\xA8\xB6" => "\xE0\xA8\xB8\xE0\xA8\xBC", + "\xE0\xA9\x99" => "\xE0\xA8\x96\xE0\xA8\xBC", + "\xE0\xA9\x9A" => "\xE0\xA8\x97\xE0\xA8\xBC", + "\xE0\xA9\x9B" => "\xE0\xA8\x9C\xE0\xA8\xBC", + "\xE0\xA9\x9E" => "\xE0\xA8\xAB\xE0\xA8\xBC", + "\xE0\xAD\x9C" => "\xE0\xAC\xA1\xE0\xAC\xBC", + "\xE0\xAD\x9D" => "\xE0\xAC\xA2\xE0\xAC\xBC", + "\xE0\xB8\xB3" => "\xE0\xB9\x8D\xE0\xB8\xB2", + "\xE0\xBA\xB3" => "\xE0\xBB\x8D\xE0\xBA\xB2", + "\xE0\xBB\x9C" => "\xE0\xBA\xAB\xE0\xBA\x99", + "\xE0\xBB\x9D" => "\xE0\xBA\xAB\xE0\xBA\xA1", + "\xE0\xBC\x8C" => "\xE0\xBC\x8B", + "\xE0\xBD\x83" => "\xE0\xBD\x82\xE0\xBE\xB7", + "\xE0\xBD\x8D" => "\xE0\xBD\x8C\xE0\xBE\xB7", + "\xE0\xBD\x92" => "\xE0\xBD\x91\xE0\xBE\xB7", + "\xE0\xBD\x97" => "\xE0\xBD\x96\xE0\xBE\xB7", + "\xE0\xBD\x9C" => "\xE0\xBD\x9B\xE0\xBE\xB7", + "\xE0\xBD\xA9" => "\xE0\xBD\x80\xE0\xBE\xB5", + "\xE0\xBD\xB3" => "\xE0\xBD\xB1\xE0\xBD\xB2", + "\xE0\xBD\xB5" => "\xE0\xBD\xB1\xE0\xBD\xB4", + "\xE0\xBD\xB6" => "\xE0\xBE\xB2\xE0\xBE\x80", + "\xE0\xBD\xB7" => "\xE0\xBE\xB2\xE0\xBD\xB1\xE0\xBE\x80", + "\xE0\xBD\xB8" => "\xE0\xBE\xB3\xE0\xBE\x80", + "\xE0\xBD\xB9" => "\xE0\xBE\xB3\xE0\xBD\xB1\xE0\xBE\x80", + "\xE0\xBE\x81" => "\xE0\xBD\xB1\xE0\xBE\x80", + "\xE0\xBE\x93" => "\xE0\xBE\x92\xE0\xBE\xB7", + "\xE0\xBE\x9D" => "\xE0\xBE\x9C\xE0\xBE\xB7", + "\xE0\xBE\xA2" => "\xE0\xBE\xA1\xE0\xBE\xB7", + "\xE0\xBE\xA7" => "\xE0\xBE\xA6\xE0\xBE\xB7", + "\xE0\xBE\xAC" => "\xE0\xBE\xAB\xE0\xBE\xB7", + "\xE0\xBE\xB9" => "\xE0\xBE\x90\xE0\xBE\xB5", + "\xE1\x83\x87" => "\xE2\xB4\xA7", + "\xE1\x83\x8D" => "\xE2\xB4\xAD", + "\xE1\x83\xBC" => "\xE1\x83\x9C", + "\xE1\x8F\xB8" => "\xE1\x8F\xB0", + "\xE1\x8F\xB9" => "\xE1\x8F\xB1", + "\xE1\x8F\xBA" => "\xE1\x8F\xB2", + "\xE1\x8F\xBB" => "\xE1\x8F\xB3", + "\xE1\x8F\xBC" => "\xE1\x8F\xB4", + "\xE1\x8F\xBD" => "\xE1\x8F\xB5", + "\xE1\xB2\x80" => "\xD0\xB2", + "\xE1\xB2\x81" => "\xD0\xB4", + "\xE1\xB2\x82" => "\xD0\xBE", + "\xE1\xB2\x83" => "\xD1\x81", + "\xE1\xB2\x84" => "\xD1\x82", + "\xE1\xB2\x85" => "\xD1\x82", + "\xE1\xB2\x86" => "\xD1\x8A", + "\xE1\xB2\x87" => "\xD1\xA3", + "\xE1\xB2\x88" => "\xEA\x99\x8B", + "\xE1\xB2\x90" => "\xE1\x83\x90", + "\xE1\xB2\x91" => "\xE1\x83\x91", + "\xE1\xB2\x92" => "\xE1\x83\x92", + "\xE1\xB2\x93" => "\xE1\x83\x93", + "\xE1\xB2\x94" => "\xE1\x83\x94", + "\xE1\xB2\x95" => "\xE1\x83\x95", + "\xE1\xB2\x96" => "\xE1\x83\x96", + "\xE1\xB2\x97" => "\xE1\x83\x97", + "\xE1\xB2\x98" => "\xE1\x83\x98", + "\xE1\xB2\x99" => "\xE1\x83\x99", + "\xE1\xB2\x9A" => "\xE1\x83\x9A", + "\xE1\xB2\x9B" => "\xE1\x83\x9B", + "\xE1\xB2\x9C" => "\xE1\x83\x9C", + "\xE1\xB2\x9D" => "\xE1\x83\x9D", + "\xE1\xB2\x9E" => "\xE1\x83\x9E", + "\xE1\xB2\x9F" => "\xE1\x83\x9F", + "\xE1\xB2\xA0" => "\xE1\x83\xA0", + "\xE1\xB2\xA1" => "\xE1\x83\xA1", + "\xE1\xB2\xA2" => "\xE1\x83\xA2", + "\xE1\xB2\xA3" => "\xE1\x83\xA3", + "\xE1\xB2\xA4" => "\xE1\x83\xA4", + "\xE1\xB2\xA5" => "\xE1\x83\xA5", + "\xE1\xB2\xA6" => "\xE1\x83\xA6", + "\xE1\xB2\xA7" => "\xE1\x83\xA7", + "\xE1\xB2\xA8" => "\xE1\x83\xA8", + "\xE1\xB2\xA9" => "\xE1\x83\xA9", + "\xE1\xB2\xAA" => "\xE1\x83\xAA", + "\xE1\xB2\xAB" => "\xE1\x83\xAB", + "\xE1\xB2\xAC" => "\xE1\x83\xAC", + "\xE1\xB2\xAD" => "\xE1\x83\xAD", + "\xE1\xB2\xAE" => "\xE1\x83\xAE", + "\xE1\xB2\xAF" => "\xE1\x83\xAF", + "\xE1\xB2\xB0" => "\xE1\x83\xB0", + "\xE1\xB2\xB1" => "\xE1\x83\xB1", + "\xE1\xB2\xB2" => "\xE1\x83\xB2", + "\xE1\xB2\xB3" => "\xE1\x83\xB3", + "\xE1\xB2\xB4" => "\xE1\x83\xB4", + "\xE1\xB2\xB5" => "\xE1\x83\xB5", + "\xE1\xB2\xB6" => "\xE1\x83\xB6", + "\xE1\xB2\xB7" => "\xE1\x83\xB7", + "\xE1\xB2\xB8" => "\xE1\x83\xB8", + "\xE1\xB2\xB9" => "\xE1\x83\xB9", + "\xE1\xB2\xBA" => "\xE1\x83\xBA", + "\xE1\xB2\xBD" => "\xE1\x83\xBD", + "\xE1\xB2\xBE" => "\xE1\x83\xBE", + "\xE1\xB2\xBF" => "\xE1\x83\xBF", + "\xE1\xB4\xAC" => "\x61", + "\xE1\xB4\xAD" => "\xC3\xA6", + "\xE1\xB4\xAE" => "\x62", + "\xE1\xB4\xB0" => "\x64", + "\xE1\xB4\xB1" => "\x65", + "\xE1\xB4\xB2" => "\xC7\x9D", + "\xE1\xB4\xB3" => "\x67", + "\xE1\xB4\xB4" => "\x68", + "\xE1\xB4\xB5" => "\x69", + "\xE1\xB4\xB6" => "\x6A", + "\xE1\xB4\xB7" => "\x6B", + "\xE1\xB4\xB8" => "\x6C", + "\xE1\xB4\xB9" => "\x6D", + "\xE1\xB4\xBA" => "\x6E", + "\xE1\xB4\xBC" => "\x6F", + "\xE1\xB4\xBD" => "\xC8\xA3", + "\xE1\xB4\xBE" => "\x70", + "\xE1\xB4\xBF" => "\x72", + "\xE1\xB5\x80" => "\x74", + "\xE1\xB5\x81" => "\x75", + "\xE1\xB5\x82" => "\x77", + "\xE1\xB5\x83" => "\x61", + "\xE1\xB5\x84" => "\xC9\x90", + "\xE1\xB5\x85" => "\xC9\x91", + "\xE1\xB5\x86" => "\xE1\xB4\x82", + "\xE1\xB5\x87" => "\x62", + "\xE1\xB5\x88" => "\x64", + "\xE1\xB5\x89" => "\x65", + "\xE1\xB5\x8A" => "\xC9\x99", + "\xE1\xB5\x8B" => "\xC9\x9B", + "\xE1\xB5\x8C" => "\xC9\x9C", + "\xE1\xB5\x8D" => "\x67", + "\xE1\xB5\x8F" => "\x6B", + "\xE1\xB5\x90" => "\x6D", + "\xE1\xB5\x91" => "\xC5\x8B", + "\xE1\xB5\x92" => "\x6F", + "\xE1\xB5\x93" => "\xC9\x94", + "\xE1\xB5\x94" => "\xE1\xB4\x96", + "\xE1\xB5\x95" => "\xE1\xB4\x97", + "\xE1\xB5\x96" => "\x70", + "\xE1\xB5\x97" => "\x74", + "\xE1\xB5\x98" => "\x75", + "\xE1\xB5\x99" => "\xE1\xB4\x9D", + "\xE1\xB5\x9A" => "\xC9\xAF", + "\xE1\xB5\x9B" => "\x76", + "\xE1\xB5\x9C" => "\xE1\xB4\xA5", + "\xE1\xB5\x9D" => "\xCE\xB2", + "\xE1\xB5\x9E" => "\xCE\xB3", + "\xE1\xB5\x9F" => "\xCE\xB4", + "\xE1\xB5\xA0" => "\xCF\x86", + "\xE1\xB5\xA1" => "\xCF\x87", + "\xE1\xB5\xA2" => "\x69", + "\xE1\xB5\xA3" => "\x72", + "\xE1\xB5\xA4" => "\x75", + "\xE1\xB5\xA5" => "\x76", + "\xE1\xB5\xA6" => "\xCE\xB2", + "\xE1\xB5\xA7" => "\xCE\xB3", + "\xE1\xB5\xA8" => "\xCF\x81", + "\xE1\xB5\xA9" => "\xCF\x86", + "\xE1\xB5\xAA" => "\xCF\x87", + "\xE1\xB5\xB8" => "\xD0\xBD", + "\xE1\xB6\x9B" => "\xC9\x92", + "\xE1\xB6\x9C" => "\x63", + "\xE1\xB6\x9D" => "\xC9\x95", + "\xE1\xB6\x9E" => "\xC3\xB0", + "\xE1\xB6\x9F" => "\xC9\x9C", + "\xE1\xB6\xA0" => "\x66", + "\xE1\xB6\xA1" => "\xC9\x9F", + "\xE1\xB6\xA2" => "\xC9\xA1", + "\xE1\xB6\xA3" => "\xC9\xA5", + "\xE1\xB6\xA4" => "\xC9\xA8", + "\xE1\xB6\xA5" => "\xC9\xA9", + "\xE1\xB6\xA6" => "\xC9\xAA", + "\xE1\xB6\xA7" => "\xE1\xB5\xBB", + "\xE1\xB6\xA8" => "\xCA\x9D", + "\xE1\xB6\xA9" => "\xC9\xAD", + "\xE1\xB6\xAA" => "\xE1\xB6\x85", + "\xE1\xB6\xAB" => "\xCA\x9F", + "\xE1\xB6\xAC" => "\xC9\xB1", + "\xE1\xB6\xAD" => "\xC9\xB0", + "\xE1\xB6\xAE" => "\xC9\xB2", + "\xE1\xB6\xAF" => "\xC9\xB3", + "\xE1\xB6\xB0" => "\xC9\xB4", + "\xE1\xB6\xB1" => "\xC9\xB5", + "\xE1\xB6\xB2" => "\xC9\xB8", + "\xE1\xB6\xB3" => "\xCA\x82", + "\xE1\xB6\xB4" => "\xCA\x83", + "\xE1\xB6\xB5" => "\xC6\xAB", + "\xE1\xB6\xB6" => "\xCA\x89", + "\xE1\xB6\xB7" => "\xCA\x8A", + "\xE1\xB6\xB8" => "\xE1\xB4\x9C", + "\xE1\xB6\xB9" => "\xCA\x8B", + "\xE1\xB6\xBA" => "\xCA\x8C", + "\xE1\xB6\xBB" => "\x7A", + "\xE1\xB6\xBC" => "\xCA\x90", + "\xE1\xB6\xBD" => "\xCA\x91", + "\xE1\xB6\xBE" => "\xCA\x92", + "\xE1\xB6\xBF" => "\xCE\xB8", + "\xE1\xB8\x80" => "\xE1\xB8\x81", + "\xE1\xB8\x82" => "\xE1\xB8\x83", + "\xE1\xB8\x84" => "\xE1\xB8\x85", + "\xE1\xB8\x86" => "\xE1\xB8\x87", + "\xE1\xB8\x88" => "\xE1\xB8\x89", + "\xE1\xB8\x8A" => "\xE1\xB8\x8B", + "\xE1\xB8\x8C" => "\xE1\xB8\x8D", + "\xE1\xB8\x8E" => "\xE1\xB8\x8F", + "\xE1\xB8\x90" => "\xE1\xB8\x91", + "\xE1\xB8\x92" => "\xE1\xB8\x93", + "\xE1\xB8\x94" => "\xE1\xB8\x95", + "\xE1\xB8\x96" => "\xE1\xB8\x97", + "\xE1\xB8\x98" => "\xE1\xB8\x99", + "\xE1\xB8\x9A" => "\xE1\xB8\x9B", + "\xE1\xB8\x9C" => "\xE1\xB8\x9D", + "\xE1\xB8\x9E" => "\xE1\xB8\x9F", + "\xE1\xB8\xA0" => "\xE1\xB8\xA1", + "\xE1\xB8\xA2" => "\xE1\xB8\xA3", + "\xE1\xB8\xA4" => "\xE1\xB8\xA5", + "\xE1\xB8\xA6" => "\xE1\xB8\xA7", + "\xE1\xB8\xA8" => "\xE1\xB8\xA9", + "\xE1\xB8\xAA" => "\xE1\xB8\xAB", + "\xE1\xB8\xAC" => "\xE1\xB8\xAD", + "\xE1\xB8\xAE" => "\xE1\xB8\xAF", + "\xE1\xB8\xB0" => "\xE1\xB8\xB1", + "\xE1\xB8\xB2" => "\xE1\xB8\xB3", + "\xE1\xB8\xB4" => "\xE1\xB8\xB5", + "\xE1\xB8\xB6" => "\xE1\xB8\xB7", + "\xE1\xB8\xB8" => "\xE1\xB8\xB9", + "\xE1\xB8\xBA" => "\xE1\xB8\xBB", + "\xE1\xB8\xBC" => "\xE1\xB8\xBD", + "\xE1\xB8\xBE" => "\xE1\xB8\xBF", + "\xE1\xB9\x80" => "\xE1\xB9\x81", + "\xE1\xB9\x82" => "\xE1\xB9\x83", + "\xE1\xB9\x84" => "\xE1\xB9\x85", + "\xE1\xB9\x86" => "\xE1\xB9\x87", + "\xE1\xB9\x88" => "\xE1\xB9\x89", + "\xE1\xB9\x8A" => "\xE1\xB9\x8B", + "\xE1\xB9\x8C" => "\xE1\xB9\x8D", + "\xE1\xB9\x8E" => "\xE1\xB9\x8F", + "\xE1\xB9\x90" => "\xE1\xB9\x91", + "\xE1\xB9\x92" => "\xE1\xB9\x93", + "\xE1\xB9\x94" => "\xE1\xB9\x95", + "\xE1\xB9\x96" => "\xE1\xB9\x97", + "\xE1\xB9\x98" => "\xE1\xB9\x99", + "\xE1\xB9\x9A" => "\xE1\xB9\x9B", + "\xE1\xB9\x9C" => "\xE1\xB9\x9D", + "\xE1\xB9\x9E" => "\xE1\xB9\x9F", + "\xE1\xB9\xA0" => "\xE1\xB9\xA1", + "\xE1\xB9\xA2" => "\xE1\xB9\xA3", + "\xE1\xB9\xA4" => "\xE1\xB9\xA5", + "\xE1\xB9\xA6" => "\xE1\xB9\xA7", + "\xE1\xB9\xA8" => "\xE1\xB9\xA9", + "\xE1\xB9\xAA" => "\xE1\xB9\xAB", + "\xE1\xB9\xAC" => "\xE1\xB9\xAD", + "\xE1\xB9\xAE" => "\xE1\xB9\xAF", + "\xE1\xB9\xB0" => "\xE1\xB9\xB1", + "\xE1\xB9\xB2" => "\xE1\xB9\xB3", + "\xE1\xB9\xB4" => "\xE1\xB9\xB5", + "\xE1\xB9\xB6" => "\xE1\xB9\xB7", + "\xE1\xB9\xB8" => "\xE1\xB9\xB9", + "\xE1\xB9\xBA" => "\xE1\xB9\xBB", + "\xE1\xB9\xBC" => "\xE1\xB9\xBD", + "\xE1\xB9\xBE" => "\xE1\xB9\xBF", + "\xE1\xBA\x80" => "\xE1\xBA\x81", + "\xE1\xBA\x82" => "\xE1\xBA\x83", + "\xE1\xBA\x84" => "\xE1\xBA\x85", + "\xE1\xBA\x86" => "\xE1\xBA\x87", + "\xE1\xBA\x88" => "\xE1\xBA\x89", + "\xE1\xBA\x8A" => "\xE1\xBA\x8B", + "\xE1\xBA\x8C" => "\xE1\xBA\x8D", + "\xE1\xBA\x8E" => "\xE1\xBA\x8F", + "\xE1\xBA\x90" => "\xE1\xBA\x91", + "\xE1\xBA\x92" => "\xE1\xBA\x93", + "\xE1\xBA\x94" => "\xE1\xBA\x95", + "\xE1\xBA\x9A" => "\x61\xCA\xBE", + "\xE1\xBA\x9B" => "\xE1\xB9\xA1", + "\xE1\xBA\x9E" => "\x73\x73", + "\xE1\xBA\xA0" => "\xE1\xBA\xA1", + "\xE1\xBA\xA2" => "\xE1\xBA\xA3", + "\xE1\xBA\xA4" => "\xE1\xBA\xA5", + "\xE1\xBA\xA6" => "\xE1\xBA\xA7", + "\xE1\xBA\xA8" => "\xE1\xBA\xA9", + "\xE1\xBA\xAA" => "\xE1\xBA\xAB", + "\xE1\xBA\xAC" => "\xE1\xBA\xAD", + "\xE1\xBA\xAE" => "\xE1\xBA\xAF", + "\xE1\xBA\xB0" => "\xE1\xBA\xB1", + "\xE1\xBA\xB2" => "\xE1\xBA\xB3", + "\xE1\xBA\xB4" => "\xE1\xBA\xB5", + "\xE1\xBA\xB6" => "\xE1\xBA\xB7", + "\xE1\xBA\xB8" => "\xE1\xBA\xB9", + "\xE1\xBA\xBA" => "\xE1\xBA\xBB", + "\xE1\xBA\xBC" => "\xE1\xBA\xBD", + "\xE1\xBA\xBE" => "\xE1\xBA\xBF", + "\xE1\xBB\x80" => "\xE1\xBB\x81", + "\xE1\xBB\x82" => "\xE1\xBB\x83", + "\xE1\xBB\x84" => "\xE1\xBB\x85", + "\xE1\xBB\x86" => "\xE1\xBB\x87", + "\xE1\xBB\x88" => "\xE1\xBB\x89", + "\xE1\xBB\x8A" => "\xE1\xBB\x8B", + "\xE1\xBB\x8C" => "\xE1\xBB\x8D", + "\xE1\xBB\x8E" => "\xE1\xBB\x8F", + "\xE1\xBB\x90" => "\xE1\xBB\x91", + "\xE1\xBB\x92" => "\xE1\xBB\x93", + "\xE1\xBB\x94" => "\xE1\xBB\x95", + "\xE1\xBB\x96" => "\xE1\xBB\x97", + "\xE1\xBB\x98" => "\xE1\xBB\x99", + "\xE1\xBB\x9A" => "\xE1\xBB\x9B", + "\xE1\xBB\x9C" => "\xE1\xBB\x9D", + "\xE1\xBB\x9E" => "\xE1\xBB\x9F", + "\xE1\xBB\xA0" => "\xE1\xBB\xA1", + "\xE1\xBB\xA2" => "\xE1\xBB\xA3", + "\xE1\xBB\xA4" => "\xE1\xBB\xA5", + "\xE1\xBB\xA6" => "\xE1\xBB\xA7", + "\xE1\xBB\xA8" => "\xE1\xBB\xA9", + "\xE1\xBB\xAA" => "\xE1\xBB\xAB", + "\xE1\xBB\xAC" => "\xE1\xBB\xAD", + "\xE1\xBB\xAE" => "\xE1\xBB\xAF", + "\xE1\xBB\xB0" => "\xE1\xBB\xB1", + "\xE1\xBB\xB2" => "\xE1\xBB\xB3", + "\xE1\xBB\xB4" => "\xE1\xBB\xB5", + "\xE1\xBB\xB6" => "\xE1\xBB\xB7", + "\xE1\xBB\xB8" => "\xE1\xBB\xB9", + "\xE1\xBB\xBA" => "\xE1\xBB\xBB", + "\xE1\xBB\xBC" => "\xE1\xBB\xBD", + "\xE1\xBB\xBE" => "\xE1\xBB\xBF", + "\xE1\xBC\x88" => "\xE1\xBC\x80", + "\xE1\xBC\x89" => "\xE1\xBC\x81", + "\xE1\xBC\x8A" => "\xE1\xBC\x82", + "\xE1\xBC\x8B" => "\xE1\xBC\x83", + "\xE1\xBC\x8C" => "\xE1\xBC\x84", + "\xE1\xBC\x8D" => "\xE1\xBC\x85", + "\xE1\xBC\x8E" => "\xE1\xBC\x86", + "\xE1\xBC\x8F" => "\xE1\xBC\x87", + "\xE1\xBC\x98" => "\xE1\xBC\x90", + "\xE1\xBC\x99" => "\xE1\xBC\x91", + "\xE1\xBC\x9A" => "\xE1\xBC\x92", + "\xE1\xBC\x9B" => "\xE1\xBC\x93", + "\xE1\xBC\x9C" => "\xE1\xBC\x94", + "\xE1\xBC\x9D" => "\xE1\xBC\x95", + "\xE1\xBC\xA8" => "\xE1\xBC\xA0", + "\xE1\xBC\xA9" => "\xE1\xBC\xA1", + "\xE1\xBC\xAA" => "\xE1\xBC\xA2", + "\xE1\xBC\xAB" => "\xE1\xBC\xA3", + "\xE1\xBC\xAC" => "\xE1\xBC\xA4", + "\xE1\xBC\xAD" => "\xE1\xBC\xA5", + "\xE1\xBC\xAE" => "\xE1\xBC\xA6", + "\xE1\xBC\xAF" => "\xE1\xBC\xA7", + "\xE1\xBC\xB8" => "\xE1\xBC\xB0", + "\xE1\xBC\xB9" => "\xE1\xBC\xB1", + "\xE1\xBC\xBA" => "\xE1\xBC\xB2", + "\xE1\xBC\xBB" => "\xE1\xBC\xB3", + "\xE1\xBC\xBC" => "\xE1\xBC\xB4", + "\xE1\xBC\xBD" => "\xE1\xBC\xB5", + "\xE1\xBC\xBE" => "\xE1\xBC\xB6", + "\xE1\xBC\xBF" => "\xE1\xBC\xB7", + "\xE1\xBD\x88" => "\xE1\xBD\x80", + "\xE1\xBD\x89" => "\xE1\xBD\x81", + "\xE1\xBD\x8A" => "\xE1\xBD\x82", + "\xE1\xBD\x8B" => "\xE1\xBD\x83", + "\xE1\xBD\x8C" => "\xE1\xBD\x84", + "\xE1\xBD\x8D" => "\xE1\xBD\x85", + "\xE1\xBD\x99" => "\xE1\xBD\x91", + "\xE1\xBD\x9B" => "\xE1\xBD\x93", + "\xE1\xBD\x9D" => "\xE1\xBD\x95", + "\xE1\xBD\x9F" => "\xE1\xBD\x97", + "\xE1\xBD\xA8" => "\xE1\xBD\xA0", + "\xE1\xBD\xA9" => "\xE1\xBD\xA1", + "\xE1\xBD\xAA" => "\xE1\xBD\xA2", + "\xE1\xBD\xAB" => "\xE1\xBD\xA3", + "\xE1\xBD\xAC" => "\xE1\xBD\xA4", + "\xE1\xBD\xAD" => "\xE1\xBD\xA5", + "\xE1\xBD\xAE" => "\xE1\xBD\xA6", + "\xE1\xBD\xAF" => "\xE1\xBD\xA7", + "\xE1\xBD\xB1" => "\xCE\xAC", + "\xE1\xBD\xB3" => "\xCE\xAD", + "\xE1\xBD\xB5" => "\xCE\xAE", + "\xE1\xBD\xB7" => "\xCE\xAF", + "\xE1\xBD\xB9" => "\xCF\x8C", + "\xE1\xBD\xBB" => "\xCF\x8D", + "\xE1\xBD\xBD" => "\xCF\x8E", + "\xE1\xBE\x80" => "\xE1\xBC\x80\xCE\xB9", + "\xE1\xBE\x81" => "\xE1\xBC\x81\xCE\xB9", + "\xE1\xBE\x82" => "\xE1\xBC\x82\xCE\xB9", + "\xE1\xBE\x83" => "\xE1\xBC\x83\xCE\xB9", + "\xE1\xBE\x84" => "\xE1\xBC\x84\xCE\xB9", + "\xE1\xBE\x85" => "\xE1\xBC\x85\xCE\xB9", + "\xE1\xBE\x86" => "\xE1\xBC\x86\xCE\xB9", + "\xE1\xBE\x87" => "\xE1\xBC\x87\xCE\xB9", + "\xE1\xBE\x88" => "\xE1\xBC\x80\xCE\xB9", + "\xE1\xBE\x89" => "\xE1\xBC\x81\xCE\xB9", + "\xE1\xBE\x8A" => "\xE1\xBC\x82\xCE\xB9", + "\xE1\xBE\x8B" => "\xE1\xBC\x83\xCE\xB9", + "\xE1\xBE\x8C" => "\xE1\xBC\x84\xCE\xB9", + "\xE1\xBE\x8D" => "\xE1\xBC\x85\xCE\xB9", + "\xE1\xBE\x8E" => "\xE1\xBC\x86\xCE\xB9", + "\xE1\xBE\x8F" => "\xE1\xBC\x87\xCE\xB9", + "\xE1\xBE\x90" => "\xE1\xBC\xA0\xCE\xB9", + "\xE1\xBE\x91" => "\xE1\xBC\xA1\xCE\xB9", + "\xE1\xBE\x92" => "\xE1\xBC\xA2\xCE\xB9", + "\xE1\xBE\x93" => "\xE1\xBC\xA3\xCE\xB9", + "\xE1\xBE\x94" => "\xE1\xBC\xA4\xCE\xB9", + "\xE1\xBE\x95" => "\xE1\xBC\xA5\xCE\xB9", + "\xE1\xBE\x96" => "\xE1\xBC\xA6\xCE\xB9", + "\xE1\xBE\x97" => "\xE1\xBC\xA7\xCE\xB9", + "\xE1\xBE\x98" => "\xE1\xBC\xA0\xCE\xB9", + "\xE1\xBE\x99" => "\xE1\xBC\xA1\xCE\xB9", + "\xE1\xBE\x9A" => "\xE1\xBC\xA2\xCE\xB9", + "\xE1\xBE\x9B" => "\xE1\xBC\xA3\xCE\xB9", + "\xE1\xBE\x9C" => "\xE1\xBC\xA4\xCE\xB9", + "\xE1\xBE\x9D" => "\xE1\xBC\xA5\xCE\xB9", + "\xE1\xBE\x9E" => "\xE1\xBC\xA6\xCE\xB9", + "\xE1\xBE\x9F" => "\xE1\xBC\xA7\xCE\xB9", + "\xE1\xBE\xA0" => "\xE1\xBD\xA0\xCE\xB9", + "\xE1\xBE\xA1" => "\xE1\xBD\xA1\xCE\xB9", + "\xE1\xBE\xA2" => "\xE1\xBD\xA2\xCE\xB9", + "\xE1\xBE\xA3" => "\xE1\xBD\xA3\xCE\xB9", + "\xE1\xBE\xA4" => "\xE1\xBD\xA4\xCE\xB9", + "\xE1\xBE\xA5" => "\xE1\xBD\xA5\xCE\xB9", + "\xE1\xBE\xA6" => "\xE1\xBD\xA6\xCE\xB9", + "\xE1\xBE\xA7" => "\xE1\xBD\xA7\xCE\xB9", + "\xE1\xBE\xA8" => "\xE1\xBD\xA0\xCE\xB9", + "\xE1\xBE\xA9" => "\xE1\xBD\xA1\xCE\xB9", + "\xE1\xBE\xAA" => "\xE1\xBD\xA2\xCE\xB9", + "\xE1\xBE\xAB" => "\xE1\xBD\xA3\xCE\xB9", + "\xE1\xBE\xAC" => "\xE1\xBD\xA4\xCE\xB9", + "\xE1\xBE\xAD" => "\xE1\xBD\xA5\xCE\xB9", + "\xE1\xBE\xAE" => "\xE1\xBD\xA6\xCE\xB9", + "\xE1\xBE\xAF" => "\xE1\xBD\xA7\xCE\xB9", + "\xE1\xBE\xB2" => "\xE1\xBD\xB0\xCE\xB9", + "\xE1\xBE\xB3" => "\xCE\xB1\xCE\xB9", + "\xE1\xBE\xB4" => "\xCE\xAC\xCE\xB9", + "\xE1\xBE\xB7" => "\xE1\xBE\xB6\xCE\xB9", + "\xE1\xBE\xB8" => "\xE1\xBE\xB0", + "\xE1\xBE\xB9" => "\xE1\xBE\xB1", + "\xE1\xBE\xBA" => "\xE1\xBD\xB0", + "\xE1\xBE\xBB" => "\xCE\xAC", + "\xE1\xBE\xBC" => "\xCE\xB1\xCE\xB9", + "\xE1\xBE\xBE" => "\xCE\xB9", + "\xE1\xBF\x82" => "\xE1\xBD\xB4\xCE\xB9", + "\xE1\xBF\x83" => "\xCE\xB7\xCE\xB9", + "\xE1\xBF\x84" => "\xCE\xAE\xCE\xB9", + "\xE1\xBF\x87" => "\xE1\xBF\x86\xCE\xB9", + "\xE1\xBF\x88" => "\xE1\xBD\xB2", + "\xE1\xBF\x89" => "\xCE\xAD", + "\xE1\xBF\x8A" => "\xE1\xBD\xB4", + "\xE1\xBF\x8B" => "\xCE\xAE", + "\xE1\xBF\x8C" => "\xCE\xB7\xCE\xB9", + "\xE1\xBF\x93" => "\xCE\x90", + "\xE1\xBF\x98" => "\xE1\xBF\x90", + "\xE1\xBF\x99" => "\xE1\xBF\x91", + "\xE1\xBF\x9A" => "\xE1\xBD\xB6", + "\xE1\xBF\x9B" => "\xCE\xAF", + "\xE1\xBF\xA3" => "\xCE\xB0", + "\xE1\xBF\xA8" => "\xE1\xBF\xA0", + "\xE1\xBF\xA9" => "\xE1\xBF\xA1", + "\xE1\xBF\xAA" => "\xE1\xBD\xBA", + "\xE1\xBF\xAB" => "\xCF\x8D", + "\xE1\xBF\xAC" => "\xE1\xBF\xA5", + "\xE1\xBF\xB2" => "\xE1\xBD\xBC\xCE\xB9", + "\xE1\xBF\xB3" => "\xCF\x89\xCE\xB9", + "\xE1\xBF\xB4" => "\xCF\x8E\xCE\xB9", + "\xE1\xBF\xB7" => "\xE1\xBF\xB6\xCE\xB9", + "\xE1\xBF\xB8" => "\xE1\xBD\xB8", + "\xE1\xBF\xB9" => "\xCF\x8C", + "\xE1\xBF\xBA" => "\xE1\xBD\xBC", + "\xE1\xBF\xBB" => "\xCF\x8E", + "\xE1\xBF\xBC" => "\xCF\x89\xCE\xB9", + "\xE2\x80\x91" => "\xE2\x80\x90", + "\xE2\x80\xB3" => "\xE2\x80\xB2\xE2\x80\xB2", + "\xE2\x80\xB4" => "\xE2\x80\xB2\xE2\x80\xB2\xE2\x80\xB2", + "\xE2\x80\xB6" => "\xE2\x80\xB5\xE2\x80\xB5", + "\xE2\x80\xB7" => "\xE2\x80\xB5\xE2\x80\xB5\xE2\x80\xB5", + "\xE2\x81\x97" => "\xE2\x80\xB2\xE2\x80\xB2\xE2\x80\xB2\xE2\x80\xB2", + "\xE2\x81\xB0" => "\x30", + "\xE2\x81\xB1" => "\x69", + "\xE2\x81\xB4" => "\x34", + "\xE2\x81\xB5" => "\x35", + "\xE2\x81\xB6" => "\x36", + "\xE2\x81\xB7" => "\x37", + "\xE2\x81\xB8" => "\x38", + "\xE2\x81\xB9" => "\x39", + "\xE2\x81\xBB" => "\xE2\x88\x92", + "\xE2\x81\xBF" => "\x6E", + "\xE2\x82\x80" => "\x30", + "\xE2\x82\x81" => "\x31", + "\xE2\x82\x82" => "\x32", + "\xE2\x82\x83" => "\x33", + "\xE2\x82\x84" => "\x34", + "\xE2\x82\x85" => "\x35", + "\xE2\x82\x86" => "\x36", + "\xE2\x82\x87" => "\x37", + "\xE2\x82\x88" => "\x38", + "\xE2\x82\x89" => "\x39", + "\xE2\x82\x8B" => "\xE2\x88\x92", + "\xE2\x82\x90" => "\x61", + "\xE2\x82\x91" => "\x65", + "\xE2\x82\x92" => "\x6F", + "\xE2\x82\x93" => "\x78", + "\xE2\x82\x94" => "\xC9\x99", + "\xE2\x82\x95" => "\x68", + "\xE2\x82\x96" => "\x6B", + "\xE2\x82\x97" => "\x6C", + "\xE2\x82\x98" => "\x6D", + "\xE2\x82\x99" => "\x6E", + "\xE2\x82\x9A" => "\x70", + "\xE2\x82\x9B" => "\x73", + "\xE2\x82\x9C" => "\x74", + "\xE2\x82\xA8" => "\x72\x73", + "\xE2\x84\x82" => "\x63", + "\xE2\x84\x83" => "\xC2\xB0\x63", + "\xE2\x84\x87" => "\xC9\x9B", + "\xE2\x84\x89" => "\xC2\xB0\x66", + "\xE2\x84\x8A" => "\x67", + "\xE2\x84\x8B" => "\x68", + "\xE2\x84\x8C" => "\x68", + "\xE2\x84\x8D" => "\x68", + "\xE2\x84\x8E" => "\x68", + "\xE2\x84\x8F" => "\xC4\xA7", + "\xE2\x84\x90" => "\x69", + "\xE2\x84\x91" => "\x69", + "\xE2\x84\x92" => "\x6C", + "\xE2\x84\x93" => "\x6C", + "\xE2\x84\x95" => "\x6E", + "\xE2\x84\x96" => "\x6E\x6F", + "\xE2\x84\x99" => "\x70", + "\xE2\x84\x9A" => "\x71", + "\xE2\x84\x9B" => "\x72", + "\xE2\x84\x9C" => "\x72", + "\xE2\x84\x9D" => "\x72", + "\xE2\x84\xA0" => "\x73\x6D", + "\xE2\x84\xA1" => "\x74\x65\x6C", + "\xE2\x84\xA2" => "\x74\x6D", + "\xE2\x84\xA4" => "\x7A", + "\xE2\x84\xA6" => "\xCF\x89", + "\xE2\x84\xA8" => "\x7A", + "\xE2\x84\xAA" => "\x6B", + "\xE2\x84\xAB" => "\xC3\xA5", + "\xE2\x84\xAC" => "\x62", + "\xE2\x84\xAD" => "\x63", + "\xE2\x84\xAF" => "\x65", + "\xE2\x84\xB0" => "\x65", + "\xE2\x84\xB1" => "\x66", + "\xE2\x84\xB3" => "\x6D", + "\xE2\x84\xB4" => "\x6F", + "\xE2\x84\xB5" => "\xD7\x90", + "\xE2\x84\xB6" => "\xD7\x91", + "\xE2\x84\xB7" => "\xD7\x92", + "\xE2\x84\xB8" => "\xD7\x93", + "\xE2\x84\xB9" => "\x69", + "\xE2\x84\xBB" => "\x66\x61\x78", + "\xE2\x84\xBC" => "\xCF\x80", + "\xE2\x84\xBD" => "\xCE\xB3", + "\xE2\x84\xBE" => "\xCE\xB3", + "\xE2\x84\xBF" => "\xCF\x80", + "\xE2\x85\x80" => "\xE2\x88\x91", + "\xE2\x85\x85" => "\x64", + "\xE2\x85\x86" => "\x64", + "\xE2\x85\x87" => "\x65", + "\xE2\x85\x88" => "\x69", + "\xE2\x85\x89" => "\x6A", + "\xE2\x85\x90" => "\x31\xE2\x81\x84\x37", + "\xE2\x85\x91" => "\x31\xE2\x81\x84\x39", + "\xE2\x85\x92" => "\x31\xE2\x81\x84\x31\x30", + "\xE2\x85\x93" => "\x31\xE2\x81\x84\x33", + "\xE2\x85\x94" => "\x32\xE2\x81\x84\x33", + "\xE2\x85\x95" => "\x31\xE2\x81\x84\x35", + "\xE2\x85\x96" => "\x32\xE2\x81\x84\x35", + "\xE2\x85\x97" => "\x33\xE2\x81\x84\x35", + "\xE2\x85\x98" => "\x34\xE2\x81\x84\x35", + "\xE2\x85\x99" => "\x31\xE2\x81\x84\x36", + "\xE2\x85\x9A" => "\x35\xE2\x81\x84\x36", + "\xE2\x85\x9B" => "\x31\xE2\x81\x84\x38", + "\xE2\x85\x9C" => "\x33\xE2\x81\x84\x38", + "\xE2\x85\x9D" => "\x35\xE2\x81\x84\x38", + "\xE2\x85\x9E" => "\x37\xE2\x81\x84\x38", + "\xE2\x85\x9F" => "\x31\xE2\x81\x84", + "\xE2\x85\xA0" => "\x69", + "\xE2\x85\xA1" => "\x69\x69", + "\xE2\x85\xA2" => "\x69\x69\x69", + "\xE2\x85\xA3" => "\x69\x76", + "\xE2\x85\xA4" => "\x76", + "\xE2\x85\xA5" => "\x76\x69", + "\xE2\x85\xA6" => "\x76\x69\x69", + "\xE2\x85\xA7" => "\x76\x69\x69\x69", + "\xE2\x85\xA8" => "\x69\x78", + "\xE2\x85\xA9" => "\x78", + "\xE2\x85\xAA" => "\x78\x69", + "\xE2\x85\xAB" => "\x78\x69\x69", + "\xE2\x85\xAC" => "\x6C", + "\xE2\x85\xAD" => "\x63", + "\xE2\x85\xAE" => "\x64", + "\xE2\x85\xAF" => "\x6D", + "\xE2\x85\xB0" => "\x69", + "\xE2\x85\xB1" => "\x69\x69", + "\xE2\x85\xB2" => "\x69\x69\x69", + "\xE2\x85\xB3" => "\x69\x76", + "\xE2\x85\xB4" => "\x76", + "\xE2\x85\xB5" => "\x76\x69", + "\xE2\x85\xB6" => "\x76\x69\x69", + "\xE2\x85\xB7" => "\x76\x69\x69\x69", + "\xE2\x85\xB8" => "\x69\x78", + "\xE2\x85\xB9" => "\x78", + "\xE2\x85\xBA" => "\x78\x69", + "\xE2\x85\xBB" => "\x78\x69\x69", + "\xE2\x85\xBC" => "\x6C", + "\xE2\x85\xBD" => "\x63", + "\xE2\x85\xBE" => "\x64", + "\xE2\x85\xBF" => "\x6D", + "\xE2\x86\x89" => "\x30\xE2\x81\x84\x33", + "\xE2\x88\xAC" => "\xE2\x88\xAB\xE2\x88\xAB", + "\xE2\x88\xAD" => "\xE2\x88\xAB\xE2\x88\xAB\xE2\x88\xAB", + "\xE2\x88\xAF" => "\xE2\x88\xAE\xE2\x88\xAE", + "\xE2\x88\xB0" => "\xE2\x88\xAE\xE2\x88\xAE\xE2\x88\xAE", + "\xE2\x8C\xA9" => "\xE3\x80\x88", + "\xE2\x8C\xAA" => "\xE3\x80\x89", + "\xE2\x91\xA0" => "\x31", + "\xE2\x91\xA1" => "\x32", + "\xE2\x91\xA2" => "\x33", + "\xE2\x91\xA3" => "\x34", + "\xE2\x91\xA4" => "\x35", + "\xE2\x91\xA5" => "\x36", + "\xE2\x91\xA6" => "\x37", + "\xE2\x91\xA7" => "\x38", + "\xE2\x91\xA8" => "\x39", + "\xE2\x91\xA9" => "\x31\x30", + "\xE2\x91\xAA" => "\x31\x31", + "\xE2\x91\xAB" => "\x31\x32", + "\xE2\x91\xAC" => "\x31\x33", + "\xE2\x91\xAD" => "\x31\x34", + "\xE2\x91\xAE" => "\x31\x35", + "\xE2\x91\xAF" => "\x31\x36", + "\xE2\x91\xB0" => "\x31\x37", + "\xE2\x91\xB1" => "\x31\x38", + "\xE2\x91\xB2" => "\x31\x39", + "\xE2\x91\xB3" => "\x32\x30", + "\xE2\x92\xB6" => "\x61", + "\xE2\x92\xB7" => "\x62", + "\xE2\x92\xB8" => "\x63", + "\xE2\x92\xB9" => "\x64", + "\xE2\x92\xBA" => "\x65", + "\xE2\x92\xBB" => "\x66", + "\xE2\x92\xBC" => "\x67", + "\xE2\x92\xBD" => "\x68", + "\xE2\x92\xBE" => "\x69", + "\xE2\x92\xBF" => "\x6A", + "\xE2\x93\x80" => "\x6B", + "\xE2\x93\x81" => "\x6C", + "\xE2\x93\x82" => "\x6D", + "\xE2\x93\x83" => "\x6E", + "\xE2\x93\x84" => "\x6F", + "\xE2\x93\x85" => "\x70", + "\xE2\x93\x86" => "\x71", + "\xE2\x93\x87" => "\x72", + "\xE2\x93\x88" => "\x73", + "\xE2\x93\x89" => "\x74", + "\xE2\x93\x8A" => "\x75", + "\xE2\x93\x8B" => "\x76", + "\xE2\x93\x8C" => "\x77", + "\xE2\x93\x8D" => "\x78", + "\xE2\x93\x8E" => "\x79", + "\xE2\x93\x8F" => "\x7A", + "\xE2\x93\x90" => "\x61", + "\xE2\x93\x91" => "\x62", + "\xE2\x93\x92" => "\x63", + "\xE2\x93\x93" => "\x64", + "\xE2\x93\x94" => "\x65", + "\xE2\x93\x95" => "\x66", + "\xE2\x93\x96" => "\x67", + "\xE2\x93\x97" => "\x68", + "\xE2\x93\x98" => "\x69", + "\xE2\x93\x99" => "\x6A", + "\xE2\x93\x9A" => "\x6B", + "\xE2\x93\x9B" => "\x6C", + "\xE2\x93\x9C" => "\x6D", + "\xE2\x93\x9D" => "\x6E", + "\xE2\x93\x9E" => "\x6F", + "\xE2\x93\x9F" => "\x70", + "\xE2\x93\xA0" => "\x71", + "\xE2\x93\xA1" => "\x72", + "\xE2\x93\xA2" => "\x73", + "\xE2\x93\xA3" => "\x74", + "\xE2\x93\xA4" => "\x75", + "\xE2\x93\xA5" => "\x76", + "\xE2\x93\xA6" => "\x77", + "\xE2\x93\xA7" => "\x78", + "\xE2\x93\xA8" => "\x79", + "\xE2\x93\xA9" => "\x7A", + "\xE2\x93\xAA" => "\x30", + "\xE2\xA8\x8C" => "\xE2\x88\xAB\xE2\x88\xAB\xE2\x88\xAB\xE2\x88\xAB", + "\xE2\xAB\x9C" => "\xE2\xAB\x9D\xCC\xB8", + "\xE2\xB0\x80" => "\xE2\xB0\xB0", + "\xE2\xB0\x81" => "\xE2\xB0\xB1", + "\xE2\xB0\x82" => "\xE2\xB0\xB2", + "\xE2\xB0\x83" => "\xE2\xB0\xB3", + "\xE2\xB0\x84" => "\xE2\xB0\xB4", + "\xE2\xB0\x85" => "\xE2\xB0\xB5", + "\xE2\xB0\x86" => "\xE2\xB0\xB6", + "\xE2\xB0\x87" => "\xE2\xB0\xB7", + "\xE2\xB0\x88" => "\xE2\xB0\xB8", + "\xE2\xB0\x89" => "\xE2\xB0\xB9", + "\xE2\xB0\x8A" => "\xE2\xB0\xBA", + "\xE2\xB0\x8B" => "\xE2\xB0\xBB", + "\xE2\xB0\x8C" => "\xE2\xB0\xBC", + "\xE2\xB0\x8D" => "\xE2\xB0\xBD", + "\xE2\xB0\x8E" => "\xE2\xB0\xBE", + "\xE2\xB0\x8F" => "\xE2\xB0\xBF", + "\xE2\xB0\x90" => "\xE2\xB1\x80", + "\xE2\xB0\x91" => "\xE2\xB1\x81", + "\xE2\xB0\x92" => "\xE2\xB1\x82", + "\xE2\xB0\x93" => "\xE2\xB1\x83", + "\xE2\xB0\x94" => "\xE2\xB1\x84", + "\xE2\xB0\x95" => "\xE2\xB1\x85", + "\xE2\xB0\x96" => "\xE2\xB1\x86", + "\xE2\xB0\x97" => "\xE2\xB1\x87", + "\xE2\xB0\x98" => "\xE2\xB1\x88", + "\xE2\xB0\x99" => "\xE2\xB1\x89", + "\xE2\xB0\x9A" => "\xE2\xB1\x8A", + "\xE2\xB0\x9B" => "\xE2\xB1\x8B", + "\xE2\xB0\x9C" => "\xE2\xB1\x8C", + "\xE2\xB0\x9D" => "\xE2\xB1\x8D", + "\xE2\xB0\x9E" => "\xE2\xB1\x8E", + "\xE2\xB0\x9F" => "\xE2\xB1\x8F", + "\xE2\xB0\xA0" => "\xE2\xB1\x90", + "\xE2\xB0\xA1" => "\xE2\xB1\x91", + "\xE2\xB0\xA2" => "\xE2\xB1\x92", + "\xE2\xB0\xA3" => "\xE2\xB1\x93", + "\xE2\xB0\xA4" => "\xE2\xB1\x94", + "\xE2\xB0\xA5" => "\xE2\xB1\x95", + "\xE2\xB0\xA6" => "\xE2\xB1\x96", + "\xE2\xB0\xA7" => "\xE2\xB1\x97", + "\xE2\xB0\xA8" => "\xE2\xB1\x98", + "\xE2\xB0\xA9" => "\xE2\xB1\x99", + "\xE2\xB0\xAA" => "\xE2\xB1\x9A", + "\xE2\xB0\xAB" => "\xE2\xB1\x9B", + "\xE2\xB0\xAC" => "\xE2\xB1\x9C", + "\xE2\xB0\xAD" => "\xE2\xB1\x9D", + "\xE2\xB0\xAE" => "\xE2\xB1\x9E", + "\xE2\xB0\xAF" => "\xE2\xB1\x9F", + "\xE2\xB1\xA0" => "\xE2\xB1\xA1", + "\xE2\xB1\xA2" => "\xC9\xAB", + "\xE2\xB1\xA3" => "\xE1\xB5\xBD", + "\xE2\xB1\xA4" => "\xC9\xBD", + "\xE2\xB1\xA7" => "\xE2\xB1\xA8", + "\xE2\xB1\xA9" => "\xE2\xB1\xAA", + "\xE2\xB1\xAB" => "\xE2\xB1\xAC", + "\xE2\xB1\xAD" => "\xC9\x91", + "\xE2\xB1\xAE" => "\xC9\xB1", + "\xE2\xB1\xAF" => "\xC9\x90", + "\xE2\xB1\xB0" => "\xC9\x92", + "\xE2\xB1\xB2" => "\xE2\xB1\xB3", + "\xE2\xB1\xB5" => "\xE2\xB1\xB6", + "\xE2\xB1\xBC" => "\x6A", + "\xE2\xB1\xBD" => "\x76", + "\xE2\xB1\xBE" => "\xC8\xBF", + "\xE2\xB1\xBF" => "\xC9\x80", + "\xE2\xB2\x80" => "\xE2\xB2\x81", + "\xE2\xB2\x82" => "\xE2\xB2\x83", + "\xE2\xB2\x84" => "\xE2\xB2\x85", + "\xE2\xB2\x86" => "\xE2\xB2\x87", + "\xE2\xB2\x88" => "\xE2\xB2\x89", + "\xE2\xB2\x8A" => "\xE2\xB2\x8B", + "\xE2\xB2\x8C" => "\xE2\xB2\x8D", + "\xE2\xB2\x8E" => "\xE2\xB2\x8F", + "\xE2\xB2\x90" => "\xE2\xB2\x91", + "\xE2\xB2\x92" => "\xE2\xB2\x93", + "\xE2\xB2\x94" => "\xE2\xB2\x95", + "\xE2\xB2\x96" => "\xE2\xB2\x97", + "\xE2\xB2\x98" => "\xE2\xB2\x99", + "\xE2\xB2\x9A" => "\xE2\xB2\x9B", + "\xE2\xB2\x9C" => "\xE2\xB2\x9D", + "\xE2\xB2\x9E" => "\xE2\xB2\x9F", + "\xE2\xB2\xA0" => "\xE2\xB2\xA1", + "\xE2\xB2\xA2" => "\xE2\xB2\xA3", + "\xE2\xB2\xA4" => "\xE2\xB2\xA5", + "\xE2\xB2\xA6" => "\xE2\xB2\xA7", + "\xE2\xB2\xA8" => "\xE2\xB2\xA9", + "\xE2\xB2\xAA" => "\xE2\xB2\xAB", + "\xE2\xB2\xAC" => "\xE2\xB2\xAD", + "\xE2\xB2\xAE" => "\xE2\xB2\xAF", + "\xE2\xB2\xB0" => "\xE2\xB2\xB1", + "\xE2\xB2\xB2" => "\xE2\xB2\xB3", + "\xE2\xB2\xB4" => "\xE2\xB2\xB5", + "\xE2\xB2\xB6" => "\xE2\xB2\xB7", + "\xE2\xB2\xB8" => "\xE2\xB2\xB9", + "\xE2\xB2\xBA" => "\xE2\xB2\xBB", + "\xE2\xB2\xBC" => "\xE2\xB2\xBD", + "\xE2\xB2\xBE" => "\xE2\xB2\xBF", + "\xE2\xB3\x80" => "\xE2\xB3\x81", + "\xE2\xB3\x82" => "\xE2\xB3\x83", + "\xE2\xB3\x84" => "\xE2\xB3\x85", + "\xE2\xB3\x86" => "\xE2\xB3\x87", + "\xE2\xB3\x88" => "\xE2\xB3\x89", + "\xE2\xB3\x8A" => "\xE2\xB3\x8B", + "\xE2\xB3\x8C" => "\xE2\xB3\x8D", + "\xE2\xB3\x8E" => "\xE2\xB3\x8F", + "\xE2\xB3\x90" => "\xE2\xB3\x91", + "\xE2\xB3\x92" => "\xE2\xB3\x93", + "\xE2\xB3\x94" => "\xE2\xB3\x95", + "\xE2\xB3\x96" => "\xE2\xB3\x97", + "\xE2\xB3\x98" => "\xE2\xB3\x99", + "\xE2\xB3\x9A" => "\xE2\xB3\x9B", + "\xE2\xB3\x9C" => "\xE2\xB3\x9D", + "\xE2\xB3\x9E" => "\xE2\xB3\x9F", + "\xE2\xB3\xA0" => "\xE2\xB3\xA1", + "\xE2\xB3\xA2" => "\xE2\xB3\xA3", + "\xE2\xB3\xAB" => "\xE2\xB3\xAC", + "\xE2\xB3\xAD" => "\xE2\xB3\xAE", + "\xE2\xB3\xB2" => "\xE2\xB3\xB3", + "\xE2\xB5\xAF" => "\xE2\xB5\xA1", + "\xE2\xBA\x9F" => "\xE6\xAF\x8D", + "\xE2\xBB\xB3" => "\xE9\xBE\x9F", + "\xE2\xBC\x80" => "\xE4\xB8\x80", + "\xE2\xBC\x81" => "\xE4\xB8\xA8", + "\xE2\xBC\x82" => "\xE4\xB8\xB6", + "\xE2\xBC\x83" => "\xE4\xB8\xBF", + "\xE2\xBC\x84" => "\xE4\xB9\x99", + "\xE2\xBC\x85" => "\xE4\xBA\x85", + "\xE2\xBC\x86" => "\xE4\xBA\x8C", + "\xE2\xBC\x87" => "\xE4\xBA\xA0", + "\xE2\xBC\x88" => "\xE4\xBA\xBA", + "\xE2\xBC\x89" => "\xE5\x84\xBF", + "\xE2\xBC\x8A" => "\xE5\x85\xA5", + "\xE2\xBC\x8B" => "\xE5\x85\xAB", + "\xE2\xBC\x8C" => "\xE5\x86\x82", + "\xE2\xBC\x8D" => "\xE5\x86\x96", + "\xE2\xBC\x8E" => "\xE5\x86\xAB", + "\xE2\xBC\x8F" => "\xE5\x87\xA0", + "\xE2\xBC\x90" => "\xE5\x87\xB5", + "\xE2\xBC\x91" => "\xE5\x88\x80", + "\xE2\xBC\x92" => "\xE5\x8A\x9B", + "\xE2\xBC\x93" => "\xE5\x8B\xB9", + "\xE2\xBC\x94" => "\xE5\x8C\x95", + "\xE2\xBC\x95" => "\xE5\x8C\x9A", + "\xE2\xBC\x96" => "\xE5\x8C\xB8", + "\xE2\xBC\x97" => "\xE5\x8D\x81", + "\xE2\xBC\x98" => "\xE5\x8D\x9C", + "\xE2\xBC\x99" => "\xE5\x8D\xA9", + "\xE2\xBC\x9A" => "\xE5\x8E\x82", + "\xE2\xBC\x9B" => "\xE5\x8E\xB6", + "\xE2\xBC\x9C" => "\xE5\x8F\x88", + "\xE2\xBC\x9D" => "\xE5\x8F\xA3", + "\xE2\xBC\x9E" => "\xE5\x9B\x97", + "\xE2\xBC\x9F" => "\xE5\x9C\x9F", + "\xE2\xBC\xA0" => "\xE5\xA3\xAB", + "\xE2\xBC\xA1" => "\xE5\xA4\x82", + "\xE2\xBC\xA2" => "\xE5\xA4\x8A", + "\xE2\xBC\xA3" => "\xE5\xA4\x95", + "\xE2\xBC\xA4" => "\xE5\xA4\xA7", + "\xE2\xBC\xA5" => "\xE5\xA5\xB3", + "\xE2\xBC\xA6" => "\xE5\xAD\x90", + "\xE2\xBC\xA7" => "\xE5\xAE\x80", + "\xE2\xBC\xA8" => "\xE5\xAF\xB8", + "\xE2\xBC\xA9" => "\xE5\xB0\x8F", + "\xE2\xBC\xAA" => "\xE5\xB0\xA2", + "\xE2\xBC\xAB" => "\xE5\xB0\xB8", + "\xE2\xBC\xAC" => "\xE5\xB1\xAE", + "\xE2\xBC\xAD" => "\xE5\xB1\xB1", + "\xE2\xBC\xAE" => "\xE5\xB7\x9B", + "\xE2\xBC\xAF" => "\xE5\xB7\xA5", + "\xE2\xBC\xB0" => "\xE5\xB7\xB1", + "\xE2\xBC\xB1" => "\xE5\xB7\xBE", + "\xE2\xBC\xB2" => "\xE5\xB9\xB2", + "\xE2\xBC\xB3" => "\xE5\xB9\xBA", + "\xE2\xBC\xB4" => "\xE5\xB9\xBF", + "\xE2\xBC\xB5" => "\xE5\xBB\xB4", + "\xE2\xBC\xB6" => "\xE5\xBB\xBE", + "\xE2\xBC\xB7" => "\xE5\xBC\x8B", + "\xE2\xBC\xB8" => "\xE5\xBC\x93", + "\xE2\xBC\xB9" => "\xE5\xBD\x90", + "\xE2\xBC\xBA" => "\xE5\xBD\xA1", + "\xE2\xBC\xBB" => "\xE5\xBD\xB3", + "\xE2\xBC\xBC" => "\xE5\xBF\x83", + "\xE2\xBC\xBD" => "\xE6\x88\x88", + "\xE2\xBC\xBE" => "\xE6\x88\xB6", + "\xE2\xBC\xBF" => "\xE6\x89\x8B", + "\xE2\xBD\x80" => "\xE6\x94\xAF", + "\xE2\xBD\x81" => "\xE6\x94\xB4", + "\xE2\xBD\x82" => "\xE6\x96\x87", + "\xE2\xBD\x83" => "\xE6\x96\x97", + "\xE2\xBD\x84" => "\xE6\x96\xA4", + "\xE2\xBD\x85" => "\xE6\x96\xB9", + "\xE2\xBD\x86" => "\xE6\x97\xA0", + "\xE2\xBD\x87" => "\xE6\x97\xA5", + "\xE2\xBD\x88" => "\xE6\x9B\xB0", + "\xE2\xBD\x89" => "\xE6\x9C\x88", + "\xE2\xBD\x8A" => "\xE6\x9C\xA8", + "\xE2\xBD\x8B" => "\xE6\xAC\xA0", + "\xE2\xBD\x8C" => "\xE6\xAD\xA2", + "\xE2\xBD\x8D" => "\xE6\xAD\xB9", + "\xE2\xBD\x8E" => "\xE6\xAE\xB3", + "\xE2\xBD\x8F" => "\xE6\xAF\x8B", + "\xE2\xBD\x90" => "\xE6\xAF\x94", + "\xE2\xBD\x91" => "\xE6\xAF\x9B", + "\xE2\xBD\x92" => "\xE6\xB0\x8F", + "\xE2\xBD\x93" => "\xE6\xB0\x94", + "\xE2\xBD\x94" => "\xE6\xB0\xB4", + "\xE2\xBD\x95" => "\xE7\x81\xAB", + "\xE2\xBD\x96" => "\xE7\x88\xAA", + "\xE2\xBD\x97" => "\xE7\x88\xB6", + "\xE2\xBD\x98" => "\xE7\x88\xBB", + "\xE2\xBD\x99" => "\xE7\x88\xBF", + "\xE2\xBD\x9A" => "\xE7\x89\x87", + "\xE2\xBD\x9B" => "\xE7\x89\x99", + "\xE2\xBD\x9C" => "\xE7\x89\x9B", + "\xE2\xBD\x9D" => "\xE7\x8A\xAC", + "\xE2\xBD\x9E" => "\xE7\x8E\x84", + "\xE2\xBD\x9F" => "\xE7\x8E\x89", + "\xE2\xBD\xA0" => "\xE7\x93\x9C", + "\xE2\xBD\xA1" => "\xE7\x93\xA6", + "\xE2\xBD\xA2" => "\xE7\x94\x98", + "\xE2\xBD\xA3" => "\xE7\x94\x9F", + "\xE2\xBD\xA4" => "\xE7\x94\xA8", + "\xE2\xBD\xA5" => "\xE7\x94\xB0", + "\xE2\xBD\xA6" => "\xE7\x96\x8B", + "\xE2\xBD\xA7" => "\xE7\x96\x92", + "\xE2\xBD\xA8" => "\xE7\x99\xB6", + "\xE2\xBD\xA9" => "\xE7\x99\xBD", + "\xE2\xBD\xAA" => "\xE7\x9A\xAE", + "\xE2\xBD\xAB" => "\xE7\x9A\xBF", + "\xE2\xBD\xAC" => "\xE7\x9B\xAE", + "\xE2\xBD\xAD" => "\xE7\x9F\x9B", + "\xE2\xBD\xAE" => "\xE7\x9F\xA2", + "\xE2\xBD\xAF" => "\xE7\x9F\xB3", + "\xE2\xBD\xB0" => "\xE7\xA4\xBA", + "\xE2\xBD\xB1" => "\xE7\xA6\xB8", + "\xE2\xBD\xB2" => "\xE7\xA6\xBE", + "\xE2\xBD\xB3" => "\xE7\xA9\xB4", + "\xE2\xBD\xB4" => "\xE7\xAB\x8B", + "\xE2\xBD\xB5" => "\xE7\xAB\xB9", + "\xE2\xBD\xB6" => "\xE7\xB1\xB3", + "\xE2\xBD\xB7" => "\xE7\xB3\xB8", + "\xE2\xBD\xB8" => "\xE7\xBC\xB6", + "\xE2\xBD\xB9" => "\xE7\xBD\x91", + "\xE2\xBD\xBA" => "\xE7\xBE\x8A", + "\xE2\xBD\xBB" => "\xE7\xBE\xBD", + "\xE2\xBD\xBC" => "\xE8\x80\x81", + "\xE2\xBD\xBD" => "\xE8\x80\x8C", + "\xE2\xBD\xBE" => "\xE8\x80\x92", + "\xE2\xBD\xBF" => "\xE8\x80\xB3", + "\xE2\xBE\x80" => "\xE8\x81\xBF", + "\xE2\xBE\x81" => "\xE8\x82\x89", + "\xE2\xBE\x82" => "\xE8\x87\xA3", + "\xE2\xBE\x83" => "\xE8\x87\xAA", + "\xE2\xBE\x84" => "\xE8\x87\xB3", + "\xE2\xBE\x85" => "\xE8\x87\xBC", + "\xE2\xBE\x86" => "\xE8\x88\x8C", + "\xE2\xBE\x87" => "\xE8\x88\x9B", + "\xE2\xBE\x88" => "\xE8\x88\x9F", + "\xE2\xBE\x89" => "\xE8\x89\xAE", + "\xE2\xBE\x8A" => "\xE8\x89\xB2", + "\xE2\xBE\x8B" => "\xE8\x89\xB8", + "\xE2\xBE\x8C" => "\xE8\x99\x8D", + "\xE2\xBE\x8D" => "\xE8\x99\xAB", + "\xE2\xBE\x8E" => "\xE8\xA1\x80", + "\xE2\xBE\x8F" => "\xE8\xA1\x8C", + "\xE2\xBE\x90" => "\xE8\xA1\xA3", + "\xE2\xBE\x91" => "\xE8\xA5\xBE", + "\xE2\xBE\x92" => "\xE8\xA6\x8B", + "\xE2\xBE\x93" => "\xE8\xA7\x92", + "\xE2\xBE\x94" => "\xE8\xA8\x80", + "\xE2\xBE\x95" => "\xE8\xB0\xB7", + "\xE2\xBE\x96" => "\xE8\xB1\x86", + "\xE2\xBE\x97" => "\xE8\xB1\x95", + "\xE2\xBE\x98" => "\xE8\xB1\xB8", + "\xE2\xBE\x99" => "\xE8\xB2\x9D", + "\xE2\xBE\x9A" => "\xE8\xB5\xA4", + "\xE2\xBE\x9B" => "\xE8\xB5\xB0", + "\xE2\xBE\x9C" => "\xE8\xB6\xB3", + "\xE2\xBE\x9D" => "\xE8\xBA\xAB", + "\xE2\xBE\x9E" => "\xE8\xBB\x8A", + "\xE2\xBE\x9F" => "\xE8\xBE\x9B", + "\xE2\xBE\xA0" => "\xE8\xBE\xB0", + "\xE2\xBE\xA1" => "\xE8\xBE\xB5", + "\xE2\xBE\xA2" => "\xE9\x82\x91", + "\xE2\xBE\xA3" => "\xE9\x85\x89", + "\xE2\xBE\xA4" => "\xE9\x87\x86", + "\xE2\xBE\xA5" => "\xE9\x87\x8C", + "\xE2\xBE\xA6" => "\xE9\x87\x91", + "\xE2\xBE\xA7" => "\xE9\x95\xB7", + "\xE2\xBE\xA8" => "\xE9\x96\x80", + "\xE2\xBE\xA9" => "\xE9\x98\x9C", + "\xE2\xBE\xAA" => "\xE9\x9A\xB6", + "\xE2\xBE\xAB" => "\xE9\x9A\xB9", + "\xE2\xBE\xAC" => "\xE9\x9B\xA8", + "\xE2\xBE\xAD" => "\xE9\x9D\x91", + "\xE2\xBE\xAE" => "\xE9\x9D\x9E", + "\xE2\xBE\xAF" => "\xE9\x9D\xA2", + "\xE2\xBE\xB0" => "\xE9\x9D\xA9", + "\xE2\xBE\xB1" => "\xE9\x9F\x8B", + "\xE2\xBE\xB2" => "\xE9\x9F\xAD", + "\xE2\xBE\xB3" => "\xE9\x9F\xB3", + "\xE2\xBE\xB4" => "\xE9\xA0\x81", + "\xE2\xBE\xB5" => "\xE9\xA2\xA8", + "\xE2\xBE\xB6" => "\xE9\xA3\x9B", + "\xE2\xBE\xB7" => "\xE9\xA3\x9F", + "\xE2\xBE\xB8" => "\xE9\xA6\x96", + "\xE2\xBE\xB9" => "\xE9\xA6\x99", + "\xE2\xBE\xBA" => "\xE9\xA6\xAC", + "\xE2\xBE\xBB" => "\xE9\xAA\xA8", + "\xE2\xBE\xBC" => "\xE9\xAB\x98", + "\xE2\xBE\xBD" => "\xE9\xAB\x9F", + "\xE2\xBE\xBE" => "\xE9\xAC\xA5", + "\xE2\xBE\xBF" => "\xE9\xAC\xAF", + "\xE2\xBF\x80" => "\xE9\xAC\xB2", + "\xE2\xBF\x81" => "\xE9\xAC\xBC", + "\xE2\xBF\x82" => "\xE9\xAD\x9A", + "\xE2\xBF\x83" => "\xE9\xB3\xA5", + "\xE2\xBF\x84" => "\xE9\xB9\xB5", + "\xE2\xBF\x85" => "\xE9\xB9\xBF", + "\xE2\xBF\x86" => "\xE9\xBA\xA5", + "\xE2\xBF\x87" => "\xE9\xBA\xBB", + "\xE2\xBF\x88" => "\xE9\xBB\x83", + "\xE2\xBF\x89" => "\xE9\xBB\x8D", + "\xE2\xBF\x8A" => "\xE9\xBB\x91", + "\xE2\xBF\x8B" => "\xE9\xBB\xB9", + "\xE2\xBF\x8C" => "\xE9\xBB\xBD", + "\xE2\xBF\x8D" => "\xE9\xBC\x8E", + "\xE2\xBF\x8E" => "\xE9\xBC\x93", + "\xE2\xBF\x8F" => "\xE9\xBC\xA0", + "\xE2\xBF\x90" => "\xE9\xBC\xBB", + "\xE2\xBF\x91" => "\xE9\xBD\x8A", + "\xE2\xBF\x92" => "\xE9\xBD\x92", + "\xE2\xBF\x93" => "\xE9\xBE\x8D", + "\xE2\xBF\x94" => "\xE9\xBE\x9C", + "\xE2\xBF\x95" => "\xE9\xBE\xA0", + "\xE3\x80\x82" => "\x2E", + "\xE3\x80\xB6" => "\xE3\x80\x92", + "\xE3\x80\xB8" => "\xE5\x8D\x81", + "\xE3\x80\xB9" => "\xE5\x8D\x84", + "\xE3\x80\xBA" => "\xE5\x8D\x85", + "\xE3\x82\x9F" => "\xE3\x82\x88\xE3\x82\x8A", + "\xE3\x83\xBF" => "\xE3\x82\xB3\xE3\x83\x88", + "\xE3\x84\xB1" => "\xE1\x84\x80", + "\xE3\x84\xB2" => "\xE1\x84\x81", + "\xE3\x84\xB3" => "\xE1\x86\xAA", + "\xE3\x84\xB4" => "\xE1\x84\x82", + "\xE3\x84\xB5" => "\xE1\x86\xAC", + "\xE3\x84\xB6" => "\xE1\x86\xAD", + "\xE3\x84\xB7" => "\xE1\x84\x83", + "\xE3\x84\xB8" => "\xE1\x84\x84", + "\xE3\x84\xB9" => "\xE1\x84\x85", + "\xE3\x84\xBA" => "\xE1\x86\xB0", + "\xE3\x84\xBB" => "\xE1\x86\xB1", + "\xE3\x84\xBC" => "\xE1\x86\xB2", + "\xE3\x84\xBD" => "\xE1\x86\xB3", + "\xE3\x84\xBE" => "\xE1\x86\xB4", + "\xE3\x84\xBF" => "\xE1\x86\xB5", + "\xE3\x85\x80" => "\xE1\x84\x9A", + "\xE3\x85\x81" => "\xE1\x84\x86", + "\xE3\x85\x82" => "\xE1\x84\x87", + "\xE3\x85\x83" => "\xE1\x84\x88", + "\xE3\x85\x84" => "\xE1\x84\xA1", + "\xE3\x85\x85" => "\xE1\x84\x89", + "\xE3\x85\x86" => "\xE1\x84\x8A", + "\xE3\x85\x87" => "\xE1\x84\x8B", + "\xE3\x85\x88" => "\xE1\x84\x8C", + "\xE3\x85\x89" => "\xE1\x84\x8D", + "\xE3\x85\x8A" => "\xE1\x84\x8E", + "\xE3\x85\x8B" => "\xE1\x84\x8F", + "\xE3\x85\x8C" => "\xE1\x84\x90", + "\xE3\x85\x8D" => "\xE1\x84\x91", + "\xE3\x85\x8E" => "\xE1\x84\x92", + "\xE3\x85\x8F" => "\xE1\x85\xA1", + "\xE3\x85\x90" => "\xE1\x85\xA2", + "\xE3\x85\x91" => "\xE1\x85\xA3", + "\xE3\x85\x92" => "\xE1\x85\xA4", + "\xE3\x85\x93" => "\xE1\x85\xA5", + "\xE3\x85\x94" => "\xE1\x85\xA6", + "\xE3\x85\x95" => "\xE1\x85\xA7", + "\xE3\x85\x96" => "\xE1\x85\xA8", + "\xE3\x85\x97" => "\xE1\x85\xA9", + "\xE3\x85\x98" => "\xE1\x85\xAA", + "\xE3\x85\x99" => "\xE1\x85\xAB", + "\xE3\x85\x9A" => "\xE1\x85\xAC", + "\xE3\x85\x9B" => "\xE1\x85\xAD", + "\xE3\x85\x9C" => "\xE1\x85\xAE", + "\xE3\x85\x9D" => "\xE1\x85\xAF", + "\xE3\x85\x9E" => "\xE1\x85\xB0", + "\xE3\x85\x9F" => "\xE1\x85\xB1", + "\xE3\x85\xA0" => "\xE1\x85\xB2", + "\xE3\x85\xA1" => "\xE1\x85\xB3", + "\xE3\x85\xA2" => "\xE1\x85\xB4", + "\xE3\x85\xA3" => "\xE1\x85\xB5", + "\xE3\x85\xA5" => "\xE1\x84\x94", + "\xE3\x85\xA6" => "\xE1\x84\x95", + "\xE3\x85\xA7" => "\xE1\x87\x87", + "\xE3\x85\xA8" => "\xE1\x87\x88", + "\xE3\x85\xA9" => "\xE1\x87\x8C", + "\xE3\x85\xAA" => "\xE1\x87\x8E", + "\xE3\x85\xAB" => "\xE1\x87\x93", + "\xE3\x85\xAC" => "\xE1\x87\x97", + "\xE3\x85\xAD" => "\xE1\x87\x99", + "\xE3\x85\xAE" => "\xE1\x84\x9C", + "\xE3\x85\xAF" => "\xE1\x87\x9D", + "\xE3\x85\xB0" => "\xE1\x87\x9F", + "\xE3\x85\xB1" => "\xE1\x84\x9D", + "\xE3\x85\xB2" => "\xE1\x84\x9E", + "\xE3\x85\xB3" => "\xE1\x84\xA0", + "\xE3\x85\xB4" => "\xE1\x84\xA2", + "\xE3\x85\xB5" => "\xE1\x84\xA3", + "\xE3\x85\xB6" => "\xE1\x84\xA7", + "\xE3\x85\xB7" => "\xE1\x84\xA9", + "\xE3\x85\xB8" => "\xE1\x84\xAB", + "\xE3\x85\xB9" => "\xE1\x84\xAC", + "\xE3\x85\xBA" => "\xE1\x84\xAD", + "\xE3\x85\xBB" => "\xE1\x84\xAE", + "\xE3\x85\xBC" => "\xE1\x84\xAF", + "\xE3\x85\xBD" => "\xE1\x84\xB2", + "\xE3\x85\xBE" => "\xE1\x84\xB6", + "\xE3\x85\xBF" => "\xE1\x85\x80", + "\xE3\x86\x80" => "\xE1\x85\x87", + "\xE3\x86\x81" => "\xE1\x85\x8C", + "\xE3\x86\x82" => "\xE1\x87\xB1", + "\xE3\x86\x83" => "\xE1\x87\xB2", + "\xE3\x86\x84" => "\xE1\x85\x97", + "\xE3\x86\x85" => "\xE1\x85\x98", + "\xE3\x86\x86" => "\xE1\x85\x99", + "\xE3\x86\x87" => "\xE1\x86\x84", + "\xE3\x86\x88" => "\xE1\x86\x85", + "\xE3\x86\x89" => "\xE1\x86\x88", + "\xE3\x86\x8A" => "\xE1\x86\x91", + "\xE3\x86\x8B" => "\xE1\x86\x92", + "\xE3\x86\x8C" => "\xE1\x86\x94", + "\xE3\x86\x8D" => "\xE1\x86\x9E", + "\xE3\x86\x8E" => "\xE1\x86\xA1", + "\xE3\x86\x92" => "\xE4\xB8\x80", + "\xE3\x86\x93" => "\xE4\xBA\x8C", + "\xE3\x86\x94" => "\xE4\xB8\x89", + "\xE3\x86\x95" => "\xE5\x9B\x9B", + "\xE3\x86\x96" => "\xE4\xB8\x8A", + "\xE3\x86\x97" => "\xE4\xB8\xAD", + "\xE3\x86\x98" => "\xE4\xB8\x8B", + "\xE3\x86\x99" => "\xE7\x94\xB2", + "\xE3\x86\x9A" => "\xE4\xB9\x99", + "\xE3\x86\x9B" => "\xE4\xB8\x99", + "\xE3\x86\x9C" => "\xE4\xB8\x81", + "\xE3\x86\x9D" => "\xE5\xA4\xA9", + "\xE3\x86\x9E" => "\xE5\x9C\xB0", + "\xE3\x86\x9F" => "\xE4\xBA\xBA", + "\xE3\x89\x84" => "\xE5\x95\x8F", + "\xE3\x89\x85" => "\xE5\xB9\xBC", + "\xE3\x89\x86" => "\xE6\x96\x87", + "\xE3\x89\x87" => "\xE7\xAE\x8F", + "\xE3\x89\x90" => "\x70\x74\x65", + "\xE3\x89\x91" => "\x32\x31", + "\xE3\x89\x92" => "\x32\x32", + "\xE3\x89\x93" => "\x32\x33", + "\xE3\x89\x94" => "\x32\x34", + "\xE3\x89\x95" => "\x32\x35", + "\xE3\x89\x96" => "\x32\x36", + "\xE3\x89\x97" => "\x32\x37", + "\xE3\x89\x98" => "\x32\x38", + "\xE3\x89\x99" => "\x32\x39", + "\xE3\x89\x9A" => "\x33\x30", + "\xE3\x89\x9B" => "\x33\x31", + "\xE3\x89\x9C" => "\x33\x32", + "\xE3\x89\x9D" => "\x33\x33", + "\xE3\x89\x9E" => "\x33\x34", + "\xE3\x89\x9F" => "\x33\x35", + "\xE3\x89\xA0" => "\xE1\x84\x80", + "\xE3\x89\xA1" => "\xE1\x84\x82", + "\xE3\x89\xA2" => "\xE1\x84\x83", + "\xE3\x89\xA3" => "\xE1\x84\x85", + "\xE3\x89\xA4" => "\xE1\x84\x86", + "\xE3\x89\xA5" => "\xE1\x84\x87", + "\xE3\x89\xA6" => "\xE1\x84\x89", + "\xE3\x89\xA7" => "\xE1\x84\x8B", + "\xE3\x89\xA8" => "\xE1\x84\x8C", + "\xE3\x89\xA9" => "\xE1\x84\x8E", + "\xE3\x89\xAA" => "\xE1\x84\x8F", + "\xE3\x89\xAB" => "\xE1\x84\x90", + "\xE3\x89\xAC" => "\xE1\x84\x91", + "\xE3\x89\xAD" => "\xE1\x84\x92", + "\xE3\x89\xAE" => "\xEA\xB0\x80", + "\xE3\x89\xAF" => "\xEB\x82\x98", + "\xE3\x89\xB0" => "\xEB\x8B\xA4", + "\xE3\x89\xB1" => "\xEB\x9D\xBC", + "\xE3\x89\xB2" => "\xEB\xA7\x88", + "\xE3\x89\xB3" => "\xEB\xB0\x94", + "\xE3\x89\xB4" => "\xEC\x82\xAC", + "\xE3\x89\xB5" => "\xEC\x95\x84", + "\xE3\x89\xB6" => "\xEC\x9E\x90", + "\xE3\x89\xB7" => "\xEC\xB0\xA8", + "\xE3\x89\xB8" => "\xEC\xB9\xB4", + "\xE3\x89\xB9" => "\xED\x83\x80", + "\xE3\x89\xBA" => "\xED\x8C\x8C", + "\xE3\x89\xBB" => "\xED\x95\x98", + "\xE3\x89\xBC" => "\xEC\xB0\xB8\xEA\xB3\xA0", + "\xE3\x89\xBD" => "\xEC\xA3\xBC\xEC\x9D\x98", + "\xE3\x89\xBE" => "\xEC\x9A\xB0", + "\xE3\x8A\x80" => "\xE4\xB8\x80", + "\xE3\x8A\x81" => "\xE4\xBA\x8C", + "\xE3\x8A\x82" => "\xE4\xB8\x89", + "\xE3\x8A\x83" => "\xE5\x9B\x9B", + "\xE3\x8A\x84" => "\xE4\xBA\x94", + "\xE3\x8A\x85" => "\xE5\x85\xAD", + "\xE3\x8A\x86" => "\xE4\xB8\x83", + "\xE3\x8A\x87" => "\xE5\x85\xAB", + "\xE3\x8A\x88" => "\xE4\xB9\x9D", + "\xE3\x8A\x89" => "\xE5\x8D\x81", + "\xE3\x8A\x8A" => "\xE6\x9C\x88", + "\xE3\x8A\x8B" => "\xE7\x81\xAB", + "\xE3\x8A\x8C" => "\xE6\xB0\xB4", + "\xE3\x8A\x8D" => "\xE6\x9C\xA8", + "\xE3\x8A\x8E" => "\xE9\x87\x91", + "\xE3\x8A\x8F" => "\xE5\x9C\x9F", + "\xE3\x8A\x90" => "\xE6\x97\xA5", + "\xE3\x8A\x91" => "\xE6\xA0\xAA", + "\xE3\x8A\x92" => "\xE6\x9C\x89", + "\xE3\x8A\x93" => "\xE7\xA4\xBE", + "\xE3\x8A\x94" => "\xE5\x90\x8D", + "\xE3\x8A\x95" => "\xE7\x89\xB9", + "\xE3\x8A\x96" => "\xE8\xB2\xA1", + "\xE3\x8A\x97" => "\xE7\xA5\x9D", + "\xE3\x8A\x98" => "\xE5\x8A\xB4", + "\xE3\x8A\x99" => "\xE7\xA7\x98", + "\xE3\x8A\x9A" => "\xE7\x94\xB7", + "\xE3\x8A\x9B" => "\xE5\xA5\xB3", + "\xE3\x8A\x9C" => "\xE9\x81\xA9", + "\xE3\x8A\x9D" => "\xE5\x84\xAA", + "\xE3\x8A\x9E" => "\xE5\x8D\xB0", + "\xE3\x8A\x9F" => "\xE6\xB3\xA8", + "\xE3\x8A\xA0" => "\xE9\xA0\x85", + "\xE3\x8A\xA1" => "\xE4\xBC\x91", + "\xE3\x8A\xA2" => "\xE5\x86\x99", + "\xE3\x8A\xA3" => "\xE6\xAD\xA3", + "\xE3\x8A\xA4" => "\xE4\xB8\x8A", + "\xE3\x8A\xA5" => "\xE4\xB8\xAD", + "\xE3\x8A\xA6" => "\xE4\xB8\x8B", + "\xE3\x8A\xA7" => "\xE5\xB7\xA6", + "\xE3\x8A\xA8" => "\xE5\x8F\xB3", + "\xE3\x8A\xA9" => "\xE5\x8C\xBB", + "\xE3\x8A\xAA" => "\xE5\xAE\x97", + "\xE3\x8A\xAB" => "\xE5\xAD\xA6", + "\xE3\x8A\xAC" => "\xE7\x9B\xA3", + "\xE3\x8A\xAD" => "\xE4\xBC\x81", + "\xE3\x8A\xAE" => "\xE8\xB3\x87", + "\xE3\x8A\xAF" => "\xE5\x8D\x94", + "\xE3\x8A\xB0" => "\xE5\xA4\x9C", + "\xE3\x8A\xB1" => "\x33\x36", + "\xE3\x8A\xB2" => "\x33\x37", + "\xE3\x8A\xB3" => "\x33\x38", + "\xE3\x8A\xB4" => "\x33\x39", + "\xE3\x8A\xB5" => "\x34\x30", + "\xE3\x8A\xB6" => "\x34\x31", + "\xE3\x8A\xB7" => "\x34\x32", + "\xE3\x8A\xB8" => "\x34\x33", + "\xE3\x8A\xB9" => "\x34\x34", + "\xE3\x8A\xBA" => "\x34\x35", + "\xE3\x8A\xBB" => "\x34\x36", + "\xE3\x8A\xBC" => "\x34\x37", + "\xE3\x8A\xBD" => "\x34\x38", + "\xE3\x8A\xBE" => "\x34\x39", + "\xE3\x8A\xBF" => "\x35\x30", + "\xE3\x8B\x80" => "\x31\xE6\x9C\x88", + "\xE3\x8B\x81" => "\x32\xE6\x9C\x88", + "\xE3\x8B\x82" => "\x33\xE6\x9C\x88", + "\xE3\x8B\x83" => "\x34\xE6\x9C\x88", + "\xE3\x8B\x84" => "\x35\xE6\x9C\x88", + "\xE3\x8B\x85" => "\x36\xE6\x9C\x88", + "\xE3\x8B\x86" => "\x37\xE6\x9C\x88", + "\xE3\x8B\x87" => "\x38\xE6\x9C\x88", + "\xE3\x8B\x88" => "\x39\xE6\x9C\x88", + "\xE3\x8B\x89" => "\x31\x30\xE6\x9C\x88", + "\xE3\x8B\x8A" => "\x31\x31\xE6\x9C\x88", + "\xE3\x8B\x8B" => "\x31\x32\xE6\x9C\x88", + "\xE3\x8B\x8C" => "\x68\x67", + "\xE3\x8B\x8D" => "\x65\x72\x67", + "\xE3\x8B\x8E" => "\x65\x76", + "\xE3\x8B\x8F" => "\x6C\x74\x64", + "\xE3\x8B\x90" => "\xE3\x82\xA2", + "\xE3\x8B\x91" => "\xE3\x82\xA4", + "\xE3\x8B\x92" => "\xE3\x82\xA6", + "\xE3\x8B\x93" => "\xE3\x82\xA8", + "\xE3\x8B\x94" => "\xE3\x82\xAA", + "\xE3\x8B\x95" => "\xE3\x82\xAB", + "\xE3\x8B\x96" => "\xE3\x82\xAD", + "\xE3\x8B\x97" => "\xE3\x82\xAF", + "\xE3\x8B\x98" => "\xE3\x82\xB1", + "\xE3\x8B\x99" => "\xE3\x82\xB3", + "\xE3\x8B\x9A" => "\xE3\x82\xB5", + "\xE3\x8B\x9B" => "\xE3\x82\xB7", + "\xE3\x8B\x9C" => "\xE3\x82\xB9", + "\xE3\x8B\x9D" => "\xE3\x82\xBB", + "\xE3\x8B\x9E" => "\xE3\x82\xBD", + "\xE3\x8B\x9F" => "\xE3\x82\xBF", + "\xE3\x8B\xA0" => "\xE3\x83\x81", + "\xE3\x8B\xA1" => "\xE3\x83\x84", + "\xE3\x8B\xA2" => "\xE3\x83\x86", + "\xE3\x8B\xA3" => "\xE3\x83\x88", + "\xE3\x8B\xA4" => "\xE3\x83\x8A", + "\xE3\x8B\xA5" => "\xE3\x83\x8B", + "\xE3\x8B\xA6" => "\xE3\x83\x8C", + "\xE3\x8B\xA7" => "\xE3\x83\x8D", + "\xE3\x8B\xA8" => "\xE3\x83\x8E", + "\xE3\x8B\xA9" => "\xE3\x83\x8F", + "\xE3\x8B\xAA" => "\xE3\x83\x92", + "\xE3\x8B\xAB" => "\xE3\x83\x95", + "\xE3\x8B\xAC" => "\xE3\x83\x98", + "\xE3\x8B\xAD" => "\xE3\x83\x9B", + "\xE3\x8B\xAE" => "\xE3\x83\x9E", + "\xE3\x8B\xAF" => "\xE3\x83\x9F", + "\xE3\x8B\xB0" => "\xE3\x83\xA0", + "\xE3\x8B\xB1" => "\xE3\x83\xA1", + "\xE3\x8B\xB2" => "\xE3\x83\xA2", + "\xE3\x8B\xB3" => "\xE3\x83\xA4", + "\xE3\x8B\xB4" => "\xE3\x83\xA6", + "\xE3\x8B\xB5" => "\xE3\x83\xA8", + "\xE3\x8B\xB6" => "\xE3\x83\xA9", + "\xE3\x8B\xB7" => "\xE3\x83\xAA", + "\xE3\x8B\xB8" => "\xE3\x83\xAB", + "\xE3\x8B\xB9" => "\xE3\x83\xAC", + "\xE3\x8B\xBA" => "\xE3\x83\xAD", + "\xE3\x8B\xBB" => "\xE3\x83\xAF", + "\xE3\x8B\xBC" => "\xE3\x83\xB0", + "\xE3\x8B\xBD" => "\xE3\x83\xB1", + "\xE3\x8B\xBE" => "\xE3\x83\xB2", + "\xE3\x8B\xBF" => "\xE4\xBB\xA4\xE5\x92\x8C", + "\xE3\x8C\x80" => "\xE3\x82\xA2\xE3\x83\x91\xE3\x83\xBC\xE3\x83\x88", + "\xE3\x8C\x81" => "\xE3\x82\xA2\xE3\x83\xAB\xE3\x83\x95\xE3\x82\xA1", + "\xE3\x8C\x82" => "\xE3\x82\xA2\xE3\x83\xB3\xE3\x83\x9A\xE3\x82\xA2", + "\xE3\x8C\x83" => "\xE3\x82\xA2\xE3\x83\xBC\xE3\x83\xAB", + "\xE3\x8C\x84" => "\xE3\x82\xA4\xE3\x83\x8B\xE3\x83\xB3\xE3\x82\xB0", + "\xE3\x8C\x85" => "\xE3\x82\xA4\xE3\x83\xB3\xE3\x83\x81", + "\xE3\x8C\x86" => "\xE3\x82\xA6\xE3\x82\xA9\xE3\x83\xB3", + "\xE3\x8C\x87" => "\xE3\x82\xA8\xE3\x82\xB9\xE3\x82\xAF\xE3\x83\xBC\xE3\x83\x89", + "\xE3\x8C\x88" => "\xE3\x82\xA8\xE3\x83\xBC\xE3\x82\xAB\xE3\x83\xBC", + "\xE3\x8C\x89" => "\xE3\x82\xAA\xE3\x83\xB3\xE3\x82\xB9", + "\xE3\x8C\x8A" => "\xE3\x82\xAA\xE3\x83\xBC\xE3\x83\xA0", + "\xE3\x8C\x8B" => "\xE3\x82\xAB\xE3\x82\xA4\xE3\x83\xAA", + "\xE3\x8C\x8C" => "\xE3\x82\xAB\xE3\x83\xA9\xE3\x83\x83\xE3\x83\x88", + "\xE3\x8C\x8D" => "\xE3\x82\xAB\xE3\x83\xAD\xE3\x83\xAA\xE3\x83\xBC", + "\xE3\x8C\x8E" => "\xE3\x82\xAC\xE3\x83\xAD\xE3\x83\xB3", + "\xE3\x8C\x8F" => "\xE3\x82\xAC\xE3\x83\xB3\xE3\x83\x9E", + "\xE3\x8C\x90" => "\xE3\x82\xAE\xE3\x82\xAC", + "\xE3\x8C\x91" => "\xE3\x82\xAE\xE3\x83\x8B\xE3\x83\xBC", + "\xE3\x8C\x92" => "\xE3\x82\xAD\xE3\x83\xA5\xE3\x83\xAA\xE3\x83\xBC", + "\xE3\x8C\x93" => "\xE3\x82\xAE\xE3\x83\xAB\xE3\x83\x80\xE3\x83\xBC", + "\xE3\x8C\x94" => "\xE3\x82\xAD\xE3\x83\xAD", + "\xE3\x8C\x95" => "\xE3\x82\xAD\xE3\x83\xAD\xE3\x82\xB0\xE3\x83\xA9\xE3\x83\xA0", + "\xE3\x8C\x96" => "\xE3\x82\xAD\xE3\x83\xAD\xE3\x83\xA1\xE3\x83\xBC\xE3\x83\x88\xE3\x83\xAB", + "\xE3\x8C\x97" => "\xE3\x82\xAD\xE3\x83\xAD\xE3\x83\xAF\xE3\x83\x83\xE3\x83\x88", + "\xE3\x8C\x98" => "\xE3\x82\xB0\xE3\x83\xA9\xE3\x83\xA0", + "\xE3\x8C\x99" => "\xE3\x82\xB0\xE3\x83\xA9\xE3\x83\xA0\xE3\x83\x88\xE3\x83\xB3", + "\xE3\x8C\x9A" => "\xE3\x82\xAF\xE3\x83\xAB\xE3\x82\xBC\xE3\x82\xA4\xE3\x83\xAD", + "\xE3\x8C\x9B" => "\xE3\x82\xAF\xE3\x83\xAD\xE3\x83\xBC\xE3\x83\x8D", + "\xE3\x8C\x9C" => "\xE3\x82\xB1\xE3\x83\xBC\xE3\x82\xB9", + "\xE3\x8C\x9D" => "\xE3\x82\xB3\xE3\x83\xAB\xE3\x83\x8A", + "\xE3\x8C\x9E" => "\xE3\x82\xB3\xE3\x83\xBC\xE3\x83\x9D", + "\xE3\x8C\x9F" => "\xE3\x82\xB5\xE3\x82\xA4\xE3\x82\xAF\xE3\x83\xAB", + "\xE3\x8C\xA0" => "\xE3\x82\xB5\xE3\x83\xB3\xE3\x83\x81\xE3\x83\xBC\xE3\x83\xA0", + "\xE3\x8C\xA1" => "\xE3\x82\xB7\xE3\x83\xAA\xE3\x83\xB3\xE3\x82\xB0", + "\xE3\x8C\xA2" => "\xE3\x82\xBB\xE3\x83\xB3\xE3\x83\x81", + "\xE3\x8C\xA3" => "\xE3\x82\xBB\xE3\x83\xB3\xE3\x83\x88", + "\xE3\x8C\xA4" => "\xE3\x83\x80\xE3\x83\xBC\xE3\x82\xB9", + "\xE3\x8C\xA5" => "\xE3\x83\x87\xE3\x82\xB7", + "\xE3\x8C\xA6" => "\xE3\x83\x89\xE3\x83\xAB", + "\xE3\x8C\xA7" => "\xE3\x83\x88\xE3\x83\xB3", + "\xE3\x8C\xA8" => "\xE3\x83\x8A\xE3\x83\x8E", + "\xE3\x8C\xA9" => "\xE3\x83\x8E\xE3\x83\x83\xE3\x83\x88", + "\xE3\x8C\xAA" => "\xE3\x83\x8F\xE3\x82\xA4\xE3\x83\x84", + "\xE3\x8C\xAB" => "\xE3\x83\x91\xE3\x83\xBC\xE3\x82\xBB\xE3\x83\xB3\xE3\x83\x88", + "\xE3\x8C\xAC" => "\xE3\x83\x91\xE3\x83\xBC\xE3\x83\x84", + "\xE3\x8C\xAD" => "\xE3\x83\x90\xE3\x83\xBC\xE3\x83\xAC\xE3\x83\xAB", + "\xE3\x8C\xAE" => "\xE3\x83\x94\xE3\x82\xA2\xE3\x82\xB9\xE3\x83\x88\xE3\x83\xAB", + "\xE3\x8C\xAF" => "\xE3\x83\x94\xE3\x82\xAF\xE3\x83\xAB", + "\xE3\x8C\xB0" => "\xE3\x83\x94\xE3\x82\xB3", + "\xE3\x8C\xB1" => "\xE3\x83\x93\xE3\x83\xAB", + "\xE3\x8C\xB2" => "\xE3\x83\x95\xE3\x82\xA1\xE3\x83\xA9\xE3\x83\x83\xE3\x83\x89", + "\xE3\x8C\xB3" => "\xE3\x83\x95\xE3\x82\xA3\xE3\x83\xBC\xE3\x83\x88", + "\xE3\x8C\xB4" => "\xE3\x83\x96\xE3\x83\x83\xE3\x82\xB7\xE3\x82\xA7\xE3\x83\xAB", + "\xE3\x8C\xB5" => "\xE3\x83\x95\xE3\x83\xA9\xE3\x83\xB3", + "\xE3\x8C\xB6" => "\xE3\x83\x98\xE3\x82\xAF\xE3\x82\xBF\xE3\x83\xBC\xE3\x83\xAB", + "\xE3\x8C\xB7" => "\xE3\x83\x9A\xE3\x82\xBD", + "\xE3\x8C\xB8" => "\xE3\x83\x9A\xE3\x83\x8B\xE3\x83\x92", + "\xE3\x8C\xB9" => "\xE3\x83\x98\xE3\x83\xAB\xE3\x83\x84", + "\xE3\x8C\xBA" => "\xE3\x83\x9A\xE3\x83\xB3\xE3\x82\xB9", + "\xE3\x8C\xBB" => "\xE3\x83\x9A\xE3\x83\xBC\xE3\x82\xB8", + "\xE3\x8C\xBC" => "\xE3\x83\x99\xE3\x83\xBC\xE3\x82\xBF", + "\xE3\x8C\xBD" => "\xE3\x83\x9D\xE3\x82\xA4\xE3\x83\xB3\xE3\x83\x88", + "\xE3\x8C\xBE" => "\xE3\x83\x9C\xE3\x83\xAB\xE3\x83\x88", + "\xE3\x8C\xBF" => "\xE3\x83\x9B\xE3\x83\xB3", + "\xE3\x8D\x80" => "\xE3\x83\x9D\xE3\x83\xB3\xE3\x83\x89", + "\xE3\x8D\x81" => "\xE3\x83\x9B\xE3\x83\xBC\xE3\x83\xAB", + "\xE3\x8D\x82" => "\xE3\x83\x9B\xE3\x83\xBC\xE3\x83\xB3", + "\xE3\x8D\x83" => "\xE3\x83\x9E\xE3\x82\xA4\xE3\x82\xAF\xE3\x83\xAD", + "\xE3\x8D\x84" => "\xE3\x83\x9E\xE3\x82\xA4\xE3\x83\xAB", + "\xE3\x8D\x85" => "\xE3\x83\x9E\xE3\x83\x83\xE3\x83\x8F", + "\xE3\x8D\x86" => "\xE3\x83\x9E\xE3\x83\xAB\xE3\x82\xAF", + "\xE3\x8D\x87" => "\xE3\x83\x9E\xE3\x83\xB3\xE3\x82\xB7\xE3\x83\xA7\xE3\x83\xB3", + "\xE3\x8D\x88" => "\xE3\x83\x9F\xE3\x82\xAF\xE3\x83\xAD\xE3\x83\xB3", + "\xE3\x8D\x89" => "\xE3\x83\x9F\xE3\x83\xAA", + "\xE3\x8D\x8A" => "\xE3\x83\x9F\xE3\x83\xAA\xE3\x83\x90\xE3\x83\xBC\xE3\x83\xAB", + "\xE3\x8D\x8B" => "\xE3\x83\xA1\xE3\x82\xAC", + "\xE3\x8D\x8C" => "\xE3\x83\xA1\xE3\x82\xAC\xE3\x83\x88\xE3\x83\xB3", + "\xE3\x8D\x8D" => "\xE3\x83\xA1\xE3\x83\xBC\xE3\x83\x88\xE3\x83\xAB", + "\xE3\x8D\x8E" => "\xE3\x83\xA4\xE3\x83\xBC\xE3\x83\x89", + "\xE3\x8D\x8F" => "\xE3\x83\xA4\xE3\x83\xBC\xE3\x83\xAB", + "\xE3\x8D\x90" => "\xE3\x83\xA6\xE3\x82\xA2\xE3\x83\xB3", + "\xE3\x8D\x91" => "\xE3\x83\xAA\xE3\x83\x83\xE3\x83\x88\xE3\x83\xAB", + "\xE3\x8D\x92" => "\xE3\x83\xAA\xE3\x83\xA9", + "\xE3\x8D\x93" => "\xE3\x83\xAB\xE3\x83\x94\xE3\x83\xBC", + "\xE3\x8D\x94" => "\xE3\x83\xAB\xE3\x83\xBC\xE3\x83\x96\xE3\x83\xAB", + "\xE3\x8D\x95" => "\xE3\x83\xAC\xE3\x83\xA0", + "\xE3\x8D\x96" => "\xE3\x83\xAC\xE3\x83\xB3\xE3\x83\x88\xE3\x82\xB2\xE3\x83\xB3", + "\xE3\x8D\x97" => "\xE3\x83\xAF\xE3\x83\x83\xE3\x83\x88", + "\xE3\x8D\x98" => "\x30\xE7\x82\xB9", + "\xE3\x8D\x99" => "\x31\xE7\x82\xB9", + "\xE3\x8D\x9A" => "\x32\xE7\x82\xB9", + "\xE3\x8D\x9B" => "\x33\xE7\x82\xB9", + "\xE3\x8D\x9C" => "\x34\xE7\x82\xB9", + "\xE3\x8D\x9D" => "\x35\xE7\x82\xB9", + "\xE3\x8D\x9E" => "\x36\xE7\x82\xB9", + "\xE3\x8D\x9F" => "\x37\xE7\x82\xB9", + "\xE3\x8D\xA0" => "\x38\xE7\x82\xB9", + "\xE3\x8D\xA1" => "\x39\xE7\x82\xB9", + "\xE3\x8D\xA2" => "\x31\x30\xE7\x82\xB9", + "\xE3\x8D\xA3" => "\x31\x31\xE7\x82\xB9", + "\xE3\x8D\xA4" => "\x31\x32\xE7\x82\xB9", + "\xE3\x8D\xA5" => "\x31\x33\xE7\x82\xB9", + "\xE3\x8D\xA6" => "\x31\x34\xE7\x82\xB9", + "\xE3\x8D\xA7" => "\x31\x35\xE7\x82\xB9", + "\xE3\x8D\xA8" => "\x31\x36\xE7\x82\xB9", + "\xE3\x8D\xA9" => "\x31\x37\xE7\x82\xB9", + "\xE3\x8D\xAA" => "\x31\x38\xE7\x82\xB9", + "\xE3\x8D\xAB" => "\x31\x39\xE7\x82\xB9", + "\xE3\x8D\xAC" => "\x32\x30\xE7\x82\xB9", + "\xE3\x8D\xAD" => "\x32\x31\xE7\x82\xB9", + "\xE3\x8D\xAE" => "\x32\x32\xE7\x82\xB9", + "\xE3\x8D\xAF" => "\x32\x33\xE7\x82\xB9", + "\xE3\x8D\xB0" => "\x32\x34\xE7\x82\xB9", + "\xE3\x8D\xB1" => "\x68\x70\x61", + "\xE3\x8D\xB2" => "\x64\x61", + "\xE3\x8D\xB3" => "\x61\x75", + "\xE3\x8D\xB4" => "\x62\x61\x72", + "\xE3\x8D\xB5" => "\x6F\x76", + "\xE3\x8D\xB6" => "\x70\x63", + "\xE3\x8D\xB7" => "\x64\x6D", + "\xE3\x8D\xB8" => "\x64\x6D\x32", + "\xE3\x8D\xB9" => "\x64\x6D\x33", + "\xE3\x8D\xBA" => "\x69\x75", + "\xE3\x8D\xBB" => "\xE5\xB9\xB3\xE6\x88\x90", + "\xE3\x8D\xBC" => "\xE6\x98\xAD\xE5\x92\x8C", + "\xE3\x8D\xBD" => "\xE5\xA4\xA7\xE6\xAD\xA3", + "\xE3\x8D\xBE" => "\xE6\x98\x8E\xE6\xB2\xBB", + "\xE3\x8D\xBF" => "\xE6\xA0\xAA\xE5\xBC\x8F\xE4\xBC\x9A\xE7\xA4\xBE", + "\xE3\x8E\x80" => "\x70\x61", + "\xE3\x8E\x81" => "\x6E\x61", + "\xE3\x8E\x82" => "\xCE\xBC\x61", + "\xE3\x8E\x83" => "\x6D\x61", + "\xE3\x8E\x84" => "\x6B\x61", + "\xE3\x8E\x85" => "\x6B\x62", + "\xE3\x8E\x86" => "\x6D\x62", + "\xE3\x8E\x87" => "\x67\x62", + "\xE3\x8E\x88" => "\x63\x61\x6C", + "\xE3\x8E\x89" => "\x6B\x63\x61\x6C", + "\xE3\x8E\x8A" => "\x70\x66", + "\xE3\x8E\x8B" => "\x6E\x66", + "\xE3\x8E\x8C" => "\xCE\xBC\x66", + "\xE3\x8E\x8D" => "\xCE\xBC\x67", + "\xE3\x8E\x8E" => "\x6D\x67", + "\xE3\x8E\x8F" => "\x6B\x67", + "\xE3\x8E\x90" => "\x68\x7A", + "\xE3\x8E\x91" => "\x6B\x68\x7A", + "\xE3\x8E\x92" => "\x6D\x68\x7A", + "\xE3\x8E\x93" => "\x67\x68\x7A", + "\xE3\x8E\x94" => "\x74\x68\x7A", + "\xE3\x8E\x95" => "\xCE\xBC\x6C", + "\xE3\x8E\x96" => "\x6D\x6C", + "\xE3\x8E\x97" => "\x64\x6C", + "\xE3\x8E\x98" => "\x6B\x6C", + "\xE3\x8E\x99" => "\x66\x6D", + "\xE3\x8E\x9A" => "\x6E\x6D", + "\xE3\x8E\x9B" => "\xCE\xBC\x6D", + "\xE3\x8E\x9C" => "\x6D\x6D", + "\xE3\x8E\x9D" => "\x63\x6D", + "\xE3\x8E\x9E" => "\x6B\x6D", + "\xE3\x8E\x9F" => "\x6D\x6D\x32", + "\xE3\x8E\xA0" => "\x63\x6D\x32", + "\xE3\x8E\xA1" => "\x6D\x32", + "\xE3\x8E\xA2" => "\x6B\x6D\x32", + "\xE3\x8E\xA3" => "\x6D\x6D\x33", + "\xE3\x8E\xA4" => "\x63\x6D\x33", + "\xE3\x8E\xA5" => "\x6D\x33", + "\xE3\x8E\xA6" => "\x6B\x6D\x33", + "\xE3\x8E\xA7" => "\x6D\xE2\x88\x95\x73", + "\xE3\x8E\xA8" => "\x6D\xE2\x88\x95\x73\x32", + "\xE3\x8E\xA9" => "\x70\x61", + "\xE3\x8E\xAA" => "\x6B\x70\x61", + "\xE3\x8E\xAB" => "\x6D\x70\x61", + "\xE3\x8E\xAC" => "\x67\x70\x61", + "\xE3\x8E\xAD" => "\x72\x61\x64", + "\xE3\x8E\xAE" => "\x72\x61\x64\xE2\x88\x95\x73", + "\xE3\x8E\xAF" => "\x72\x61\x64\xE2\x88\x95\x73\x32", + "\xE3\x8E\xB0" => "\x70\x73", + "\xE3\x8E\xB1" => "\x6E\x73", + "\xE3\x8E\xB2" => "\xCE\xBC\x73", + "\xE3\x8E\xB3" => "\x6D\x73", + "\xE3\x8E\xB4" => "\x70\x76", + "\xE3\x8E\xB5" => "\x6E\x76", + "\xE3\x8E\xB6" => "\xCE\xBC\x76", + "\xE3\x8E\xB7" => "\x6D\x76", + "\xE3\x8E\xB8" => "\x6B\x76", + "\xE3\x8E\xB9" => "\x6D\x76", + "\xE3\x8E\xBA" => "\x70\x77", + "\xE3\x8E\xBB" => "\x6E\x77", + "\xE3\x8E\xBC" => "\xCE\xBC\x77", + "\xE3\x8E\xBD" => "\x6D\x77", + "\xE3\x8E\xBE" => "\x6B\x77", + "\xE3\x8E\xBF" => "\x6D\x77", + "\xE3\x8F\x80" => "\x6B\xCF\x89", + "\xE3\x8F\x81" => "\x6D\xCF\x89", + "\xE3\x8F\x83" => "\x62\x71", + "\xE3\x8F\x84" => "\x63\x63", + "\xE3\x8F\x85" => "\x63\x64", + "\xE3\x8F\x86" => "\x63\xE2\x88\x95\x6B\x67", + "\xE3\x8F\x88" => "\x64\x62", + "\xE3\x8F\x89" => "\x67\x79", + "\xE3\x8F\x8A" => "\x68\x61", + "\xE3\x8F\x8B" => "\x68\x70", + "\xE3\x8F\x8C" => "\x69\x6E", + "\xE3\x8F\x8D" => "\x6B\x6B", + "\xE3\x8F\x8E" => "\x6B\x6D", + "\xE3\x8F\x8F" => "\x6B\x74", + "\xE3\x8F\x90" => "\x6C\x6D", + "\xE3\x8F\x91" => "\x6C\x6E", + "\xE3\x8F\x92" => "\x6C\x6F\x67", + "\xE3\x8F\x93" => "\x6C\x78", + "\xE3\x8F\x94" => "\x6D\x62", + "\xE3\x8F\x95" => "\x6D\x69\x6C", + "\xE3\x8F\x96" => "\x6D\x6F\x6C", + "\xE3\x8F\x97" => "\x70\x68", + "\xE3\x8F\x99" => "\x70\x70\x6D", + "\xE3\x8F\x9A" => "\x70\x72", + "\xE3\x8F\x9B" => "\x73\x72", + "\xE3\x8F\x9C" => "\x73\x76", + "\xE3\x8F\x9D" => "\x77\x62", + "\xE3\x8F\x9E" => "\x76\xE2\x88\x95\x6D", + "\xE3\x8F\x9F" => "\x61\xE2\x88\x95\x6D", + "\xE3\x8F\xA0" => "\x31\xE6\x97\xA5", + "\xE3\x8F\xA1" => "\x32\xE6\x97\xA5", + "\xE3\x8F\xA2" => "\x33\xE6\x97\xA5", + "\xE3\x8F\xA3" => "\x34\xE6\x97\xA5", + "\xE3\x8F\xA4" => "\x35\xE6\x97\xA5", + "\xE3\x8F\xA5" => "\x36\xE6\x97\xA5", + "\xE3\x8F\xA6" => "\x37\xE6\x97\xA5", + "\xE3\x8F\xA7" => "\x38\xE6\x97\xA5", + "\xE3\x8F\xA8" => "\x39\xE6\x97\xA5", + "\xE3\x8F\xA9" => "\x31\x30\xE6\x97\xA5", + "\xE3\x8F\xAA" => "\x31\x31\xE6\x97\xA5", + "\xE3\x8F\xAB" => "\x31\x32\xE6\x97\xA5", + "\xE3\x8F\xAC" => "\x31\x33\xE6\x97\xA5", + "\xE3\x8F\xAD" => "\x31\x34\xE6\x97\xA5", + "\xE3\x8F\xAE" => "\x31\x35\xE6\x97\xA5", + "\xE3\x8F\xAF" => "\x31\x36\xE6\x97\xA5", + "\xE3\x8F\xB0" => "\x31\x37\xE6\x97\xA5", + "\xE3\x8F\xB1" => "\x31\x38\xE6\x97\xA5", + "\xE3\x8F\xB2" => "\x31\x39\xE6\x97\xA5", + "\xE3\x8F\xB3" => "\x32\x30\xE6\x97\xA5", + "\xE3\x8F\xB4" => "\x32\x31\xE6\x97\xA5", + "\xE3\x8F\xB5" => "\x32\x32\xE6\x97\xA5", + "\xE3\x8F\xB6" => "\x32\x33\xE6\x97\xA5", + "\xE3\x8F\xB7" => "\x32\x34\xE6\x97\xA5", + "\xE3\x8F\xB8" => "\x32\x35\xE6\x97\xA5", + "\xE3\x8F\xB9" => "\x32\x36\xE6\x97\xA5", + "\xE3\x8F\xBA" => "\x32\x37\xE6\x97\xA5", + "\xE3\x8F\xBB" => "\x32\x38\xE6\x97\xA5", + "\xE3\x8F\xBC" => "\x32\x39\xE6\x97\xA5", + "\xE3\x8F\xBD" => "\x33\x30\xE6\x97\xA5", + "\xE3\x8F\xBE" => "\x33\x31\xE6\x97\xA5", + "\xE3\x8F\xBF" => "\x67\x61\x6C", + "\xEA\x99\x80" => "\xEA\x99\x81", + "\xEA\x99\x82" => "\xEA\x99\x83", + "\xEA\x99\x84" => "\xEA\x99\x85", + "\xEA\x99\x86" => "\xEA\x99\x87", + "\xEA\x99\x88" => "\xEA\x99\x89", + "\xEA\x99\x8A" => "\xEA\x99\x8B", + "\xEA\x99\x8C" => "\xEA\x99\x8D", + "\xEA\x99\x8E" => "\xEA\x99\x8F", + "\xEA\x99\x90" => "\xEA\x99\x91", + "\xEA\x99\x92" => "\xEA\x99\x93", + "\xEA\x99\x94" => "\xEA\x99\x95", + "\xEA\x99\x96" => "\xEA\x99\x97", + "\xEA\x99\x98" => "\xEA\x99\x99", + "\xEA\x99\x9A" => "\xEA\x99\x9B", + "\xEA\x99\x9C" => "\xEA\x99\x9D", + "\xEA\x99\x9E" => "\xEA\x99\x9F", + "\xEA\x99\xA0" => "\xEA\x99\xA1", + "\xEA\x99\xA2" => "\xEA\x99\xA3", + "\xEA\x99\xA4" => "\xEA\x99\xA5", + "\xEA\x99\xA6" => "\xEA\x99\xA7", + "\xEA\x99\xA8" => "\xEA\x99\xA9", + "\xEA\x99\xAA" => "\xEA\x99\xAB", + "\xEA\x99\xAC" => "\xEA\x99\xAD", + "\xEA\x9A\x80" => "\xEA\x9A\x81", + "\xEA\x9A\x82" => "\xEA\x9A\x83", + "\xEA\x9A\x84" => "\xEA\x9A\x85", + "\xEA\x9A\x86" => "\xEA\x9A\x87", + "\xEA\x9A\x88" => "\xEA\x9A\x89", + "\xEA\x9A\x8A" => "\xEA\x9A\x8B", + "\xEA\x9A\x8C" => "\xEA\x9A\x8D", + "\xEA\x9A\x8E" => "\xEA\x9A\x8F", + "\xEA\x9A\x90" => "\xEA\x9A\x91", + "\xEA\x9A\x92" => "\xEA\x9A\x93", + "\xEA\x9A\x94" => "\xEA\x9A\x95", + "\xEA\x9A\x96" => "\xEA\x9A\x97", + "\xEA\x9A\x98" => "\xEA\x9A\x99", + "\xEA\x9A\x9A" => "\xEA\x9A\x9B", + "\xEA\x9A\x9C" => "\xD1\x8A", + "\xEA\x9A\x9D" => "\xD1\x8C", + "\xEA\x9C\xA2" => "\xEA\x9C\xA3", + "\xEA\x9C\xA4" => "\xEA\x9C\xA5", + "\xEA\x9C\xA6" => "\xEA\x9C\xA7", + "\xEA\x9C\xA8" => "\xEA\x9C\xA9", + "\xEA\x9C\xAA" => "\xEA\x9C\xAB", + "\xEA\x9C\xAC" => "\xEA\x9C\xAD", + "\xEA\x9C\xAE" => "\xEA\x9C\xAF", + "\xEA\x9C\xB2" => "\xEA\x9C\xB3", + "\xEA\x9C\xB4" => "\xEA\x9C\xB5", + "\xEA\x9C\xB6" => "\xEA\x9C\xB7", + "\xEA\x9C\xB8" => "\xEA\x9C\xB9", + "\xEA\x9C\xBA" => "\xEA\x9C\xBB", + "\xEA\x9C\xBC" => "\xEA\x9C\xBD", + "\xEA\x9C\xBE" => "\xEA\x9C\xBF", + "\xEA\x9D\x80" => "\xEA\x9D\x81", + "\xEA\x9D\x82" => "\xEA\x9D\x83", + "\xEA\x9D\x84" => "\xEA\x9D\x85", + "\xEA\x9D\x86" => "\xEA\x9D\x87", + "\xEA\x9D\x88" => "\xEA\x9D\x89", + "\xEA\x9D\x8A" => "\xEA\x9D\x8B", + "\xEA\x9D\x8C" => "\xEA\x9D\x8D", + "\xEA\x9D\x8E" => "\xEA\x9D\x8F", + "\xEA\x9D\x90" => "\xEA\x9D\x91", + "\xEA\x9D\x92" => "\xEA\x9D\x93", + "\xEA\x9D\x94" => "\xEA\x9D\x95", + "\xEA\x9D\x96" => "\xEA\x9D\x97", + "\xEA\x9D\x98" => "\xEA\x9D\x99", + "\xEA\x9D\x9A" => "\xEA\x9D\x9B", + "\xEA\x9D\x9C" => "\xEA\x9D\x9D", + "\xEA\x9D\x9E" => "\xEA\x9D\x9F", + "\xEA\x9D\xA0" => "\xEA\x9D\xA1", + "\xEA\x9D\xA2" => "\xEA\x9D\xA3", + "\xEA\x9D\xA4" => "\xEA\x9D\xA5", + "\xEA\x9D\xA6" => "\xEA\x9D\xA7", + "\xEA\x9D\xA8" => "\xEA\x9D\xA9", + "\xEA\x9D\xAA" => "\xEA\x9D\xAB", + "\xEA\x9D\xAC" => "\xEA\x9D\xAD", + "\xEA\x9D\xAE" => "\xEA\x9D\xAF", + "\xEA\x9D\xB0" => "\xEA\x9D\xAF", + "\xEA\x9D\xB9" => "\xEA\x9D\xBA", + "\xEA\x9D\xBB" => "\xEA\x9D\xBC", + "\xEA\x9D\xBD" => "\xE1\xB5\xB9", + "\xEA\x9D\xBE" => "\xEA\x9D\xBF", + "\xEA\x9E\x80" => "\xEA\x9E\x81", + "\xEA\x9E\x82" => "\xEA\x9E\x83", + "\xEA\x9E\x84" => "\xEA\x9E\x85", + "\xEA\x9E\x86" => "\xEA\x9E\x87", + "\xEA\x9E\x8B" => "\xEA\x9E\x8C", + "\xEA\x9E\x8D" => "\xC9\xA5", + "\xEA\x9E\x90" => "\xEA\x9E\x91", + "\xEA\x9E\x92" => "\xEA\x9E\x93", + "\xEA\x9E\x96" => "\xEA\x9E\x97", + "\xEA\x9E\x98" => "\xEA\x9E\x99", + "\xEA\x9E\x9A" => "\xEA\x9E\x9B", + "\xEA\x9E\x9C" => "\xEA\x9E\x9D", + "\xEA\x9E\x9E" => "\xEA\x9E\x9F", + "\xEA\x9E\xA0" => "\xEA\x9E\xA1", + "\xEA\x9E\xA2" => "\xEA\x9E\xA3", + "\xEA\x9E\xA4" => "\xEA\x9E\xA5", + "\xEA\x9E\xA6" => "\xEA\x9E\xA7", + "\xEA\x9E\xA8" => "\xEA\x9E\xA9", + "\xEA\x9E\xAA" => "\xC9\xA6", + "\xEA\x9E\xAB" => "\xC9\x9C", + "\xEA\x9E\xAC" => "\xC9\xA1", + "\xEA\x9E\xAD" => "\xC9\xAC", + "\xEA\x9E\xAE" => "\xC9\xAA", + "\xEA\x9E\xB0" => "\xCA\x9E", + "\xEA\x9E\xB1" => "\xCA\x87", + "\xEA\x9E\xB2" => "\xCA\x9D", + "\xEA\x9E\xB3" => "\xEA\xAD\x93", + "\xEA\x9E\xB4" => "\xEA\x9E\xB5", + "\xEA\x9E\xB6" => "\xEA\x9E\xB7", + "\xEA\x9E\xB8" => "\xEA\x9E\xB9", + "\xEA\x9E\xBA" => "\xEA\x9E\xBB", + "\xEA\x9E\xBC" => "\xEA\x9E\xBD", + "\xEA\x9E\xBE" => "\xEA\x9E\xBF", + "\xEA\x9F\x80" => "\xEA\x9F\x81", + "\xEA\x9F\x82" => "\xEA\x9F\x83", + "\xEA\x9F\x84" => "\xEA\x9E\x94", + "\xEA\x9F\x85" => "\xCA\x82", + "\xEA\x9F\x86" => "\xE1\xB6\x8E", + "\xEA\x9F\x87" => "\xEA\x9F\x88", + "\xEA\x9F\x89" => "\xEA\x9F\x8A", + "\xEA\x9F\x90" => "\xEA\x9F\x91", + "\xEA\x9F\x96" => "\xEA\x9F\x97", + "\xEA\x9F\x98" => "\xEA\x9F\x99", + "\xEA\x9F\xB2" => "\x63", + "\xEA\x9F\xB3" => "\x66", + "\xEA\x9F\xB4" => "\x71", + "\xEA\x9F\xB5" => "\xEA\x9F\xB6", + "\xEA\x9F\xB8" => "\xC4\xA7", + "\xEA\x9F\xB9" => "\xC5\x93", + "\xEA\xAD\x9C" => "\xEA\x9C\xA7", + "\xEA\xAD\x9D" => "\xEA\xAC\xB7", + "\xEA\xAD\x9E" => "\xC9\xAB", + "\xEA\xAD\x9F" => "\xEA\xAD\x92", + "\xEA\xAD\xA9" => "\xCA\x8D", + "\xEA\xAD\xB0" => "\xE1\x8E\xA0", + "\xEA\xAD\xB1" => "\xE1\x8E\xA1", + "\xEA\xAD\xB2" => "\xE1\x8E\xA2", + "\xEA\xAD\xB3" => "\xE1\x8E\xA3", + "\xEA\xAD\xB4" => "\xE1\x8E\xA4", + "\xEA\xAD\xB5" => "\xE1\x8E\xA5", + "\xEA\xAD\xB6" => "\xE1\x8E\xA6", + "\xEA\xAD\xB7" => "\xE1\x8E\xA7", + "\xEA\xAD\xB8" => "\xE1\x8E\xA8", + "\xEA\xAD\xB9" => "\xE1\x8E\xA9", + "\xEA\xAD\xBA" => "\xE1\x8E\xAA", + "\xEA\xAD\xBB" => "\xE1\x8E\xAB", + "\xEA\xAD\xBC" => "\xE1\x8E\xAC", + "\xEA\xAD\xBD" => "\xE1\x8E\xAD", + "\xEA\xAD\xBE" => "\xE1\x8E\xAE", + "\xEA\xAD\xBF" => "\xE1\x8E\xAF", + "\xEA\xAE\x80" => "\xE1\x8E\xB0", + "\xEA\xAE\x81" => "\xE1\x8E\xB1", + "\xEA\xAE\x82" => "\xE1\x8E\xB2", + "\xEA\xAE\x83" => "\xE1\x8E\xB3", + "\xEA\xAE\x84" => "\xE1\x8E\xB4", + "\xEA\xAE\x85" => "\xE1\x8E\xB5", + "\xEA\xAE\x86" => "\xE1\x8E\xB6", + "\xEA\xAE\x87" => "\xE1\x8E\xB7", + "\xEA\xAE\x88" => "\xE1\x8E\xB8", + "\xEA\xAE\x89" => "\xE1\x8E\xB9", + "\xEA\xAE\x8A" => "\xE1\x8E\xBA", + "\xEA\xAE\x8B" => "\xE1\x8E\xBB", + "\xEA\xAE\x8C" => "\xE1\x8E\xBC", + "\xEA\xAE\x8D" => "\xE1\x8E\xBD", + "\xEA\xAE\x8E" => "\xE1\x8E\xBE", + "\xEA\xAE\x8F" => "\xE1\x8E\xBF", + "\xEA\xAE\x90" => "\xE1\x8F\x80", + "\xEA\xAE\x91" => "\xE1\x8F\x81", + "\xEA\xAE\x92" => "\xE1\x8F\x82", + "\xEA\xAE\x93" => "\xE1\x8F\x83", + "\xEA\xAE\x94" => "\xE1\x8F\x84", + "\xEA\xAE\x95" => "\xE1\x8F\x85", + "\xEA\xAE\x96" => "\xE1\x8F\x86", + "\xEA\xAE\x97" => "\xE1\x8F\x87", + "\xEA\xAE\x98" => "\xE1\x8F\x88", + "\xEA\xAE\x99" => "\xE1\x8F\x89", + "\xEA\xAE\x9A" => "\xE1\x8F\x8A", + "\xEA\xAE\x9B" => "\xE1\x8F\x8B", + "\xEA\xAE\x9C" => "\xE1\x8F\x8C", + "\xEA\xAE\x9D" => "\xE1\x8F\x8D", + "\xEA\xAE\x9E" => "\xE1\x8F\x8E", + "\xEA\xAE\x9F" => "\xE1\x8F\x8F", + "\xEA\xAE\xA0" => "\xE1\x8F\x90", + "\xEA\xAE\xA1" => "\xE1\x8F\x91", + "\xEA\xAE\xA2" => "\xE1\x8F\x92", + "\xEA\xAE\xA3" => "\xE1\x8F\x93", + "\xEA\xAE\xA4" => "\xE1\x8F\x94", + "\xEA\xAE\xA5" => "\xE1\x8F\x95", + "\xEA\xAE\xA6" => "\xE1\x8F\x96", + "\xEA\xAE\xA7" => "\xE1\x8F\x97", + "\xEA\xAE\xA8" => "\xE1\x8F\x98", + "\xEA\xAE\xA9" => "\xE1\x8F\x99", + "\xEA\xAE\xAA" => "\xE1\x8F\x9A", + "\xEA\xAE\xAB" => "\xE1\x8F\x9B", + "\xEA\xAE\xAC" => "\xE1\x8F\x9C", + "\xEA\xAE\xAD" => "\xE1\x8F\x9D", + "\xEA\xAE\xAE" => "\xE1\x8F\x9E", + "\xEA\xAE\xAF" => "\xE1\x8F\x9F", + "\xEA\xAE\xB0" => "\xE1\x8F\xA0", + "\xEA\xAE\xB1" => "\xE1\x8F\xA1", + "\xEA\xAE\xB2" => "\xE1\x8F\xA2", + "\xEA\xAE\xB3" => "\xE1\x8F\xA3", + "\xEA\xAE\xB4" => "\xE1\x8F\xA4", + "\xEA\xAE\xB5" => "\xE1\x8F\xA5", + "\xEA\xAE\xB6" => "\xE1\x8F\xA6", + "\xEA\xAE\xB7" => "\xE1\x8F\xA7", + "\xEA\xAE\xB8" => "\xE1\x8F\xA8", + "\xEA\xAE\xB9" => "\xE1\x8F\xA9", + "\xEA\xAE\xBA" => "\xE1\x8F\xAA", + "\xEA\xAE\xBB" => "\xE1\x8F\xAB", + "\xEA\xAE\xBC" => "\xE1\x8F\xAC", + "\xEA\xAE\xBD" => "\xE1\x8F\xAD", + "\xEA\xAE\xBE" => "\xE1\x8F\xAE", + "\xEA\xAE\xBF" => "\xE1\x8F\xAF", + "\xEF\xA4\x80" => "\xE8\xB1\x88", + "\xEF\xA4\x81" => "\xE6\x9B\xB4", + "\xEF\xA4\x82" => "\xE8\xBB\x8A", + "\xEF\xA4\x83" => "\xE8\xB3\x88", + "\xEF\xA4\x84" => "\xE6\xBB\x91", + "\xEF\xA4\x85" => "\xE4\xB8\xB2", + "\xEF\xA4\x86" => "\xE5\x8F\xA5", + "\xEF\xA4\x87" => "\xE9\xBE\x9C", + "\xEF\xA4\x88" => "\xE9\xBE\x9C", + "\xEF\xA4\x89" => "\xE5\xA5\x91", + "\xEF\xA4\x8A" => "\xE9\x87\x91", + "\xEF\xA4\x8B" => "\xE5\x96\x87", + "\xEF\xA4\x8C" => "\xE5\xA5\x88", + "\xEF\xA4\x8D" => "\xE6\x87\xB6", + "\xEF\xA4\x8E" => "\xE7\x99\xA9", + "\xEF\xA4\x8F" => "\xE7\xBE\x85", + "\xEF\xA4\x90" => "\xE8\x98\xBF", + "\xEF\xA4\x91" => "\xE8\x9E\xBA", + "\xEF\xA4\x92" => "\xE8\xA3\xB8", + "\xEF\xA4\x93" => "\xE9\x82\x8F", + "\xEF\xA4\x94" => "\xE6\xA8\x82", + "\xEF\xA4\x95" => "\xE6\xB4\x9B", + "\xEF\xA4\x96" => "\xE7\x83\x99", + "\xEF\xA4\x97" => "\xE7\x8F\x9E", + "\xEF\xA4\x98" => "\xE8\x90\xBD", + "\xEF\xA4\x99" => "\xE9\x85\xAA", + "\xEF\xA4\x9A" => "\xE9\xA7\xB1", + "\xEF\xA4\x9B" => "\xE4\xBA\x82", + "\xEF\xA4\x9C" => "\xE5\x8D\xB5", + "\xEF\xA4\x9D" => "\xE6\xAC\x84", + "\xEF\xA4\x9E" => "\xE7\x88\x9B", + "\xEF\xA4\x9F" => "\xE8\x98\xAD", + "\xEF\xA4\xA0" => "\xE9\xB8\x9E", + "\xEF\xA4\xA1" => "\xE5\xB5\x90", + "\xEF\xA4\xA2" => "\xE6\xBF\xAB", + "\xEF\xA4\xA3" => "\xE8\x97\x8D", + "\xEF\xA4\xA4" => "\xE8\xA5\xA4", + "\xEF\xA4\xA5" => "\xE6\x8B\x89", + "\xEF\xA4\xA6" => "\xE8\x87\x98", + "\xEF\xA4\xA7" => "\xE8\xA0\x9F", + "\xEF\xA4\xA8" => "\xE5\xBB\x8A", + "\xEF\xA4\xA9" => "\xE6\x9C\x97", + "\xEF\xA4\xAA" => "\xE6\xB5\xAA", + "\xEF\xA4\xAB" => "\xE7\x8B\xBC", + "\xEF\xA4\xAC" => "\xE9\x83\x8E", + "\xEF\xA4\xAD" => "\xE4\xBE\x86", + "\xEF\xA4\xAE" => "\xE5\x86\xB7", + "\xEF\xA4\xAF" => "\xE5\x8B\x9E", + "\xEF\xA4\xB0" => "\xE6\x93\x84", + "\xEF\xA4\xB1" => "\xE6\xAB\x93", + "\xEF\xA4\xB2" => "\xE7\x88\x90", + "\xEF\xA4\xB3" => "\xE7\x9B\xA7", + "\xEF\xA4\xB4" => "\xE8\x80\x81", + "\xEF\xA4\xB5" => "\xE8\x98\x86", + "\xEF\xA4\xB6" => "\xE8\x99\x9C", + "\xEF\xA4\xB7" => "\xE8\xB7\xAF", + "\xEF\xA4\xB8" => "\xE9\x9C\xB2", + "\xEF\xA4\xB9" => "\xE9\xAD\xAF", + "\xEF\xA4\xBA" => "\xE9\xB7\xBA", + "\xEF\xA4\xBB" => "\xE7\xA2\x8C", + "\xEF\xA4\xBC" => "\xE7\xA5\xBF", + "\xEF\xA4\xBD" => "\xE7\xB6\xA0", + "\xEF\xA4\xBE" => "\xE8\x8F\x89", + "\xEF\xA4\xBF" => "\xE9\x8C\x84", + "\xEF\xA5\x80" => "\xE9\xB9\xBF", + "\xEF\xA5\x81" => "\xE8\xAB\x96", + "\xEF\xA5\x82" => "\xE5\xA3\x9F", + "\xEF\xA5\x83" => "\xE5\xBC\x84", + "\xEF\xA5\x84" => "\xE7\xB1\xA0", + "\xEF\xA5\x85" => "\xE8\x81\xBE", + "\xEF\xA5\x86" => "\xE7\x89\xA2", + "\xEF\xA5\x87" => "\xE7\xA3\x8A", + "\xEF\xA5\x88" => "\xE8\xB3\x82", + "\xEF\xA5\x89" => "\xE9\x9B\xB7", + "\xEF\xA5\x8A" => "\xE5\xA3\x98", + "\xEF\xA5\x8B" => "\xE5\xB1\xA2", + "\xEF\xA5\x8C" => "\xE6\xA8\x93", + "\xEF\xA5\x8D" => "\xE6\xB7\x9A", + "\xEF\xA5\x8E" => "\xE6\xBC\x8F", + "\xEF\xA5\x8F" => "\xE7\xB4\xAF", + "\xEF\xA5\x90" => "\xE7\xB8\xB7", + "\xEF\xA5\x91" => "\xE9\x99\x8B", + "\xEF\xA5\x92" => "\xE5\x8B\x92", + "\xEF\xA5\x93" => "\xE8\x82\x8B", + "\xEF\xA5\x94" => "\xE5\x87\x9C", + "\xEF\xA5\x95" => "\xE5\x87\x8C", + "\xEF\xA5\x96" => "\xE7\xA8\x9C", + "\xEF\xA5\x97" => "\xE7\xB6\xBE", + "\xEF\xA5\x98" => "\xE8\x8F\xB1", + "\xEF\xA5\x99" => "\xE9\x99\xB5", + "\xEF\xA5\x9A" => "\xE8\xAE\x80", + "\xEF\xA5\x9B" => "\xE6\x8B\x8F", + "\xEF\xA5\x9C" => "\xE6\xA8\x82", + "\xEF\xA5\x9D" => "\xE8\xAB\xBE", + "\xEF\xA5\x9E" => "\xE4\xB8\xB9", + "\xEF\xA5\x9F" => "\xE5\xAF\xA7", + "\xEF\xA5\xA0" => "\xE6\x80\x92", + "\xEF\xA5\xA1" => "\xE7\x8E\x87", + "\xEF\xA5\xA2" => "\xE7\x95\xB0", + "\xEF\xA5\xA3" => "\xE5\x8C\x97", + "\xEF\xA5\xA4" => "\xE7\xA3\xBB", + "\xEF\xA5\xA5" => "\xE4\xBE\xBF", + "\xEF\xA5\xA6" => "\xE5\xBE\xA9", + "\xEF\xA5\xA7" => "\xE4\xB8\x8D", + "\xEF\xA5\xA8" => "\xE6\xB3\x8C", + "\xEF\xA5\xA9" => "\xE6\x95\xB8", + "\xEF\xA5\xAA" => "\xE7\xB4\xA2", + "\xEF\xA5\xAB" => "\xE5\x8F\x83", + "\xEF\xA5\xAC" => "\xE5\xA1\x9E", + "\xEF\xA5\xAD" => "\xE7\x9C\x81", + "\xEF\xA5\xAE" => "\xE8\x91\x89", + "\xEF\xA5\xAF" => "\xE8\xAA\xAA", + "\xEF\xA5\xB0" => "\xE6\xAE\xBA", + "\xEF\xA5\xB1" => "\xE8\xBE\xB0", + "\xEF\xA5\xB2" => "\xE6\xB2\x88", + "\xEF\xA5\xB3" => "\xE6\x8B\xBE", + "\xEF\xA5\xB4" => "\xE8\x8B\xA5", + "\xEF\xA5\xB5" => "\xE6\x8E\xA0", + "\xEF\xA5\xB6" => "\xE7\x95\xA5", + "\xEF\xA5\xB7" => "\xE4\xBA\xAE", + "\xEF\xA5\xB8" => "\xE5\x85\xA9", + "\xEF\xA5\xB9" => "\xE5\x87\x89", + "\xEF\xA5\xBA" => "\xE6\xA2\x81", + "\xEF\xA5\xBB" => "\xE7\xB3\xA7", + "\xEF\xA5\xBC" => "\xE8\x89\xAF", + "\xEF\xA5\xBD" => "\xE8\xAB\x92", + "\xEF\xA5\xBE" => "\xE9\x87\x8F", + "\xEF\xA5\xBF" => "\xE5\x8B\xB5", + "\xEF\xA6\x80" => "\xE5\x91\x82", + "\xEF\xA6\x81" => "\xE5\xA5\xB3", + "\xEF\xA6\x82" => "\xE5\xBB\xAC", + "\xEF\xA6\x83" => "\xE6\x97\x85", + "\xEF\xA6\x84" => "\xE6\xBF\xBE", + "\xEF\xA6\x85" => "\xE7\xA4\xAA", + "\xEF\xA6\x86" => "\xE9\x96\xAD", + "\xEF\xA6\x87" => "\xE9\xA9\xAA", + "\xEF\xA6\x88" => "\xE9\xBA\x97", + "\xEF\xA6\x89" => "\xE9\xBB\x8E", + "\xEF\xA6\x8A" => "\xE5\x8A\x9B", + "\xEF\xA6\x8B" => "\xE6\x9B\x86", + "\xEF\xA6\x8C" => "\xE6\xAD\xB7", + "\xEF\xA6\x8D" => "\xE8\xBD\xA2", + "\xEF\xA6\x8E" => "\xE5\xB9\xB4", + "\xEF\xA6\x8F" => "\xE6\x86\x90", + "\xEF\xA6\x90" => "\xE6\x88\x80", + "\xEF\xA6\x91" => "\xE6\x92\x9A", + "\xEF\xA6\x92" => "\xE6\xBC\xA3", + "\xEF\xA6\x93" => "\xE7\x85\x89", + "\xEF\xA6\x94" => "\xE7\x92\x89", + "\xEF\xA6\x95" => "\xE7\xA7\x8A", + "\xEF\xA6\x96" => "\xE7\xB7\xB4", + "\xEF\xA6\x97" => "\xE8\x81\xAF", + "\xEF\xA6\x98" => "\xE8\xBC\xA6", + "\xEF\xA6\x99" => "\xE8\x93\xAE", + "\xEF\xA6\x9A" => "\xE9\x80\xA3", + "\xEF\xA6\x9B" => "\xE9\x8D\x8A", + "\xEF\xA6\x9C" => "\xE5\x88\x97", + "\xEF\xA6\x9D" => "\xE5\x8A\xA3", + "\xEF\xA6\x9E" => "\xE5\x92\xBD", + "\xEF\xA6\x9F" => "\xE7\x83\x88", + "\xEF\xA6\xA0" => "\xE8\xA3\x82", + "\xEF\xA6\xA1" => "\xE8\xAA\xAA", + "\xEF\xA6\xA2" => "\xE5\xBB\x89", + "\xEF\xA6\xA3" => "\xE5\xBF\xB5", + "\xEF\xA6\xA4" => "\xE6\x8D\xBB", + "\xEF\xA6\xA5" => "\xE6\xAE\xAE", + "\xEF\xA6\xA6" => "\xE7\xB0\xBE", + "\xEF\xA6\xA7" => "\xE7\x8D\xB5", + "\xEF\xA6\xA8" => "\xE4\xBB\xA4", + "\xEF\xA6\xA9" => "\xE5\x9B\xB9", + "\xEF\xA6\xAA" => "\xE5\xAF\xA7", + "\xEF\xA6\xAB" => "\xE5\xB6\xBA", + "\xEF\xA6\xAC" => "\xE6\x80\x9C", + "\xEF\xA6\xAD" => "\xE7\x8E\xB2", + "\xEF\xA6\xAE" => "\xE7\x91\xA9", + "\xEF\xA6\xAF" => "\xE7\xBE\x9A", + "\xEF\xA6\xB0" => "\xE8\x81\x86", + "\xEF\xA6\xB1" => "\xE9\x88\xB4", + "\xEF\xA6\xB2" => "\xE9\x9B\xB6", + "\xEF\xA6\xB3" => "\xE9\x9D\x88", + "\xEF\xA6\xB4" => "\xE9\xA0\x98", + "\xEF\xA6\xB5" => "\xE4\xBE\x8B", + "\xEF\xA6\xB6" => "\xE7\xA6\xAE", + "\xEF\xA6\xB7" => "\xE9\x86\xB4", + "\xEF\xA6\xB8" => "\xE9\x9A\xB8", + "\xEF\xA6\xB9" => "\xE6\x83\xA1", + "\xEF\xA6\xBA" => "\xE4\xBA\x86", + "\xEF\xA6\xBB" => "\xE5\x83\x9A", + "\xEF\xA6\xBC" => "\xE5\xAF\xAE", + "\xEF\xA6\xBD" => "\xE5\xB0\xBF", + "\xEF\xA6\xBE" => "\xE6\x96\x99", + "\xEF\xA6\xBF" => "\xE6\xA8\x82", + "\xEF\xA7\x80" => "\xE7\x87\x8E", + "\xEF\xA7\x81" => "\xE7\x99\x82", + "\xEF\xA7\x82" => "\xE8\x93\xBC", + "\xEF\xA7\x83" => "\xE9\x81\xBC", + "\xEF\xA7\x84" => "\xE9\xBE\x8D", + "\xEF\xA7\x85" => "\xE6\x9A\x88", + "\xEF\xA7\x86" => "\xE9\x98\xAE", + "\xEF\xA7\x87" => "\xE5\x8A\x89", + "\xEF\xA7\x88" => "\xE6\x9D\xBB", + "\xEF\xA7\x89" => "\xE6\x9F\xB3", + "\xEF\xA7\x8A" => "\xE6\xB5\x81", + "\xEF\xA7\x8B" => "\xE6\xBA\x9C", + "\xEF\xA7\x8C" => "\xE7\x90\x89", + "\xEF\xA7\x8D" => "\xE7\x95\x99", + "\xEF\xA7\x8E" => "\xE7\xA1\xAB", + "\xEF\xA7\x8F" => "\xE7\xB4\x90", + "\xEF\xA7\x90" => "\xE9\xA1\x9E", + "\xEF\xA7\x91" => "\xE5\x85\xAD", + "\xEF\xA7\x92" => "\xE6\x88\xAE", + "\xEF\xA7\x93" => "\xE9\x99\xB8", + "\xEF\xA7\x94" => "\xE5\x80\xAB", + "\xEF\xA7\x95" => "\xE5\xB4\x99", + "\xEF\xA7\x96" => "\xE6\xB7\xAA", + "\xEF\xA7\x97" => "\xE8\xBC\xAA", + "\xEF\xA7\x98" => "\xE5\xBE\x8B", + "\xEF\xA7\x99" => "\xE6\x85\x84", + "\xEF\xA7\x9A" => "\xE6\xA0\x97", + "\xEF\xA7\x9B" => "\xE7\x8E\x87", + "\xEF\xA7\x9C" => "\xE9\x9A\x86", + "\xEF\xA7\x9D" => "\xE5\x88\xA9", + "\xEF\xA7\x9E" => "\xE5\x90\x8F", + "\xEF\xA7\x9F" => "\xE5\xB1\xA5", + "\xEF\xA7\xA0" => "\xE6\x98\x93", + "\xEF\xA7\xA1" => "\xE6\x9D\x8E", + "\xEF\xA7\xA2" => "\xE6\xA2\xA8", + "\xEF\xA7\xA3" => "\xE6\xB3\xA5", + "\xEF\xA7\xA4" => "\xE7\x90\x86", + "\xEF\xA7\xA5" => "\xE7\x97\xA2", + "\xEF\xA7\xA6" => "\xE7\xBD\xB9", + "\xEF\xA7\xA7" => "\xE8\xA3\x8F", + "\xEF\xA7\xA8" => "\xE8\xA3\xA1", + "\xEF\xA7\xA9" => "\xE9\x87\x8C", + "\xEF\xA7\xAA" => "\xE9\x9B\xA2", + "\xEF\xA7\xAB" => "\xE5\x8C\xBF", + "\xEF\xA7\xAC" => "\xE6\xBA\xBA", + "\xEF\xA7\xAD" => "\xE5\x90\x9D", + "\xEF\xA7\xAE" => "\xE7\x87\x90", + "\xEF\xA7\xAF" => "\xE7\x92\x98", + "\xEF\xA7\xB0" => "\xE8\x97\xBA", + "\xEF\xA7\xB1" => "\xE9\x9A\xA3", + "\xEF\xA7\xB2" => "\xE9\xB1\x97", + "\xEF\xA7\xB3" => "\xE9\xBA\x9F", + "\xEF\xA7\xB4" => "\xE6\x9E\x97", + "\xEF\xA7\xB5" => "\xE6\xB7\x8B", + "\xEF\xA7\xB6" => "\xE8\x87\xA8", + "\xEF\xA7\xB7" => "\xE7\xAB\x8B", + "\xEF\xA7\xB8" => "\xE7\xAC\xA0", + "\xEF\xA7\xB9" => "\xE7\xB2\x92", + "\xEF\xA7\xBA" => "\xE7\x8B\x80", + "\xEF\xA7\xBB" => "\xE7\x82\x99", + "\xEF\xA7\xBC" => "\xE8\xAD\x98", + "\xEF\xA7\xBD" => "\xE4\xBB\x80", + "\xEF\xA7\xBE" => "\xE8\x8C\xB6", + "\xEF\xA7\xBF" => "\xE5\x88\xBA", + "\xEF\xA8\x80" => "\xE5\x88\x87", + "\xEF\xA8\x81" => "\xE5\xBA\xA6", + "\xEF\xA8\x82" => "\xE6\x8B\x93", + "\xEF\xA8\x83" => "\xE7\xB3\x96", + "\xEF\xA8\x84" => "\xE5\xAE\x85", + "\xEF\xA8\x85" => "\xE6\xB4\x9E", + "\xEF\xA8\x86" => "\xE6\x9A\xB4", + "\xEF\xA8\x87" => "\xE8\xBC\xBB", + "\xEF\xA8\x88" => "\xE8\xA1\x8C", + "\xEF\xA8\x89" => "\xE9\x99\x8D", + "\xEF\xA8\x8A" => "\xE8\xA6\x8B", + "\xEF\xA8\x8B" => "\xE5\xBB\x93", + "\xEF\xA8\x8C" => "\xE5\x85\x80", + "\xEF\xA8\x8D" => "\xE5\x97\x80", + "\xEF\xA8\x90" => "\xE5\xA1\x9A", + "\xEF\xA8\x92" => "\xE6\x99\xB4", + "\xEF\xA8\x95" => "\xE5\x87\x9E", + "\xEF\xA8\x96" => "\xE7\x8C\xAA", + "\xEF\xA8\x97" => "\xE7\x9B\x8A", + "\xEF\xA8\x98" => "\xE7\xA4\xBC", + "\xEF\xA8\x99" => "\xE7\xA5\x9E", + "\xEF\xA8\x9A" => "\xE7\xA5\xA5", + "\xEF\xA8\x9B" => "\xE7\xA6\x8F", + "\xEF\xA8\x9C" => "\xE9\x9D\x96", + "\xEF\xA8\x9D" => "\xE7\xB2\xBE", + "\xEF\xA8\x9E" => "\xE7\xBE\xBD", + "\xEF\xA8\xA0" => "\xE8\x98\x92", + "\xEF\xA8\xA2" => "\xE8\xAB\xB8", + "\xEF\xA8\xA5" => "\xE9\x80\xB8", + "\xEF\xA8\xA6" => "\xE9\x83\xBD", + "\xEF\xA8\xAA" => "\xE9\xA3\xAF", + "\xEF\xA8\xAB" => "\xE9\xA3\xBC", + "\xEF\xA8\xAC" => "\xE9\xA4\xA8", + "\xEF\xA8\xAD" => "\xE9\xB6\xB4", + "\xEF\xA8\xAE" => "\xE9\x83\x9E", + "\xEF\xA8\xAF" => "\xE9\x9A\xB7", + "\xEF\xA8\xB0" => "\xE4\xBE\xAE", + "\xEF\xA8\xB1" => "\xE5\x83\xA7", + "\xEF\xA8\xB2" => "\xE5\x85\x8D", + "\xEF\xA8\xB3" => "\xE5\x8B\x89", + "\xEF\xA8\xB4" => "\xE5\x8B\xA4", + "\xEF\xA8\xB5" => "\xE5\x8D\x91", + "\xEF\xA8\xB6" => "\xE5\x96\x9D", + "\xEF\xA8\xB7" => "\xE5\x98\x86", + "\xEF\xA8\xB8" => "\xE5\x99\xA8", + "\xEF\xA8\xB9" => "\xE5\xA1\x80", + "\xEF\xA8\xBA" => "\xE5\xA2\xA8", + "\xEF\xA8\xBB" => "\xE5\xB1\xA4", + "\xEF\xA8\xBC" => "\xE5\xB1\xAE", + "\xEF\xA8\xBD" => "\xE6\x82\x94", + "\xEF\xA8\xBE" => "\xE6\x85\xA8", + "\xEF\xA8\xBF" => "\xE6\x86\x8E", + "\xEF\xA9\x80" => "\xE6\x87\xB2", + "\xEF\xA9\x81" => "\xE6\x95\x8F", + "\xEF\xA9\x82" => "\xE6\x97\xA2", + "\xEF\xA9\x83" => "\xE6\x9A\x91", + "\xEF\xA9\x84" => "\xE6\xA2\x85", + "\xEF\xA9\x85" => "\xE6\xB5\xB7", + "\xEF\xA9\x86" => "\xE6\xB8\x9A", + "\xEF\xA9\x87" => "\xE6\xBC\xA2", + "\xEF\xA9\x88" => "\xE7\x85\xAE", + "\xEF\xA9\x89" => "\xE7\x88\xAB", + "\xEF\xA9\x8A" => "\xE7\x90\xA2", + "\xEF\xA9\x8B" => "\xE7\xA2\x91", + "\xEF\xA9\x8C" => "\xE7\xA4\xBE", + "\xEF\xA9\x8D" => "\xE7\xA5\x89", + "\xEF\xA9\x8E" => "\xE7\xA5\x88", + "\xEF\xA9\x8F" => "\xE7\xA5\x90", + "\xEF\xA9\x90" => "\xE7\xA5\x96", + "\xEF\xA9\x91" => "\xE7\xA5\x9D", + "\xEF\xA9\x92" => "\xE7\xA6\x8D", + "\xEF\xA9\x93" => "\xE7\xA6\x8E", + "\xEF\xA9\x94" => "\xE7\xA9\x80", + "\xEF\xA9\x95" => "\xE7\xAA\x81", + "\xEF\xA9\x96" => "\xE7\xAF\x80", + "\xEF\xA9\x97" => "\xE7\xB7\xB4", + "\xEF\xA9\x98" => "\xE7\xB8\x89", + "\xEF\xA9\x99" => "\xE7\xB9\x81", + "\xEF\xA9\x9A" => "\xE7\xBD\xB2", + "\xEF\xA9\x9B" => "\xE8\x80\x85", + "\xEF\xA9\x9C" => "\xE8\x87\xAD", + "\xEF\xA9\x9D" => "\xE8\x89\xB9", + "\xEF\xA9\x9E" => "\xE8\x89\xB9", + "\xEF\xA9\x9F" => "\xE8\x91\x97", + "\xEF\xA9\xA0" => "\xE8\xA4\x90", + "\xEF\xA9\xA1" => "\xE8\xA6\x96", + "\xEF\xA9\xA2" => "\xE8\xAC\x81", + "\xEF\xA9\xA3" => "\xE8\xAC\xB9", + "\xEF\xA9\xA4" => "\xE8\xB3\x93", + "\xEF\xA9\xA5" => "\xE8\xB4\x88", + "\xEF\xA9\xA6" => "\xE8\xBE\xB6", + "\xEF\xA9\xA7" => "\xE9\x80\xB8", + "\xEF\xA9\xA8" => "\xE9\x9B\xA3", + "\xEF\xA9\xA9" => "\xE9\x9F\xBF", + "\xEF\xA9\xAA" => "\xE9\xA0\xBB", + "\xEF\xA9\xAB" => "\xE6\x81\xB5", + "\xEF\xA9\xAC" => "\xF0\xA4\x8B\xAE", + "\xEF\xA9\xAD" => "\xE8\x88\x98", + "\xEF\xA9\xB0" => "\xE4\xB8\xA6", + "\xEF\xA9\xB1" => "\xE5\x86\xB5", + "\xEF\xA9\xB2" => "\xE5\x85\xA8", + "\xEF\xA9\xB3" => "\xE4\xBE\x80", + "\xEF\xA9\xB4" => "\xE5\x85\x85", + "\xEF\xA9\xB5" => "\xE5\x86\x80", + "\xEF\xA9\xB6" => "\xE5\x8B\x87", + "\xEF\xA9\xB7" => "\xE5\x8B\xBA", + "\xEF\xA9\xB8" => "\xE5\x96\x9D", + "\xEF\xA9\xB9" => "\xE5\x95\x95", + "\xEF\xA9\xBA" => "\xE5\x96\x99", + "\xEF\xA9\xBB" => "\xE5\x97\xA2", + "\xEF\xA9\xBC" => "\xE5\xA1\x9A", + "\xEF\xA9\xBD" => "\xE5\xA2\xB3", + "\xEF\xA9\xBE" => "\xE5\xA5\x84", + "\xEF\xA9\xBF" => "\xE5\xA5\x94", + "\xEF\xAA\x80" => "\xE5\xA9\xA2", + "\xEF\xAA\x81" => "\xE5\xAC\xA8", + "\xEF\xAA\x82" => "\xE5\xBB\x92", + "\xEF\xAA\x83" => "\xE5\xBB\x99", + "\xEF\xAA\x84" => "\xE5\xBD\xA9", + "\xEF\xAA\x85" => "\xE5\xBE\xAD", + "\xEF\xAA\x86" => "\xE6\x83\x98", + "\xEF\xAA\x87" => "\xE6\x85\x8E", + "\xEF\xAA\x88" => "\xE6\x84\x88", + "\xEF\xAA\x89" => "\xE6\x86\x8E", + "\xEF\xAA\x8A" => "\xE6\x85\xA0", + "\xEF\xAA\x8B" => "\xE6\x87\xB2", + "\xEF\xAA\x8C" => "\xE6\x88\xB4", + "\xEF\xAA\x8D" => "\xE6\x8F\x84", + "\xEF\xAA\x8E" => "\xE6\x90\x9C", + "\xEF\xAA\x8F" => "\xE6\x91\x92", + "\xEF\xAA\x90" => "\xE6\x95\x96", + "\xEF\xAA\x91" => "\xE6\x99\xB4", + "\xEF\xAA\x92" => "\xE6\x9C\x97", + "\xEF\xAA\x93" => "\xE6\x9C\x9B", + "\xEF\xAA\x94" => "\xE6\x9D\x96", + "\xEF\xAA\x95" => "\xE6\xAD\xB9", + "\xEF\xAA\x96" => "\xE6\xAE\xBA", + "\xEF\xAA\x97" => "\xE6\xB5\x81", + "\xEF\xAA\x98" => "\xE6\xBB\x9B", + "\xEF\xAA\x99" => "\xE6\xBB\x8B", + "\xEF\xAA\x9A" => "\xE6\xBC\xA2", + "\xEF\xAA\x9B" => "\xE7\x80\x9E", + "\xEF\xAA\x9C" => "\xE7\x85\xAE", + "\xEF\xAA\x9D" => "\xE7\x9E\xA7", + "\xEF\xAA\x9E" => "\xE7\x88\xB5", + "\xEF\xAA\x9F" => "\xE7\x8A\xAF", + "\xEF\xAA\xA0" => "\xE7\x8C\xAA", + "\xEF\xAA\xA1" => "\xE7\x91\xB1", + "\xEF\xAA\xA2" => "\xE7\x94\x86", + "\xEF\xAA\xA3" => "\xE7\x94\xBB", + "\xEF\xAA\xA4" => "\xE7\x98\x9D", + "\xEF\xAA\xA5" => "\xE7\x98\x9F", + "\xEF\xAA\xA6" => "\xE7\x9B\x8A", + "\xEF\xAA\xA7" => "\xE7\x9B\x9B", + "\xEF\xAA\xA8" => "\xE7\x9B\xB4", + "\xEF\xAA\xA9" => "\xE7\x9D\x8A", + "\xEF\xAA\xAA" => "\xE7\x9D\x80", + "\xEF\xAA\xAB" => "\xE7\xA3\x8C", + "\xEF\xAA\xAC" => "\xE7\xAA\xB1", + "\xEF\xAA\xAD" => "\xE7\xAF\x80", + "\xEF\xAA\xAE" => "\xE7\xB1\xBB", + "\xEF\xAA\xAF" => "\xE7\xB5\x9B", + "\xEF\xAA\xB0" => "\xE7\xB7\xB4", + "\xEF\xAA\xB1" => "\xE7\xBC\xBE", + "\xEF\xAA\xB2" => "\xE8\x80\x85", + "\xEF\xAA\xB3" => "\xE8\x8D\x92", + "\xEF\xAA\xB4" => "\xE8\x8F\xAF", + "\xEF\xAA\xB5" => "\xE8\x9D\xB9", + "\xEF\xAA\xB6" => "\xE8\xA5\x81", + "\xEF\xAA\xB7" => "\xE8\xA6\x86", + "\xEF\xAA\xB8" => "\xE8\xA6\x96", + "\xEF\xAA\xB9" => "\xE8\xAA\xBF", + "\xEF\xAA\xBA" => "\xE8\xAB\xB8", + "\xEF\xAA\xBB" => "\xE8\xAB\x8B", + "\xEF\xAA\xBC" => "\xE8\xAC\x81", + "\xEF\xAA\xBD" => "\xE8\xAB\xBE", + "\xEF\xAA\xBE" => "\xE8\xAB\xAD", + "\xEF\xAA\xBF" => "\xE8\xAC\xB9", + "\xEF\xAB\x80" => "\xE8\xAE\x8A", + "\xEF\xAB\x81" => "\xE8\xB4\x88", + "\xEF\xAB\x82" => "\xE8\xBC\xB8", + "\xEF\xAB\x83" => "\xE9\x81\xB2", + "\xEF\xAB\x84" => "\xE9\x86\x99", + "\xEF\xAB\x85" => "\xE9\x89\xB6", + "\xEF\xAB\x86" => "\xE9\x99\xBC", + "\xEF\xAB\x87" => "\xE9\x9B\xA3", + "\xEF\xAB\x88" => "\xE9\x9D\x96", + "\xEF\xAB\x89" => "\xE9\x9F\x9B", + "\xEF\xAB\x8A" => "\xE9\x9F\xBF", + "\xEF\xAB\x8B" => "\xE9\xA0\x8B", + "\xEF\xAB\x8C" => "\xE9\xA0\xBB", + "\xEF\xAB\x8D" => "\xE9\xAC\x92", + "\xEF\xAB\x8E" => "\xE9\xBE\x9C", + "\xEF\xAB\x8F" => "\xF0\xA2\xA1\x8A", + "\xEF\xAB\x90" => "\xF0\xA2\xA1\x84", + "\xEF\xAB\x91" => "\xF0\xA3\x8F\x95", + "\xEF\xAB\x92" => "\xE3\xAE\x9D", + "\xEF\xAB\x93" => "\xE4\x80\x98", + "\xEF\xAB\x94" => "\xE4\x80\xB9", + "\xEF\xAB\x95" => "\xF0\xA5\x89\x89", + "\xEF\xAB\x96" => "\xF0\xA5\xB3\x90", + "\xEF\xAB\x97" => "\xF0\xA7\xBB\x93", + "\xEF\xAB\x98" => "\xE9\xBD\x83", + "\xEF\xAB\x99" => "\xE9\xBE\x8E", + "\xEF\xAC\x80" => "\x66\x66", + "\xEF\xAC\x81" => "\x66\x69", + "\xEF\xAC\x82" => "\x66\x6C", + "\xEF\xAC\x83" => "\x66\x66\x69", + "\xEF\xAC\x84" => "\x66\x66\x6C", + "\xEF\xAC\x85" => "\x73\x74", + "\xEF\xAC\x86" => "\x73\x74", + "\xEF\xAC\x93" => "\xD5\xB4\xD5\xB6", + "\xEF\xAC\x94" => "\xD5\xB4\xD5\xA5", + "\xEF\xAC\x95" => "\xD5\xB4\xD5\xAB", + "\xEF\xAC\x96" => "\xD5\xBE\xD5\xB6", + "\xEF\xAC\x97" => "\xD5\xB4\xD5\xAD", + "\xEF\xAC\x9D" => "\xD7\x99\xD6\xB4", + "\xEF\xAC\x9F" => "\xD7\xB2\xD6\xB7", + "\xEF\xAC\xA0" => "\xD7\xA2", + "\xEF\xAC\xA1" => "\xD7\x90", + "\xEF\xAC\xA2" => "\xD7\x93", + "\xEF\xAC\xA3" => "\xD7\x94", + "\xEF\xAC\xA4" => "\xD7\x9B", + "\xEF\xAC\xA5" => "\xD7\x9C", + "\xEF\xAC\xA6" => "\xD7\x9D", + "\xEF\xAC\xA7" => "\xD7\xA8", + "\xEF\xAC\xA8" => "\xD7\xAA", + "\xEF\xAC\xAA" => "\xD7\xA9\xD7\x81", + "\xEF\xAC\xAB" => "\xD7\xA9\xD7\x82", + "\xEF\xAC\xAC" => "\xD7\xA9\xD6\xBC\xD7\x81", + "\xEF\xAC\xAD" => "\xD7\xA9\xD6\xBC\xD7\x82", + "\xEF\xAC\xAE" => "\xD7\x90\xD6\xB7", + "\xEF\xAC\xAF" => "\xD7\x90\xD6\xB8", + "\xEF\xAC\xB0" => "\xD7\x90\xD6\xBC", + "\xEF\xAC\xB1" => "\xD7\x91\xD6\xBC", + "\xEF\xAC\xB2" => "\xD7\x92\xD6\xBC", + "\xEF\xAC\xB3" => "\xD7\x93\xD6\xBC", + "\xEF\xAC\xB4" => "\xD7\x94\xD6\xBC", + "\xEF\xAC\xB5" => "\xD7\x95\xD6\xBC", + "\xEF\xAC\xB6" => "\xD7\x96\xD6\xBC", + "\xEF\xAC\xB8" => "\xD7\x98\xD6\xBC", + "\xEF\xAC\xB9" => "\xD7\x99\xD6\xBC", + "\xEF\xAC\xBA" => "\xD7\x9A\xD6\xBC", + "\xEF\xAC\xBB" => "\xD7\x9B\xD6\xBC", + "\xEF\xAC\xBC" => "\xD7\x9C\xD6\xBC", + "\xEF\xAC\xBE" => "\xD7\x9E\xD6\xBC", + "\xEF\xAD\x80" => "\xD7\xA0\xD6\xBC", + "\xEF\xAD\x81" => "\xD7\xA1\xD6\xBC", + "\xEF\xAD\x83" => "\xD7\xA3\xD6\xBC", + "\xEF\xAD\x84" => "\xD7\xA4\xD6\xBC", + "\xEF\xAD\x86" => "\xD7\xA6\xD6\xBC", + "\xEF\xAD\x87" => "\xD7\xA7\xD6\xBC", + "\xEF\xAD\x88" => "\xD7\xA8\xD6\xBC", + "\xEF\xAD\x89" => "\xD7\xA9\xD6\xBC", + "\xEF\xAD\x8A" => "\xD7\xAA\xD6\xBC", + "\xEF\xAD\x8B" => "\xD7\x95\xD6\xB9", + "\xEF\xAD\x8C" => "\xD7\x91\xD6\xBF", + "\xEF\xAD\x8D" => "\xD7\x9B\xD6\xBF", + "\xEF\xAD\x8E" => "\xD7\xA4\xD6\xBF", + "\xEF\xAD\x8F" => "\xD7\x90\xD7\x9C", + "\xEF\xAD\x90" => "\xD9\xB1", + "\xEF\xAD\x91" => "\xD9\xB1", + "\xEF\xAD\x92" => "\xD9\xBB", + "\xEF\xAD\x93" => "\xD9\xBB", + "\xEF\xAD\x94" => "\xD9\xBB", + "\xEF\xAD\x95" => "\xD9\xBB", + "\xEF\xAD\x96" => "\xD9\xBE", + "\xEF\xAD\x97" => "\xD9\xBE", + "\xEF\xAD\x98" => "\xD9\xBE", + "\xEF\xAD\x99" => "\xD9\xBE", + "\xEF\xAD\x9A" => "\xDA\x80", + "\xEF\xAD\x9B" => "\xDA\x80", + "\xEF\xAD\x9C" => "\xDA\x80", + "\xEF\xAD\x9D" => "\xDA\x80", + "\xEF\xAD\x9E" => "\xD9\xBA", + "\xEF\xAD\x9F" => "\xD9\xBA", + "\xEF\xAD\xA0" => "\xD9\xBA", + "\xEF\xAD\xA1" => "\xD9\xBA", + "\xEF\xAD\xA2" => "\xD9\xBF", + "\xEF\xAD\xA3" => "\xD9\xBF", + "\xEF\xAD\xA4" => "\xD9\xBF", + "\xEF\xAD\xA5" => "\xD9\xBF", + "\xEF\xAD\xA6" => "\xD9\xB9", + "\xEF\xAD\xA7" => "\xD9\xB9", + "\xEF\xAD\xA8" => "\xD9\xB9", + "\xEF\xAD\xA9" => "\xD9\xB9", + "\xEF\xAD\xAA" => "\xDA\xA4", + "\xEF\xAD\xAB" => "\xDA\xA4", + "\xEF\xAD\xAC" => "\xDA\xA4", + "\xEF\xAD\xAD" => "\xDA\xA4", + "\xEF\xAD\xAE" => "\xDA\xA6", + "\xEF\xAD\xAF" => "\xDA\xA6", + "\xEF\xAD\xB0" => "\xDA\xA6", + "\xEF\xAD\xB1" => "\xDA\xA6", + "\xEF\xAD\xB2" => "\xDA\x84", + "\xEF\xAD\xB3" => "\xDA\x84", + "\xEF\xAD\xB4" => "\xDA\x84", + "\xEF\xAD\xB5" => "\xDA\x84", + "\xEF\xAD\xB6" => "\xDA\x83", + "\xEF\xAD\xB7" => "\xDA\x83", + "\xEF\xAD\xB8" => "\xDA\x83", + "\xEF\xAD\xB9" => "\xDA\x83", + "\xEF\xAD\xBA" => "\xDA\x86", + "\xEF\xAD\xBB" => "\xDA\x86", + "\xEF\xAD\xBC" => "\xDA\x86", + "\xEF\xAD\xBD" => "\xDA\x86", + "\xEF\xAD\xBE" => "\xDA\x87", + "\xEF\xAD\xBF" => "\xDA\x87", + "\xEF\xAE\x80" => "\xDA\x87", + "\xEF\xAE\x81" => "\xDA\x87", + "\xEF\xAE\x82" => "\xDA\x8D", + "\xEF\xAE\x83" => "\xDA\x8D", + "\xEF\xAE\x84" => "\xDA\x8C", + "\xEF\xAE\x85" => "\xDA\x8C", + "\xEF\xAE\x86" => "\xDA\x8E", + "\xEF\xAE\x87" => "\xDA\x8E", + "\xEF\xAE\x88" => "\xDA\x88", + "\xEF\xAE\x89" => "\xDA\x88", + "\xEF\xAE\x8A" => "\xDA\x98", + "\xEF\xAE\x8B" => "\xDA\x98", + "\xEF\xAE\x8C" => "\xDA\x91", + "\xEF\xAE\x8D" => "\xDA\x91", + "\xEF\xAE\x8E" => "\xDA\xA9", + "\xEF\xAE\x8F" => "\xDA\xA9", + "\xEF\xAE\x90" => "\xDA\xA9", + "\xEF\xAE\x91" => "\xDA\xA9", + "\xEF\xAE\x92" => "\xDA\xAF", + "\xEF\xAE\x93" => "\xDA\xAF", + "\xEF\xAE\x94" => "\xDA\xAF", + "\xEF\xAE\x95" => "\xDA\xAF", + "\xEF\xAE\x96" => "\xDA\xB3", + "\xEF\xAE\x97" => "\xDA\xB3", + "\xEF\xAE\x98" => "\xDA\xB3", + "\xEF\xAE\x99" => "\xDA\xB3", + "\xEF\xAE\x9A" => "\xDA\xB1", + "\xEF\xAE\x9B" => "\xDA\xB1", + "\xEF\xAE\x9C" => "\xDA\xB1", + "\xEF\xAE\x9D" => "\xDA\xB1", + "\xEF\xAE\x9E" => "\xDA\xBA", + "\xEF\xAE\x9F" => "\xDA\xBA", + "\xEF\xAE\xA0" => "\xDA\xBB", + "\xEF\xAE\xA1" => "\xDA\xBB", + "\xEF\xAE\xA2" => "\xDA\xBB", + "\xEF\xAE\xA3" => "\xDA\xBB", + "\xEF\xAE\xA4" => "\xDB\x80", + "\xEF\xAE\xA5" => "\xDB\x80", + "\xEF\xAE\xA6" => "\xDB\x81", + "\xEF\xAE\xA7" => "\xDB\x81", + "\xEF\xAE\xA8" => "\xDB\x81", + "\xEF\xAE\xA9" => "\xDB\x81", + "\xEF\xAE\xAA" => "\xDA\xBE", + "\xEF\xAE\xAB" => "\xDA\xBE", + "\xEF\xAE\xAC" => "\xDA\xBE", + "\xEF\xAE\xAD" => "\xDA\xBE", + "\xEF\xAE\xAE" => "\xDB\x92", + "\xEF\xAE\xAF" => "\xDB\x92", + "\xEF\xAE\xB0" => "\xDB\x93", + "\xEF\xAE\xB1" => "\xDB\x93", + "\xEF\xAF\x93" => "\xDA\xAD", + "\xEF\xAF\x94" => "\xDA\xAD", + "\xEF\xAF\x95" => "\xDA\xAD", + "\xEF\xAF\x96" => "\xDA\xAD", + "\xEF\xAF\x97" => "\xDB\x87", + "\xEF\xAF\x98" => "\xDB\x87", + "\xEF\xAF\x99" => "\xDB\x86", + "\xEF\xAF\x9A" => "\xDB\x86", + "\xEF\xAF\x9B" => "\xDB\x88", + "\xEF\xAF\x9C" => "\xDB\x88", + "\xEF\xAF\x9D" => "\xDB\x87\xD9\xB4", + "\xEF\xAF\x9E" => "\xDB\x8B", + "\xEF\xAF\x9F" => "\xDB\x8B", + "\xEF\xAF\xA0" => "\xDB\x85", + "\xEF\xAF\xA1" => "\xDB\x85", + "\xEF\xAF\xA2" => "\xDB\x89", + "\xEF\xAF\xA3" => "\xDB\x89", + "\xEF\xAF\xA4" => "\xDB\x90", + "\xEF\xAF\xA5" => "\xDB\x90", + "\xEF\xAF\xA6" => "\xDB\x90", + "\xEF\xAF\xA7" => "\xDB\x90", + "\xEF\xAF\xA8" => "\xD9\x89", + "\xEF\xAF\xA9" => "\xD9\x89", + "\xEF\xAF\xAA" => "\xD8\xA6\xD8\xA7", + "\xEF\xAF\xAB" => "\xD8\xA6\xD8\xA7", + "\xEF\xAF\xAC" => "\xD8\xA6\xDB\x95", + "\xEF\xAF\xAD" => "\xD8\xA6\xDB\x95", + "\xEF\xAF\xAE" => "\xD8\xA6\xD9\x88", + "\xEF\xAF\xAF" => "\xD8\xA6\xD9\x88", + "\xEF\xAF\xB0" => "\xD8\xA6\xDB\x87", + "\xEF\xAF\xB1" => "\xD8\xA6\xDB\x87", + "\xEF\xAF\xB2" => "\xD8\xA6\xDB\x86", + "\xEF\xAF\xB3" => "\xD8\xA6\xDB\x86", + "\xEF\xAF\xB4" => "\xD8\xA6\xDB\x88", + "\xEF\xAF\xB5" => "\xD8\xA6\xDB\x88", + "\xEF\xAF\xB6" => "\xD8\xA6\xDB\x90", + "\xEF\xAF\xB7" => "\xD8\xA6\xDB\x90", + "\xEF\xAF\xB8" => "\xD8\xA6\xDB\x90", + "\xEF\xAF\xB9" => "\xD8\xA6\xD9\x89", + "\xEF\xAF\xBA" => "\xD8\xA6\xD9\x89", + "\xEF\xAF\xBB" => "\xD8\xA6\xD9\x89", + "\xEF\xAF\xBC" => "\xDB\x8C", + "\xEF\xAF\xBD" => "\xDB\x8C", + "\xEF\xAF\xBE" => "\xDB\x8C", + "\xEF\xAF\xBF" => "\xDB\x8C", + "\xEF\xB0\x80" => "\xD8\xA6\xD8\xAC", + "\xEF\xB0\x81" => "\xD8\xA6\xD8\xAD", + "\xEF\xB0\x82" => "\xD8\xA6\xD9\x85", + "\xEF\xB0\x83" => "\xD8\xA6\xD9\x89", + "\xEF\xB0\x84" => "\xD8\xA6\xD9\x8A", + "\xEF\xB0\x85" => "\xD8\xA8\xD8\xAC", + "\xEF\xB0\x86" => "\xD8\xA8\xD8\xAD", + "\xEF\xB0\x87" => "\xD8\xA8\xD8\xAE", + "\xEF\xB0\x88" => "\xD8\xA8\xD9\x85", + "\xEF\xB0\x89" => "\xD8\xA8\xD9\x89", + "\xEF\xB0\x8A" => "\xD8\xA8\xD9\x8A", + "\xEF\xB0\x8B" => "\xD8\xAA\xD8\xAC", + "\xEF\xB0\x8C" => "\xD8\xAA\xD8\xAD", + "\xEF\xB0\x8D" => "\xD8\xAA\xD8\xAE", + "\xEF\xB0\x8E" => "\xD8\xAA\xD9\x85", + "\xEF\xB0\x8F" => "\xD8\xAA\xD9\x89", + "\xEF\xB0\x90" => "\xD8\xAA\xD9\x8A", + "\xEF\xB0\x91" => "\xD8\xAB\xD8\xAC", + "\xEF\xB0\x92" => "\xD8\xAB\xD9\x85", + "\xEF\xB0\x93" => "\xD8\xAB\xD9\x89", + "\xEF\xB0\x94" => "\xD8\xAB\xD9\x8A", + "\xEF\xB0\x95" => "\xD8\xAC\xD8\xAD", + "\xEF\xB0\x96" => "\xD8\xAC\xD9\x85", + "\xEF\xB0\x97" => "\xD8\xAD\xD8\xAC", + "\xEF\xB0\x98" => "\xD8\xAD\xD9\x85", + "\xEF\xB0\x99" => "\xD8\xAE\xD8\xAC", + "\xEF\xB0\x9A" => "\xD8\xAE\xD8\xAD", + "\xEF\xB0\x9B" => "\xD8\xAE\xD9\x85", + "\xEF\xB0\x9C" => "\xD8\xB3\xD8\xAC", + "\xEF\xB0\x9D" => "\xD8\xB3\xD8\xAD", + "\xEF\xB0\x9E" => "\xD8\xB3\xD8\xAE", + "\xEF\xB0\x9F" => "\xD8\xB3\xD9\x85", + "\xEF\xB0\xA0" => "\xD8\xB5\xD8\xAD", + "\xEF\xB0\xA1" => "\xD8\xB5\xD9\x85", + "\xEF\xB0\xA2" => "\xD8\xB6\xD8\xAC", + "\xEF\xB0\xA3" => "\xD8\xB6\xD8\xAD", + "\xEF\xB0\xA4" => "\xD8\xB6\xD8\xAE", + "\xEF\xB0\xA5" => "\xD8\xB6\xD9\x85", + "\xEF\xB0\xA6" => "\xD8\xB7\xD8\xAD", + "\xEF\xB0\xA7" => "\xD8\xB7\xD9\x85", + "\xEF\xB0\xA8" => "\xD8\xB8\xD9\x85", + "\xEF\xB0\xA9" => "\xD8\xB9\xD8\xAC", + "\xEF\xB0\xAA" => "\xD8\xB9\xD9\x85", + "\xEF\xB0\xAB" => "\xD8\xBA\xD8\xAC", + "\xEF\xB0\xAC" => "\xD8\xBA\xD9\x85", + "\xEF\xB0\xAD" => "\xD9\x81\xD8\xAC", + "\xEF\xB0\xAE" => "\xD9\x81\xD8\xAD", + "\xEF\xB0\xAF" => "\xD9\x81\xD8\xAE", + "\xEF\xB0\xB0" => "\xD9\x81\xD9\x85", + "\xEF\xB0\xB1" => "\xD9\x81\xD9\x89", + "\xEF\xB0\xB2" => "\xD9\x81\xD9\x8A", + "\xEF\xB0\xB3" => "\xD9\x82\xD8\xAD", + "\xEF\xB0\xB4" => "\xD9\x82\xD9\x85", + "\xEF\xB0\xB5" => "\xD9\x82\xD9\x89", + "\xEF\xB0\xB6" => "\xD9\x82\xD9\x8A", + "\xEF\xB0\xB7" => "\xD9\x83\xD8\xA7", + "\xEF\xB0\xB8" => "\xD9\x83\xD8\xAC", + "\xEF\xB0\xB9" => "\xD9\x83\xD8\xAD", + "\xEF\xB0\xBA" => "\xD9\x83\xD8\xAE", + "\xEF\xB0\xBB" => "\xD9\x83\xD9\x84", + "\xEF\xB0\xBC" => "\xD9\x83\xD9\x85", + "\xEF\xB0\xBD" => "\xD9\x83\xD9\x89", + "\xEF\xB0\xBE" => "\xD9\x83\xD9\x8A", + "\xEF\xB0\xBF" => "\xD9\x84\xD8\xAC", + "\xEF\xB1\x80" => "\xD9\x84\xD8\xAD", + "\xEF\xB1\x81" => "\xD9\x84\xD8\xAE", + "\xEF\xB1\x82" => "\xD9\x84\xD9\x85", + "\xEF\xB1\x83" => "\xD9\x84\xD9\x89", + "\xEF\xB1\x84" => "\xD9\x84\xD9\x8A", + "\xEF\xB1\x85" => "\xD9\x85\xD8\xAC", + "\xEF\xB1\x86" => "\xD9\x85\xD8\xAD", + "\xEF\xB1\x87" => "\xD9\x85\xD8\xAE", + "\xEF\xB1\x88" => "\xD9\x85\xD9\x85", + "\xEF\xB1\x89" => "\xD9\x85\xD9\x89", + "\xEF\xB1\x8A" => "\xD9\x85\xD9\x8A", + "\xEF\xB1\x8B" => "\xD9\x86\xD8\xAC", + "\xEF\xB1\x8C" => "\xD9\x86\xD8\xAD", + "\xEF\xB1\x8D" => "\xD9\x86\xD8\xAE", + "\xEF\xB1\x8E" => "\xD9\x86\xD9\x85", + "\xEF\xB1\x8F" => "\xD9\x86\xD9\x89", + "\xEF\xB1\x90" => "\xD9\x86\xD9\x8A", + "\xEF\xB1\x91" => "\xD9\x87\xD8\xAC", + "\xEF\xB1\x92" => "\xD9\x87\xD9\x85", + "\xEF\xB1\x93" => "\xD9\x87\xD9\x89", + "\xEF\xB1\x94" => "\xD9\x87\xD9\x8A", + "\xEF\xB1\x95" => "\xD9\x8A\xD8\xAC", + "\xEF\xB1\x96" => "\xD9\x8A\xD8\xAD", + "\xEF\xB1\x97" => "\xD9\x8A\xD8\xAE", + "\xEF\xB1\x98" => "\xD9\x8A\xD9\x85", + "\xEF\xB1\x99" => "\xD9\x8A\xD9\x89", + "\xEF\xB1\x9A" => "\xD9\x8A\xD9\x8A", + "\xEF\xB1\x9B" => "\xD8\xB0\xD9\xB0", + "\xEF\xB1\x9C" => "\xD8\xB1\xD9\xB0", + "\xEF\xB1\x9D" => "\xD9\x89\xD9\xB0", + "\xEF\xB1\xA4" => "\xD8\xA6\xD8\xB1", + "\xEF\xB1\xA5" => "\xD8\xA6\xD8\xB2", + "\xEF\xB1\xA6" => "\xD8\xA6\xD9\x85", + "\xEF\xB1\xA7" => "\xD8\xA6\xD9\x86", + "\xEF\xB1\xA8" => "\xD8\xA6\xD9\x89", + "\xEF\xB1\xA9" => "\xD8\xA6\xD9\x8A", + "\xEF\xB1\xAA" => "\xD8\xA8\xD8\xB1", + "\xEF\xB1\xAB" => "\xD8\xA8\xD8\xB2", + "\xEF\xB1\xAC" => "\xD8\xA8\xD9\x85", + "\xEF\xB1\xAD" => "\xD8\xA8\xD9\x86", + "\xEF\xB1\xAE" => "\xD8\xA8\xD9\x89", + "\xEF\xB1\xAF" => "\xD8\xA8\xD9\x8A", + "\xEF\xB1\xB0" => "\xD8\xAA\xD8\xB1", + "\xEF\xB1\xB1" => "\xD8\xAA\xD8\xB2", + "\xEF\xB1\xB2" => "\xD8\xAA\xD9\x85", + "\xEF\xB1\xB3" => "\xD8\xAA\xD9\x86", + "\xEF\xB1\xB4" => "\xD8\xAA\xD9\x89", + "\xEF\xB1\xB5" => "\xD8\xAA\xD9\x8A", + "\xEF\xB1\xB6" => "\xD8\xAB\xD8\xB1", + "\xEF\xB1\xB7" => "\xD8\xAB\xD8\xB2", + "\xEF\xB1\xB8" => "\xD8\xAB\xD9\x85", + "\xEF\xB1\xB9" => "\xD8\xAB\xD9\x86", + "\xEF\xB1\xBA" => "\xD8\xAB\xD9\x89", + "\xEF\xB1\xBB" => "\xD8\xAB\xD9\x8A", + "\xEF\xB1\xBC" => "\xD9\x81\xD9\x89", + "\xEF\xB1\xBD" => "\xD9\x81\xD9\x8A", + "\xEF\xB1\xBE" => "\xD9\x82\xD9\x89", + "\xEF\xB1\xBF" => "\xD9\x82\xD9\x8A", + "\xEF\xB2\x80" => "\xD9\x83\xD8\xA7", + "\xEF\xB2\x81" => "\xD9\x83\xD9\x84", + "\xEF\xB2\x82" => "\xD9\x83\xD9\x85", + "\xEF\xB2\x83" => "\xD9\x83\xD9\x89", + "\xEF\xB2\x84" => "\xD9\x83\xD9\x8A", + "\xEF\xB2\x85" => "\xD9\x84\xD9\x85", + "\xEF\xB2\x86" => "\xD9\x84\xD9\x89", + "\xEF\xB2\x87" => "\xD9\x84\xD9\x8A", + "\xEF\xB2\x88" => "\xD9\x85\xD8\xA7", + "\xEF\xB2\x89" => "\xD9\x85\xD9\x85", + "\xEF\xB2\x8A" => "\xD9\x86\xD8\xB1", + "\xEF\xB2\x8B" => "\xD9\x86\xD8\xB2", + "\xEF\xB2\x8C" => "\xD9\x86\xD9\x85", + "\xEF\xB2\x8D" => "\xD9\x86\xD9\x86", + "\xEF\xB2\x8E" => "\xD9\x86\xD9\x89", + "\xEF\xB2\x8F" => "\xD9\x86\xD9\x8A", + "\xEF\xB2\x90" => "\xD9\x89\xD9\xB0", + "\xEF\xB2\x91" => "\xD9\x8A\xD8\xB1", + "\xEF\xB2\x92" => "\xD9\x8A\xD8\xB2", + "\xEF\xB2\x93" => "\xD9\x8A\xD9\x85", + "\xEF\xB2\x94" => "\xD9\x8A\xD9\x86", + "\xEF\xB2\x95" => "\xD9\x8A\xD9\x89", + "\xEF\xB2\x96" => "\xD9\x8A\xD9\x8A", + "\xEF\xB2\x97" => "\xD8\xA6\xD8\xAC", + "\xEF\xB2\x98" => "\xD8\xA6\xD8\xAD", + "\xEF\xB2\x99" => "\xD8\xA6\xD8\xAE", + "\xEF\xB2\x9A" => "\xD8\xA6\xD9\x85", + "\xEF\xB2\x9B" => "\xD8\xA6\xD9\x87", + "\xEF\xB2\x9C" => "\xD8\xA8\xD8\xAC", + "\xEF\xB2\x9D" => "\xD8\xA8\xD8\xAD", + "\xEF\xB2\x9E" => "\xD8\xA8\xD8\xAE", + "\xEF\xB2\x9F" => "\xD8\xA8\xD9\x85", + "\xEF\xB2\xA0" => "\xD8\xA8\xD9\x87", + "\xEF\xB2\xA1" => "\xD8\xAA\xD8\xAC", + "\xEF\xB2\xA2" => "\xD8\xAA\xD8\xAD", + "\xEF\xB2\xA3" => "\xD8\xAA\xD8\xAE", + "\xEF\xB2\xA4" => "\xD8\xAA\xD9\x85", + "\xEF\xB2\xA5" => "\xD8\xAA\xD9\x87", + "\xEF\xB2\xA6" => "\xD8\xAB\xD9\x85", + "\xEF\xB2\xA7" => "\xD8\xAC\xD8\xAD", + "\xEF\xB2\xA8" => "\xD8\xAC\xD9\x85", + "\xEF\xB2\xA9" => "\xD8\xAD\xD8\xAC", + "\xEF\xB2\xAA" => "\xD8\xAD\xD9\x85", + "\xEF\xB2\xAB" => "\xD8\xAE\xD8\xAC", + "\xEF\xB2\xAC" => "\xD8\xAE\xD9\x85", + "\xEF\xB2\xAD" => "\xD8\xB3\xD8\xAC", + "\xEF\xB2\xAE" => "\xD8\xB3\xD8\xAD", + "\xEF\xB2\xAF" => "\xD8\xB3\xD8\xAE", + "\xEF\xB2\xB0" => "\xD8\xB3\xD9\x85", + "\xEF\xB2\xB1" => "\xD8\xB5\xD8\xAD", + "\xEF\xB2\xB2" => "\xD8\xB5\xD8\xAE", + "\xEF\xB2\xB3" => "\xD8\xB5\xD9\x85", + "\xEF\xB2\xB4" => "\xD8\xB6\xD8\xAC", + "\xEF\xB2\xB5" => "\xD8\xB6\xD8\xAD", + "\xEF\xB2\xB6" => "\xD8\xB6\xD8\xAE", + "\xEF\xB2\xB7" => "\xD8\xB6\xD9\x85", + "\xEF\xB2\xB8" => "\xD8\xB7\xD8\xAD", + "\xEF\xB2\xB9" => "\xD8\xB8\xD9\x85", + "\xEF\xB2\xBA" => "\xD8\xB9\xD8\xAC", + "\xEF\xB2\xBB" => "\xD8\xB9\xD9\x85", + "\xEF\xB2\xBC" => "\xD8\xBA\xD8\xAC", + "\xEF\xB2\xBD" => "\xD8\xBA\xD9\x85", + "\xEF\xB2\xBE" => "\xD9\x81\xD8\xAC", + "\xEF\xB2\xBF" => "\xD9\x81\xD8\xAD", + "\xEF\xB3\x80" => "\xD9\x81\xD8\xAE", + "\xEF\xB3\x81" => "\xD9\x81\xD9\x85", + "\xEF\xB3\x82" => "\xD9\x82\xD8\xAD", + "\xEF\xB3\x83" => "\xD9\x82\xD9\x85", + "\xEF\xB3\x84" => "\xD9\x83\xD8\xAC", + "\xEF\xB3\x85" => "\xD9\x83\xD8\xAD", + "\xEF\xB3\x86" => "\xD9\x83\xD8\xAE", + "\xEF\xB3\x87" => "\xD9\x83\xD9\x84", + "\xEF\xB3\x88" => "\xD9\x83\xD9\x85", + "\xEF\xB3\x89" => "\xD9\x84\xD8\xAC", + "\xEF\xB3\x8A" => "\xD9\x84\xD8\xAD", + "\xEF\xB3\x8B" => "\xD9\x84\xD8\xAE", + "\xEF\xB3\x8C" => "\xD9\x84\xD9\x85", + "\xEF\xB3\x8D" => "\xD9\x84\xD9\x87", + "\xEF\xB3\x8E" => "\xD9\x85\xD8\xAC", + "\xEF\xB3\x8F" => "\xD9\x85\xD8\xAD", + "\xEF\xB3\x90" => "\xD9\x85\xD8\xAE", + "\xEF\xB3\x91" => "\xD9\x85\xD9\x85", + "\xEF\xB3\x92" => "\xD9\x86\xD8\xAC", + "\xEF\xB3\x93" => "\xD9\x86\xD8\xAD", + "\xEF\xB3\x94" => "\xD9\x86\xD8\xAE", + "\xEF\xB3\x95" => "\xD9\x86\xD9\x85", + "\xEF\xB3\x96" => "\xD9\x86\xD9\x87", + "\xEF\xB3\x97" => "\xD9\x87\xD8\xAC", + "\xEF\xB3\x98" => "\xD9\x87\xD9\x85", + "\xEF\xB3\x99" => "\xD9\x87\xD9\xB0", + "\xEF\xB3\x9A" => "\xD9\x8A\xD8\xAC", + "\xEF\xB3\x9B" => "\xD9\x8A\xD8\xAD", + "\xEF\xB3\x9C" => "\xD9\x8A\xD8\xAE", + "\xEF\xB3\x9D" => "\xD9\x8A\xD9\x85", + "\xEF\xB3\x9E" => "\xD9\x8A\xD9\x87", + "\xEF\xB3\x9F" => "\xD8\xA6\xD9\x85", + "\xEF\xB3\xA0" => "\xD8\xA6\xD9\x87", + "\xEF\xB3\xA1" => "\xD8\xA8\xD9\x85", + "\xEF\xB3\xA2" => "\xD8\xA8\xD9\x87", + "\xEF\xB3\xA3" => "\xD8\xAA\xD9\x85", + "\xEF\xB3\xA4" => "\xD8\xAA\xD9\x87", + "\xEF\xB3\xA5" => "\xD8\xAB\xD9\x85", + "\xEF\xB3\xA6" => "\xD8\xAB\xD9\x87", + "\xEF\xB3\xA7" => "\xD8\xB3\xD9\x85", + "\xEF\xB3\xA8" => "\xD8\xB3\xD9\x87", + "\xEF\xB3\xA9" => "\xD8\xB4\xD9\x85", + "\xEF\xB3\xAA" => "\xD8\xB4\xD9\x87", + "\xEF\xB3\xAB" => "\xD9\x83\xD9\x84", + "\xEF\xB3\xAC" => "\xD9\x83\xD9\x85", + "\xEF\xB3\xAD" => "\xD9\x84\xD9\x85", + "\xEF\xB3\xAE" => "\xD9\x86\xD9\x85", + "\xEF\xB3\xAF" => "\xD9\x86\xD9\x87", + "\xEF\xB3\xB0" => "\xD9\x8A\xD9\x85", + "\xEF\xB3\xB1" => "\xD9\x8A\xD9\x87", + "\xEF\xB3\xB2" => "\xD9\x80\xD9\x8E\xD9\x91", + "\xEF\xB3\xB3" => "\xD9\x80\xD9\x8F\xD9\x91", + "\xEF\xB3\xB4" => "\xD9\x80\xD9\x90\xD9\x91", + "\xEF\xB3\xB5" => "\xD8\xB7\xD9\x89", + "\xEF\xB3\xB6" => "\xD8\xB7\xD9\x8A", + "\xEF\xB3\xB7" => "\xD8\xB9\xD9\x89", + "\xEF\xB3\xB8" => "\xD8\xB9\xD9\x8A", + "\xEF\xB3\xB9" => "\xD8\xBA\xD9\x89", + "\xEF\xB3\xBA" => "\xD8\xBA\xD9\x8A", + "\xEF\xB3\xBB" => "\xD8\xB3\xD9\x89", + "\xEF\xB3\xBC" => "\xD8\xB3\xD9\x8A", + "\xEF\xB3\xBD" => "\xD8\xB4\xD9\x89", + "\xEF\xB3\xBE" => "\xD8\xB4\xD9\x8A", + "\xEF\xB3\xBF" => "\xD8\xAD\xD9\x89", + "\xEF\xB4\x80" => "\xD8\xAD\xD9\x8A", + "\xEF\xB4\x81" => "\xD8\xAC\xD9\x89", + "\xEF\xB4\x82" => "\xD8\xAC\xD9\x8A", + "\xEF\xB4\x83" => "\xD8\xAE\xD9\x89", + "\xEF\xB4\x84" => "\xD8\xAE\xD9\x8A", + "\xEF\xB4\x85" => "\xD8\xB5\xD9\x89", + "\xEF\xB4\x86" => "\xD8\xB5\xD9\x8A", + "\xEF\xB4\x87" => "\xD8\xB6\xD9\x89", + "\xEF\xB4\x88" => "\xD8\xB6\xD9\x8A", + "\xEF\xB4\x89" => "\xD8\xB4\xD8\xAC", + "\xEF\xB4\x8A" => "\xD8\xB4\xD8\xAD", + "\xEF\xB4\x8B" => "\xD8\xB4\xD8\xAE", + "\xEF\xB4\x8C" => "\xD8\xB4\xD9\x85", + "\xEF\xB4\x8D" => "\xD8\xB4\xD8\xB1", + "\xEF\xB4\x8E" => "\xD8\xB3\xD8\xB1", + "\xEF\xB4\x8F" => "\xD8\xB5\xD8\xB1", + "\xEF\xB4\x90" => "\xD8\xB6\xD8\xB1", + "\xEF\xB4\x91" => "\xD8\xB7\xD9\x89", + "\xEF\xB4\x92" => "\xD8\xB7\xD9\x8A", + "\xEF\xB4\x93" => "\xD8\xB9\xD9\x89", + "\xEF\xB4\x94" => "\xD8\xB9\xD9\x8A", + "\xEF\xB4\x95" => "\xD8\xBA\xD9\x89", + "\xEF\xB4\x96" => "\xD8\xBA\xD9\x8A", + "\xEF\xB4\x97" => "\xD8\xB3\xD9\x89", + "\xEF\xB4\x98" => "\xD8\xB3\xD9\x8A", + "\xEF\xB4\x99" => "\xD8\xB4\xD9\x89", + "\xEF\xB4\x9A" => "\xD8\xB4\xD9\x8A", + "\xEF\xB4\x9B" => "\xD8\xAD\xD9\x89", + "\xEF\xB4\x9C" => "\xD8\xAD\xD9\x8A", + "\xEF\xB4\x9D" => "\xD8\xAC\xD9\x89", + "\xEF\xB4\x9E" => "\xD8\xAC\xD9\x8A", + "\xEF\xB4\x9F" => "\xD8\xAE\xD9\x89", + "\xEF\xB4\xA0" => "\xD8\xAE\xD9\x8A", + "\xEF\xB4\xA1" => "\xD8\xB5\xD9\x89", + "\xEF\xB4\xA2" => "\xD8\xB5\xD9\x8A", + "\xEF\xB4\xA3" => "\xD8\xB6\xD9\x89", + "\xEF\xB4\xA4" => "\xD8\xB6\xD9\x8A", + "\xEF\xB4\xA5" => "\xD8\xB4\xD8\xAC", + "\xEF\xB4\xA6" => "\xD8\xB4\xD8\xAD", + "\xEF\xB4\xA7" => "\xD8\xB4\xD8\xAE", + "\xEF\xB4\xA8" => "\xD8\xB4\xD9\x85", + "\xEF\xB4\xA9" => "\xD8\xB4\xD8\xB1", + "\xEF\xB4\xAA" => "\xD8\xB3\xD8\xB1", + "\xEF\xB4\xAB" => "\xD8\xB5\xD8\xB1", + "\xEF\xB4\xAC" => "\xD8\xB6\xD8\xB1", + "\xEF\xB4\xAD" => "\xD8\xB4\xD8\xAC", + "\xEF\xB4\xAE" => "\xD8\xB4\xD8\xAD", + "\xEF\xB4\xAF" => "\xD8\xB4\xD8\xAE", + "\xEF\xB4\xB0" => "\xD8\xB4\xD9\x85", + "\xEF\xB4\xB1" => "\xD8\xB3\xD9\x87", + "\xEF\xB4\xB2" => "\xD8\xB4\xD9\x87", + "\xEF\xB4\xB3" => "\xD8\xB7\xD9\x85", + "\xEF\xB4\xB4" => "\xD8\xB3\xD8\xAC", + "\xEF\xB4\xB5" => "\xD8\xB3\xD8\xAD", + "\xEF\xB4\xB6" => "\xD8\xB3\xD8\xAE", + "\xEF\xB4\xB7" => "\xD8\xB4\xD8\xAC", + "\xEF\xB4\xB8" => "\xD8\xB4\xD8\xAD", + "\xEF\xB4\xB9" => "\xD8\xB4\xD8\xAE", + "\xEF\xB4\xBA" => "\xD8\xB7\xD9\x85", + "\xEF\xB4\xBB" => "\xD8\xB8\xD9\x85", + "\xEF\xB4\xBC" => "\xD8\xA7\xD9\x8B", + "\xEF\xB4\xBD" => "\xD8\xA7\xD9\x8B", + "\xEF\xB5\x90" => "\xD8\xAA\xD8\xAC\xD9\x85", + "\xEF\xB5\x91" => "\xD8\xAA\xD8\xAD\xD8\xAC", + "\xEF\xB5\x92" => "\xD8\xAA\xD8\xAD\xD8\xAC", + "\xEF\xB5\x93" => "\xD8\xAA\xD8\xAD\xD9\x85", + "\xEF\xB5\x94" => "\xD8\xAA\xD8\xAE\xD9\x85", + "\xEF\xB5\x95" => "\xD8\xAA\xD9\x85\xD8\xAC", + "\xEF\xB5\x96" => "\xD8\xAA\xD9\x85\xD8\xAD", + "\xEF\xB5\x97" => "\xD8\xAA\xD9\x85\xD8\xAE", + "\xEF\xB5\x98" => "\xD8\xAC\xD9\x85\xD8\xAD", + "\xEF\xB5\x99" => "\xD8\xAC\xD9\x85\xD8\xAD", + "\xEF\xB5\x9A" => "\xD8\xAD\xD9\x85\xD9\x8A", + "\xEF\xB5\x9B" => "\xD8\xAD\xD9\x85\xD9\x89", + "\xEF\xB5\x9C" => "\xD8\xB3\xD8\xAD\xD8\xAC", + "\xEF\xB5\x9D" => "\xD8\xB3\xD8\xAC\xD8\xAD", + "\xEF\xB5\x9E" => "\xD8\xB3\xD8\xAC\xD9\x89", + "\xEF\xB5\x9F" => "\xD8\xB3\xD9\x85\xD8\xAD", + "\xEF\xB5\xA0" => "\xD8\xB3\xD9\x85\xD8\xAD", + "\xEF\xB5\xA1" => "\xD8\xB3\xD9\x85\xD8\xAC", + "\xEF\xB5\xA2" => "\xD8\xB3\xD9\x85\xD9\x85", + "\xEF\xB5\xA3" => "\xD8\xB3\xD9\x85\xD9\x85", + "\xEF\xB5\xA4" => "\xD8\xB5\xD8\xAD\xD8\xAD", + "\xEF\xB5\xA5" => "\xD8\xB5\xD8\xAD\xD8\xAD", + "\xEF\xB5\xA6" => "\xD8\xB5\xD9\x85\xD9\x85", + "\xEF\xB5\xA7" => "\xD8\xB4\xD8\xAD\xD9\x85", + "\xEF\xB5\xA8" => "\xD8\xB4\xD8\xAD\xD9\x85", + "\xEF\xB5\xA9" => "\xD8\xB4\xD8\xAC\xD9\x8A", + "\xEF\xB5\xAA" => "\xD8\xB4\xD9\x85\xD8\xAE", + "\xEF\xB5\xAB" => "\xD8\xB4\xD9\x85\xD8\xAE", + "\xEF\xB5\xAC" => "\xD8\xB4\xD9\x85\xD9\x85", + "\xEF\xB5\xAD" => "\xD8\xB4\xD9\x85\xD9\x85", + "\xEF\xB5\xAE" => "\xD8\xB6\xD8\xAD\xD9\x89", + "\xEF\xB5\xAF" => "\xD8\xB6\xD8\xAE\xD9\x85", + "\xEF\xB5\xB0" => "\xD8\xB6\xD8\xAE\xD9\x85", + "\xEF\xB5\xB1" => "\xD8\xB7\xD9\x85\xD8\xAD", + "\xEF\xB5\xB2" => "\xD8\xB7\xD9\x85\xD8\xAD", + "\xEF\xB5\xB3" => "\xD8\xB7\xD9\x85\xD9\x85", + "\xEF\xB5\xB4" => "\xD8\xB7\xD9\x85\xD9\x8A", + "\xEF\xB5\xB5" => "\xD8\xB9\xD8\xAC\xD9\x85", + "\xEF\xB5\xB6" => "\xD8\xB9\xD9\x85\xD9\x85", + "\xEF\xB5\xB7" => "\xD8\xB9\xD9\x85\xD9\x85", + "\xEF\xB5\xB8" => "\xD8\xB9\xD9\x85\xD9\x89", + "\xEF\xB5\xB9" => "\xD8\xBA\xD9\x85\xD9\x85", + "\xEF\xB5\xBA" => "\xD8\xBA\xD9\x85\xD9\x8A", + "\xEF\xB5\xBB" => "\xD8\xBA\xD9\x85\xD9\x89", + "\xEF\xB5\xBC" => "\xD9\x81\xD8\xAE\xD9\x85", + "\xEF\xB5\xBD" => "\xD9\x81\xD8\xAE\xD9\x85", + "\xEF\xB5\xBE" => "\xD9\x82\xD9\x85\xD8\xAD", + "\xEF\xB5\xBF" => "\xD9\x82\xD9\x85\xD9\x85", + "\xEF\xB6\x80" => "\xD9\x84\xD8\xAD\xD9\x85", + "\xEF\xB6\x81" => "\xD9\x84\xD8\xAD\xD9\x8A", + "\xEF\xB6\x82" => "\xD9\x84\xD8\xAD\xD9\x89", + "\xEF\xB6\x83" => "\xD9\x84\xD8\xAC\xD8\xAC", + "\xEF\xB6\x84" => "\xD9\x84\xD8\xAC\xD8\xAC", + "\xEF\xB6\x85" => "\xD9\x84\xD8\xAE\xD9\x85", + "\xEF\xB6\x86" => "\xD9\x84\xD8\xAE\xD9\x85", + "\xEF\xB6\x87" => "\xD9\x84\xD9\x85\xD8\xAD", + "\xEF\xB6\x88" => "\xD9\x84\xD9\x85\xD8\xAD", + "\xEF\xB6\x89" => "\xD9\x85\xD8\xAD\xD8\xAC", + "\xEF\xB6\x8A" => "\xD9\x85\xD8\xAD\xD9\x85", + "\xEF\xB6\x8B" => "\xD9\x85\xD8\xAD\xD9\x8A", + "\xEF\xB6\x8C" => "\xD9\x85\xD8\xAC\xD8\xAD", + "\xEF\xB6\x8D" => "\xD9\x85\xD8\xAC\xD9\x85", + "\xEF\xB6\x8E" => "\xD9\x85\xD8\xAE\xD8\xAC", + "\xEF\xB6\x8F" => "\xD9\x85\xD8\xAE\xD9\x85", + "\xEF\xB6\x92" => "\xD9\x85\xD8\xAC\xD8\xAE", + "\xEF\xB6\x93" => "\xD9\x87\xD9\x85\xD8\xAC", + "\xEF\xB6\x94" => "\xD9\x87\xD9\x85\xD9\x85", + "\xEF\xB6\x95" => "\xD9\x86\xD8\xAD\xD9\x85", + "\xEF\xB6\x96" => "\xD9\x86\xD8\xAD\xD9\x89", + "\xEF\xB6\x97" => "\xD9\x86\xD8\xAC\xD9\x85", + "\xEF\xB6\x98" => "\xD9\x86\xD8\xAC\xD9\x85", + "\xEF\xB6\x99" => "\xD9\x86\xD8\xAC\xD9\x89", + "\xEF\xB6\x9A" => "\xD9\x86\xD9\x85\xD9\x8A", + "\xEF\xB6\x9B" => "\xD9\x86\xD9\x85\xD9\x89", + "\xEF\xB6\x9C" => "\xD9\x8A\xD9\x85\xD9\x85", + "\xEF\xB6\x9D" => "\xD9\x8A\xD9\x85\xD9\x85", + "\xEF\xB6\x9E" => "\xD8\xA8\xD8\xAE\xD9\x8A", + "\xEF\xB6\x9F" => "\xD8\xAA\xD8\xAC\xD9\x8A", + "\xEF\xB6\xA0" => "\xD8\xAA\xD8\xAC\xD9\x89", + "\xEF\xB6\xA1" => "\xD8\xAA\xD8\xAE\xD9\x8A", + "\xEF\xB6\xA2" => "\xD8\xAA\xD8\xAE\xD9\x89", + "\xEF\xB6\xA3" => "\xD8\xAA\xD9\x85\xD9\x8A", + "\xEF\xB6\xA4" => "\xD8\xAA\xD9\x85\xD9\x89", + "\xEF\xB6\xA5" => "\xD8\xAC\xD9\x85\xD9\x8A", + "\xEF\xB6\xA6" => "\xD8\xAC\xD8\xAD\xD9\x89", + "\xEF\xB6\xA7" => "\xD8\xAC\xD9\x85\xD9\x89", + "\xEF\xB6\xA8" => "\xD8\xB3\xD8\xAE\xD9\x89", + "\xEF\xB6\xA9" => "\xD8\xB5\xD8\xAD\xD9\x8A", + "\xEF\xB6\xAA" => "\xD8\xB4\xD8\xAD\xD9\x8A", + "\xEF\xB6\xAB" => "\xD8\xB6\xD8\xAD\xD9\x8A", + "\xEF\xB6\xAC" => "\xD9\x84\xD8\xAC\xD9\x8A", + "\xEF\xB6\xAD" => "\xD9\x84\xD9\x85\xD9\x8A", + "\xEF\xB6\xAE" => "\xD9\x8A\xD8\xAD\xD9\x8A", + "\xEF\xB6\xAF" => "\xD9\x8A\xD8\xAC\xD9\x8A", + "\xEF\xB6\xB0" => "\xD9\x8A\xD9\x85\xD9\x8A", + "\xEF\xB6\xB1" => "\xD9\x85\xD9\x85\xD9\x8A", + "\xEF\xB6\xB2" => "\xD9\x82\xD9\x85\xD9\x8A", + "\xEF\xB6\xB3" => "\xD9\x86\xD8\xAD\xD9\x8A", + "\xEF\xB6\xB4" => "\xD9\x82\xD9\x85\xD8\xAD", + "\xEF\xB6\xB5" => "\xD9\x84\xD8\xAD\xD9\x85", + "\xEF\xB6\xB6" => "\xD8\xB9\xD9\x85\xD9\x8A", + "\xEF\xB6\xB7" => "\xD9\x83\xD9\x85\xD9\x8A", + "\xEF\xB6\xB8" => "\xD9\x86\xD8\xAC\xD8\xAD", + "\xEF\xB6\xB9" => "\xD9\x85\xD8\xAE\xD9\x8A", + "\xEF\xB6\xBA" => "\xD9\x84\xD8\xAC\xD9\x85", + "\xEF\xB6\xBB" => "\xD9\x83\xD9\x85\xD9\x85", + "\xEF\xB6\xBC" => "\xD9\x84\xD8\xAC\xD9\x85", + "\xEF\xB6\xBD" => "\xD9\x86\xD8\xAC\xD8\xAD", + "\xEF\xB6\xBE" => "\xD8\xAC\xD8\xAD\xD9\x8A", + "\xEF\xB6\xBF" => "\xD8\xAD\xD8\xAC\xD9\x8A", + "\xEF\xB7\x80" => "\xD9\x85\xD8\xAC\xD9\x8A", + "\xEF\xB7\x81" => "\xD9\x81\xD9\x85\xD9\x8A", + "\xEF\xB7\x82" => "\xD8\xA8\xD8\xAD\xD9\x8A", + "\xEF\xB7\x83" => "\xD9\x83\xD9\x85\xD9\x85", + "\xEF\xB7\x84" => "\xD8\xB9\xD8\xAC\xD9\x85", + "\xEF\xB7\x85" => "\xD8\xB5\xD9\x85\xD9\x85", + "\xEF\xB7\x86" => "\xD8\xB3\xD8\xAE\xD9\x8A", + "\xEF\xB7\x87" => "\xD9\x86\xD8\xAC\xD9\x8A", + "\xEF\xB7\xB0" => "\xD8\xB5\xD9\x84\xDB\x92", + "\xEF\xB7\xB1" => "\xD9\x82\xD9\x84\xDB\x92", + "\xEF\xB7\xB2" => "\xD8\xA7\xD9\x84\xD9\x84\xD9\x87", + "\xEF\xB7\xB3" => "\xD8\xA7\xD9\x83\xD8\xA8\xD8\xB1", + "\xEF\xB7\xB4" => "\xD9\x85\xD8\xAD\xD9\x85\xD8\xAF", + "\xEF\xB7\xB5" => "\xD8\xB5\xD9\x84\xD8\xB9\xD9\x85", + "\xEF\xB7\xB6" => "\xD8\xB1\xD8\xB3\xD9\x88\xD9\x84", + "\xEF\xB7\xB7" => "\xD8\xB9\xD9\x84\xD9\x8A\xD9\x87", + "\xEF\xB7\xB8" => "\xD9\x88\xD8\xB3\xD9\x84\xD9\x85", + "\xEF\xB7\xB9" => "\xD8\xB5\xD9\x84\xD9\x89", + "\xEF\xB7\xBC" => "\xD8\xB1\xDB\x8C\xD8\xA7\xD9\x84", + "\xEF\xB8\x91" => "\xE3\x80\x81", + "\xEF\xB8\x97" => "\xE3\x80\x96", + "\xEF\xB8\x98" => "\xE3\x80\x97", + "\xEF\xB8\xB1" => "\xE2\x80\x94", + "\xEF\xB8\xB2" => "\xE2\x80\x93", + "\xEF\xB8\xB9" => "\xE3\x80\x94", + "\xEF\xB8\xBA" => "\xE3\x80\x95", + "\xEF\xB8\xBB" => "\xE3\x80\x90", + "\xEF\xB8\xBC" => "\xE3\x80\x91", + "\xEF\xB8\xBD" => "\xE3\x80\x8A", + "\xEF\xB8\xBE" => "\xE3\x80\x8B", + "\xEF\xB8\xBF" => "\xE3\x80\x88", + "\xEF\xB9\x80" => "\xE3\x80\x89", + "\xEF\xB9\x81" => "\xE3\x80\x8C", + "\xEF\xB9\x82" => "\xE3\x80\x8D", + "\xEF\xB9\x83" => "\xE3\x80\x8E", + "\xEF\xB9\x84" => "\xE3\x80\x8F", + "\xEF\xB9\x91" => "\xE3\x80\x81", + "\xEF\xB9\x98" => "\xE2\x80\x94", + "\xEF\xB9\x9D" => "\xE3\x80\x94", + "\xEF\xB9\x9E" => "\xE3\x80\x95", + "\xEF\xB9\xA3" => "\x2D", + "\xEF\xB9\xB1" => "\xD9\x80\xD9\x8B", + "\xEF\xB9\xB7" => "\xD9\x80\xD9\x8E", + "\xEF\xB9\xB9" => "\xD9\x80\xD9\x8F", + "\xEF\xB9\xBB" => "\xD9\x80\xD9\x90", + "\xEF\xB9\xBD" => "\xD9\x80\xD9\x91", + "\xEF\xB9\xBF" => "\xD9\x80\xD9\x92", + "\xEF\xBA\x80" => "\xD8\xA1", + "\xEF\xBA\x81" => "\xD8\xA2", + "\xEF\xBA\x82" => "\xD8\xA2", + "\xEF\xBA\x83" => "\xD8\xA3", + "\xEF\xBA\x84" => "\xD8\xA3", + "\xEF\xBA\x85" => "\xD8\xA4", + "\xEF\xBA\x86" => "\xD8\xA4", + "\xEF\xBA\x87" => "\xD8\xA5", + "\xEF\xBA\x88" => "\xD8\xA5", + "\xEF\xBA\x89" => "\xD8\xA6", + "\xEF\xBA\x8A" => "\xD8\xA6", + "\xEF\xBA\x8B" => "\xD8\xA6", + "\xEF\xBA\x8C" => "\xD8\xA6", + "\xEF\xBA\x8D" => "\xD8\xA7", + "\xEF\xBA\x8E" => "\xD8\xA7", + "\xEF\xBA\x8F" => "\xD8\xA8", + "\xEF\xBA\x90" => "\xD8\xA8", + "\xEF\xBA\x91" => "\xD8\xA8", + "\xEF\xBA\x92" => "\xD8\xA8", + "\xEF\xBA\x93" => "\xD8\xA9", + "\xEF\xBA\x94" => "\xD8\xA9", + "\xEF\xBA\x95" => "\xD8\xAA", + "\xEF\xBA\x96" => "\xD8\xAA", + "\xEF\xBA\x97" => "\xD8\xAA", + "\xEF\xBA\x98" => "\xD8\xAA", + "\xEF\xBA\x99" => "\xD8\xAB", + "\xEF\xBA\x9A" => "\xD8\xAB", + "\xEF\xBA\x9B" => "\xD8\xAB", + "\xEF\xBA\x9C" => "\xD8\xAB", + "\xEF\xBA\x9D" => "\xD8\xAC", + "\xEF\xBA\x9E" => "\xD8\xAC", + "\xEF\xBA\x9F" => "\xD8\xAC", + "\xEF\xBA\xA0" => "\xD8\xAC", + "\xEF\xBA\xA1" => "\xD8\xAD", + "\xEF\xBA\xA2" => "\xD8\xAD", + "\xEF\xBA\xA3" => "\xD8\xAD", + "\xEF\xBA\xA4" => "\xD8\xAD", + "\xEF\xBA\xA5" => "\xD8\xAE", + "\xEF\xBA\xA6" => "\xD8\xAE", + "\xEF\xBA\xA7" => "\xD8\xAE", + "\xEF\xBA\xA8" => "\xD8\xAE", + "\xEF\xBA\xA9" => "\xD8\xAF", + "\xEF\xBA\xAA" => "\xD8\xAF", + "\xEF\xBA\xAB" => "\xD8\xB0", + "\xEF\xBA\xAC" => "\xD8\xB0", + "\xEF\xBA\xAD" => "\xD8\xB1", + "\xEF\xBA\xAE" => "\xD8\xB1", + "\xEF\xBA\xAF" => "\xD8\xB2", + "\xEF\xBA\xB0" => "\xD8\xB2", + "\xEF\xBA\xB1" => "\xD8\xB3", + "\xEF\xBA\xB2" => "\xD8\xB3", + "\xEF\xBA\xB3" => "\xD8\xB3", + "\xEF\xBA\xB4" => "\xD8\xB3", + "\xEF\xBA\xB5" => "\xD8\xB4", + "\xEF\xBA\xB6" => "\xD8\xB4", + "\xEF\xBA\xB7" => "\xD8\xB4", + "\xEF\xBA\xB8" => "\xD8\xB4", + "\xEF\xBA\xB9" => "\xD8\xB5", + "\xEF\xBA\xBA" => "\xD8\xB5", + "\xEF\xBA\xBB" => "\xD8\xB5", + "\xEF\xBA\xBC" => "\xD8\xB5", + "\xEF\xBA\xBD" => "\xD8\xB6", + "\xEF\xBA\xBE" => "\xD8\xB6", + "\xEF\xBA\xBF" => "\xD8\xB6", + "\xEF\xBB\x80" => "\xD8\xB6", + "\xEF\xBB\x81" => "\xD8\xB7", + "\xEF\xBB\x82" => "\xD8\xB7", + "\xEF\xBB\x83" => "\xD8\xB7", + "\xEF\xBB\x84" => "\xD8\xB7", + "\xEF\xBB\x85" => "\xD8\xB8", + "\xEF\xBB\x86" => "\xD8\xB8", + "\xEF\xBB\x87" => "\xD8\xB8", + "\xEF\xBB\x88" => "\xD8\xB8", + "\xEF\xBB\x89" => "\xD8\xB9", + "\xEF\xBB\x8A" => "\xD8\xB9", + "\xEF\xBB\x8B" => "\xD8\xB9", + "\xEF\xBB\x8C" => "\xD8\xB9", + "\xEF\xBB\x8D" => "\xD8\xBA", + "\xEF\xBB\x8E" => "\xD8\xBA", + "\xEF\xBB\x8F" => "\xD8\xBA", + "\xEF\xBB\x90" => "\xD8\xBA", + "\xEF\xBB\x91" => "\xD9\x81", + "\xEF\xBB\x92" => "\xD9\x81", + "\xEF\xBB\x93" => "\xD9\x81", + "\xEF\xBB\x94" => "\xD9\x81", + "\xEF\xBB\x95" => "\xD9\x82", + "\xEF\xBB\x96" => "\xD9\x82", + "\xEF\xBB\x97" => "\xD9\x82", + "\xEF\xBB\x98" => "\xD9\x82", + "\xEF\xBB\x99" => "\xD9\x83", + "\xEF\xBB\x9A" => "\xD9\x83", + "\xEF\xBB\x9B" => "\xD9\x83", + "\xEF\xBB\x9C" => "\xD9\x83", + "\xEF\xBB\x9D" => "\xD9\x84", + "\xEF\xBB\x9E" => "\xD9\x84", + "\xEF\xBB\x9F" => "\xD9\x84", + "\xEF\xBB\xA0" => "\xD9\x84", + "\xEF\xBB\xA1" => "\xD9\x85", + "\xEF\xBB\xA2" => "\xD9\x85", + "\xEF\xBB\xA3" => "\xD9\x85", + "\xEF\xBB\xA4" => "\xD9\x85", + "\xEF\xBB\xA5" => "\xD9\x86", + "\xEF\xBB\xA6" => "\xD9\x86", + "\xEF\xBB\xA7" => "\xD9\x86", + "\xEF\xBB\xA8" => "\xD9\x86", + "\xEF\xBB\xA9" => "\xD9\x87", + "\xEF\xBB\xAA" => "\xD9\x87", + "\xEF\xBB\xAB" => "\xD9\x87", + "\xEF\xBB\xAC" => "\xD9\x87", + "\xEF\xBB\xAD" => "\xD9\x88", + "\xEF\xBB\xAE" => "\xD9\x88", + "\xEF\xBB\xAF" => "\xD9\x89", + "\xEF\xBB\xB0" => "\xD9\x89", + "\xEF\xBB\xB1" => "\xD9\x8A", + "\xEF\xBB\xB2" => "\xD9\x8A", + "\xEF\xBB\xB3" => "\xD9\x8A", + "\xEF\xBB\xB4" => "\xD9\x8A", + "\xEF\xBB\xB5" => "\xD9\x84\xD8\xA2", + "\xEF\xBB\xB6" => "\xD9\x84\xD8\xA2", + "\xEF\xBB\xB7" => "\xD9\x84\xD8\xA3", + "\xEF\xBB\xB8" => "\xD9\x84\xD8\xA3", + "\xEF\xBB\xB9" => "\xD9\x84\xD8\xA5", + "\xEF\xBB\xBA" => "\xD9\x84\xD8\xA5", + "\xEF\xBB\xBB" => "\xD9\x84\xD8\xA7", + "\xEF\xBB\xBC" => "\xD9\x84\xD8\xA7", + "\xEF\xBC\x8D" => "\x2D", + "\xEF\xBC\x8E" => "\x2E", + "\xEF\xBC\x90" => "\x30", + "\xEF\xBC\x91" => "\x31", + "\xEF\xBC\x92" => "\x32", + "\xEF\xBC\x93" => "\x33", + "\xEF\xBC\x94" => "\x34", + "\xEF\xBC\x95" => "\x35", + "\xEF\xBC\x96" => "\x36", + "\xEF\xBC\x97" => "\x37", + "\xEF\xBC\x98" => "\x38", + "\xEF\xBC\x99" => "\x39", + "\xEF\xBC\xA1" => "\x61", + "\xEF\xBC\xA2" => "\x62", + "\xEF\xBC\xA3" => "\x63", + "\xEF\xBC\xA4" => "\x64", + "\xEF\xBC\xA5" => "\x65", + "\xEF\xBC\xA6" => "\x66", + "\xEF\xBC\xA7" => "\x67", + "\xEF\xBC\xA8" => "\x68", + "\xEF\xBC\xA9" => "\x69", + "\xEF\xBC\xAA" => "\x6A", + "\xEF\xBC\xAB" => "\x6B", + "\xEF\xBC\xAC" => "\x6C", + "\xEF\xBC\xAD" => "\x6D", + "\xEF\xBC\xAE" => "\x6E", + "\xEF\xBC\xAF" => "\x6F", + "\xEF\xBC\xB0" => "\x70", + "\xEF\xBC\xB1" => "\x71", + "\xEF\xBC\xB2" => "\x72", + "\xEF\xBC\xB3" => "\x73", + "\xEF\xBC\xB4" => "\x74", + "\xEF\xBC\xB5" => "\x75", + "\xEF\xBC\xB6" => "\x76", + "\xEF\xBC\xB7" => "\x77", + "\xEF\xBC\xB8" => "\x78", + "\xEF\xBC\xB9" => "\x79", + "\xEF\xBC\xBA" => "\x7A", + "\xEF\xBD\x81" => "\x61", + "\xEF\xBD\x82" => "\x62", + "\xEF\xBD\x83" => "\x63", + "\xEF\xBD\x84" => "\x64", + "\xEF\xBD\x85" => "\x65", + "\xEF\xBD\x86" => "\x66", + "\xEF\xBD\x87" => "\x67", + "\xEF\xBD\x88" => "\x68", + "\xEF\xBD\x89" => "\x69", + "\xEF\xBD\x8A" => "\x6A", + "\xEF\xBD\x8B" => "\x6B", + "\xEF\xBD\x8C" => "\x6C", + "\xEF\xBD\x8D" => "\x6D", + "\xEF\xBD\x8E" => "\x6E", + "\xEF\xBD\x8F" => "\x6F", + "\xEF\xBD\x90" => "\x70", + "\xEF\xBD\x91" => "\x71", + "\xEF\xBD\x92" => "\x72", + "\xEF\xBD\x93" => "\x73", + "\xEF\xBD\x94" => "\x74", + "\xEF\xBD\x95" => "\x75", + "\xEF\xBD\x96" => "\x76", + "\xEF\xBD\x97" => "\x77", + "\xEF\xBD\x98" => "\x78", + "\xEF\xBD\x99" => "\x79", + "\xEF\xBD\x9A" => "\x7A", + "\xEF\xBD\x9F" => "\xE2\xA6\x85", + "\xEF\xBD\xA0" => "\xE2\xA6\x86", + "\xEF\xBD\xA1" => "\x2E", + "\xEF\xBD\xA2" => "\xE3\x80\x8C", + "\xEF\xBD\xA3" => "\xE3\x80\x8D", + "\xEF\xBD\xA4" => "\xE3\x80\x81", + "\xEF\xBD\xA5" => "\xE3\x83\xBB", + "\xEF\xBD\xA6" => "\xE3\x83\xB2", + "\xEF\xBD\xA7" => "\xE3\x82\xA1", + "\xEF\xBD\xA8" => "\xE3\x82\xA3", + "\xEF\xBD\xA9" => "\xE3\x82\xA5", + "\xEF\xBD\xAA" => "\xE3\x82\xA7", + "\xEF\xBD\xAB" => "\xE3\x82\xA9", + "\xEF\xBD\xAC" => "\xE3\x83\xA3", + "\xEF\xBD\xAD" => "\xE3\x83\xA5", + "\xEF\xBD\xAE" => "\xE3\x83\xA7", + "\xEF\xBD\xAF" => "\xE3\x83\x83", + "\xEF\xBD\xB0" => "\xE3\x83\xBC", + "\xEF\xBD\xB1" => "\xE3\x82\xA2", + "\xEF\xBD\xB2" => "\xE3\x82\xA4", + "\xEF\xBD\xB3" => "\xE3\x82\xA6", + "\xEF\xBD\xB4" => "\xE3\x82\xA8", + "\xEF\xBD\xB5" => "\xE3\x82\xAA", + "\xEF\xBD\xB6" => "\xE3\x82\xAB", + "\xEF\xBD\xB7" => "\xE3\x82\xAD", + "\xEF\xBD\xB8" => "\xE3\x82\xAF", + "\xEF\xBD\xB9" => "\xE3\x82\xB1", + "\xEF\xBD\xBA" => "\xE3\x82\xB3", + "\xEF\xBD\xBB" => "\xE3\x82\xB5", + "\xEF\xBD\xBC" => "\xE3\x82\xB7", + "\xEF\xBD\xBD" => "\xE3\x82\xB9", + "\xEF\xBD\xBE" => "\xE3\x82\xBB", + "\xEF\xBD\xBF" => "\xE3\x82\xBD", + "\xEF\xBE\x80" => "\xE3\x82\xBF", + "\xEF\xBE\x81" => "\xE3\x83\x81", + "\xEF\xBE\x82" => "\xE3\x83\x84", + "\xEF\xBE\x83" => "\xE3\x83\x86", + "\xEF\xBE\x84" => "\xE3\x83\x88", + "\xEF\xBE\x85" => "\xE3\x83\x8A", + "\xEF\xBE\x86" => "\xE3\x83\x8B", + "\xEF\xBE\x87" => "\xE3\x83\x8C", + "\xEF\xBE\x88" => "\xE3\x83\x8D", + "\xEF\xBE\x89" => "\xE3\x83\x8E", + "\xEF\xBE\x8A" => "\xE3\x83\x8F", + "\xEF\xBE\x8B" => "\xE3\x83\x92", + "\xEF\xBE\x8C" => "\xE3\x83\x95", + "\xEF\xBE\x8D" => "\xE3\x83\x98", + "\xEF\xBE\x8E" => "\xE3\x83\x9B", + "\xEF\xBE\x8F" => "\xE3\x83\x9E", + "\xEF\xBE\x90" => "\xE3\x83\x9F", + "\xEF\xBE\x91" => "\xE3\x83\xA0", + "\xEF\xBE\x92" => "\xE3\x83\xA1", + "\xEF\xBE\x93" => "\xE3\x83\xA2", + "\xEF\xBE\x94" => "\xE3\x83\xA4", + "\xEF\xBE\x95" => "\xE3\x83\xA6", + "\xEF\xBE\x96" => "\xE3\x83\xA8", + "\xEF\xBE\x97" => "\xE3\x83\xA9", + "\xEF\xBE\x98" => "\xE3\x83\xAA", + "\xEF\xBE\x99" => "\xE3\x83\xAB", + "\xEF\xBE\x9A" => "\xE3\x83\xAC", + "\xEF\xBE\x9B" => "\xE3\x83\xAD", + "\xEF\xBE\x9C" => "\xE3\x83\xAF", + "\xEF\xBE\x9D" => "\xE3\x83\xB3", + "\xEF\xBE\x9E" => "\xE3\x82\x99", + "\xEF\xBE\x9F" => "\xE3\x82\x9A", + "\xEF\xBE\xA1" => "\xE1\x84\x80", + "\xEF\xBE\xA2" => "\xE1\x84\x81", + "\xEF\xBE\xA3" => "\xE1\x86\xAA", + "\xEF\xBE\xA4" => "\xE1\x84\x82", + "\xEF\xBE\xA5" => "\xE1\x86\xAC", + "\xEF\xBE\xA6" => "\xE1\x86\xAD", + "\xEF\xBE\xA7" => "\xE1\x84\x83", + "\xEF\xBE\xA8" => "\xE1\x84\x84", + "\xEF\xBE\xA9" => "\xE1\x84\x85", + "\xEF\xBE\xAA" => "\xE1\x86\xB0", + "\xEF\xBE\xAB" => "\xE1\x86\xB1", + "\xEF\xBE\xAC" => "\xE1\x86\xB2", + "\xEF\xBE\xAD" => "\xE1\x86\xB3", + "\xEF\xBE\xAE" => "\xE1\x86\xB4", + "\xEF\xBE\xAF" => "\xE1\x86\xB5", + "\xEF\xBE\xB0" => "\xE1\x84\x9A", + "\xEF\xBE\xB1" => "\xE1\x84\x86", + "\xEF\xBE\xB2" => "\xE1\x84\x87", + "\xEF\xBE\xB3" => "\xE1\x84\x88", + "\xEF\xBE\xB4" => "\xE1\x84\xA1", + "\xEF\xBE\xB5" => "\xE1\x84\x89", + "\xEF\xBE\xB6" => "\xE1\x84\x8A", + "\xEF\xBE\xB7" => "\xE1\x84\x8B", + "\xEF\xBE\xB8" => "\xE1\x84\x8C", + "\xEF\xBE\xB9" => "\xE1\x84\x8D", + "\xEF\xBE\xBA" => "\xE1\x84\x8E", + "\xEF\xBE\xBB" => "\xE1\x84\x8F", + "\xEF\xBE\xBC" => "\xE1\x84\x90", + "\xEF\xBE\xBD" => "\xE1\x84\x91", + "\xEF\xBE\xBE" => "\xE1\x84\x92", + "\xEF\xBF\x82" => "\xE1\x85\xA1", + "\xEF\xBF\x83" => "\xE1\x85\xA2", + "\xEF\xBF\x84" => "\xE1\x85\xA3", + "\xEF\xBF\x85" => "\xE1\x85\xA4", + "\xEF\xBF\x86" => "\xE1\x85\xA5", + "\xEF\xBF\x87" => "\xE1\x85\xA6", + "\xEF\xBF\x8A" => "\xE1\x85\xA7", + "\xEF\xBF\x8B" => "\xE1\x85\xA8", + "\xEF\xBF\x8C" => "\xE1\x85\xA9", + "\xEF\xBF\x8D" => "\xE1\x85\xAA", + "\xEF\xBF\x8E" => "\xE1\x85\xAB", + "\xEF\xBF\x8F" => "\xE1\x85\xAC", + "\xEF\xBF\x92" => "\xE1\x85\xAD", + "\xEF\xBF\x93" => "\xE1\x85\xAE", + "\xEF\xBF\x94" => "\xE1\x85\xAF", + "\xEF\xBF\x95" => "\xE1\x85\xB0", + "\xEF\xBF\x96" => "\xE1\x85\xB1", + "\xEF\xBF\x97" => "\xE1\x85\xB2", + "\xEF\xBF\x9A" => "\xE1\x85\xB3", + "\xEF\xBF\x9B" => "\xE1\x85\xB4", + "\xEF\xBF\x9C" => "\xE1\x85\xB5", + "\xEF\xBF\xA0" => "\xC2\xA2", + "\xEF\xBF\xA1" => "\xC2\xA3", + "\xEF\xBF\xA2" => "\xC2\xAC", + "\xEF\xBF\xA4" => "\xC2\xA6", + "\xEF\xBF\xA5" => "\xC2\xA5", + "\xEF\xBF\xA6" => "\xE2\x82\xA9", + "\xEF\xBF\xA8" => "\xE2\x94\x82", + "\xEF\xBF\xA9" => "\xE2\x86\x90", + "\xEF\xBF\xAA" => "\xE2\x86\x91", + "\xEF\xBF\xAB" => "\xE2\x86\x92", + "\xEF\xBF\xAC" => "\xE2\x86\x93", + "\xEF\xBF\xAD" => "\xE2\x96\xA0", + "\xEF\xBF\xAE" => "\xE2\x97\x8B", + "\xF0\x90\x90\x80" => "\xF0\x90\x90\xA8", + "\xF0\x90\x90\x81" => "\xF0\x90\x90\xA9", + "\xF0\x90\x90\x82" => "\xF0\x90\x90\xAA", + "\xF0\x90\x90\x83" => "\xF0\x90\x90\xAB", + "\xF0\x90\x90\x84" => "\xF0\x90\x90\xAC", + "\xF0\x90\x90\x85" => "\xF0\x90\x90\xAD", + "\xF0\x90\x90\x86" => "\xF0\x90\x90\xAE", + "\xF0\x90\x90\x87" => "\xF0\x90\x90\xAF", + "\xF0\x90\x90\x88" => "\xF0\x90\x90\xB0", + "\xF0\x90\x90\x89" => "\xF0\x90\x90\xB1", + "\xF0\x90\x90\x8A" => "\xF0\x90\x90\xB2", + "\xF0\x90\x90\x8B" => "\xF0\x90\x90\xB3", + "\xF0\x90\x90\x8C" => "\xF0\x90\x90\xB4", + "\xF0\x90\x90\x8D" => "\xF0\x90\x90\xB5", + "\xF0\x90\x90\x8E" => "\xF0\x90\x90\xB6", + "\xF0\x90\x90\x8F" => "\xF0\x90\x90\xB7", + "\xF0\x90\x90\x90" => "\xF0\x90\x90\xB8", + "\xF0\x90\x90\x91" => "\xF0\x90\x90\xB9", + "\xF0\x90\x90\x92" => "\xF0\x90\x90\xBA", + "\xF0\x90\x90\x93" => "\xF0\x90\x90\xBB", + "\xF0\x90\x90\x94" => "\xF0\x90\x90\xBC", + "\xF0\x90\x90\x95" => "\xF0\x90\x90\xBD", + "\xF0\x90\x90\x96" => "\xF0\x90\x90\xBE", + "\xF0\x90\x90\x97" => "\xF0\x90\x90\xBF", + "\xF0\x90\x90\x98" => "\xF0\x90\x91\x80", + "\xF0\x90\x90\x99" => "\xF0\x90\x91\x81", + "\xF0\x90\x90\x9A" => "\xF0\x90\x91\x82", + "\xF0\x90\x90\x9B" => "\xF0\x90\x91\x83", + "\xF0\x90\x90\x9C" => "\xF0\x90\x91\x84", + "\xF0\x90\x90\x9D" => "\xF0\x90\x91\x85", + "\xF0\x90\x90\x9E" => "\xF0\x90\x91\x86", + "\xF0\x90\x90\x9F" => "\xF0\x90\x91\x87", + "\xF0\x90\x90\xA0" => "\xF0\x90\x91\x88", + "\xF0\x90\x90\xA1" => "\xF0\x90\x91\x89", + "\xF0\x90\x90\xA2" => "\xF0\x90\x91\x8A", + "\xF0\x90\x90\xA3" => "\xF0\x90\x91\x8B", + "\xF0\x90\x90\xA4" => "\xF0\x90\x91\x8C", + "\xF0\x90\x90\xA5" => "\xF0\x90\x91\x8D", + "\xF0\x90\x90\xA6" => "\xF0\x90\x91\x8E", + "\xF0\x90\x90\xA7" => "\xF0\x90\x91\x8F", + "\xF0\x90\x92\xB0" => "\xF0\x90\x93\x98", + "\xF0\x90\x92\xB1" => "\xF0\x90\x93\x99", + "\xF0\x90\x92\xB2" => "\xF0\x90\x93\x9A", + "\xF0\x90\x92\xB3" => "\xF0\x90\x93\x9B", + "\xF0\x90\x92\xB4" => "\xF0\x90\x93\x9C", + "\xF0\x90\x92\xB5" => "\xF0\x90\x93\x9D", + "\xF0\x90\x92\xB6" => "\xF0\x90\x93\x9E", + "\xF0\x90\x92\xB7" => "\xF0\x90\x93\x9F", + "\xF0\x90\x92\xB8" => "\xF0\x90\x93\xA0", + "\xF0\x90\x92\xB9" => "\xF0\x90\x93\xA1", + "\xF0\x90\x92\xBA" => "\xF0\x90\x93\xA2", + "\xF0\x90\x92\xBB" => "\xF0\x90\x93\xA3", + "\xF0\x90\x92\xBC" => "\xF0\x90\x93\xA4", + "\xF0\x90\x92\xBD" => "\xF0\x90\x93\xA5", + "\xF0\x90\x92\xBE" => "\xF0\x90\x93\xA6", + "\xF0\x90\x92\xBF" => "\xF0\x90\x93\xA7", + "\xF0\x90\x93\x80" => "\xF0\x90\x93\xA8", + "\xF0\x90\x93\x81" => "\xF0\x90\x93\xA9", + "\xF0\x90\x93\x82" => "\xF0\x90\x93\xAA", + "\xF0\x90\x93\x83" => "\xF0\x90\x93\xAB", + "\xF0\x90\x93\x84" => "\xF0\x90\x93\xAC", + "\xF0\x90\x93\x85" => "\xF0\x90\x93\xAD", + "\xF0\x90\x93\x86" => "\xF0\x90\x93\xAE", + "\xF0\x90\x93\x87" => "\xF0\x90\x93\xAF", + "\xF0\x90\x93\x88" => "\xF0\x90\x93\xB0", + "\xF0\x90\x93\x89" => "\xF0\x90\x93\xB1", + "\xF0\x90\x93\x8A" => "\xF0\x90\x93\xB2", + "\xF0\x90\x93\x8B" => "\xF0\x90\x93\xB3", + "\xF0\x90\x93\x8C" => "\xF0\x90\x93\xB4", + "\xF0\x90\x93\x8D" => "\xF0\x90\x93\xB5", + "\xF0\x90\x93\x8E" => "\xF0\x90\x93\xB6", + "\xF0\x90\x93\x8F" => "\xF0\x90\x93\xB7", + "\xF0\x90\x93\x90" => "\xF0\x90\x93\xB8", + "\xF0\x90\x93\x91" => "\xF0\x90\x93\xB9", + "\xF0\x90\x93\x92" => "\xF0\x90\x93\xBA", + "\xF0\x90\x93\x93" => "\xF0\x90\x93\xBB", + "\xF0\x90\x95\xB0" => "\xF0\x90\x96\x97", + "\xF0\x90\x95\xB1" => "\xF0\x90\x96\x98", + "\xF0\x90\x95\xB2" => "\xF0\x90\x96\x99", + "\xF0\x90\x95\xB3" => "\xF0\x90\x96\x9A", + "\xF0\x90\x95\xB4" => "\xF0\x90\x96\x9B", + "\xF0\x90\x95\xB5" => "\xF0\x90\x96\x9C", + "\xF0\x90\x95\xB6" => "\xF0\x90\x96\x9D", + "\xF0\x90\x95\xB7" => "\xF0\x90\x96\x9E", + "\xF0\x90\x95\xB8" => "\xF0\x90\x96\x9F", + "\xF0\x90\x95\xB9" => "\xF0\x90\x96\xA0", + "\xF0\x90\x95\xBA" => "\xF0\x90\x96\xA1", + "\xF0\x90\x95\xBC" => "\xF0\x90\x96\xA3", + "\xF0\x90\x95\xBD" => "\xF0\x90\x96\xA4", + "\xF0\x90\x95\xBE" => "\xF0\x90\x96\xA5", + "\xF0\x90\x95\xBF" => "\xF0\x90\x96\xA6", + "\xF0\x90\x96\x80" => "\xF0\x90\x96\xA7", + "\xF0\x90\x96\x81" => "\xF0\x90\x96\xA8", + "\xF0\x90\x96\x82" => "\xF0\x90\x96\xA9", + "\xF0\x90\x96\x83" => "\xF0\x90\x96\xAA", + "\xF0\x90\x96\x84" => "\xF0\x90\x96\xAB", + "\xF0\x90\x96\x85" => "\xF0\x90\x96\xAC", + "\xF0\x90\x96\x86" => "\xF0\x90\x96\xAD", + "\xF0\x90\x96\x87" => "\xF0\x90\x96\xAE", + "\xF0\x90\x96\x88" => "\xF0\x90\x96\xAF", + "\xF0\x90\x96\x89" => "\xF0\x90\x96\xB0", + "\xF0\x90\x96\x8A" => "\xF0\x90\x96\xB1", + "\xF0\x90\x96\x8C" => "\xF0\x90\x96\xB3", + "\xF0\x90\x96\x8D" => "\xF0\x90\x96\xB4", + "\xF0\x90\x96\x8E" => "\xF0\x90\x96\xB5", + "\xF0\x90\x96\x8F" => "\xF0\x90\x96\xB6", + "\xF0\x90\x96\x90" => "\xF0\x90\x96\xB7", + "\xF0\x90\x96\x91" => "\xF0\x90\x96\xB8", + "\xF0\x90\x96\x92" => "\xF0\x90\x96\xB9", + "\xF0\x90\x96\x94" => "\xF0\x90\x96\xBB", + "\xF0\x90\x96\x95" => "\xF0\x90\x96\xBC", + "\xF0\x90\x9E\x81" => "\xCB\x90", + "\xF0\x90\x9E\x82" => "\xCB\x91", + "\xF0\x90\x9E\x83" => "\xC3\xA6", + "\xF0\x90\x9E\x84" => "\xCA\x99", + "\xF0\x90\x9E\x85" => "\xC9\x93", + "\xF0\x90\x9E\x87" => "\xCA\xA3", + "\xF0\x90\x9E\x88" => "\xEA\xAD\xA6", + "\xF0\x90\x9E\x89" => "\xCA\xA5", + "\xF0\x90\x9E\x8A" => "\xCA\xA4", + "\xF0\x90\x9E\x8B" => "\xC9\x96", + "\xF0\x90\x9E\x8C" => "\xC9\x97", + "\xF0\x90\x9E\x8D" => "\xE1\xB6\x91", + "\xF0\x90\x9E\x8E" => "\xC9\x98", + "\xF0\x90\x9E\x8F" => "\xC9\x9E", + "\xF0\x90\x9E\x90" => "\xCA\xA9", + "\xF0\x90\x9E\x91" => "\xC9\xA4", + "\xF0\x90\x9E\x92" => "\xC9\xA2", + "\xF0\x90\x9E\x93" => "\xC9\xA0", + "\xF0\x90\x9E\x94" => "\xCA\x9B", + "\xF0\x90\x9E\x95" => "\xC4\xA7", + "\xF0\x90\x9E\x96" => "\xCA\x9C", + "\xF0\x90\x9E\x97" => "\xC9\xA7", + "\xF0\x90\x9E\x98" => "\xCA\x84", + "\xF0\x90\x9E\x99" => "\xCA\xAA", + "\xF0\x90\x9E\x9A" => "\xCA\xAB", + "\xF0\x90\x9E\x9B" => "\xC9\xAC", + "\xF0\x90\x9E\x9C" => "\xF0\x9D\xBC\x84", + "\xF0\x90\x9E\x9D" => "\xEA\x9E\x8E", + "\xF0\x90\x9E\x9E" => "\xC9\xAE", + "\xF0\x90\x9E\x9F" => "\xF0\x9D\xBC\x85", + "\xF0\x90\x9E\xA0" => "\xCA\x8E", + "\xF0\x90\x9E\xA1" => "\xF0\x9D\xBC\x86", + "\xF0\x90\x9E\xA2" => "\xC3\xB8", + "\xF0\x90\x9E\xA3" => "\xC9\xB6", + "\xF0\x90\x9E\xA4" => "\xC9\xB7", + "\xF0\x90\x9E\xA5" => "\x71", + "\xF0\x90\x9E\xA6" => "\xC9\xBA", + "\xF0\x90\x9E\xA7" => "\xF0\x9D\xBC\x88", + "\xF0\x90\x9E\xA8" => "\xC9\xBD", + "\xF0\x90\x9E\xA9" => "\xC9\xBE", + "\xF0\x90\x9E\xAA" => "\xCA\x80", + "\xF0\x90\x9E\xAB" => "\xCA\xA8", + "\xF0\x90\x9E\xAC" => "\xCA\xA6", + "\xF0\x90\x9E\xAD" => "\xEA\xAD\xA7", + "\xF0\x90\x9E\xAE" => "\xCA\xA7", + "\xF0\x90\x9E\xAF" => "\xCA\x88", + "\xF0\x90\x9E\xB0" => "\xE2\xB1\xB1", + "\xF0\x90\x9E\xB2" => "\xCA\x8F", + "\xF0\x90\x9E\xB3" => "\xCA\xA1", + "\xF0\x90\x9E\xB4" => "\xCA\xA2", + "\xF0\x90\x9E\xB5" => "\xCA\x98", + "\xF0\x90\x9E\xB6" => "\xC7\x80", + "\xF0\x90\x9E\xB7" => "\xC7\x81", + "\xF0\x90\x9E\xB8" => "\xC7\x82", + "\xF0\x90\x9E\xB9" => "\xF0\x9D\xBC\x8A", + "\xF0\x90\x9E\xBA" => "\xF0\x9D\xBC\x9E", + "\xF0\x90\xB2\x80" => "\xF0\x90\xB3\x80", + "\xF0\x90\xB2\x81" => "\xF0\x90\xB3\x81", + "\xF0\x90\xB2\x82" => "\xF0\x90\xB3\x82", + "\xF0\x90\xB2\x83" => "\xF0\x90\xB3\x83", + "\xF0\x90\xB2\x84" => "\xF0\x90\xB3\x84", + "\xF0\x90\xB2\x85" => "\xF0\x90\xB3\x85", + "\xF0\x90\xB2\x86" => "\xF0\x90\xB3\x86", + "\xF0\x90\xB2\x87" => "\xF0\x90\xB3\x87", + "\xF0\x90\xB2\x88" => "\xF0\x90\xB3\x88", + "\xF0\x90\xB2\x89" => "\xF0\x90\xB3\x89", + "\xF0\x90\xB2\x8A" => "\xF0\x90\xB3\x8A", + "\xF0\x90\xB2\x8B" => "\xF0\x90\xB3\x8B", + "\xF0\x90\xB2\x8C" => "\xF0\x90\xB3\x8C", + "\xF0\x90\xB2\x8D" => "\xF0\x90\xB3\x8D", + "\xF0\x90\xB2\x8E" => "\xF0\x90\xB3\x8E", + "\xF0\x90\xB2\x8F" => "\xF0\x90\xB3\x8F", + "\xF0\x90\xB2\x90" => "\xF0\x90\xB3\x90", + "\xF0\x90\xB2\x91" => "\xF0\x90\xB3\x91", + "\xF0\x90\xB2\x92" => "\xF0\x90\xB3\x92", + "\xF0\x90\xB2\x93" => "\xF0\x90\xB3\x93", + "\xF0\x90\xB2\x94" => "\xF0\x90\xB3\x94", + "\xF0\x90\xB2\x95" => "\xF0\x90\xB3\x95", + "\xF0\x90\xB2\x96" => "\xF0\x90\xB3\x96", + "\xF0\x90\xB2\x97" => "\xF0\x90\xB3\x97", + "\xF0\x90\xB2\x98" => "\xF0\x90\xB3\x98", + "\xF0\x90\xB2\x99" => "\xF0\x90\xB3\x99", + "\xF0\x90\xB2\x9A" => "\xF0\x90\xB3\x9A", + "\xF0\x90\xB2\x9B" => "\xF0\x90\xB3\x9B", + "\xF0\x90\xB2\x9C" => "\xF0\x90\xB3\x9C", + "\xF0\x90\xB2\x9D" => "\xF0\x90\xB3\x9D", + "\xF0\x90\xB2\x9E" => "\xF0\x90\xB3\x9E", + "\xF0\x90\xB2\x9F" => "\xF0\x90\xB3\x9F", + "\xF0\x90\xB2\xA0" => "\xF0\x90\xB3\xA0", + "\xF0\x90\xB2\xA1" => "\xF0\x90\xB3\xA1", + "\xF0\x90\xB2\xA2" => "\xF0\x90\xB3\xA2", + "\xF0\x90\xB2\xA3" => "\xF0\x90\xB3\xA3", + "\xF0\x90\xB2\xA4" => "\xF0\x90\xB3\xA4", + "\xF0\x90\xB2\xA5" => "\xF0\x90\xB3\xA5", + "\xF0\x90\xB2\xA6" => "\xF0\x90\xB3\xA6", + "\xF0\x90\xB2\xA7" => "\xF0\x90\xB3\xA7", + "\xF0\x90\xB2\xA8" => "\xF0\x90\xB3\xA8", + "\xF0\x90\xB2\xA9" => "\xF0\x90\xB3\xA9", + "\xF0\x90\xB2\xAA" => "\xF0\x90\xB3\xAA", + "\xF0\x90\xB2\xAB" => "\xF0\x90\xB3\xAB", + "\xF0\x90\xB2\xAC" => "\xF0\x90\xB3\xAC", + "\xF0\x90\xB2\xAD" => "\xF0\x90\xB3\xAD", + "\xF0\x90\xB2\xAE" => "\xF0\x90\xB3\xAE", + "\xF0\x90\xB2\xAF" => "\xF0\x90\xB3\xAF", + "\xF0\x90\xB2\xB0" => "\xF0\x90\xB3\xB0", + "\xF0\x90\xB2\xB1" => "\xF0\x90\xB3\xB1", + "\xF0\x90\xB2\xB2" => "\xF0\x90\xB3\xB2", + "\xF0\x91\xA2\xA0" => "\xF0\x91\xA3\x80", + "\xF0\x91\xA2\xA1" => "\xF0\x91\xA3\x81", + "\xF0\x91\xA2\xA2" => "\xF0\x91\xA3\x82", + "\xF0\x91\xA2\xA3" => "\xF0\x91\xA3\x83", + "\xF0\x91\xA2\xA4" => "\xF0\x91\xA3\x84", + "\xF0\x91\xA2\xA5" => "\xF0\x91\xA3\x85", + "\xF0\x91\xA2\xA6" => "\xF0\x91\xA3\x86", + "\xF0\x91\xA2\xA7" => "\xF0\x91\xA3\x87", + "\xF0\x91\xA2\xA8" => "\xF0\x91\xA3\x88", + "\xF0\x91\xA2\xA9" => "\xF0\x91\xA3\x89", + "\xF0\x91\xA2\xAA" => "\xF0\x91\xA3\x8A", + "\xF0\x91\xA2\xAB" => "\xF0\x91\xA3\x8B", + "\xF0\x91\xA2\xAC" => "\xF0\x91\xA3\x8C", + "\xF0\x91\xA2\xAD" => "\xF0\x91\xA3\x8D", + "\xF0\x91\xA2\xAE" => "\xF0\x91\xA3\x8E", + "\xF0\x91\xA2\xAF" => "\xF0\x91\xA3\x8F", + "\xF0\x91\xA2\xB0" => "\xF0\x91\xA3\x90", + "\xF0\x91\xA2\xB1" => "\xF0\x91\xA3\x91", + "\xF0\x91\xA2\xB2" => "\xF0\x91\xA3\x92", + "\xF0\x91\xA2\xB3" => "\xF0\x91\xA3\x93", + "\xF0\x91\xA2\xB4" => "\xF0\x91\xA3\x94", + "\xF0\x91\xA2\xB5" => "\xF0\x91\xA3\x95", + "\xF0\x91\xA2\xB6" => "\xF0\x91\xA3\x96", + "\xF0\x91\xA2\xB7" => "\xF0\x91\xA3\x97", + "\xF0\x91\xA2\xB8" => "\xF0\x91\xA3\x98", + "\xF0\x91\xA2\xB9" => "\xF0\x91\xA3\x99", + "\xF0\x91\xA2\xBA" => "\xF0\x91\xA3\x9A", + "\xF0\x91\xA2\xBB" => "\xF0\x91\xA3\x9B", + "\xF0\x91\xA2\xBC" => "\xF0\x91\xA3\x9C", + "\xF0\x91\xA2\xBD" => "\xF0\x91\xA3\x9D", + "\xF0\x91\xA2\xBE" => "\xF0\x91\xA3\x9E", + "\xF0\x91\xA2\xBF" => "\xF0\x91\xA3\x9F", + "\xF0\x96\xB9\x80" => "\xF0\x96\xB9\xA0", + "\xF0\x96\xB9\x81" => "\xF0\x96\xB9\xA1", + "\xF0\x96\xB9\x82" => "\xF0\x96\xB9\xA2", + "\xF0\x96\xB9\x83" => "\xF0\x96\xB9\xA3", + "\xF0\x96\xB9\x84" => "\xF0\x96\xB9\xA4", + "\xF0\x96\xB9\x85" => "\xF0\x96\xB9\xA5", + "\xF0\x96\xB9\x86" => "\xF0\x96\xB9\xA6", + "\xF0\x96\xB9\x87" => "\xF0\x96\xB9\xA7", + "\xF0\x96\xB9\x88" => "\xF0\x96\xB9\xA8", + "\xF0\x96\xB9\x89" => "\xF0\x96\xB9\xA9", + "\xF0\x96\xB9\x8A" => "\xF0\x96\xB9\xAA", + "\xF0\x96\xB9\x8B" => "\xF0\x96\xB9\xAB", + "\xF0\x96\xB9\x8C" => "\xF0\x96\xB9\xAC", + "\xF0\x96\xB9\x8D" => "\xF0\x96\xB9\xAD", + "\xF0\x96\xB9\x8E" => "\xF0\x96\xB9\xAE", + "\xF0\x96\xB9\x8F" => "\xF0\x96\xB9\xAF", + "\xF0\x96\xB9\x90" => "\xF0\x96\xB9\xB0", + "\xF0\x96\xB9\x91" => "\xF0\x96\xB9\xB1", + "\xF0\x96\xB9\x92" => "\xF0\x96\xB9\xB2", + "\xF0\x96\xB9\x93" => "\xF0\x96\xB9\xB3", + "\xF0\x96\xB9\x94" => "\xF0\x96\xB9\xB4", + "\xF0\x96\xB9\x95" => "\xF0\x96\xB9\xB5", + "\xF0\x96\xB9\x96" => "\xF0\x96\xB9\xB6", + "\xF0\x96\xB9\x97" => "\xF0\x96\xB9\xB7", + "\xF0\x96\xB9\x98" => "\xF0\x96\xB9\xB8", + "\xF0\x96\xB9\x99" => "\xF0\x96\xB9\xB9", + "\xF0\x96\xB9\x9A" => "\xF0\x96\xB9\xBA", + "\xF0\x96\xB9\x9B" => "\xF0\x96\xB9\xBB", + "\xF0\x96\xB9\x9C" => "\xF0\x96\xB9\xBC", + "\xF0\x96\xB9\x9D" => "\xF0\x96\xB9\xBD", + "\xF0\x96\xB9\x9E" => "\xF0\x96\xB9\xBE", + "\xF0\x96\xB9\x9F" => "\xF0\x96\xB9\xBF", + "\xF0\x9D\x85\x9E" => "\xF0\x9D\x85\x97\xF0\x9D\x85\xA5", + "\xF0\x9D\x85\x9F" => "\xF0\x9D\x85\x98\xF0\x9D\x85\xA5", + "\xF0\x9D\x85\xA0" => "\xF0\x9D\x85\x98\xF0\x9D\x85\xA5\xF0\x9D\x85\xAE", + "\xF0\x9D\x85\xA1" => "\xF0\x9D\x85\x98\xF0\x9D\x85\xA5\xF0\x9D\x85\xAF", + "\xF0\x9D\x85\xA2" => "\xF0\x9D\x85\x98\xF0\x9D\x85\xA5\xF0\x9D\x85\xB0", + "\xF0\x9D\x85\xA3" => "\xF0\x9D\x85\x98\xF0\x9D\x85\xA5\xF0\x9D\x85\xB1", + "\xF0\x9D\x85\xA4" => "\xF0\x9D\x85\x98\xF0\x9D\x85\xA5\xF0\x9D\x85\xB2", + "\xF0\x9D\x86\xBB" => "\xF0\x9D\x86\xB9\xF0\x9D\x85\xA5", + "\xF0\x9D\x86\xBC" => "\xF0\x9D\x86\xBA\xF0\x9D\x85\xA5", + "\xF0\x9D\x86\xBD" => "\xF0\x9D\x86\xB9\xF0\x9D\x85\xA5\xF0\x9D\x85\xAE", + "\xF0\x9D\x86\xBE" => "\xF0\x9D\x86\xBA\xF0\x9D\x85\xA5\xF0\x9D\x85\xAE", + "\xF0\x9D\x86\xBF" => "\xF0\x9D\x86\xB9\xF0\x9D\x85\xA5\xF0\x9D\x85\xAF", + "\xF0\x9D\x87\x80" => "\xF0\x9D\x86\xBA\xF0\x9D\x85\xA5\xF0\x9D\x85\xAF", + "\xF0\x9D\x90\x80" => "\x61", + "\xF0\x9D\x90\x81" => "\x62", + "\xF0\x9D\x90\x82" => "\x63", + "\xF0\x9D\x90\x83" => "\x64", + "\xF0\x9D\x90\x84" => "\x65", + "\xF0\x9D\x90\x85" => "\x66", + "\xF0\x9D\x90\x86" => "\x67", + "\xF0\x9D\x90\x87" => "\x68", + "\xF0\x9D\x90\x88" => "\x69", + "\xF0\x9D\x90\x89" => "\x6A", + "\xF0\x9D\x90\x8A" => "\x6B", + "\xF0\x9D\x90\x8B" => "\x6C", + "\xF0\x9D\x90\x8C" => "\x6D", + "\xF0\x9D\x90\x8D" => "\x6E", + "\xF0\x9D\x90\x8E" => "\x6F", + "\xF0\x9D\x90\x8F" => "\x70", + "\xF0\x9D\x90\x90" => "\x71", + "\xF0\x9D\x90\x91" => "\x72", + "\xF0\x9D\x90\x92" => "\x73", + "\xF0\x9D\x90\x93" => "\x74", + "\xF0\x9D\x90\x94" => "\x75", + "\xF0\x9D\x90\x95" => "\x76", + "\xF0\x9D\x90\x96" => "\x77", + "\xF0\x9D\x90\x97" => "\x78", + "\xF0\x9D\x90\x98" => "\x79", + "\xF0\x9D\x90\x99" => "\x7A", + "\xF0\x9D\x90\x9A" => "\x61", + "\xF0\x9D\x90\x9B" => "\x62", + "\xF0\x9D\x90\x9C" => "\x63", + "\xF0\x9D\x90\x9D" => "\x64", + "\xF0\x9D\x90\x9E" => "\x65", + "\xF0\x9D\x90\x9F" => "\x66", + "\xF0\x9D\x90\xA0" => "\x67", + "\xF0\x9D\x90\xA1" => "\x68", + "\xF0\x9D\x90\xA2" => "\x69", + "\xF0\x9D\x90\xA3" => "\x6A", + "\xF0\x9D\x90\xA4" => "\x6B", + "\xF0\x9D\x90\xA5" => "\x6C", + "\xF0\x9D\x90\xA6" => "\x6D", + "\xF0\x9D\x90\xA7" => "\x6E", + "\xF0\x9D\x90\xA8" => "\x6F", + "\xF0\x9D\x90\xA9" => "\x70", + "\xF0\x9D\x90\xAA" => "\x71", + "\xF0\x9D\x90\xAB" => "\x72", + "\xF0\x9D\x90\xAC" => "\x73", + "\xF0\x9D\x90\xAD" => "\x74", + "\xF0\x9D\x90\xAE" => "\x75", + "\xF0\x9D\x90\xAF" => "\x76", + "\xF0\x9D\x90\xB0" => "\x77", + "\xF0\x9D\x90\xB1" => "\x78", + "\xF0\x9D\x90\xB2" => "\x79", + "\xF0\x9D\x90\xB3" => "\x7A", + "\xF0\x9D\x90\xB4" => "\x61", + "\xF0\x9D\x90\xB5" => "\x62", + "\xF0\x9D\x90\xB6" => "\x63", + "\xF0\x9D\x90\xB7" => "\x64", + "\xF0\x9D\x90\xB8" => "\x65", + "\xF0\x9D\x90\xB9" => "\x66", + "\xF0\x9D\x90\xBA" => "\x67", + "\xF0\x9D\x90\xBB" => "\x68", + "\xF0\x9D\x90\xBC" => "\x69", + "\xF0\x9D\x90\xBD" => "\x6A", + "\xF0\x9D\x90\xBE" => "\x6B", + "\xF0\x9D\x90\xBF" => "\x6C", + "\xF0\x9D\x91\x80" => "\x6D", + "\xF0\x9D\x91\x81" => "\x6E", + "\xF0\x9D\x91\x82" => "\x6F", + "\xF0\x9D\x91\x83" => "\x70", + "\xF0\x9D\x91\x84" => "\x71", + "\xF0\x9D\x91\x85" => "\x72", + "\xF0\x9D\x91\x86" => "\x73", + "\xF0\x9D\x91\x87" => "\x74", + "\xF0\x9D\x91\x88" => "\x75", + "\xF0\x9D\x91\x89" => "\x76", + "\xF0\x9D\x91\x8A" => "\x77", + "\xF0\x9D\x91\x8B" => "\x78", + "\xF0\x9D\x91\x8C" => "\x79", + "\xF0\x9D\x91\x8D" => "\x7A", + "\xF0\x9D\x91\x8E" => "\x61", + "\xF0\x9D\x91\x8F" => "\x62", + "\xF0\x9D\x91\x90" => "\x63", + "\xF0\x9D\x91\x91" => "\x64", + "\xF0\x9D\x91\x92" => "\x65", + "\xF0\x9D\x91\x93" => "\x66", + "\xF0\x9D\x91\x94" => "\x67", + "\xF0\x9D\x91\x96" => "\x69", + "\xF0\x9D\x91\x97" => "\x6A", + "\xF0\x9D\x91\x98" => "\x6B", + "\xF0\x9D\x91\x99" => "\x6C", + "\xF0\x9D\x91\x9A" => "\x6D", + "\xF0\x9D\x91\x9B" => "\x6E", + "\xF0\x9D\x91\x9C" => "\x6F", + "\xF0\x9D\x91\x9D" => "\x70", + "\xF0\x9D\x91\x9E" => "\x71", + "\xF0\x9D\x91\x9F" => "\x72", + "\xF0\x9D\x91\xA0" => "\x73", + "\xF0\x9D\x91\xA1" => "\x74", + "\xF0\x9D\x91\xA2" => "\x75", + "\xF0\x9D\x91\xA3" => "\x76", + "\xF0\x9D\x91\xA4" => "\x77", + "\xF0\x9D\x91\xA5" => "\x78", + "\xF0\x9D\x91\xA6" => "\x79", + "\xF0\x9D\x91\xA7" => "\x7A", + "\xF0\x9D\x91\xA8" => "\x61", + "\xF0\x9D\x91\xA9" => "\x62", + "\xF0\x9D\x91\xAA" => "\x63", + "\xF0\x9D\x91\xAB" => "\x64", + "\xF0\x9D\x91\xAC" => "\x65", + "\xF0\x9D\x91\xAD" => "\x66", + "\xF0\x9D\x91\xAE" => "\x67", + "\xF0\x9D\x91\xAF" => "\x68", + "\xF0\x9D\x91\xB0" => "\x69", + "\xF0\x9D\x91\xB1" => "\x6A", + "\xF0\x9D\x91\xB2" => "\x6B", + "\xF0\x9D\x91\xB3" => "\x6C", + "\xF0\x9D\x91\xB4" => "\x6D", + "\xF0\x9D\x91\xB5" => "\x6E", + "\xF0\x9D\x91\xB6" => "\x6F", + "\xF0\x9D\x91\xB7" => "\x70", + "\xF0\x9D\x91\xB8" => "\x71", + "\xF0\x9D\x91\xB9" => "\x72", + "\xF0\x9D\x91\xBA" => "\x73", + "\xF0\x9D\x91\xBB" => "\x74", + "\xF0\x9D\x91\xBC" => "\x75", + "\xF0\x9D\x91\xBD" => "\x76", + "\xF0\x9D\x91\xBE" => "\x77", + "\xF0\x9D\x91\xBF" => "\x78", + "\xF0\x9D\x92\x80" => "\x79", + "\xF0\x9D\x92\x81" => "\x7A", + "\xF0\x9D\x92\x82" => "\x61", + "\xF0\x9D\x92\x83" => "\x62", + "\xF0\x9D\x92\x84" => "\x63", + "\xF0\x9D\x92\x85" => "\x64", + "\xF0\x9D\x92\x86" => "\x65", + "\xF0\x9D\x92\x87" => "\x66", + "\xF0\x9D\x92\x88" => "\x67", + "\xF0\x9D\x92\x89" => "\x68", + "\xF0\x9D\x92\x8A" => "\x69", + "\xF0\x9D\x92\x8B" => "\x6A", + "\xF0\x9D\x92\x8C" => "\x6B", + "\xF0\x9D\x92\x8D" => "\x6C", + "\xF0\x9D\x92\x8E" => "\x6D", + "\xF0\x9D\x92\x8F" => "\x6E", + "\xF0\x9D\x92\x90" => "\x6F", + "\xF0\x9D\x92\x91" => "\x70", + "\xF0\x9D\x92\x92" => "\x71", + "\xF0\x9D\x92\x93" => "\x72", + "\xF0\x9D\x92\x94" => "\x73", + "\xF0\x9D\x92\x95" => "\x74", + "\xF0\x9D\x92\x96" => "\x75", + "\xF0\x9D\x92\x97" => "\x76", + "\xF0\x9D\x92\x98" => "\x77", + "\xF0\x9D\x92\x99" => "\x78", + "\xF0\x9D\x92\x9A" => "\x79", + "\xF0\x9D\x92\x9B" => "\x7A", + "\xF0\x9D\x92\x9C" => "\x61", + "\xF0\x9D\x92\x9E" => "\x63", + "\xF0\x9D\x92\x9F" => "\x64", + "\xF0\x9D\x92\xA2" => "\x67", + "\xF0\x9D\x92\xA5" => "\x6A", + "\xF0\x9D\x92\xA6" => "\x6B", + "\xF0\x9D\x92\xA9" => "\x6E", + "\xF0\x9D\x92\xAA" => "\x6F", + "\xF0\x9D\x92\xAB" => "\x70", + "\xF0\x9D\x92\xAC" => "\x71", + "\xF0\x9D\x92\xAE" => "\x73", + "\xF0\x9D\x92\xAF" => "\x74", + "\xF0\x9D\x92\xB0" => "\x75", + "\xF0\x9D\x92\xB1" => "\x76", + "\xF0\x9D\x92\xB2" => "\x77", + "\xF0\x9D\x92\xB3" => "\x78", + "\xF0\x9D\x92\xB4" => "\x79", + "\xF0\x9D\x92\xB5" => "\x7A", + "\xF0\x9D\x92\xB6" => "\x61", + "\xF0\x9D\x92\xB7" => "\x62", + "\xF0\x9D\x92\xB8" => "\x63", + "\xF0\x9D\x92\xB9" => "\x64", + "\xF0\x9D\x92\xBB" => "\x66", + "\xF0\x9D\x92\xBD" => "\x68", + "\xF0\x9D\x92\xBE" => "\x69", + "\xF0\x9D\x92\xBF" => "\x6A", + "\xF0\x9D\x93\x80" => "\x6B", + "\xF0\x9D\x93\x81" => "\x6C", + "\xF0\x9D\x93\x82" => "\x6D", + "\xF0\x9D\x93\x83" => "\x6E", + "\xF0\x9D\x93\x85" => "\x70", + "\xF0\x9D\x93\x86" => "\x71", + "\xF0\x9D\x93\x87" => "\x72", + "\xF0\x9D\x93\x88" => "\x73", + "\xF0\x9D\x93\x89" => "\x74", + "\xF0\x9D\x93\x8A" => "\x75", + "\xF0\x9D\x93\x8B" => "\x76", + "\xF0\x9D\x93\x8C" => "\x77", + "\xF0\x9D\x93\x8D" => "\x78", + "\xF0\x9D\x93\x8E" => "\x79", + "\xF0\x9D\x93\x8F" => "\x7A", + "\xF0\x9D\x93\x90" => "\x61", + "\xF0\x9D\x93\x91" => "\x62", + "\xF0\x9D\x93\x92" => "\x63", + "\xF0\x9D\x93\x93" => "\x64", + "\xF0\x9D\x93\x94" => "\x65", + "\xF0\x9D\x93\x95" => "\x66", + "\xF0\x9D\x93\x96" => "\x67", + "\xF0\x9D\x93\x97" => "\x68", + "\xF0\x9D\x93\x98" => "\x69", + "\xF0\x9D\x93\x99" => "\x6A", + "\xF0\x9D\x93\x9A" => "\x6B", + "\xF0\x9D\x93\x9B" => "\x6C", + "\xF0\x9D\x93\x9C" => "\x6D", + "\xF0\x9D\x93\x9D" => "\x6E", + "\xF0\x9D\x93\x9E" => "\x6F", + "\xF0\x9D\x93\x9F" => "\x70", + "\xF0\x9D\x93\xA0" => "\x71", + "\xF0\x9D\x93\xA1" => "\x72", + "\xF0\x9D\x93\xA2" => "\x73", + "\xF0\x9D\x93\xA3" => "\x74", + "\xF0\x9D\x93\xA4" => "\x75", + "\xF0\x9D\x93\xA5" => "\x76", + "\xF0\x9D\x93\xA6" => "\x77", + "\xF0\x9D\x93\xA7" => "\x78", + "\xF0\x9D\x93\xA8" => "\x79", + "\xF0\x9D\x93\xA9" => "\x7A", + "\xF0\x9D\x93\xAA" => "\x61", + "\xF0\x9D\x93\xAB" => "\x62", + "\xF0\x9D\x93\xAC" => "\x63", + "\xF0\x9D\x93\xAD" => "\x64", + "\xF0\x9D\x93\xAE" => "\x65", + "\xF0\x9D\x93\xAF" => "\x66", + "\xF0\x9D\x93\xB0" => "\x67", + "\xF0\x9D\x93\xB1" => "\x68", + "\xF0\x9D\x93\xB2" => "\x69", + "\xF0\x9D\x93\xB3" => "\x6A", + "\xF0\x9D\x93\xB4" => "\x6B", + "\xF0\x9D\x93\xB5" => "\x6C", + "\xF0\x9D\x93\xB6" => "\x6D", + "\xF0\x9D\x93\xB7" => "\x6E", + "\xF0\x9D\x93\xB8" => "\x6F", + "\xF0\x9D\x93\xB9" => "\x70", + "\xF0\x9D\x93\xBA" => "\x71", + "\xF0\x9D\x93\xBB" => "\x72", + "\xF0\x9D\x93\xBC" => "\x73", + "\xF0\x9D\x93\xBD" => "\x74", + "\xF0\x9D\x93\xBE" => "\x75", + "\xF0\x9D\x93\xBF" => "\x76", + "\xF0\x9D\x94\x80" => "\x77", + "\xF0\x9D\x94\x81" => "\x78", + "\xF0\x9D\x94\x82" => "\x79", + "\xF0\x9D\x94\x83" => "\x7A", + "\xF0\x9D\x94\x84" => "\x61", + "\xF0\x9D\x94\x85" => "\x62", + "\xF0\x9D\x94\x87" => "\x64", + "\xF0\x9D\x94\x88" => "\x65", + "\xF0\x9D\x94\x89" => "\x66", + "\xF0\x9D\x94\x8A" => "\x67", + "\xF0\x9D\x94\x8D" => "\x6A", + "\xF0\x9D\x94\x8E" => "\x6B", + "\xF0\x9D\x94\x8F" => "\x6C", + "\xF0\x9D\x94\x90" => "\x6D", + "\xF0\x9D\x94\x91" => "\x6E", + "\xF0\x9D\x94\x92" => "\x6F", + "\xF0\x9D\x94\x93" => "\x70", + "\xF0\x9D\x94\x94" => "\x71", + "\xF0\x9D\x94\x96" => "\x73", + "\xF0\x9D\x94\x97" => "\x74", + "\xF0\x9D\x94\x98" => "\x75", + "\xF0\x9D\x94\x99" => "\x76", + "\xF0\x9D\x94\x9A" => "\x77", + "\xF0\x9D\x94\x9B" => "\x78", + "\xF0\x9D\x94\x9C" => "\x79", + "\xF0\x9D\x94\x9E" => "\x61", + "\xF0\x9D\x94\x9F" => "\x62", + "\xF0\x9D\x94\xA0" => "\x63", + "\xF0\x9D\x94\xA1" => "\x64", + "\xF0\x9D\x94\xA2" => "\x65", + "\xF0\x9D\x94\xA3" => "\x66", + "\xF0\x9D\x94\xA4" => "\x67", + "\xF0\x9D\x94\xA5" => "\x68", + "\xF0\x9D\x94\xA6" => "\x69", + "\xF0\x9D\x94\xA7" => "\x6A", + "\xF0\x9D\x94\xA8" => "\x6B", + "\xF0\x9D\x94\xA9" => "\x6C", + "\xF0\x9D\x94\xAA" => "\x6D", + "\xF0\x9D\x94\xAB" => "\x6E", + "\xF0\x9D\x94\xAC" => "\x6F", + "\xF0\x9D\x94\xAD" => "\x70", + "\xF0\x9D\x94\xAE" => "\x71", + "\xF0\x9D\x94\xAF" => "\x72", + "\xF0\x9D\x94\xB0" => "\x73", + "\xF0\x9D\x94\xB1" => "\x74", + "\xF0\x9D\x94\xB2" => "\x75", + "\xF0\x9D\x94\xB3" => "\x76", + "\xF0\x9D\x94\xB4" => "\x77", + "\xF0\x9D\x94\xB5" => "\x78", + "\xF0\x9D\x94\xB6" => "\x79", + "\xF0\x9D\x94\xB7" => "\x7A", + "\xF0\x9D\x94\xB8" => "\x61", + "\xF0\x9D\x94\xB9" => "\x62", + "\xF0\x9D\x94\xBB" => "\x64", + "\xF0\x9D\x94\xBC" => "\x65", + "\xF0\x9D\x94\xBD" => "\x66", + "\xF0\x9D\x94\xBE" => "\x67", + "\xF0\x9D\x95\x80" => "\x69", + "\xF0\x9D\x95\x81" => "\x6A", + "\xF0\x9D\x95\x82" => "\x6B", + "\xF0\x9D\x95\x83" => "\x6C", + "\xF0\x9D\x95\x84" => "\x6D", + "\xF0\x9D\x95\x86" => "\x6F", + "\xF0\x9D\x95\x8A" => "\x73", + "\xF0\x9D\x95\x8B" => "\x74", + "\xF0\x9D\x95\x8C" => "\x75", + "\xF0\x9D\x95\x8D" => "\x76", + "\xF0\x9D\x95\x8E" => "\x77", + "\xF0\x9D\x95\x8F" => "\x78", + "\xF0\x9D\x95\x90" => "\x79", + "\xF0\x9D\x95\x92" => "\x61", + "\xF0\x9D\x95\x93" => "\x62", + "\xF0\x9D\x95\x94" => "\x63", + "\xF0\x9D\x95\x95" => "\x64", + "\xF0\x9D\x95\x96" => "\x65", + "\xF0\x9D\x95\x97" => "\x66", + "\xF0\x9D\x95\x98" => "\x67", + "\xF0\x9D\x95\x99" => "\x68", + "\xF0\x9D\x95\x9A" => "\x69", + "\xF0\x9D\x95\x9B" => "\x6A", + "\xF0\x9D\x95\x9C" => "\x6B", + "\xF0\x9D\x95\x9D" => "\x6C", + "\xF0\x9D\x95\x9E" => "\x6D", + "\xF0\x9D\x95\x9F" => "\x6E", + "\xF0\x9D\x95\xA0" => "\x6F", + "\xF0\x9D\x95\xA1" => "\x70", + "\xF0\x9D\x95\xA2" => "\x71", + "\xF0\x9D\x95\xA3" => "\x72", + "\xF0\x9D\x95\xA4" => "\x73", + "\xF0\x9D\x95\xA5" => "\x74", + "\xF0\x9D\x95\xA6" => "\x75", + "\xF0\x9D\x95\xA7" => "\x76", + "\xF0\x9D\x95\xA8" => "\x77", + "\xF0\x9D\x95\xA9" => "\x78", + "\xF0\x9D\x95\xAA" => "\x79", + "\xF0\x9D\x95\xAB" => "\x7A", + "\xF0\x9D\x95\xAC" => "\x61", + "\xF0\x9D\x95\xAD" => "\x62", + "\xF0\x9D\x95\xAE" => "\x63", + "\xF0\x9D\x95\xAF" => "\x64", + "\xF0\x9D\x95\xB0" => "\x65", + "\xF0\x9D\x95\xB1" => "\x66", + "\xF0\x9D\x95\xB2" => "\x67", + "\xF0\x9D\x95\xB3" => "\x68", + "\xF0\x9D\x95\xB4" => "\x69", + "\xF0\x9D\x95\xB5" => "\x6A", + "\xF0\x9D\x95\xB6" => "\x6B", + "\xF0\x9D\x95\xB7" => "\x6C", + "\xF0\x9D\x95\xB8" => "\x6D", + "\xF0\x9D\x95\xB9" => "\x6E", + "\xF0\x9D\x95\xBA" => "\x6F", + "\xF0\x9D\x95\xBB" => "\x70", + "\xF0\x9D\x95\xBC" => "\x71", + "\xF0\x9D\x95\xBD" => "\x72", + "\xF0\x9D\x95\xBE" => "\x73", + "\xF0\x9D\x95\xBF" => "\x74", + "\xF0\x9D\x96\x80" => "\x75", + "\xF0\x9D\x96\x81" => "\x76", + "\xF0\x9D\x96\x82" => "\x77", + "\xF0\x9D\x96\x83" => "\x78", + "\xF0\x9D\x96\x84" => "\x79", + "\xF0\x9D\x96\x85" => "\x7A", + "\xF0\x9D\x96\x86" => "\x61", + "\xF0\x9D\x96\x87" => "\x62", + "\xF0\x9D\x96\x88" => "\x63", + "\xF0\x9D\x96\x89" => "\x64", + "\xF0\x9D\x96\x8A" => "\x65", + "\xF0\x9D\x96\x8B" => "\x66", + "\xF0\x9D\x96\x8C" => "\x67", + "\xF0\x9D\x96\x8D" => "\x68", + "\xF0\x9D\x96\x8E" => "\x69", + "\xF0\x9D\x96\x8F" => "\x6A", + "\xF0\x9D\x96\x90" => "\x6B", + "\xF0\x9D\x96\x91" => "\x6C", + "\xF0\x9D\x96\x92" => "\x6D", + "\xF0\x9D\x96\x93" => "\x6E", + "\xF0\x9D\x96\x94" => "\x6F", + "\xF0\x9D\x96\x95" => "\x70", + "\xF0\x9D\x96\x96" => "\x71", + "\xF0\x9D\x96\x97" => "\x72", + "\xF0\x9D\x96\x98" => "\x73", + "\xF0\x9D\x96\x99" => "\x74", + "\xF0\x9D\x96\x9A" => "\x75", + "\xF0\x9D\x96\x9B" => "\x76", + "\xF0\x9D\x96\x9C" => "\x77", + "\xF0\x9D\x96\x9D" => "\x78", + "\xF0\x9D\x96\x9E" => "\x79", + "\xF0\x9D\x96\x9F" => "\x7A", + "\xF0\x9D\x96\xA0" => "\x61", + "\xF0\x9D\x96\xA1" => "\x62", + "\xF0\x9D\x96\xA2" => "\x63", + "\xF0\x9D\x96\xA3" => "\x64", + "\xF0\x9D\x96\xA4" => "\x65", + "\xF0\x9D\x96\xA5" => "\x66", + "\xF0\x9D\x96\xA6" => "\x67", + "\xF0\x9D\x96\xA7" => "\x68", + "\xF0\x9D\x96\xA8" => "\x69", + "\xF0\x9D\x96\xA9" => "\x6A", + "\xF0\x9D\x96\xAA" => "\x6B", + "\xF0\x9D\x96\xAB" => "\x6C", + "\xF0\x9D\x96\xAC" => "\x6D", + "\xF0\x9D\x96\xAD" => "\x6E", + "\xF0\x9D\x96\xAE" => "\x6F", + "\xF0\x9D\x96\xAF" => "\x70", + "\xF0\x9D\x96\xB0" => "\x71", + "\xF0\x9D\x96\xB1" => "\x72", + "\xF0\x9D\x96\xB2" => "\x73", + "\xF0\x9D\x96\xB3" => "\x74", + "\xF0\x9D\x96\xB4" => "\x75", + "\xF0\x9D\x96\xB5" => "\x76", + "\xF0\x9D\x96\xB6" => "\x77", + "\xF0\x9D\x96\xB7" => "\x78", + "\xF0\x9D\x96\xB8" => "\x79", + "\xF0\x9D\x96\xB9" => "\x7A", + "\xF0\x9D\x96\xBA" => "\x61", + "\xF0\x9D\x96\xBB" => "\x62", + "\xF0\x9D\x96\xBC" => "\x63", + "\xF0\x9D\x96\xBD" => "\x64", + "\xF0\x9D\x96\xBE" => "\x65", + "\xF0\x9D\x96\xBF" => "\x66", + "\xF0\x9D\x97\x80" => "\x67", + "\xF0\x9D\x97\x81" => "\x68", + "\xF0\x9D\x97\x82" => "\x69", + "\xF0\x9D\x97\x83" => "\x6A", + "\xF0\x9D\x97\x84" => "\x6B", + "\xF0\x9D\x97\x85" => "\x6C", + "\xF0\x9D\x97\x86" => "\x6D", + "\xF0\x9D\x97\x87" => "\x6E", + "\xF0\x9D\x97\x88" => "\x6F", + "\xF0\x9D\x97\x89" => "\x70", + "\xF0\x9D\x97\x8A" => "\x71", + "\xF0\x9D\x97\x8B" => "\x72", + "\xF0\x9D\x97\x8C" => "\x73", + "\xF0\x9D\x97\x8D" => "\x74", + "\xF0\x9D\x97\x8E" => "\x75", + "\xF0\x9D\x97\x8F" => "\x76", + "\xF0\x9D\x97\x90" => "\x77", + "\xF0\x9D\x97\x91" => "\x78", + "\xF0\x9D\x97\x92" => "\x79", + "\xF0\x9D\x97\x93" => "\x7A", + "\xF0\x9D\x97\x94" => "\x61", + "\xF0\x9D\x97\x95" => "\x62", + "\xF0\x9D\x97\x96" => "\x63", + "\xF0\x9D\x97\x97" => "\x64", + "\xF0\x9D\x97\x98" => "\x65", + "\xF0\x9D\x97\x99" => "\x66", + "\xF0\x9D\x97\x9A" => "\x67", + "\xF0\x9D\x97\x9B" => "\x68", + "\xF0\x9D\x97\x9C" => "\x69", + "\xF0\x9D\x97\x9D" => "\x6A", + "\xF0\x9D\x97\x9E" => "\x6B", + "\xF0\x9D\x97\x9F" => "\x6C", + "\xF0\x9D\x97\xA0" => "\x6D", + "\xF0\x9D\x97\xA1" => "\x6E", + "\xF0\x9D\x97\xA2" => "\x6F", + "\xF0\x9D\x97\xA3" => "\x70", + "\xF0\x9D\x97\xA4" => "\x71", + "\xF0\x9D\x97\xA5" => "\x72", + "\xF0\x9D\x97\xA6" => "\x73", + "\xF0\x9D\x97\xA7" => "\x74", + "\xF0\x9D\x97\xA8" => "\x75", + "\xF0\x9D\x97\xA9" => "\x76", + "\xF0\x9D\x97\xAA" => "\x77", + "\xF0\x9D\x97\xAB" => "\x78", + "\xF0\x9D\x97\xAC" => "\x79", + "\xF0\x9D\x97\xAD" => "\x7A", + "\xF0\x9D\x97\xAE" => "\x61", + "\xF0\x9D\x97\xAF" => "\x62", + "\xF0\x9D\x97\xB0" => "\x63", + "\xF0\x9D\x97\xB1" => "\x64", + "\xF0\x9D\x97\xB2" => "\x65", + "\xF0\x9D\x97\xB3" => "\x66", + "\xF0\x9D\x97\xB4" => "\x67", + "\xF0\x9D\x97\xB5" => "\x68", + "\xF0\x9D\x97\xB6" => "\x69", + "\xF0\x9D\x97\xB7" => "\x6A", + "\xF0\x9D\x97\xB8" => "\x6B", + "\xF0\x9D\x97\xB9" => "\x6C", + "\xF0\x9D\x97\xBA" => "\x6D", + "\xF0\x9D\x97\xBB" => "\x6E", + "\xF0\x9D\x97\xBC" => "\x6F", + "\xF0\x9D\x97\xBD" => "\x70", + "\xF0\x9D\x97\xBE" => "\x71", + "\xF0\x9D\x97\xBF" => "\x72", + "\xF0\x9D\x98\x80" => "\x73", + "\xF0\x9D\x98\x81" => "\x74", + "\xF0\x9D\x98\x82" => "\x75", + "\xF0\x9D\x98\x83" => "\x76", + "\xF0\x9D\x98\x84" => "\x77", + "\xF0\x9D\x98\x85" => "\x78", + "\xF0\x9D\x98\x86" => "\x79", + "\xF0\x9D\x98\x87" => "\x7A", + "\xF0\x9D\x98\x88" => "\x61", + "\xF0\x9D\x98\x89" => "\x62", + "\xF0\x9D\x98\x8A" => "\x63", + "\xF0\x9D\x98\x8B" => "\x64", + "\xF0\x9D\x98\x8C" => "\x65", + "\xF0\x9D\x98\x8D" => "\x66", + "\xF0\x9D\x98\x8E" => "\x67", + "\xF0\x9D\x98\x8F" => "\x68", + "\xF0\x9D\x98\x90" => "\x69", + "\xF0\x9D\x98\x91" => "\x6A", + "\xF0\x9D\x98\x92" => "\x6B", + "\xF0\x9D\x98\x93" => "\x6C", + "\xF0\x9D\x98\x94" => "\x6D", + "\xF0\x9D\x98\x95" => "\x6E", + "\xF0\x9D\x98\x96" => "\x6F", + "\xF0\x9D\x98\x97" => "\x70", + "\xF0\x9D\x98\x98" => "\x71", + "\xF0\x9D\x98\x99" => "\x72", + "\xF0\x9D\x98\x9A" => "\x73", + "\xF0\x9D\x98\x9B" => "\x74", + "\xF0\x9D\x98\x9C" => "\x75", + "\xF0\x9D\x98\x9D" => "\x76", + "\xF0\x9D\x98\x9E" => "\x77", + "\xF0\x9D\x98\x9F" => "\x78", + "\xF0\x9D\x98\xA0" => "\x79", + "\xF0\x9D\x98\xA1" => "\x7A", + "\xF0\x9D\x98\xA2" => "\x61", + "\xF0\x9D\x98\xA3" => "\x62", + "\xF0\x9D\x98\xA4" => "\x63", + "\xF0\x9D\x98\xA5" => "\x64", + "\xF0\x9D\x98\xA6" => "\x65", + "\xF0\x9D\x98\xA7" => "\x66", + "\xF0\x9D\x98\xA8" => "\x67", + "\xF0\x9D\x98\xA9" => "\x68", + "\xF0\x9D\x98\xAA" => "\x69", + "\xF0\x9D\x98\xAB" => "\x6A", + "\xF0\x9D\x98\xAC" => "\x6B", + "\xF0\x9D\x98\xAD" => "\x6C", + "\xF0\x9D\x98\xAE" => "\x6D", + "\xF0\x9D\x98\xAF" => "\x6E", + "\xF0\x9D\x98\xB0" => "\x6F", + "\xF0\x9D\x98\xB1" => "\x70", + "\xF0\x9D\x98\xB2" => "\x71", + "\xF0\x9D\x98\xB3" => "\x72", + "\xF0\x9D\x98\xB4" => "\x73", + "\xF0\x9D\x98\xB5" => "\x74", + "\xF0\x9D\x98\xB6" => "\x75", + "\xF0\x9D\x98\xB7" => "\x76", + "\xF0\x9D\x98\xB8" => "\x77", + "\xF0\x9D\x98\xB9" => "\x78", + "\xF0\x9D\x98\xBA" => "\x79", + "\xF0\x9D\x98\xBB" => "\x7A", + "\xF0\x9D\x98\xBC" => "\x61", + "\xF0\x9D\x98\xBD" => "\x62", + "\xF0\x9D\x98\xBE" => "\x63", + "\xF0\x9D\x98\xBF" => "\x64", + "\xF0\x9D\x99\x80" => "\x65", + "\xF0\x9D\x99\x81" => "\x66", + "\xF0\x9D\x99\x82" => "\x67", + "\xF0\x9D\x99\x83" => "\x68", + "\xF0\x9D\x99\x84" => "\x69", + "\xF0\x9D\x99\x85" => "\x6A", + "\xF0\x9D\x99\x86" => "\x6B", + "\xF0\x9D\x99\x87" => "\x6C", + "\xF0\x9D\x99\x88" => "\x6D", + "\xF0\x9D\x99\x89" => "\x6E", + "\xF0\x9D\x99\x8A" => "\x6F", + "\xF0\x9D\x99\x8B" => "\x70", + "\xF0\x9D\x99\x8C" => "\x71", + "\xF0\x9D\x99\x8D" => "\x72", + "\xF0\x9D\x99\x8E" => "\x73", + "\xF0\x9D\x99\x8F" => "\x74", + "\xF0\x9D\x99\x90" => "\x75", + "\xF0\x9D\x99\x91" => "\x76", + "\xF0\x9D\x99\x92" => "\x77", + "\xF0\x9D\x99\x93" => "\x78", + "\xF0\x9D\x99\x94" => "\x79", + "\xF0\x9D\x99\x95" => "\x7A", + "\xF0\x9D\x99\x96" => "\x61", + "\xF0\x9D\x99\x97" => "\x62", + "\xF0\x9D\x99\x98" => "\x63", + "\xF0\x9D\x99\x99" => "\x64", + "\xF0\x9D\x99\x9A" => "\x65", + "\xF0\x9D\x99\x9B" => "\x66", + "\xF0\x9D\x99\x9C" => "\x67", + "\xF0\x9D\x99\x9D" => "\x68", + "\xF0\x9D\x99\x9E" => "\x69", + "\xF0\x9D\x99\x9F" => "\x6A", + "\xF0\x9D\x99\xA0" => "\x6B", + "\xF0\x9D\x99\xA1" => "\x6C", + "\xF0\x9D\x99\xA2" => "\x6D", + "\xF0\x9D\x99\xA3" => "\x6E", + "\xF0\x9D\x99\xA4" => "\x6F", + "\xF0\x9D\x99\xA5" => "\x70", + "\xF0\x9D\x99\xA6" => "\x71", + "\xF0\x9D\x99\xA7" => "\x72", + "\xF0\x9D\x99\xA8" => "\x73", + "\xF0\x9D\x99\xA9" => "\x74", + "\xF0\x9D\x99\xAA" => "\x75", + "\xF0\x9D\x99\xAB" => "\x76", + "\xF0\x9D\x99\xAC" => "\x77", + "\xF0\x9D\x99\xAD" => "\x78", + "\xF0\x9D\x99\xAE" => "\x79", + "\xF0\x9D\x99\xAF" => "\x7A", + "\xF0\x9D\x99\xB0" => "\x61", + "\xF0\x9D\x99\xB1" => "\x62", + "\xF0\x9D\x99\xB2" => "\x63", + "\xF0\x9D\x99\xB3" => "\x64", + "\xF0\x9D\x99\xB4" => "\x65", + "\xF0\x9D\x99\xB5" => "\x66", + "\xF0\x9D\x99\xB6" => "\x67", + "\xF0\x9D\x99\xB7" => "\x68", + "\xF0\x9D\x99\xB8" => "\x69", + "\xF0\x9D\x99\xB9" => "\x6A", + "\xF0\x9D\x99\xBA" => "\x6B", + "\xF0\x9D\x99\xBB" => "\x6C", + "\xF0\x9D\x99\xBC" => "\x6D", + "\xF0\x9D\x99\xBD" => "\x6E", + "\xF0\x9D\x99\xBE" => "\x6F", + "\xF0\x9D\x99\xBF" => "\x70", + "\xF0\x9D\x9A\x80" => "\x71", + "\xF0\x9D\x9A\x81" => "\x72", + "\xF0\x9D\x9A\x82" => "\x73", + "\xF0\x9D\x9A\x83" => "\x74", + "\xF0\x9D\x9A\x84" => "\x75", + "\xF0\x9D\x9A\x85" => "\x76", + "\xF0\x9D\x9A\x86" => "\x77", + "\xF0\x9D\x9A\x87" => "\x78", + "\xF0\x9D\x9A\x88" => "\x79", + "\xF0\x9D\x9A\x89" => "\x7A", + "\xF0\x9D\x9A\x8A" => "\x61", + "\xF0\x9D\x9A\x8B" => "\x62", + "\xF0\x9D\x9A\x8C" => "\x63", + "\xF0\x9D\x9A\x8D" => "\x64", + "\xF0\x9D\x9A\x8E" => "\x65", + "\xF0\x9D\x9A\x8F" => "\x66", + "\xF0\x9D\x9A\x90" => "\x67", + "\xF0\x9D\x9A\x91" => "\x68", + "\xF0\x9D\x9A\x92" => "\x69", + "\xF0\x9D\x9A\x93" => "\x6A", + "\xF0\x9D\x9A\x94" => "\x6B", + "\xF0\x9D\x9A\x95" => "\x6C", + "\xF0\x9D\x9A\x96" => "\x6D", + "\xF0\x9D\x9A\x97" => "\x6E", + "\xF0\x9D\x9A\x98" => "\x6F", + "\xF0\x9D\x9A\x99" => "\x70", + "\xF0\x9D\x9A\x9A" => "\x71", + "\xF0\x9D\x9A\x9B" => "\x72", + "\xF0\x9D\x9A\x9C" => "\x73", + "\xF0\x9D\x9A\x9D" => "\x74", + "\xF0\x9D\x9A\x9E" => "\x75", + "\xF0\x9D\x9A\x9F" => "\x76", + "\xF0\x9D\x9A\xA0" => "\x77", + "\xF0\x9D\x9A\xA1" => "\x78", + "\xF0\x9D\x9A\xA2" => "\x79", + "\xF0\x9D\x9A\xA3" => "\x7A", + "\xF0\x9D\x9A\xA4" => "\xC4\xB1", + "\xF0\x9D\x9A\xA5" => "\xC8\xB7", + "\xF0\x9D\x9A\xA8" => "\xCE\xB1", + "\xF0\x9D\x9A\xA9" => "\xCE\xB2", + "\xF0\x9D\x9A\xAA" => "\xCE\xB3", + "\xF0\x9D\x9A\xAB" => "\xCE\xB4", + "\xF0\x9D\x9A\xAC" => "\xCE\xB5", + "\xF0\x9D\x9A\xAD" => "\xCE\xB6", + "\xF0\x9D\x9A\xAE" => "\xCE\xB7", + "\xF0\x9D\x9A\xAF" => "\xCE\xB8", + "\xF0\x9D\x9A\xB0" => "\xCE\xB9", + "\xF0\x9D\x9A\xB1" => "\xCE\xBA", + "\xF0\x9D\x9A\xB2" => "\xCE\xBB", + "\xF0\x9D\x9A\xB3" => "\xCE\xBC", + "\xF0\x9D\x9A\xB4" => "\xCE\xBD", + "\xF0\x9D\x9A\xB5" => "\xCE\xBE", + "\xF0\x9D\x9A\xB6" => "\xCE\xBF", + "\xF0\x9D\x9A\xB7" => "\xCF\x80", + "\xF0\x9D\x9A\xB8" => "\xCF\x81", + "\xF0\x9D\x9A\xB9" => "\xCE\xB8", + "\xF0\x9D\x9A\xBA" => "\xCF\x83", + "\xF0\x9D\x9A\xBB" => "\xCF\x84", + "\xF0\x9D\x9A\xBC" => "\xCF\x85", + "\xF0\x9D\x9A\xBD" => "\xCF\x86", + "\xF0\x9D\x9A\xBE" => "\xCF\x87", + "\xF0\x9D\x9A\xBF" => "\xCF\x88", + "\xF0\x9D\x9B\x80" => "\xCF\x89", + "\xF0\x9D\x9B\x81" => "\xE2\x88\x87", + "\xF0\x9D\x9B\x82" => "\xCE\xB1", + "\xF0\x9D\x9B\x83" => "\xCE\xB2", + "\xF0\x9D\x9B\x84" => "\xCE\xB3", + "\xF0\x9D\x9B\x85" => "\xCE\xB4", + "\xF0\x9D\x9B\x86" => "\xCE\xB5", + "\xF0\x9D\x9B\x87" => "\xCE\xB6", + "\xF0\x9D\x9B\x88" => "\xCE\xB7", + "\xF0\x9D\x9B\x89" => "\xCE\xB8", + "\xF0\x9D\x9B\x8A" => "\xCE\xB9", + "\xF0\x9D\x9B\x8B" => "\xCE\xBA", + "\xF0\x9D\x9B\x8C" => "\xCE\xBB", + "\xF0\x9D\x9B\x8D" => "\xCE\xBC", + "\xF0\x9D\x9B\x8E" => "\xCE\xBD", + "\xF0\x9D\x9B\x8F" => "\xCE\xBE", + "\xF0\x9D\x9B\x90" => "\xCE\xBF", + "\xF0\x9D\x9B\x91" => "\xCF\x80", + "\xF0\x9D\x9B\x92" => "\xCF\x81", + "\xF0\x9D\x9B\x93" => "\xCF\x83", + "\xF0\x9D\x9B\x94" => "\xCF\x83", + "\xF0\x9D\x9B\x95" => "\xCF\x84", + "\xF0\x9D\x9B\x96" => "\xCF\x85", + "\xF0\x9D\x9B\x97" => "\xCF\x86", + "\xF0\x9D\x9B\x98" => "\xCF\x87", + "\xF0\x9D\x9B\x99" => "\xCF\x88", + "\xF0\x9D\x9B\x9A" => "\xCF\x89", + "\xF0\x9D\x9B\x9B" => "\xE2\x88\x82", + "\xF0\x9D\x9B\x9C" => "\xCE\xB5", + "\xF0\x9D\x9B\x9D" => "\xCE\xB8", + "\xF0\x9D\x9B\x9E" => "\xCE\xBA", + "\xF0\x9D\x9B\x9F" => "\xCF\x86", + "\xF0\x9D\x9B\xA0" => "\xCF\x81", + "\xF0\x9D\x9B\xA1" => "\xCF\x80", + "\xF0\x9D\x9B\xA2" => "\xCE\xB1", + "\xF0\x9D\x9B\xA3" => "\xCE\xB2", + "\xF0\x9D\x9B\xA4" => "\xCE\xB3", + "\xF0\x9D\x9B\xA5" => "\xCE\xB4", + "\xF0\x9D\x9B\xA6" => "\xCE\xB5", + "\xF0\x9D\x9B\xA7" => "\xCE\xB6", + "\xF0\x9D\x9B\xA8" => "\xCE\xB7", + "\xF0\x9D\x9B\xA9" => "\xCE\xB8", + "\xF0\x9D\x9B\xAA" => "\xCE\xB9", + "\xF0\x9D\x9B\xAB" => "\xCE\xBA", + "\xF0\x9D\x9B\xAC" => "\xCE\xBB", + "\xF0\x9D\x9B\xAD" => "\xCE\xBC", + "\xF0\x9D\x9B\xAE" => "\xCE\xBD", + "\xF0\x9D\x9B\xAF" => "\xCE\xBE", + "\xF0\x9D\x9B\xB0" => "\xCE\xBF", + "\xF0\x9D\x9B\xB1" => "\xCF\x80", + "\xF0\x9D\x9B\xB2" => "\xCF\x81", + "\xF0\x9D\x9B\xB3" => "\xCE\xB8", + "\xF0\x9D\x9B\xB4" => "\xCF\x83", + "\xF0\x9D\x9B\xB5" => "\xCF\x84", + "\xF0\x9D\x9B\xB6" => "\xCF\x85", + "\xF0\x9D\x9B\xB7" => "\xCF\x86", + "\xF0\x9D\x9B\xB8" => "\xCF\x87", + "\xF0\x9D\x9B\xB9" => "\xCF\x88", + "\xF0\x9D\x9B\xBA" => "\xCF\x89", + "\xF0\x9D\x9B\xBB" => "\xE2\x88\x87", + "\xF0\x9D\x9B\xBC" => "\xCE\xB1", + "\xF0\x9D\x9B\xBD" => "\xCE\xB2", + "\xF0\x9D\x9B\xBE" => "\xCE\xB3", + "\xF0\x9D\x9B\xBF" => "\xCE\xB4", + "\xF0\x9D\x9C\x80" => "\xCE\xB5", + "\xF0\x9D\x9C\x81" => "\xCE\xB6", + "\xF0\x9D\x9C\x82" => "\xCE\xB7", + "\xF0\x9D\x9C\x83" => "\xCE\xB8", + "\xF0\x9D\x9C\x84" => "\xCE\xB9", + "\xF0\x9D\x9C\x85" => "\xCE\xBA", + "\xF0\x9D\x9C\x86" => "\xCE\xBB", + "\xF0\x9D\x9C\x87" => "\xCE\xBC", + "\xF0\x9D\x9C\x88" => "\xCE\xBD", + "\xF0\x9D\x9C\x89" => "\xCE\xBE", + "\xF0\x9D\x9C\x8A" => "\xCE\xBF", + "\xF0\x9D\x9C\x8B" => "\xCF\x80", + "\xF0\x9D\x9C\x8C" => "\xCF\x81", + "\xF0\x9D\x9C\x8D" => "\xCF\x83", + "\xF0\x9D\x9C\x8E" => "\xCF\x83", + "\xF0\x9D\x9C\x8F" => "\xCF\x84", + "\xF0\x9D\x9C\x90" => "\xCF\x85", + "\xF0\x9D\x9C\x91" => "\xCF\x86", + "\xF0\x9D\x9C\x92" => "\xCF\x87", + "\xF0\x9D\x9C\x93" => "\xCF\x88", + "\xF0\x9D\x9C\x94" => "\xCF\x89", + "\xF0\x9D\x9C\x95" => "\xE2\x88\x82", + "\xF0\x9D\x9C\x96" => "\xCE\xB5", + "\xF0\x9D\x9C\x97" => "\xCE\xB8", + "\xF0\x9D\x9C\x98" => "\xCE\xBA", + "\xF0\x9D\x9C\x99" => "\xCF\x86", + "\xF0\x9D\x9C\x9A" => "\xCF\x81", + "\xF0\x9D\x9C\x9B" => "\xCF\x80", + "\xF0\x9D\x9C\x9C" => "\xCE\xB1", + "\xF0\x9D\x9C\x9D" => "\xCE\xB2", + "\xF0\x9D\x9C\x9E" => "\xCE\xB3", + "\xF0\x9D\x9C\x9F" => "\xCE\xB4", + "\xF0\x9D\x9C\xA0" => "\xCE\xB5", + "\xF0\x9D\x9C\xA1" => "\xCE\xB6", + "\xF0\x9D\x9C\xA2" => "\xCE\xB7", + "\xF0\x9D\x9C\xA3" => "\xCE\xB8", + "\xF0\x9D\x9C\xA4" => "\xCE\xB9", + "\xF0\x9D\x9C\xA5" => "\xCE\xBA", + "\xF0\x9D\x9C\xA6" => "\xCE\xBB", + "\xF0\x9D\x9C\xA7" => "\xCE\xBC", + "\xF0\x9D\x9C\xA8" => "\xCE\xBD", + "\xF0\x9D\x9C\xA9" => "\xCE\xBE", + "\xF0\x9D\x9C\xAA" => "\xCE\xBF", + "\xF0\x9D\x9C\xAB" => "\xCF\x80", + "\xF0\x9D\x9C\xAC" => "\xCF\x81", + "\xF0\x9D\x9C\xAD" => "\xCE\xB8", + "\xF0\x9D\x9C\xAE" => "\xCF\x83", + "\xF0\x9D\x9C\xAF" => "\xCF\x84", + "\xF0\x9D\x9C\xB0" => "\xCF\x85", + "\xF0\x9D\x9C\xB1" => "\xCF\x86", + "\xF0\x9D\x9C\xB2" => "\xCF\x87", + "\xF0\x9D\x9C\xB3" => "\xCF\x88", + "\xF0\x9D\x9C\xB4" => "\xCF\x89", + "\xF0\x9D\x9C\xB5" => "\xE2\x88\x87", + "\xF0\x9D\x9C\xB6" => "\xCE\xB1", + "\xF0\x9D\x9C\xB7" => "\xCE\xB2", + "\xF0\x9D\x9C\xB8" => "\xCE\xB3", + "\xF0\x9D\x9C\xB9" => "\xCE\xB4", + "\xF0\x9D\x9C\xBA" => "\xCE\xB5", + "\xF0\x9D\x9C\xBB" => "\xCE\xB6", + "\xF0\x9D\x9C\xBC" => "\xCE\xB7", + "\xF0\x9D\x9C\xBD" => "\xCE\xB8", + "\xF0\x9D\x9C\xBE" => "\xCE\xB9", + "\xF0\x9D\x9C\xBF" => "\xCE\xBA", + "\xF0\x9D\x9D\x80" => "\xCE\xBB", + "\xF0\x9D\x9D\x81" => "\xCE\xBC", + "\xF0\x9D\x9D\x82" => "\xCE\xBD", + "\xF0\x9D\x9D\x83" => "\xCE\xBE", + "\xF0\x9D\x9D\x84" => "\xCE\xBF", + "\xF0\x9D\x9D\x85" => "\xCF\x80", + "\xF0\x9D\x9D\x86" => "\xCF\x81", + "\xF0\x9D\x9D\x87" => "\xCF\x83", + "\xF0\x9D\x9D\x88" => "\xCF\x83", + "\xF0\x9D\x9D\x89" => "\xCF\x84", + "\xF0\x9D\x9D\x8A" => "\xCF\x85", + "\xF0\x9D\x9D\x8B" => "\xCF\x86", + "\xF0\x9D\x9D\x8C" => "\xCF\x87", + "\xF0\x9D\x9D\x8D" => "\xCF\x88", + "\xF0\x9D\x9D\x8E" => "\xCF\x89", + "\xF0\x9D\x9D\x8F" => "\xE2\x88\x82", + "\xF0\x9D\x9D\x90" => "\xCE\xB5", + "\xF0\x9D\x9D\x91" => "\xCE\xB8", + "\xF0\x9D\x9D\x92" => "\xCE\xBA", + "\xF0\x9D\x9D\x93" => "\xCF\x86", + "\xF0\x9D\x9D\x94" => "\xCF\x81", + "\xF0\x9D\x9D\x95" => "\xCF\x80", + "\xF0\x9D\x9D\x96" => "\xCE\xB1", + "\xF0\x9D\x9D\x97" => "\xCE\xB2", + "\xF0\x9D\x9D\x98" => "\xCE\xB3", + "\xF0\x9D\x9D\x99" => "\xCE\xB4", + "\xF0\x9D\x9D\x9A" => "\xCE\xB5", + "\xF0\x9D\x9D\x9B" => "\xCE\xB6", + "\xF0\x9D\x9D\x9C" => "\xCE\xB7", + "\xF0\x9D\x9D\x9D" => "\xCE\xB8", + "\xF0\x9D\x9D\x9E" => "\xCE\xB9", + "\xF0\x9D\x9D\x9F" => "\xCE\xBA", + "\xF0\x9D\x9D\xA0" => "\xCE\xBB", + "\xF0\x9D\x9D\xA1" => "\xCE\xBC", + "\xF0\x9D\x9D\xA2" => "\xCE\xBD", + "\xF0\x9D\x9D\xA3" => "\xCE\xBE", + "\xF0\x9D\x9D\xA4" => "\xCE\xBF", + "\xF0\x9D\x9D\xA5" => "\xCF\x80", + "\xF0\x9D\x9D\xA6" => "\xCF\x81", + "\xF0\x9D\x9D\xA7" => "\xCE\xB8", + "\xF0\x9D\x9D\xA8" => "\xCF\x83", + "\xF0\x9D\x9D\xA9" => "\xCF\x84", + "\xF0\x9D\x9D\xAA" => "\xCF\x85", + "\xF0\x9D\x9D\xAB" => "\xCF\x86", + "\xF0\x9D\x9D\xAC" => "\xCF\x87", + "\xF0\x9D\x9D\xAD" => "\xCF\x88", + "\xF0\x9D\x9D\xAE" => "\xCF\x89", + "\xF0\x9D\x9D\xAF" => "\xE2\x88\x87", + "\xF0\x9D\x9D\xB0" => "\xCE\xB1", + "\xF0\x9D\x9D\xB1" => "\xCE\xB2", + "\xF0\x9D\x9D\xB2" => "\xCE\xB3", + "\xF0\x9D\x9D\xB3" => "\xCE\xB4", + "\xF0\x9D\x9D\xB4" => "\xCE\xB5", + "\xF0\x9D\x9D\xB5" => "\xCE\xB6", + "\xF0\x9D\x9D\xB6" => "\xCE\xB7", + "\xF0\x9D\x9D\xB7" => "\xCE\xB8", + "\xF0\x9D\x9D\xB8" => "\xCE\xB9", + "\xF0\x9D\x9D\xB9" => "\xCE\xBA", + "\xF0\x9D\x9D\xBA" => "\xCE\xBB", + "\xF0\x9D\x9D\xBB" => "\xCE\xBC", + "\xF0\x9D\x9D\xBC" => "\xCE\xBD", + "\xF0\x9D\x9D\xBD" => "\xCE\xBE", + "\xF0\x9D\x9D\xBE" => "\xCE\xBF", + "\xF0\x9D\x9D\xBF" => "\xCF\x80", + "\xF0\x9D\x9E\x80" => "\xCF\x81", + "\xF0\x9D\x9E\x81" => "\xCF\x83", + "\xF0\x9D\x9E\x82" => "\xCF\x83", + "\xF0\x9D\x9E\x83" => "\xCF\x84", + "\xF0\x9D\x9E\x84" => "\xCF\x85", + "\xF0\x9D\x9E\x85" => "\xCF\x86", + "\xF0\x9D\x9E\x86" => "\xCF\x87", + "\xF0\x9D\x9E\x87" => "\xCF\x88", + "\xF0\x9D\x9E\x88" => "\xCF\x89", + "\xF0\x9D\x9E\x89" => "\xE2\x88\x82", + "\xF0\x9D\x9E\x8A" => "\xCE\xB5", + "\xF0\x9D\x9E\x8B" => "\xCE\xB8", + "\xF0\x9D\x9E\x8C" => "\xCE\xBA", + "\xF0\x9D\x9E\x8D" => "\xCF\x86", + "\xF0\x9D\x9E\x8E" => "\xCF\x81", + "\xF0\x9D\x9E\x8F" => "\xCF\x80", + "\xF0\x9D\x9E\x90" => "\xCE\xB1", + "\xF0\x9D\x9E\x91" => "\xCE\xB2", + "\xF0\x9D\x9E\x92" => "\xCE\xB3", + "\xF0\x9D\x9E\x93" => "\xCE\xB4", + "\xF0\x9D\x9E\x94" => "\xCE\xB5", + "\xF0\x9D\x9E\x95" => "\xCE\xB6", + "\xF0\x9D\x9E\x96" => "\xCE\xB7", + "\xF0\x9D\x9E\x97" => "\xCE\xB8", + "\xF0\x9D\x9E\x98" => "\xCE\xB9", + "\xF0\x9D\x9E\x99" => "\xCE\xBA", + "\xF0\x9D\x9E\x9A" => "\xCE\xBB", + "\xF0\x9D\x9E\x9B" => "\xCE\xBC", + "\xF0\x9D\x9E\x9C" => "\xCE\xBD", + "\xF0\x9D\x9E\x9D" => "\xCE\xBE", + "\xF0\x9D\x9E\x9E" => "\xCE\xBF", + "\xF0\x9D\x9E\x9F" => "\xCF\x80", + "\xF0\x9D\x9E\xA0" => "\xCF\x81", + "\xF0\x9D\x9E\xA1" => "\xCE\xB8", + "\xF0\x9D\x9E\xA2" => "\xCF\x83", + "\xF0\x9D\x9E\xA3" => "\xCF\x84", + "\xF0\x9D\x9E\xA4" => "\xCF\x85", + "\xF0\x9D\x9E\xA5" => "\xCF\x86", + "\xF0\x9D\x9E\xA6" => "\xCF\x87", + "\xF0\x9D\x9E\xA7" => "\xCF\x88", + "\xF0\x9D\x9E\xA8" => "\xCF\x89", + "\xF0\x9D\x9E\xA9" => "\xE2\x88\x87", + "\xF0\x9D\x9E\xAA" => "\xCE\xB1", + "\xF0\x9D\x9E\xAB" => "\xCE\xB2", + "\xF0\x9D\x9E\xAC" => "\xCE\xB3", + "\xF0\x9D\x9E\xAD" => "\xCE\xB4", + "\xF0\x9D\x9E\xAE" => "\xCE\xB5", + "\xF0\x9D\x9E\xAF" => "\xCE\xB6", + "\xF0\x9D\x9E\xB0" => "\xCE\xB7", + "\xF0\x9D\x9E\xB1" => "\xCE\xB8", + "\xF0\x9D\x9E\xB2" => "\xCE\xB9", + "\xF0\x9D\x9E\xB3" => "\xCE\xBA", + "\xF0\x9D\x9E\xB4" => "\xCE\xBB", + "\xF0\x9D\x9E\xB5" => "\xCE\xBC", + "\xF0\x9D\x9E\xB6" => "\xCE\xBD", + "\xF0\x9D\x9E\xB7" => "\xCE\xBE", + "\xF0\x9D\x9E\xB8" => "\xCE\xBF", + "\xF0\x9D\x9E\xB9" => "\xCF\x80", + "\xF0\x9D\x9E\xBA" => "\xCF\x81", + "\xF0\x9D\x9E\xBB" => "\xCF\x83", + "\xF0\x9D\x9E\xBC" => "\xCF\x83", + "\xF0\x9D\x9E\xBD" => "\xCF\x84", + "\xF0\x9D\x9E\xBE" => "\xCF\x85", + "\xF0\x9D\x9E\xBF" => "\xCF\x86", + "\xF0\x9D\x9F\x80" => "\xCF\x87", + "\xF0\x9D\x9F\x81" => "\xCF\x88", + "\xF0\x9D\x9F\x82" => "\xCF\x89", + "\xF0\x9D\x9F\x83" => "\xE2\x88\x82", + "\xF0\x9D\x9F\x84" => "\xCE\xB5", + "\xF0\x9D\x9F\x85" => "\xCE\xB8", + "\xF0\x9D\x9F\x86" => "\xCE\xBA", + "\xF0\x9D\x9F\x87" => "\xCF\x86", + "\xF0\x9D\x9F\x88" => "\xCF\x81", + "\xF0\x9D\x9F\x89" => "\xCF\x80", + "\xF0\x9D\x9F\x8A" => "\xCF\x9D", + "\xF0\x9D\x9F\x8B" => "\xCF\x9D", + "\xF0\x9D\x9F\x8E" => "\x30", + "\xF0\x9D\x9F\x8F" => "\x31", + "\xF0\x9D\x9F\x90" => "\x32", + "\xF0\x9D\x9F\x91" => "\x33", + "\xF0\x9D\x9F\x92" => "\x34", + "\xF0\x9D\x9F\x93" => "\x35", + "\xF0\x9D\x9F\x94" => "\x36", + "\xF0\x9D\x9F\x95" => "\x37", + "\xF0\x9D\x9F\x96" => "\x38", + "\xF0\x9D\x9F\x97" => "\x39", + "\xF0\x9D\x9F\x98" => "\x30", + "\xF0\x9D\x9F\x99" => "\x31", + "\xF0\x9D\x9F\x9A" => "\x32", + "\xF0\x9D\x9F\x9B" => "\x33", + "\xF0\x9D\x9F\x9C" => "\x34", + "\xF0\x9D\x9F\x9D" => "\x35", + "\xF0\x9D\x9F\x9E" => "\x36", + "\xF0\x9D\x9F\x9F" => "\x37", + "\xF0\x9D\x9F\xA0" => "\x38", + "\xF0\x9D\x9F\xA1" => "\x39", + "\xF0\x9D\x9F\xA2" => "\x30", + "\xF0\x9D\x9F\xA3" => "\x31", + "\xF0\x9D\x9F\xA4" => "\x32", + "\xF0\x9D\x9F\xA5" => "\x33", + "\xF0\x9D\x9F\xA6" => "\x34", + "\xF0\x9D\x9F\xA7" => "\x35", + "\xF0\x9D\x9F\xA8" => "\x36", + "\xF0\x9D\x9F\xA9" => "\x37", + "\xF0\x9D\x9F\xAA" => "\x38", + "\xF0\x9D\x9F\xAB" => "\x39", + "\xF0\x9D\x9F\xAC" => "\x30", + "\xF0\x9D\x9F\xAD" => "\x31", + "\xF0\x9D\x9F\xAE" => "\x32", + "\xF0\x9D\x9F\xAF" => "\x33", + "\xF0\x9D\x9F\xB0" => "\x34", + "\xF0\x9D\x9F\xB1" => "\x35", + "\xF0\x9D\x9F\xB2" => "\x36", + "\xF0\x9D\x9F\xB3" => "\x37", + "\xF0\x9D\x9F\xB4" => "\x38", + "\xF0\x9D\x9F\xB5" => "\x39", + "\xF0\x9D\x9F\xB6" => "\x30", + "\xF0\x9D\x9F\xB7" => "\x31", + "\xF0\x9D\x9F\xB8" => "\x32", + "\xF0\x9D\x9F\xB9" => "\x33", + "\xF0\x9D\x9F\xBA" => "\x34", + "\xF0\x9D\x9F\xBB" => "\x35", + "\xF0\x9D\x9F\xBC" => "\x36", + "\xF0\x9D\x9F\xBD" => "\x37", + "\xF0\x9D\x9F\xBE" => "\x38", + "\xF0\x9D\x9F\xBF" => "\x39", + "\xF0\x9E\x80\xB0" => "\xD0\xB0", + "\xF0\x9E\x80\xB1" => "\xD0\xB1", + "\xF0\x9E\x80\xB2" => "\xD0\xB2", + "\xF0\x9E\x80\xB3" => "\xD0\xB3", + "\xF0\x9E\x80\xB4" => "\xD0\xB4", + "\xF0\x9E\x80\xB5" => "\xD0\xB5", + "\xF0\x9E\x80\xB6" => "\xD0\xB6", + "\xF0\x9E\x80\xB7" => "\xD0\xB7", + "\xF0\x9E\x80\xB8" => "\xD0\xB8", + "\xF0\x9E\x80\xB9" => "\xD0\xBA", + "\xF0\x9E\x80\xBA" => "\xD0\xBB", + "\xF0\x9E\x80\xBB" => "\xD0\xBC", + "\xF0\x9E\x80\xBC" => "\xD0\xBE", + "\xF0\x9E\x80\xBD" => "\xD0\xBF", + "\xF0\x9E\x80\xBE" => "\xD1\x80", + "\xF0\x9E\x80\xBF" => "\xD1\x81", + "\xF0\x9E\x81\x80" => "\xD1\x82", + "\xF0\x9E\x81\x81" => "\xD1\x83", + "\xF0\x9E\x81\x82" => "\xD1\x84", + "\xF0\x9E\x81\x83" => "\xD1\x85", + "\xF0\x9E\x81\x84" => "\xD1\x86", + "\xF0\x9E\x81\x85" => "\xD1\x87", + "\xF0\x9E\x81\x86" => "\xD1\x88", + "\xF0\x9E\x81\x87" => "\xD1\x8B", + "\xF0\x9E\x81\x88" => "\xD1\x8D", + "\xF0\x9E\x81\x89" => "\xD1\x8E", + "\xF0\x9E\x81\x8A" => "\xEA\x9A\x89", + "\xF0\x9E\x81\x8B" => "\xD3\x99", + "\xF0\x9E\x81\x8C" => "\xD1\x96", + "\xF0\x9E\x81\x8D" => "\xD1\x98", + "\xF0\x9E\x81\x8E" => "\xD3\xA9", + "\xF0\x9E\x81\x8F" => "\xD2\xAF", + "\xF0\x9E\x81\x90" => "\xD3\x8F", + "\xF0\x9E\x81\x91" => "\xD0\xB0", + "\xF0\x9E\x81\x92" => "\xD0\xB1", + "\xF0\x9E\x81\x93" => "\xD0\xB2", + "\xF0\x9E\x81\x94" => "\xD0\xB3", + "\xF0\x9E\x81\x95" => "\xD0\xB4", + "\xF0\x9E\x81\x96" => "\xD0\xB5", + "\xF0\x9E\x81\x97" => "\xD0\xB6", + "\xF0\x9E\x81\x98" => "\xD0\xB7", + "\xF0\x9E\x81\x99" => "\xD0\xB8", + "\xF0\x9E\x81\x9A" => "\xD0\xBA", + "\xF0\x9E\x81\x9B" => "\xD0\xBB", + "\xF0\x9E\x81\x9C" => "\xD0\xBE", + "\xF0\x9E\x81\x9D" => "\xD0\xBF", + "\xF0\x9E\x81\x9E" => "\xD1\x81", + "\xF0\x9E\x81\x9F" => "\xD1\x83", + "\xF0\x9E\x81\xA0" => "\xD1\x84", + "\xF0\x9E\x81\xA1" => "\xD1\x85", + "\xF0\x9E\x81\xA2" => "\xD1\x86", + "\xF0\x9E\x81\xA3" => "\xD1\x87", + "\xF0\x9E\x81\xA4" => "\xD1\x88", + "\xF0\x9E\x81\xA5" => "\xD1\x8A", + "\xF0\x9E\x81\xA6" => "\xD1\x8B", + "\xF0\x9E\x81\xA7" => "\xD2\x91", + "\xF0\x9E\x81\xA8" => "\xD1\x96", + "\xF0\x9E\x81\xA9" => "\xD1\x95", + "\xF0\x9E\x81\xAA" => "\xD1\x9F", + "\xF0\x9E\x81\xAB" => "\xD2\xAB", + "\xF0\x9E\x81\xAC" => "\xEA\x99\x91", + "\xF0\x9E\x81\xAD" => "\xD2\xB1", + "\xF0\x9E\xA4\x80" => "\xF0\x9E\xA4\xA2", + "\xF0\x9E\xA4\x81" => "\xF0\x9E\xA4\xA3", + "\xF0\x9E\xA4\x82" => "\xF0\x9E\xA4\xA4", + "\xF0\x9E\xA4\x83" => "\xF0\x9E\xA4\xA5", + "\xF0\x9E\xA4\x84" => "\xF0\x9E\xA4\xA6", + "\xF0\x9E\xA4\x85" => "\xF0\x9E\xA4\xA7", + "\xF0\x9E\xA4\x86" => "\xF0\x9E\xA4\xA8", + "\xF0\x9E\xA4\x87" => "\xF0\x9E\xA4\xA9", + "\xF0\x9E\xA4\x88" => "\xF0\x9E\xA4\xAA", + "\xF0\x9E\xA4\x89" => "\xF0\x9E\xA4\xAB", + "\xF0\x9E\xA4\x8A" => "\xF0\x9E\xA4\xAC", + "\xF0\x9E\xA4\x8B" => "\xF0\x9E\xA4\xAD", + "\xF0\x9E\xA4\x8C" => "\xF0\x9E\xA4\xAE", + "\xF0\x9E\xA4\x8D" => "\xF0\x9E\xA4\xAF", + "\xF0\x9E\xA4\x8E" => "\xF0\x9E\xA4\xB0", + "\xF0\x9E\xA4\x8F" => "\xF0\x9E\xA4\xB1", + "\xF0\x9E\xA4\x90" => "\xF0\x9E\xA4\xB2", + "\xF0\x9E\xA4\x91" => "\xF0\x9E\xA4\xB3", + "\xF0\x9E\xA4\x92" => "\xF0\x9E\xA4\xB4", + "\xF0\x9E\xA4\x93" => "\xF0\x9E\xA4\xB5", + "\xF0\x9E\xA4\x94" => "\xF0\x9E\xA4\xB6", + "\xF0\x9E\xA4\x95" => "\xF0\x9E\xA4\xB7", + "\xF0\x9E\xA4\x96" => "\xF0\x9E\xA4\xB8", + "\xF0\x9E\xA4\x97" => "\xF0\x9E\xA4\xB9", + "\xF0\x9E\xA4\x98" => "\xF0\x9E\xA4\xBA", + "\xF0\x9E\xA4\x99" => "\xF0\x9E\xA4\xBB", + "\xF0\x9E\xA4\x9A" => "\xF0\x9E\xA4\xBC", + "\xF0\x9E\xA4\x9B" => "\xF0\x9E\xA4\xBD", + "\xF0\x9E\xA4\x9C" => "\xF0\x9E\xA4\xBE", + "\xF0\x9E\xA4\x9D" => "\xF0\x9E\xA4\xBF", + "\xF0\x9E\xA4\x9E" => "\xF0\x9E\xA5\x80", + "\xF0\x9E\xA4\x9F" => "\xF0\x9E\xA5\x81", + "\xF0\x9E\xA4\xA0" => "\xF0\x9E\xA5\x82", + "\xF0\x9E\xA4\xA1" => "\xF0\x9E\xA5\x83", + "\xF0\x9E\xB8\x80" => "\xD8\xA7", + "\xF0\x9E\xB8\x81" => "\xD8\xA8", + "\xF0\x9E\xB8\x82" => "\xD8\xAC", + "\xF0\x9E\xB8\x83" => "\xD8\xAF", + "\xF0\x9E\xB8\x85" => "\xD9\x88", + "\xF0\x9E\xB8\x86" => "\xD8\xB2", + "\xF0\x9E\xB8\x87" => "\xD8\xAD", + "\xF0\x9E\xB8\x88" => "\xD8\xB7", + "\xF0\x9E\xB8\x89" => "\xD9\x8A", + "\xF0\x9E\xB8\x8A" => "\xD9\x83", + "\xF0\x9E\xB8\x8B" => "\xD9\x84", + "\xF0\x9E\xB8\x8C" => "\xD9\x85", + "\xF0\x9E\xB8\x8D" => "\xD9\x86", + "\xF0\x9E\xB8\x8E" => "\xD8\xB3", + "\xF0\x9E\xB8\x8F" => "\xD8\xB9", + "\xF0\x9E\xB8\x90" => "\xD9\x81", + "\xF0\x9E\xB8\x91" => "\xD8\xB5", + "\xF0\x9E\xB8\x92" => "\xD9\x82", + "\xF0\x9E\xB8\x93" => "\xD8\xB1", + "\xF0\x9E\xB8\x94" => "\xD8\xB4", + "\xF0\x9E\xB8\x95" => "\xD8\xAA", + "\xF0\x9E\xB8\x96" => "\xD8\xAB", + "\xF0\x9E\xB8\x97" => "\xD8\xAE", + "\xF0\x9E\xB8\x98" => "\xD8\xB0", + "\xF0\x9E\xB8\x99" => "\xD8\xB6", + "\xF0\x9E\xB8\x9A" => "\xD8\xB8", + "\xF0\x9E\xB8\x9B" => "\xD8\xBA", + "\xF0\x9E\xB8\x9C" => "\xD9\xAE", + "\xF0\x9E\xB8\x9D" => "\xDA\xBA", + "\xF0\x9E\xB8\x9E" => "\xDA\xA1", + "\xF0\x9E\xB8\x9F" => "\xD9\xAF", + "\xF0\x9E\xB8\xA1" => "\xD8\xA8", + "\xF0\x9E\xB8\xA2" => "\xD8\xAC", + "\xF0\x9E\xB8\xA4" => "\xD9\x87", + "\xF0\x9E\xB8\xA7" => "\xD8\xAD", + "\xF0\x9E\xB8\xA9" => "\xD9\x8A", + "\xF0\x9E\xB8\xAA" => "\xD9\x83", + "\xF0\x9E\xB8\xAB" => "\xD9\x84", + "\xF0\x9E\xB8\xAC" => "\xD9\x85", + "\xF0\x9E\xB8\xAD" => "\xD9\x86", + "\xF0\x9E\xB8\xAE" => "\xD8\xB3", + "\xF0\x9E\xB8\xAF" => "\xD8\xB9", + "\xF0\x9E\xB8\xB0" => "\xD9\x81", + "\xF0\x9E\xB8\xB1" => "\xD8\xB5", + "\xF0\x9E\xB8\xB2" => "\xD9\x82", + "\xF0\x9E\xB8\xB4" => "\xD8\xB4", + "\xF0\x9E\xB8\xB5" => "\xD8\xAA", + "\xF0\x9E\xB8\xB6" => "\xD8\xAB", + "\xF0\x9E\xB8\xB7" => "\xD8\xAE", + "\xF0\x9E\xB8\xB9" => "\xD8\xB6", + "\xF0\x9E\xB8\xBB" => "\xD8\xBA", + "\xF0\x9E\xB9\x82" => "\xD8\xAC", + "\xF0\x9E\xB9\x87" => "\xD8\xAD", + "\xF0\x9E\xB9\x89" => "\xD9\x8A", + "\xF0\x9E\xB9\x8B" => "\xD9\x84", + "\xF0\x9E\xB9\x8D" => "\xD9\x86", + "\xF0\x9E\xB9\x8E" => "\xD8\xB3", + "\xF0\x9E\xB9\x8F" => "\xD8\xB9", + "\xF0\x9E\xB9\x91" => "\xD8\xB5", + "\xF0\x9E\xB9\x92" => "\xD9\x82", + "\xF0\x9E\xB9\x94" => "\xD8\xB4", + "\xF0\x9E\xB9\x97" => "\xD8\xAE", + "\xF0\x9E\xB9\x99" => "\xD8\xB6", + "\xF0\x9E\xB9\x9B" => "\xD8\xBA", + "\xF0\x9E\xB9\x9D" => "\xDA\xBA", + "\xF0\x9E\xB9\x9F" => "\xD9\xAF", + "\xF0\x9E\xB9\xA1" => "\xD8\xA8", + "\xF0\x9E\xB9\xA2" => "\xD8\xAC", + "\xF0\x9E\xB9\xA4" => "\xD9\x87", + "\xF0\x9E\xB9\xA7" => "\xD8\xAD", + "\xF0\x9E\xB9\xA8" => "\xD8\xB7", + "\xF0\x9E\xB9\xA9" => "\xD9\x8A", + "\xF0\x9E\xB9\xAA" => "\xD9\x83", + "\xF0\x9E\xB9\xAC" => "\xD9\x85", + "\xF0\x9E\xB9\xAD" => "\xD9\x86", + "\xF0\x9E\xB9\xAE" => "\xD8\xB3", + "\xF0\x9E\xB9\xAF" => "\xD8\xB9", + "\xF0\x9E\xB9\xB0" => "\xD9\x81", + "\xF0\x9E\xB9\xB1" => "\xD8\xB5", + "\xF0\x9E\xB9\xB2" => "\xD9\x82", + "\xF0\x9E\xB9\xB4" => "\xD8\xB4", + "\xF0\x9E\xB9\xB5" => "\xD8\xAA", + "\xF0\x9E\xB9\xB6" => "\xD8\xAB", + "\xF0\x9E\xB9\xB7" => "\xD8\xAE", + "\xF0\x9E\xB9\xB9" => "\xD8\xB6", + "\xF0\x9E\xB9\xBA" => "\xD8\xB8", + "\xF0\x9E\xB9\xBB" => "\xD8\xBA", + "\xF0\x9E\xB9\xBC" => "\xD9\xAE", + "\xF0\x9E\xB9\xBE" => "\xDA\xA1", + "\xF0\x9E\xBA\x80" => "\xD8\xA7", + "\xF0\x9E\xBA\x81" => "\xD8\xA8", + "\xF0\x9E\xBA\x82" => "\xD8\xAC", + "\xF0\x9E\xBA\x83" => "\xD8\xAF", + "\xF0\x9E\xBA\x84" => "\xD9\x87", + "\xF0\x9E\xBA\x85" => "\xD9\x88", + "\xF0\x9E\xBA\x86" => "\xD8\xB2", + "\xF0\x9E\xBA\x87" => "\xD8\xAD", + "\xF0\x9E\xBA\x88" => "\xD8\xB7", + "\xF0\x9E\xBA\x89" => "\xD9\x8A", + "\xF0\x9E\xBA\x8B" => "\xD9\x84", + "\xF0\x9E\xBA\x8C" => "\xD9\x85", + "\xF0\x9E\xBA\x8D" => "\xD9\x86", + "\xF0\x9E\xBA\x8E" => "\xD8\xB3", + "\xF0\x9E\xBA\x8F" => "\xD8\xB9", + "\xF0\x9E\xBA\x90" => "\xD9\x81", + "\xF0\x9E\xBA\x91" => "\xD8\xB5", + "\xF0\x9E\xBA\x92" => "\xD9\x82", + "\xF0\x9E\xBA\x93" => "\xD8\xB1", + "\xF0\x9E\xBA\x94" => "\xD8\xB4", + "\xF0\x9E\xBA\x95" => "\xD8\xAA", + "\xF0\x9E\xBA\x96" => "\xD8\xAB", + "\xF0\x9E\xBA\x97" => "\xD8\xAE", + "\xF0\x9E\xBA\x98" => "\xD8\xB0", + "\xF0\x9E\xBA\x99" => "\xD8\xB6", + "\xF0\x9E\xBA\x9A" => "\xD8\xB8", + "\xF0\x9E\xBA\x9B" => "\xD8\xBA", + "\xF0\x9E\xBA\xA1" => "\xD8\xA8", + "\xF0\x9E\xBA\xA2" => "\xD8\xAC", + "\xF0\x9E\xBA\xA3" => "\xD8\xAF", + "\xF0\x9E\xBA\xA5" => "\xD9\x88", + "\xF0\x9E\xBA\xA6" => "\xD8\xB2", + "\xF0\x9E\xBA\xA7" => "\xD8\xAD", + "\xF0\x9E\xBA\xA8" => "\xD8\xB7", + "\xF0\x9E\xBA\xA9" => "\xD9\x8A", + "\xF0\x9E\xBA\xAB" => "\xD9\x84", + "\xF0\x9E\xBA\xAC" => "\xD9\x85", + "\xF0\x9E\xBA\xAD" => "\xD9\x86", + "\xF0\x9E\xBA\xAE" => "\xD8\xB3", + "\xF0\x9E\xBA\xAF" => "\xD8\xB9", + "\xF0\x9E\xBA\xB0" => "\xD9\x81", + "\xF0\x9E\xBA\xB1" => "\xD8\xB5", + "\xF0\x9E\xBA\xB2" => "\xD9\x82", + "\xF0\x9E\xBA\xB3" => "\xD8\xB1", + "\xF0\x9E\xBA\xB4" => "\xD8\xB4", + "\xF0\x9E\xBA\xB5" => "\xD8\xAA", + "\xF0\x9E\xBA\xB6" => "\xD8\xAB", + "\xF0\x9E\xBA\xB7" => "\xD8\xAE", + "\xF0\x9E\xBA\xB8" => "\xD8\xB0", + "\xF0\x9E\xBA\xB9" => "\xD8\xB6", + "\xF0\x9E\xBA\xBA" => "\xD8\xB8", + "\xF0\x9E\xBA\xBB" => "\xD8\xBA", + "\xF0\x9F\x84\xAA" => "\xE3\x80\x94\x73\xE3\x80\x95", + "\xF0\x9F\x84\xAB" => "\x63", + "\xF0\x9F\x84\xAC" => "\x72", + "\xF0\x9F\x84\xAD" => "\x63\x64", + "\xF0\x9F\x84\xAE" => "\x77\x7A", + "\xF0\x9F\x84\xB0" => "\x61", + "\xF0\x9F\x84\xB1" => "\x62", + "\xF0\x9F\x84\xB2" => "\x63", + "\xF0\x9F\x84\xB3" => "\x64", + "\xF0\x9F\x84\xB4" => "\x65", + "\xF0\x9F\x84\xB5" => "\x66", + "\xF0\x9F\x84\xB6" => "\x67", + "\xF0\x9F\x84\xB7" => "\x68", + "\xF0\x9F\x84\xB8" => "\x69", + "\xF0\x9F\x84\xB9" => "\x6A", + "\xF0\x9F\x84\xBA" => "\x6B", + "\xF0\x9F\x84\xBB" => "\x6C", + "\xF0\x9F\x84\xBC" => "\x6D", + "\xF0\x9F\x84\xBD" => "\x6E", + "\xF0\x9F\x84\xBE" => "\x6F", + "\xF0\x9F\x84\xBF" => "\x70", + "\xF0\x9F\x85\x80" => "\x71", + "\xF0\x9F\x85\x81" => "\x72", + "\xF0\x9F\x85\x82" => "\x73", + "\xF0\x9F\x85\x83" => "\x74", + "\xF0\x9F\x85\x84" => "\x75", + "\xF0\x9F\x85\x85" => "\x76", + "\xF0\x9F\x85\x86" => "\x77", + "\xF0\x9F\x85\x87" => "\x78", + "\xF0\x9F\x85\x88" => "\x79", + "\xF0\x9F\x85\x89" => "\x7A", + "\xF0\x9F\x85\x8A" => "\x68\x76", + "\xF0\x9F\x85\x8B" => "\x6D\x76", + "\xF0\x9F\x85\x8C" => "\x73\x64", + "\xF0\x9F\x85\x8D" => "\x73\x73", + "\xF0\x9F\x85\x8E" => "\x70\x70\x76", + "\xF0\x9F\x85\x8F" => "\x77\x63", + "\xF0\x9F\x85\xAA" => "\x6D\x63", + "\xF0\x9F\x85\xAB" => "\x6D\x64", + "\xF0\x9F\x85\xAC" => "\x6D\x72", + "\xF0\x9F\x86\x90" => "\x64\x6A", + "\xF0\x9F\x88\x80" => "\xE3\x81\xBB\xE3\x81\x8B", + "\xF0\x9F\x88\x81" => "\xE3\x82\xB3\xE3\x82\xB3", + "\xF0\x9F\x88\x82" => "\xE3\x82\xB5", + "\xF0\x9F\x88\x90" => "\xE6\x89\x8B", + "\xF0\x9F\x88\x91" => "\xE5\xAD\x97", + "\xF0\x9F\x88\x92" => "\xE5\x8F\x8C", + "\xF0\x9F\x88\x93" => "\xE3\x83\x87", + "\xF0\x9F\x88\x94" => "\xE4\xBA\x8C", + "\xF0\x9F\x88\x95" => "\xE5\xA4\x9A", + "\xF0\x9F\x88\x96" => "\xE8\xA7\xA3", + "\xF0\x9F\x88\x97" => "\xE5\xA4\xA9", + "\xF0\x9F\x88\x98" => "\xE4\xBA\xA4", + "\xF0\x9F\x88\x99" => "\xE6\x98\xA0", + "\xF0\x9F\x88\x9A" => "\xE7\x84\xA1", + "\xF0\x9F\x88\x9B" => "\xE6\x96\x99", + "\xF0\x9F\x88\x9C" => "\xE5\x89\x8D", + "\xF0\x9F\x88\x9D" => "\xE5\xBE\x8C", + "\xF0\x9F\x88\x9E" => "\xE5\x86\x8D", + "\xF0\x9F\x88\x9F" => "\xE6\x96\xB0", + "\xF0\x9F\x88\xA0" => "\xE5\x88\x9D", + "\xF0\x9F\x88\xA1" => "\xE7\xB5\x82", + "\xF0\x9F\x88\xA2" => "\xE7\x94\x9F", + "\xF0\x9F\x88\xA3" => "\xE8\xB2\xA9", + "\xF0\x9F\x88\xA4" => "\xE5\xA3\xB0", + "\xF0\x9F\x88\xA5" => "\xE5\x90\xB9", + "\xF0\x9F\x88\xA6" => "\xE6\xBC\x94", + "\xF0\x9F\x88\xA7" => "\xE6\x8A\x95", + "\xF0\x9F\x88\xA8" => "\xE6\x8D\x95", + "\xF0\x9F\x88\xA9" => "\xE4\xB8\x80", + "\xF0\x9F\x88\xAA" => "\xE4\xB8\x89", + "\xF0\x9F\x88\xAB" => "\xE9\x81\x8A", + "\xF0\x9F\x88\xAC" => "\xE5\xB7\xA6", + "\xF0\x9F\x88\xAD" => "\xE4\xB8\xAD", + "\xF0\x9F\x88\xAE" => "\xE5\x8F\xB3", + "\xF0\x9F\x88\xAF" => "\xE6\x8C\x87", + "\xF0\x9F\x88\xB0" => "\xE8\xB5\xB0", + "\xF0\x9F\x88\xB1" => "\xE6\x89\x93", + "\xF0\x9F\x88\xB2" => "\xE7\xA6\x81", + "\xF0\x9F\x88\xB3" => "\xE7\xA9\xBA", + "\xF0\x9F\x88\xB4" => "\xE5\x90\x88", + "\xF0\x9F\x88\xB5" => "\xE6\xBA\x80", + "\xF0\x9F\x88\xB6" => "\xE6\x9C\x89", + "\xF0\x9F\x88\xB7" => "\xE6\x9C\x88", + "\xF0\x9F\x88\xB8" => "\xE7\x94\xB3", + "\xF0\x9F\x88\xB9" => "\xE5\x89\xB2", + "\xF0\x9F\x88\xBA" => "\xE5\x96\xB6", + "\xF0\x9F\x88\xBB" => "\xE9\x85\x8D", + "\xF0\x9F\x89\x80" => "\xE3\x80\x94\xE6\x9C\xAC\xE3\x80\x95", + "\xF0\x9F\x89\x81" => "\xE3\x80\x94\xE4\xB8\x89\xE3\x80\x95", + "\xF0\x9F\x89\x82" => "\xE3\x80\x94\xE4\xBA\x8C\xE3\x80\x95", + "\xF0\x9F\x89\x83" => "\xE3\x80\x94\xE5\xAE\x89\xE3\x80\x95", + "\xF0\x9F\x89\x84" => "\xE3\x80\x94\xE7\x82\xB9\xE3\x80\x95", + "\xF0\x9F\x89\x85" => "\xE3\x80\x94\xE6\x89\x93\xE3\x80\x95", + "\xF0\x9F\x89\x86" => "\xE3\x80\x94\xE7\x9B\x97\xE3\x80\x95", + "\xF0\x9F\x89\x87" => "\xE3\x80\x94\xE5\x8B\x9D\xE3\x80\x95", + "\xF0\x9F\x89\x88" => "\xE3\x80\x94\xE6\x95\x97\xE3\x80\x95", + "\xF0\x9F\x89\x90" => "\xE5\xBE\x97", + "\xF0\x9F\x89\x91" => "\xE5\x8F\xAF", + "\xF0\x9F\xAF\xB0" => "\x30", + "\xF0\x9F\xAF\xB1" => "\x31", + "\xF0\x9F\xAF\xB2" => "\x32", + "\xF0\x9F\xAF\xB3" => "\x33", + "\xF0\x9F\xAF\xB4" => "\x34", + "\xF0\x9F\xAF\xB5" => "\x35", + "\xF0\x9F\xAF\xB6" => "\x36", + "\xF0\x9F\xAF\xB7" => "\x37", + "\xF0\x9F\xAF\xB8" => "\x38", + "\xF0\x9F\xAF\xB9" => "\x39", + "\xF0\xAF\xA0\x80" => "\xE4\xB8\xBD", + "\xF0\xAF\xA0\x81" => "\xE4\xB8\xB8", + "\xF0\xAF\xA0\x82" => "\xE4\xB9\x81", + "\xF0\xAF\xA0\x83" => "\xF0\xA0\x84\xA2", + "\xF0\xAF\xA0\x84" => "\xE4\xBD\xA0", + "\xF0\xAF\xA0\x85" => "\xE4\xBE\xAE", + "\xF0\xAF\xA0\x86" => "\xE4\xBE\xBB", + "\xF0\xAF\xA0\x87" => "\xE5\x80\x82", + "\xF0\xAF\xA0\x88" => "\xE5\x81\xBA", + "\xF0\xAF\xA0\x89" => "\xE5\x82\x99", + "\xF0\xAF\xA0\x8A" => "\xE5\x83\xA7", + "\xF0\xAF\xA0\x8B" => "\xE5\x83\x8F", + "\xF0\xAF\xA0\x8C" => "\xE3\x92\x9E", + "\xF0\xAF\xA0\x8D" => "\xF0\xA0\x98\xBA", + "\xF0\xAF\xA0\x8E" => "\xE5\x85\x8D", + "\xF0\xAF\xA0\x8F" => "\xE5\x85\x94", + "\xF0\xAF\xA0\x90" => "\xE5\x85\xA4", + "\xF0\xAF\xA0\x91" => "\xE5\x85\xB7", + "\xF0\xAF\xA0\x92" => "\xF0\xA0\x94\x9C", + "\xF0\xAF\xA0\x93" => "\xE3\x92\xB9", + "\xF0\xAF\xA0\x94" => "\xE5\x85\xA7", + "\xF0\xAF\xA0\x95" => "\xE5\x86\x8D", + "\xF0\xAF\xA0\x96" => "\xF0\xA0\x95\x8B", + "\xF0\xAF\xA0\x97" => "\xE5\x86\x97", + "\xF0\xAF\xA0\x98" => "\xE5\x86\xA4", + "\xF0\xAF\xA0\x99" => "\xE4\xBB\x8C", + "\xF0\xAF\xA0\x9A" => "\xE5\x86\xAC", + "\xF0\xAF\xA0\x9B" => "\xE5\x86\xB5", + "\xF0\xAF\xA0\x9C" => "\xF0\xA9\x87\x9F", + "\xF0\xAF\xA0\x9D" => "\xE5\x87\xB5", + "\xF0\xAF\xA0\x9E" => "\xE5\x88\x83", + "\xF0\xAF\xA0\x9F" => "\xE3\x93\x9F", + "\xF0\xAF\xA0\xA0" => "\xE5\x88\xBB", + "\xF0\xAF\xA0\xA1" => "\xE5\x89\x86", + "\xF0\xAF\xA0\xA2" => "\xE5\x89\xB2", + "\xF0\xAF\xA0\xA3" => "\xE5\x89\xB7", + "\xF0\xAF\xA0\xA4" => "\xE3\x94\x95", + "\xF0\xAF\xA0\xA5" => "\xE5\x8B\x87", + "\xF0\xAF\xA0\xA6" => "\xE5\x8B\x89", + "\xF0\xAF\xA0\xA7" => "\xE5\x8B\xA4", + "\xF0\xAF\xA0\xA8" => "\xE5\x8B\xBA", + "\xF0\xAF\xA0\xA9" => "\xE5\x8C\x85", + "\xF0\xAF\xA0\xAA" => "\xE5\x8C\x86", + "\xF0\xAF\xA0\xAB" => "\xE5\x8C\x97", + "\xF0\xAF\xA0\xAC" => "\xE5\x8D\x89", + "\xF0\xAF\xA0\xAD" => "\xE5\x8D\x91", + "\xF0\xAF\xA0\xAE" => "\xE5\x8D\x9A", + "\xF0\xAF\xA0\xAF" => "\xE5\x8D\xB3", + "\xF0\xAF\xA0\xB0" => "\xE5\x8D\xBD", + "\xF0\xAF\xA0\xB1" => "\xE5\x8D\xBF", + "\xF0\xAF\xA0\xB2" => "\xE5\x8D\xBF", + "\xF0\xAF\xA0\xB3" => "\xE5\x8D\xBF", + "\xF0\xAF\xA0\xB4" => "\xF0\xA0\xA8\xAC", + "\xF0\xAF\xA0\xB5" => "\xE7\x81\xB0", + "\xF0\xAF\xA0\xB6" => "\xE5\x8F\x8A", + "\xF0\xAF\xA0\xB7" => "\xE5\x8F\x9F", + "\xF0\xAF\xA0\xB8" => "\xF0\xA0\xAD\xA3", + "\xF0\xAF\xA0\xB9" => "\xE5\x8F\xAB", + "\xF0\xAF\xA0\xBA" => "\xE5\x8F\xB1", + "\xF0\xAF\xA0\xBB" => "\xE5\x90\x86", + "\xF0\xAF\xA0\xBC" => "\xE5\x92\x9E", + "\xF0\xAF\xA0\xBD" => "\xE5\x90\xB8", + "\xF0\xAF\xA0\xBE" => "\xE5\x91\x88", + "\xF0\xAF\xA0\xBF" => "\xE5\x91\xA8", + "\xF0\xAF\xA1\x80" => "\xE5\x92\xA2", + "\xF0\xAF\xA1\x81" => "\xE5\x93\xB6", + "\xF0\xAF\xA1\x82" => "\xE5\x94\x90", + "\xF0\xAF\xA1\x83" => "\xE5\x95\x93", + "\xF0\xAF\xA1\x84" => "\xE5\x95\xA3", + "\xF0\xAF\xA1\x85" => "\xE5\x96\x84", + "\xF0\xAF\xA1\x86" => "\xE5\x96\x84", + "\xF0\xAF\xA1\x87" => "\xE5\x96\x99", + "\xF0\xAF\xA1\x88" => "\xE5\x96\xAB", + "\xF0\xAF\xA1\x89" => "\xE5\x96\xB3", + "\xF0\xAF\xA1\x8A" => "\xE5\x97\x82", + "\xF0\xAF\xA1\x8B" => "\xE5\x9C\x96", + "\xF0\xAF\xA1\x8C" => "\xE5\x98\x86", + "\xF0\xAF\xA1\x8D" => "\xE5\x9C\x97", + "\xF0\xAF\xA1\x8E" => "\xE5\x99\x91", + "\xF0\xAF\xA1\x8F" => "\xE5\x99\xB4", + "\xF0\xAF\xA1\x90" => "\xE5\x88\x87", + "\xF0\xAF\xA1\x91" => "\xE5\xA3\xAE", + "\xF0\xAF\xA1\x92" => "\xE5\x9F\x8E", + "\xF0\xAF\xA1\x93" => "\xE5\x9F\xB4", + "\xF0\xAF\xA1\x94" => "\xE5\xA0\x8D", + "\xF0\xAF\xA1\x95" => "\xE5\x9E\x8B", + "\xF0\xAF\xA1\x96" => "\xE5\xA0\xB2", + "\xF0\xAF\xA1\x97" => "\xE5\xA0\xB1", + "\xF0\xAF\xA1\x98" => "\xE5\xA2\xAC", + "\xF0\xAF\xA1\x99" => "\xF0\xA1\x93\xA4", + "\xF0\xAF\xA1\x9A" => "\xE5\xA3\xB2", + "\xF0\xAF\xA1\x9B" => "\xE5\xA3\xB7", + "\xF0\xAF\xA1\x9C" => "\xE5\xA4\x86", + "\xF0\xAF\xA1\x9D" => "\xE5\xA4\x9A", + "\xF0\xAF\xA1\x9E" => "\xE5\xA4\xA2", + "\xF0\xAF\xA1\x9F" => "\xE5\xA5\xA2", + "\xF0\xAF\xA1\xA0" => "\xF0\xA1\x9A\xA8", + "\xF0\xAF\xA1\xA1" => "\xF0\xA1\x9B\xAA", + "\xF0\xAF\xA1\xA2" => "\xE5\xA7\xAC", + "\xF0\xAF\xA1\xA3" => "\xE5\xA8\x9B", + "\xF0\xAF\xA1\xA4" => "\xE5\xA8\xA7", + "\xF0\xAF\xA1\xA5" => "\xE5\xA7\x98", + "\xF0\xAF\xA1\xA6" => "\xE5\xA9\xA6", + "\xF0\xAF\xA1\xA7" => "\xE3\x9B\xAE", + "\xF0\xAF\xA1\xA9" => "\xE5\xAC\x88", + "\xF0\xAF\xA1\xAA" => "\xE5\xAC\xBE", + "\xF0\xAF\xA1\xAB" => "\xE5\xAC\xBE", + "\xF0\xAF\xA1\xAC" => "\xF0\xA1\xA7\x88", + "\xF0\xAF\xA1\xAD" => "\xE5\xAF\x83", + "\xF0\xAF\xA1\xAE" => "\xE5\xAF\x98", + "\xF0\xAF\xA1\xAF" => "\xE5\xAF\xA7", + "\xF0\xAF\xA1\xB0" => "\xE5\xAF\xB3", + "\xF0\xAF\xA1\xB1" => "\xF0\xA1\xAC\x98", + "\xF0\xAF\xA1\xB2" => "\xE5\xAF\xBF", + "\xF0\xAF\xA1\xB3" => "\xE5\xB0\x86", + "\xF0\xAF\xA1\xB5" => "\xE5\xB0\xA2", + "\xF0\xAF\xA1\xB6" => "\xE3\x9E\x81", + "\xF0\xAF\xA1\xB7" => "\xE5\xB1\xA0", + "\xF0\xAF\xA1\xB8" => "\xE5\xB1\xAE", + "\xF0\xAF\xA1\xB9" => "\xE5\xB3\x80", + "\xF0\xAF\xA1\xBA" => "\xE5\xB2\x8D", + "\xF0\xAF\xA1\xBB" => "\xF0\xA1\xB7\xA4", + "\xF0\xAF\xA1\xBC" => "\xE5\xB5\x83", + "\xF0\xAF\xA1\xBD" => "\xF0\xA1\xB7\xA6", + "\xF0\xAF\xA1\xBE" => "\xE5\xB5\xAE", + "\xF0\xAF\xA1\xBF" => "\xE5\xB5\xAB", + "\xF0\xAF\xA2\x80" => "\xE5\xB5\xBC", + "\xF0\xAF\xA2\x81" => "\xE5\xB7\xA1", + "\xF0\xAF\xA2\x82" => "\xE5\xB7\xA2", + "\xF0\xAF\xA2\x83" => "\xE3\xA0\xAF", + "\xF0\xAF\xA2\x84" => "\xE5\xB7\xBD", + "\xF0\xAF\xA2\x85" => "\xE5\xB8\xA8", + "\xF0\xAF\xA2\x86" => "\xE5\xB8\xBD", + "\xF0\xAF\xA2\x87" => "\xE5\xB9\xA9", + "\xF0\xAF\xA2\x88" => "\xE3\xA1\xA2", + "\xF0\xAF\xA2\x89" => "\xF0\xA2\x86\x83", + "\xF0\xAF\xA2\x8A" => "\xE3\xA1\xBC", + "\xF0\xAF\xA2\x8B" => "\xE5\xBA\xB0", + "\xF0\xAF\xA2\x8C" => "\xE5\xBA\xB3", + "\xF0\xAF\xA2\x8D" => "\xE5\xBA\xB6", + "\xF0\xAF\xA2\x8E" => "\xE5\xBB\x8A", + "\xF0\xAF\xA2\x8F" => "\xF0\xAA\x8E\x92", + "\xF0\xAF\xA2\x90" => "\xE5\xBB\xBE", + "\xF0\xAF\xA2\x91" => "\xF0\xA2\x8C\xB1", + "\xF0\xAF\xA2\x92" => "\xF0\xA2\x8C\xB1", + "\xF0\xAF\xA2\x93" => "\xE8\x88\x81", + "\xF0\xAF\xA2\x94" => "\xE5\xBC\xA2", + "\xF0\xAF\xA2\x95" => "\xE5\xBC\xA2", + "\xF0\xAF\xA2\x96" => "\xE3\xA3\x87", + "\xF0\xAF\xA2\x97" => "\xF0\xA3\x8A\xB8", + "\xF0\xAF\xA2\x98" => "\xF0\xA6\x87\x9A", + "\xF0\xAF\xA2\x99" => "\xE5\xBD\xA2", + "\xF0\xAF\xA2\x9A" => "\xE5\xBD\xAB", + "\xF0\xAF\xA2\x9B" => "\xE3\xA3\xA3", + "\xF0\xAF\xA2\x9C" => "\xE5\xBE\x9A", + "\xF0\xAF\xA2\x9D" => "\xE5\xBF\x8D", + "\xF0\xAF\xA2\x9E" => "\xE5\xBF\x97", + "\xF0\xAF\xA2\x9F" => "\xE5\xBF\xB9", + "\xF0\xAF\xA2\xA0" => "\xE6\x82\x81", + "\xF0\xAF\xA2\xA1" => "\xE3\xA4\xBA", + "\xF0\xAF\xA2\xA2" => "\xE3\xA4\x9C", + "\xF0\xAF\xA2\xA3" => "\xE6\x82\x94", + "\xF0\xAF\xA2\xA4" => "\xF0\xA2\x9B\x94", + "\xF0\xAF\xA2\xA5" => "\xE6\x83\x87", + "\xF0\xAF\xA2\xA6" => "\xE6\x85\x88", + "\xF0\xAF\xA2\xA7" => "\xE6\x85\x8C", + "\xF0\xAF\xA2\xA8" => "\xE6\x85\x8E", + "\xF0\xAF\xA2\xA9" => "\xE6\x85\x8C", + "\xF0\xAF\xA2\xAA" => "\xE6\x85\xBA", + "\xF0\xAF\xA2\xAB" => "\xE6\x86\x8E", + "\xF0\xAF\xA2\xAC" => "\xE6\x86\xB2", + "\xF0\xAF\xA2\xAD" => "\xE6\x86\xA4", + "\xF0\xAF\xA2\xAE" => "\xE6\x86\xAF", + "\xF0\xAF\xA2\xAF" => "\xE6\x87\x9E", + "\xF0\xAF\xA2\xB0" => "\xE6\x87\xB2", + "\xF0\xAF\xA2\xB1" => "\xE6\x87\xB6", + "\xF0\xAF\xA2\xB2" => "\xE6\x88\x90", + "\xF0\xAF\xA2\xB3" => "\xE6\x88\x9B", + "\xF0\xAF\xA2\xB4" => "\xE6\x89\x9D", + "\xF0\xAF\xA2\xB5" => "\xE6\x8A\xB1", + "\xF0\xAF\xA2\xB6" => "\xE6\x8B\x94", + "\xF0\xAF\xA2\xB7" => "\xE6\x8D\x90", + "\xF0\xAF\xA2\xB8" => "\xF0\xA2\xAC\x8C", + "\xF0\xAF\xA2\xB9" => "\xE6\x8C\xBD", + "\xF0\xAF\xA2\xBA" => "\xE6\x8B\xBC", + "\xF0\xAF\xA2\xBB" => "\xE6\x8D\xA8", + "\xF0\xAF\xA2\xBC" => "\xE6\x8E\x83", + "\xF0\xAF\xA2\xBD" => "\xE6\x8F\xA4", + "\xF0\xAF\xA2\xBE" => "\xF0\xA2\xAF\xB1", + "\xF0\xAF\xA2\xBF" => "\xE6\x90\xA2", + "\xF0\xAF\xA3\x80" => "\xE6\x8F\x85", + "\xF0\xAF\xA3\x81" => "\xE6\x8E\xA9", + "\xF0\xAF\xA3\x82" => "\xE3\xA8\xAE", + "\xF0\xAF\xA3\x83" => "\xE6\x91\xA9", + "\xF0\xAF\xA3\x84" => "\xE6\x91\xBE", + "\xF0\xAF\xA3\x85" => "\xE6\x92\x9D", + "\xF0\xAF\xA3\x86" => "\xE6\x91\xB7", + "\xF0\xAF\xA3\x87" => "\xE3\xA9\xAC", + "\xF0\xAF\xA3\x88" => "\xE6\x95\x8F", + "\xF0\xAF\xA3\x89" => "\xE6\x95\xAC", + "\xF0\xAF\xA3\x8A" => "\xF0\xA3\x80\x8A", + "\xF0\xAF\xA3\x8B" => "\xE6\x97\xA3", + "\xF0\xAF\xA3\x8C" => "\xE6\x9B\xB8", + "\xF0\xAF\xA3\x8D" => "\xE6\x99\x89", + "\xF0\xAF\xA3\x8E" => "\xE3\xAC\x99", + "\xF0\xAF\xA3\x8F" => "\xE6\x9A\x91", + "\xF0\xAF\xA3\x90" => "\xE3\xAC\x88", + "\xF0\xAF\xA3\x91" => "\xE3\xAB\xA4", + "\xF0\xAF\xA3\x92" => "\xE5\x86\x92", + "\xF0\xAF\xA3\x93" => "\xE5\x86\x95", + "\xF0\xAF\xA3\x94" => "\xE6\x9C\x80", + "\xF0\xAF\xA3\x95" => "\xE6\x9A\x9C", + "\xF0\xAF\xA3\x96" => "\xE8\x82\xAD", + "\xF0\xAF\xA3\x97" => "\xE4\x8F\x99", + "\xF0\xAF\xA3\x98" => "\xE6\x9C\x97", + "\xF0\xAF\xA3\x99" => "\xE6\x9C\x9B", + "\xF0\xAF\xA3\x9A" => "\xE6\x9C\xA1", + "\xF0\xAF\xA3\x9B" => "\xE6\x9D\x9E", + "\xF0\xAF\xA3\x9C" => "\xE6\x9D\x93", + "\xF0\xAF\xA3\x9D" => "\xF0\xA3\x8F\x83", + "\xF0\xAF\xA3\x9E" => "\xE3\xAD\x89", + "\xF0\xAF\xA3\x9F" => "\xE6\x9F\xBA", + "\xF0\xAF\xA3\xA0" => "\xE6\x9E\x85", + "\xF0\xAF\xA3\xA1" => "\xE6\xA1\x92", + "\xF0\xAF\xA3\xA2" => "\xE6\xA2\x85", + "\xF0\xAF\xA3\xA3" => "\xF0\xA3\x91\xAD", + "\xF0\xAF\xA3\xA4" => "\xE6\xA2\x8E", + "\xF0\xAF\xA3\xA5" => "\xE6\xA0\x9F", + "\xF0\xAF\xA3\xA6" => "\xE6\xA4\x94", + "\xF0\xAF\xA3\xA7" => "\xE3\xAE\x9D", + "\xF0\xAF\xA3\xA8" => "\xE6\xA5\x82", + "\xF0\xAF\xA3\xA9" => "\xE6\xA6\xA3", + "\xF0\xAF\xA3\xAA" => "\xE6\xA7\xAA", + "\xF0\xAF\xA3\xAB" => "\xE6\xAA\xA8", + "\xF0\xAF\xA3\xAC" => "\xF0\xA3\x9A\xA3", + "\xF0\xAF\xA3\xAD" => "\xE6\xAB\x9B", + "\xF0\xAF\xA3\xAE" => "\xE3\xB0\x98", + "\xF0\xAF\xA3\xAF" => "\xE6\xAC\xA1", + "\xF0\xAF\xA3\xB0" => "\xF0\xA3\xA2\xA7", + "\xF0\xAF\xA3\xB1" => "\xE6\xAD\x94", + "\xF0\xAF\xA3\xB2" => "\xE3\xB1\x8E", + "\xF0\xAF\xA3\xB3" => "\xE6\xAD\xB2", + "\xF0\xAF\xA3\xB4" => "\xE6\xAE\x9F", + "\xF0\xAF\xA3\xB5" => "\xE6\xAE\xBA", + "\xF0\xAF\xA3\xB6" => "\xE6\xAE\xBB", + "\xF0\xAF\xA3\xB7" => "\xF0\xA3\xAA\x8D", + "\xF0\xAF\xA3\xB8" => "\xF0\xA1\xB4\x8B", + "\xF0\xAF\xA3\xB9" => "\xF0\xA3\xAB\xBA", + "\xF0\xAF\xA3\xBA" => "\xE6\xB1\x8E", + "\xF0\xAF\xA3\xBB" => "\xF0\xA3\xB2\xBC", + "\xF0\xAF\xA3\xBC" => "\xE6\xB2\xBF", + "\xF0\xAF\xA3\xBD" => "\xE6\xB3\x8D", + "\xF0\xAF\xA3\xBE" => "\xE6\xB1\xA7", + "\xF0\xAF\xA3\xBF" => "\xE6\xB4\x96", + "\xF0\xAF\xA4\x80" => "\xE6\xB4\xBE", + "\xF0\xAF\xA4\x81" => "\xE6\xB5\xB7", + "\xF0\xAF\xA4\x82" => "\xE6\xB5\x81", + "\xF0\xAF\xA4\x83" => "\xE6\xB5\xA9", + "\xF0\xAF\xA4\x84" => "\xE6\xB5\xB8", + "\xF0\xAF\xA4\x85" => "\xE6\xB6\x85", + "\xF0\xAF\xA4\x86" => "\xF0\xA3\xB4\x9E", + "\xF0\xAF\xA4\x87" => "\xE6\xB4\xB4", + "\xF0\xAF\xA4\x88" => "\xE6\xB8\xAF", + "\xF0\xAF\xA4\x89" => "\xE6\xB9\xAE", + "\xF0\xAF\xA4\x8A" => "\xE3\xB4\xB3", + "\xF0\xAF\xA4\x8B" => "\xE6\xBB\x8B", + "\xF0\xAF\xA4\x8C" => "\xE6\xBB\x87", + "\xF0\xAF\xA4\x8D" => "\xF0\xA3\xBB\x91", + "\xF0\xAF\xA4\x8E" => "\xE6\xB7\xB9", + "\xF0\xAF\xA4\x8F" => "\xE6\xBD\xAE", + "\xF0\xAF\xA4\x90" => "\xF0\xA3\xBD\x9E", + "\xF0\xAF\xA4\x91" => "\xF0\xA3\xBE\x8E", + "\xF0\xAF\xA4\x92" => "\xE6\xBF\x86", + "\xF0\xAF\xA4\x93" => "\xE7\x80\xB9", + "\xF0\xAF\xA4\x94" => "\xE7\x80\x9E", + "\xF0\xAF\xA4\x95" => "\xE7\x80\x9B", + "\xF0\xAF\xA4\x96" => "\xE3\xB6\x96", + "\xF0\xAF\xA4\x97" => "\xE7\x81\x8A", + "\xF0\xAF\xA4\x98" => "\xE7\x81\xBD", + "\xF0\xAF\xA4\x99" => "\xE7\x81\xB7", + "\xF0\xAF\xA4\x9A" => "\xE7\x82\xAD", + "\xF0\xAF\xA4\x9B" => "\xF0\xA0\x94\xA5", + "\xF0\xAF\xA4\x9C" => "\xE7\x85\x85", + "\xF0\xAF\xA4\x9D" => "\xF0\xA4\x89\xA3", + "\xF0\xAF\xA4\x9E" => "\xE7\x86\x9C", + "\xF0\xAF\xA4\xA0" => "\xE7\x88\xA8", + "\xF0\xAF\xA4\xA1" => "\xE7\x88\xB5", + "\xF0\xAF\xA4\xA2" => "\xE7\x89\x90", + "\xF0\xAF\xA4\xA3" => "\xF0\xA4\x98\x88", + "\xF0\xAF\xA4\xA4" => "\xE7\x8A\x80", + "\xF0\xAF\xA4\xA5" => "\xE7\x8A\x95", + "\xF0\xAF\xA4\xA6" => "\xF0\xA4\x9C\xB5", + "\xF0\xAF\xA4\xA7" => "\xF0\xA4\xA0\x94", + "\xF0\xAF\xA4\xA8" => "\xE7\x8D\xBA", + "\xF0\xAF\xA4\xA9" => "\xE7\x8E\x8B", + "\xF0\xAF\xA4\xAA" => "\xE3\xBA\xAC", + "\xF0\xAF\xA4\xAB" => "\xE7\x8E\xA5", + "\xF0\xAF\xA4\xAC" => "\xE3\xBA\xB8", + "\xF0\xAF\xA4\xAD" => "\xE3\xBA\xB8", + "\xF0\xAF\xA4\xAE" => "\xE7\x91\x87", + "\xF0\xAF\xA4\xAF" => "\xE7\x91\x9C", + "\xF0\xAF\xA4\xB0" => "\xE7\x91\xB1", + "\xF0\xAF\xA4\xB1" => "\xE7\x92\x85", + "\xF0\xAF\xA4\xB2" => "\xE7\x93\x8A", + "\xF0\xAF\xA4\xB3" => "\xE3\xBC\x9B", + "\xF0\xAF\xA4\xB4" => "\xE7\x94\xA4", + "\xF0\xAF\xA4\xB5" => "\xF0\xA4\xB0\xB6", + "\xF0\xAF\xA4\xB6" => "\xE7\x94\xBE", + "\xF0\xAF\xA4\xB7" => "\xF0\xA4\xB2\x92", + "\xF0\xAF\xA4\xB8" => "\xE7\x95\xB0", + "\xF0\xAF\xA4\xB9" => "\xF0\xA2\x86\x9F", + "\xF0\xAF\xA4\xBA" => "\xE7\x98\x90", + "\xF0\xAF\xA4\xBB" => "\xF0\xA4\xBE\xA1", + "\xF0\xAF\xA4\xBC" => "\xF0\xA4\xBE\xB8", + "\xF0\xAF\xA4\xBD" => "\xF0\xA5\x81\x84", + "\xF0\xAF\xA4\xBE" => "\xE3\xBF\xBC", + "\xF0\xAF\xA4\xBF" => "\xE4\x80\x88", + "\xF0\xAF\xA5\x80" => "\xE7\x9B\xB4", + "\xF0\xAF\xA5\x81" => "\xF0\xA5\x83\xB3", + "\xF0\xAF\xA5\x82" => "\xF0\xA5\x83\xB2", + "\xF0\xAF\xA5\x83" => "\xF0\xA5\x84\x99", + "\xF0\xAF\xA5\x84" => "\xF0\xA5\x84\xB3", + "\xF0\xAF\xA5\x85" => "\xE7\x9C\x9E", + "\xF0\xAF\xA5\x86" => "\xE7\x9C\x9F", + "\xF0\xAF\xA5\x87" => "\xE7\x9C\x9F", + "\xF0\xAF\xA5\x88" => "\xE7\x9D\x8A", + "\xF0\xAF\xA5\x89" => "\xE4\x80\xB9", + "\xF0\xAF\xA5\x8A" => "\xE7\x9E\x8B", + "\xF0\xAF\xA5\x8B" => "\xE4\x81\x86", + "\xF0\xAF\xA5\x8C" => "\xE4\x82\x96", + "\xF0\xAF\xA5\x8D" => "\xF0\xA5\x90\x9D", + "\xF0\xAF\xA5\x8E" => "\xE7\xA1\x8E", + "\xF0\xAF\xA5\x8F" => "\xE7\xA2\x8C", + "\xF0\xAF\xA5\x90" => "\xE7\xA3\x8C", + "\xF0\xAF\xA5\x91" => "\xE4\x83\xA3", + "\xF0\xAF\xA5\x92" => "\xF0\xA5\x98\xA6", + "\xF0\xAF\xA5\x93" => "\xE7\xA5\x96", + "\xF0\xAF\xA5\x94" => "\xF0\xA5\x9A\x9A", + "\xF0\xAF\xA5\x95" => "\xF0\xA5\x9B\x85", + "\xF0\xAF\xA5\x96" => "\xE7\xA6\x8F", + "\xF0\xAF\xA5\x97" => "\xE7\xA7\xAB", + "\xF0\xAF\xA5\x98" => "\xE4\x84\xAF", + "\xF0\xAF\xA5\x99" => "\xE7\xA9\x80", + "\xF0\xAF\xA5\x9A" => "\xE7\xA9\x8A", + "\xF0\xAF\xA5\x9B" => "\xE7\xA9\x8F", + "\xF0\xAF\xA5\x9C" => "\xF0\xA5\xA5\xBC", + "\xF0\xAF\xA5\x9D" => "\xF0\xA5\xAA\xA7", + "\xF0\xAF\xA5\x9E" => "\xF0\xA5\xAA\xA7", + "\xF0\xAF\xA5\xA0" => "\xE4\x88\x82", + "\xF0\xAF\xA5\xA1" => "\xF0\xA5\xAE\xAB", + "\xF0\xAF\xA5\xA2" => "\xE7\xAF\x86", + "\xF0\xAF\xA5\xA3" => "\xE7\xAF\x89", + "\xF0\xAF\xA5\xA4" => "\xE4\x88\xA7", + "\xF0\xAF\xA5\xA5" => "\xF0\xA5\xB2\x80", + "\xF0\xAF\xA5\xA6" => "\xE7\xB3\x92", + "\xF0\xAF\xA5\xA7" => "\xE4\x8A\xA0", + "\xF0\xAF\xA5\xA8" => "\xE7\xB3\xA8", + "\xF0\xAF\xA5\xA9" => "\xE7\xB3\xA3", + "\xF0\xAF\xA5\xAA" => "\xE7\xB4\x80", + "\xF0\xAF\xA5\xAB" => "\xF0\xA5\xBE\x86", + "\xF0\xAF\xA5\xAC" => "\xE7\xB5\xA3", + "\xF0\xAF\xA5\xAD" => "\xE4\x8C\x81", + "\xF0\xAF\xA5\xAE" => "\xE7\xB7\x87", + "\xF0\xAF\xA5\xAF" => "\xE7\xB8\x82", + "\xF0\xAF\xA5\xB0" => "\xE7\xB9\x85", + "\xF0\xAF\xA5\xB1" => "\xE4\x8C\xB4", + "\xF0\xAF\xA5\xB2" => "\xF0\xA6\x88\xA8", + "\xF0\xAF\xA5\xB3" => "\xF0\xA6\x89\x87", + "\xF0\xAF\xA5\xB4" => "\xE4\x8D\x99", + "\xF0\xAF\xA5\xB5" => "\xF0\xA6\x8B\x99", + "\xF0\xAF\xA5\xB6" => "\xE7\xBD\xBA", + "\xF0\xAF\xA5\xB7" => "\xF0\xA6\x8C\xBE", + "\xF0\xAF\xA5\xB8" => "\xE7\xBE\x95", + "\xF0\xAF\xA5\xB9" => "\xE7\xBF\xBA", + "\xF0\xAF\xA5\xBA" => "\xE8\x80\x85", + "\xF0\xAF\xA5\xBB" => "\xF0\xA6\x93\x9A", + "\xF0\xAF\xA5\xBC" => "\xF0\xA6\x94\xA3", + "\xF0\xAF\xA5\xBD" => "\xE8\x81\xA0", + "\xF0\xAF\xA5\xBE" => "\xF0\xA6\x96\xA8", + "\xF0\xAF\xA5\xBF" => "\xE8\x81\xB0", + "\xF0\xAF\xA6\x80" => "\xF0\xA3\x8D\x9F", + "\xF0\xAF\xA6\x81" => "\xE4\x8F\x95", + "\xF0\xAF\xA6\x82" => "\xE8\x82\xB2", + "\xF0\xAF\xA6\x83" => "\xE8\x84\x83", + "\xF0\xAF\xA6\x84" => "\xE4\x90\x8B", + "\xF0\xAF\xA6\x85" => "\xE8\x84\xBE", + "\xF0\xAF\xA6\x86" => "\xE5\xAA\xB5", + "\xF0\xAF\xA6\x87" => "\xF0\xA6\x9E\xA7", + "\xF0\xAF\xA6\x88" => "\xF0\xA6\x9E\xB5", + "\xF0\xAF\xA6\x89" => "\xF0\xA3\x8E\x93", + "\xF0\xAF\xA6\x8A" => "\xF0\xA3\x8E\x9C", + "\xF0\xAF\xA6\x8B" => "\xE8\x88\x81", + "\xF0\xAF\xA6\x8C" => "\xE8\x88\x84", + "\xF0\xAF\xA6\x8D" => "\xE8\xBE\x9E", + "\xF0\xAF\xA6\x8E" => "\xE4\x91\xAB", + "\xF0\xAF\xA6\x8F" => "\xE8\x8A\x91", + "\xF0\xAF\xA6\x90" => "\xE8\x8A\x8B", + "\xF0\xAF\xA6\x91" => "\xE8\x8A\x9D", + "\xF0\xAF\xA6\x92" => "\xE5\x8A\xB3", + "\xF0\xAF\xA6\x93" => "\xE8\x8A\xB1", + "\xF0\xAF\xA6\x94" => "\xE8\x8A\xB3", + "\xF0\xAF\xA6\x95" => "\xE8\x8A\xBD", + "\xF0\xAF\xA6\x96" => "\xE8\x8B\xA6", + "\xF0\xAF\xA6\x97" => "\xF0\xA6\xAC\xBC", + "\xF0\xAF\xA6\x98" => "\xE8\x8B\xA5", + "\xF0\xAF\xA6\x99" => "\xE8\x8C\x9D", + "\xF0\xAF\xA6\x9A" => "\xE8\x8D\xA3", + "\xF0\xAF\xA6\x9B" => "\xE8\x8E\xAD", + "\xF0\xAF\xA6\x9C" => "\xE8\x8C\xA3", + "\xF0\xAF\xA6\x9D" => "\xE8\x8E\xBD", + "\xF0\xAF\xA6\x9E" => "\xE8\x8F\xA7", + "\xF0\xAF\xA6\x9F" => "\xE8\x91\x97", + "\xF0\xAF\xA6\xA0" => "\xE8\x8D\x93", + "\xF0\xAF\xA6\xA1" => "\xE8\x8F\x8A", + "\xF0\xAF\xA6\xA2" => "\xE8\x8F\x8C", + "\xF0\xAF\xA6\xA3" => "\xE8\x8F\x9C", + "\xF0\xAF\xA6\xA4" => "\xF0\xA6\xB0\xB6", + "\xF0\xAF\xA6\xA5" => "\xF0\xA6\xB5\xAB", + "\xF0\xAF\xA6\xA6" => "\xF0\xA6\xB3\x95", + "\xF0\xAF\xA6\xA7" => "\xE4\x94\xAB", + "\xF0\xAF\xA6\xA8" => "\xE8\x93\xB1", + "\xF0\xAF\xA6\xA9" => "\xE8\x93\xB3", + "\xF0\xAF\xA6\xAA" => "\xE8\x94\x96", + "\xF0\xAF\xA6\xAB" => "\xF0\xA7\x8F\x8A", + "\xF0\xAF\xA6\xAC" => "\xE8\x95\xA4", + "\xF0\xAF\xA6\xAD" => "\xF0\xA6\xBC\xAC", + "\xF0\xAF\xA6\xAE" => "\xE4\x95\x9D", + "\xF0\xAF\xA6\xAF" => "\xE4\x95\xA1", + "\xF0\xAF\xA6\xB0" => "\xF0\xA6\xBE\xB1", + "\xF0\xAF\xA6\xB1" => "\xF0\xA7\x83\x92", + "\xF0\xAF\xA6\xB2" => "\xE4\x95\xAB", + "\xF0\xAF\xA6\xB3" => "\xE8\x99\x90", + "\xF0\xAF\xA6\xB4" => "\xE8\x99\x9C", + "\xF0\xAF\xA6\xB5" => "\xE8\x99\xA7", + "\xF0\xAF\xA6\xB6" => "\xE8\x99\xA9", + "\xF0\xAF\xA6\xB7" => "\xE8\x9A\xA9", + "\xF0\xAF\xA6\xB8" => "\xE8\x9A\x88", + "\xF0\xAF\xA6\xB9" => "\xE8\x9C\x8E", + "\xF0\xAF\xA6\xBA" => "\xE8\x9B\xA2", + "\xF0\xAF\xA6\xBB" => "\xE8\x9D\xB9", + "\xF0\xAF\xA6\xBC" => "\xE8\x9C\xA8", + "\xF0\xAF\xA6\xBD" => "\xE8\x9D\xAB", + "\xF0\xAF\xA6\xBE" => "\xE8\x9E\x86", + "\xF0\xAF\xA7\x80" => "\xE8\x9F\xA1", + "\xF0\xAF\xA7\x81" => "\xE8\xA0\x81", + "\xF0\xAF\xA7\x82" => "\xE4\x97\xB9", + "\xF0\xAF\xA7\x83" => "\xE8\xA1\xA0", + "\xF0\xAF\xA7\x84" => "\xE8\xA1\xA3", + "\xF0\xAF\xA7\x85" => "\xF0\xA7\x99\xA7", + "\xF0\xAF\xA7\x86" => "\xE8\xA3\x97", + "\xF0\xAF\xA7\x87" => "\xE8\xA3\x9E", + "\xF0\xAF\xA7\x88" => "\xE4\x98\xB5", + "\xF0\xAF\xA7\x89" => "\xE8\xA3\xBA", + "\xF0\xAF\xA7\x8A" => "\xE3\x92\xBB", + "\xF0\xAF\xA7\x8B" => "\xF0\xA7\xA2\xAE", + "\xF0\xAF\xA7\x8C" => "\xF0\xA7\xA5\xA6", + "\xF0\xAF\xA7\x8D" => "\xE4\x9A\xBE", + "\xF0\xAF\xA7\x8E" => "\xE4\x9B\x87", + "\xF0\xAF\xA7\x8F" => "\xE8\xAA\xA0", + "\xF0\xAF\xA7\x90" => "\xE8\xAB\xAD", + "\xF0\xAF\xA7\x91" => "\xE8\xAE\x8A", + "\xF0\xAF\xA7\x92" => "\xE8\xB1\x95", + "\xF0\xAF\xA7\x93" => "\xF0\xA7\xB2\xA8", + "\xF0\xAF\xA7\x94" => "\xE8\xB2\xAB", + "\xF0\xAF\xA7\x95" => "\xE8\xB3\x81", + "\xF0\xAF\xA7\x96" => "\xE8\xB4\x9B", + "\xF0\xAF\xA7\x97" => "\xE8\xB5\xB7", + "\xF0\xAF\xA7\x98" => "\xF0\xA7\xBC\xAF", + "\xF0\xAF\xA7\x99" => "\xF0\xA0\xA0\x84", + "\xF0\xAF\xA7\x9A" => "\xE8\xB7\x8B", + "\xF0\xAF\xA7\x9B" => "\xE8\xB6\xBC", + "\xF0\xAF\xA7\x9C" => "\xE8\xB7\xB0", + "\xF0\xAF\xA7\x9D" => "\xF0\xA0\xA3\x9E", + "\xF0\xAF\xA7\x9E" => "\xE8\xBB\x94", + "\xF0\xAF\xA7\x9F" => "\xE8\xBC\xB8", + "\xF0\xAF\xA7\xA0" => "\xF0\xA8\x97\x92", + "\xF0\xAF\xA7\xA1" => "\xF0\xA8\x97\xAD", + "\xF0\xAF\xA7\xA2" => "\xE9\x82\x94", + "\xF0\xAF\xA7\xA3" => "\xE9\x83\xB1", + "\xF0\xAF\xA7\xA4" => "\xE9\x84\x91", + "\xF0\xAF\xA7\xA5" => "\xF0\xA8\x9C\xAE", + "\xF0\xAF\xA7\xA6" => "\xE9\x84\x9B", + "\xF0\xAF\xA7\xA7" => "\xE9\x88\xB8", + "\xF0\xAF\xA7\xA8" => "\xE9\x8B\x97", + "\xF0\xAF\xA7\xA9" => "\xE9\x8B\x98", + "\xF0\xAF\xA7\xAA" => "\xE9\x89\xBC", + "\xF0\xAF\xA7\xAB" => "\xE9\x8F\xB9", + "\xF0\xAF\xA7\xAC" => "\xE9\x90\x95", + "\xF0\xAF\xA7\xAD" => "\xF0\xA8\xAF\xBA", + "\xF0\xAF\xA7\xAE" => "\xE9\x96\x8B", + "\xF0\xAF\xA7\xAF" => "\xE4\xA6\x95", + "\xF0\xAF\xA7\xB0" => "\xE9\x96\xB7", + "\xF0\xAF\xA7\xB1" => "\xF0\xA8\xB5\xB7", + "\xF0\xAF\xA7\xB2" => "\xE4\xA7\xA6", + "\xF0\xAF\xA7\xB3" => "\xE9\x9B\x83", + "\xF0\xAF\xA7\xB4" => "\xE5\xB6\xB2", + "\xF0\xAF\xA7\xB5" => "\xE9\x9C\xA3", + "\xF0\xAF\xA7\xB6" => "\xF0\xA9\x85\x85", + "\xF0\xAF\xA7\xB7" => "\xF0\xA9\x88\x9A", + "\xF0\xAF\xA7\xB8" => "\xE4\xA9\xAE", + "\xF0\xAF\xA7\xB9" => "\xE4\xA9\xB6", + "\xF0\xAF\xA7\xBA" => "\xE9\x9F\xA0", + "\xF0\xAF\xA7\xBB" => "\xF0\xA9\x90\x8A", + "\xF0\xAF\xA7\xBC" => "\xE4\xAA\xB2", + "\xF0\xAF\xA7\xBD" => "\xF0\xA9\x92\x96", + "\xF0\xAF\xA7\xBE" => "\xE9\xA0\x8B", + "\xF0\xAF\xA7\xBF" => "\xE9\xA0\x8B", + "\xF0\xAF\xA8\x80" => "\xE9\xA0\xA9", + "\xF0\xAF\xA8\x81" => "\xF0\xA9\x96\xB6", + "\xF0\xAF\xA8\x82" => "\xE9\xA3\xA2", + "\xF0\xAF\xA8\x83" => "\xE4\xAC\xB3", + "\xF0\xAF\xA8\x84" => "\xE9\xA4\xA9", + "\xF0\xAF\xA8\x85" => "\xE9\xA6\xA7", + "\xF0\xAF\xA8\x86" => "\xE9\xA7\x82", + "\xF0\xAF\xA8\x87" => "\xE9\xA7\xBE", + "\xF0\xAF\xA8\x88" => "\xE4\xAF\x8E", + "\xF0\xAF\xA8\x89" => "\xF0\xA9\xAC\xB0", + "\xF0\xAF\xA8\x8A" => "\xE9\xAC\x92", + "\xF0\xAF\xA8\x8B" => "\xE9\xB1\x80", + "\xF0\xAF\xA8\x8C" => "\xE9\xB3\xBD", + "\xF0\xAF\xA8\x8D" => "\xE4\xB3\x8E", + "\xF0\xAF\xA8\x8E" => "\xE4\xB3\xAD", + "\xF0\xAF\xA8\x8F" => "\xE9\xB5\xA7", + "\xF0\xAF\xA8\x90" => "\xF0\xAA\x83\x8E", + "\xF0\xAF\xA8\x91" => "\xE4\xB3\xB8", + "\xF0\xAF\xA8\x92" => "\xF0\xAA\x84\x85", + "\xF0\xAF\xA8\x93" => "\xF0\xAA\x88\x8E", + "\xF0\xAF\xA8\x94" => "\xF0\xAA\x8A\x91", + "\xF0\xAF\xA8\x95" => "\xE9\xBA\xBB", + "\xF0\xAF\xA8\x96" => "\xE4\xB5\x96", + "\xF0\xAF\xA8\x97" => "\xE9\xBB\xB9", + "\xF0\xAF\xA8\x98" => "\xE9\xBB\xBE", + "\xF0\xAF\xA8\x99" => "\xE9\xBC\x85", + "\xF0\xAF\xA8\x9A" => "\xE9\xBC\x8F", + "\xF0\xAF\xA8\x9B" => "\xE9\xBC\x96", + "\xF0\xAF\xA8\x9C" => "\xE9\xBC\xBB", + "\xF0\xAF\xA8\x9D" => "\xF0\xAA\x98\x80", + ); +} + +/** + * Helper function for idn_to_* polyfills. + * + * Developers: Do not update the data in this function manually. Instead, + * run "php -f other/update_unicode_data.php" on the command line. + * + * @return array "Deviation" character maps for IDNA processing. + */ +function idna_maps_deviation() +{ + return array( + "\xC3\x9F" => "\x73\x73", + "\xCF\x82" => "\xCF\x83", + "\xE2\x80\x8C" => "", + "\xE2\x80\x8D" => "", + ); +} + +/** + * Helper function for idn_to_* polyfills. + * + * Developers: Do not update the data in this function manually. Instead, + * run "php -f other/update_unicode_data.php" on the command line. + * + * @return array Non-STD3 character maps for IDNA processing. + */ +function idna_maps_not_std3() +{ + return array( + "\xC2\xA0" => "\x20", + "\xC2\xA8" => "\x20\xCC\x88", + "\xC2\xAF" => "\x20\xCC\x84", + "\xC2\xB4" => "\x20\xCC\x81", + "\xC2\xB8" => "\x20\xCC\xA7", + "\xCB\x98" => "\x20\xCC\x86", + "\xCB\x99" => "\x20\xCC\x87", + "\xCB\x9A" => "\x20\xCC\x8A", + "\xCB\x9B" => "\x20\xCC\xA8", + "\xCB\x9C" => "\x20\xCC\x83", + "\xCB\x9D" => "\x20\xCC\x8B", + "\xCD\xBA" => "\x20\xCE\xB9", + "\xCD\xBE" => "\x3B", + "\xCE\x84" => "\x20\xCC\x81", + "\xCE\x85" => "\x20\xCC\x88\xCC\x81", + "\xE1\xBE\xBD" => "\x20\xCC\x93", + "\xE1\xBE\xBF" => "\x20\xCC\x93", + "\xE1\xBF\x80" => "\x20\xCD\x82", + "\xE1\xBF\x81" => "\x20\xCC\x88\xCD\x82", + "\xE1\xBF\x8D" => "\x20\xCC\x93\xCC\x80", + "\xE1\xBF\x8E" => "\x20\xCC\x93\xCC\x81", + "\xE1\xBF\x8F" => "\x20\xCC\x93\xCD\x82", + "\xE1\xBF\x9D" => "\x20\xCC\x94\xCC\x80", + "\xE1\xBF\x9E" => "\x20\xCC\x94\xCC\x81", + "\xE1\xBF\x9F" => "\x20\xCC\x94\xCD\x82", + "\xE1\xBF\xAD" => "\x20\xCC\x88\xCC\x80", + "\xE1\xBF\xAE" => "\x20\xCC\x88\xCC\x81", + "\xE1\xBF\xAF" => "\x60", + "\xE1\xBF\xBD" => "\x20\xCC\x81", + "\xE1\xBF\xBE" => "\x20\xCC\x94", + "\xE2\x80\x80" => "\x20", + "\xE2\x80\x81" => "\x20", + "\xE2\x80\x82" => "\x20", + "\xE2\x80\x83" => "\x20", + "\xE2\x80\x84" => "\x20", + "\xE2\x80\x85" => "\x20", + "\xE2\x80\x86" => "\x20", + "\xE2\x80\x87" => "\x20", + "\xE2\x80\x88" => "\x20", + "\xE2\x80\x89" => "\x20", + "\xE2\x80\x8A" => "\x20", + "\xE2\x80\x97" => "\x20\xCC\xB3", + "\xE2\x80\xAF" => "\x20", + "\xE2\x80\xBC" => "\x21\x21", + "\xE2\x80\xBE" => "\x20\xCC\x85", + "\xE2\x81\x87" => "\x3F\x3F", + "\xE2\x81\x88" => "\x3F\x21", + "\xE2\x81\x89" => "\x21\x3F", + "\xE2\x81\x9F" => "\x20", + "\xE2\x81\xBA" => "\x2B", + "\xE2\x81\xBC" => "\x3D", + "\xE2\x81\xBD" => "\x28", + "\xE2\x81\xBE" => "\x29", + "\xE2\x82\x8A" => "\x2B", + "\xE2\x82\x8C" => "\x3D", + "\xE2\x82\x8D" => "\x28", + "\xE2\x82\x8E" => "\x29", + "\xE2\x84\x80" => "\x61\x2F\x63", + "\xE2\x84\x81" => "\x61\x2F\x73", + "\xE2\x84\x85" => "\x63\x2F\x6F", + "\xE2\x84\x86" => "\x63\x2F\x75", + "\xE2\x91\xB4" => "\x28\x31\x29", + "\xE2\x91\xB5" => "\x28\x32\x29", + "\xE2\x91\xB6" => "\x28\x33\x29", + "\xE2\x91\xB7" => "\x28\x34\x29", + "\xE2\x91\xB8" => "\x28\x35\x29", + "\xE2\x91\xB9" => "\x28\x36\x29", + "\xE2\x91\xBA" => "\x28\x37\x29", + "\xE2\x91\xBB" => "\x28\x38\x29", + "\xE2\x91\xBC" => "\x28\x39\x29", + "\xE2\x91\xBD" => "\x28\x31\x30\x29", + "\xE2\x91\xBE" => "\x28\x31\x31\x29", + "\xE2\x91\xBF" => "\x28\x31\x32\x29", + "\xE2\x92\x80" => "\x28\x31\x33\x29", + "\xE2\x92\x81" => "\x28\x31\x34\x29", + "\xE2\x92\x82" => "\x28\x31\x35\x29", + "\xE2\x92\x83" => "\x28\x31\x36\x29", + "\xE2\x92\x84" => "\x28\x31\x37\x29", + "\xE2\x92\x85" => "\x28\x31\x38\x29", + "\xE2\x92\x86" => "\x28\x31\x39\x29", + "\xE2\x92\x87" => "\x28\x32\x30\x29", + "\xE2\x92\x9C" => "\x28\x61\x29", + "\xE2\x92\x9D" => "\x28\x62\x29", + "\xE2\x92\x9E" => "\x28\x63\x29", + "\xE2\x92\x9F" => "\x28\x64\x29", + "\xE2\x92\xA0" => "\x28\x65\x29", + "\xE2\x92\xA1" => "\x28\x66\x29", + "\xE2\x92\xA2" => "\x28\x67\x29", + "\xE2\x92\xA3" => "\x28\x68\x29", + "\xE2\x92\xA4" => "\x28\x69\x29", + "\xE2\x92\xA5" => "\x28\x6A\x29", + "\xE2\x92\xA6" => "\x28\x6B\x29", + "\xE2\x92\xA7" => "\x28\x6C\x29", + "\xE2\x92\xA8" => "\x28\x6D\x29", + "\xE2\x92\xA9" => "\x28\x6E\x29", + "\xE2\x92\xAA" => "\x28\x6F\x29", + "\xE2\x92\xAB" => "\x28\x70\x29", + "\xE2\x92\xAC" => "\x28\x71\x29", + "\xE2\x92\xAD" => "\x28\x72\x29", + "\xE2\x92\xAE" => "\x28\x73\x29", + "\xE2\x92\xAF" => "\x28\x74\x29", + "\xE2\x92\xB0" => "\x28\x75\x29", + "\xE2\x92\xB1" => "\x28\x76\x29", + "\xE2\x92\xB2" => "\x28\x77\x29", + "\xE2\x92\xB3" => "\x28\x78\x29", + "\xE2\x92\xB4" => "\x28\x79\x29", + "\xE2\x92\xB5" => "\x28\x7A\x29", + "\xE2\xA9\xB4" => "\x3A\x3A\x3D", + "\xE2\xA9\xB5" => "\x3D\x3D", + "\xE2\xA9\xB6" => "\x3D\x3D\x3D", + "\xE3\x80\x80" => "\x20", + "\xE3\x82\x9B" => "\x20\xE3\x82\x99", + "\xE3\x82\x9C" => "\x20\xE3\x82\x9A", + "\xE3\x88\x80" => "\x28\xE1\x84\x80\x29", + "\xE3\x88\x81" => "\x28\xE1\x84\x82\x29", + "\xE3\x88\x82" => "\x28\xE1\x84\x83\x29", + "\xE3\x88\x83" => "\x28\xE1\x84\x85\x29", + "\xE3\x88\x84" => "\x28\xE1\x84\x86\x29", + "\xE3\x88\x85" => "\x28\xE1\x84\x87\x29", + "\xE3\x88\x86" => "\x28\xE1\x84\x89\x29", + "\xE3\x88\x87" => "\x28\xE1\x84\x8B\x29", + "\xE3\x88\x88" => "\x28\xE1\x84\x8C\x29", + "\xE3\x88\x89" => "\x28\xE1\x84\x8E\x29", + "\xE3\x88\x8A" => "\x28\xE1\x84\x8F\x29", + "\xE3\x88\x8B" => "\x28\xE1\x84\x90\x29", + "\xE3\x88\x8C" => "\x28\xE1\x84\x91\x29", + "\xE3\x88\x8D" => "\x28\xE1\x84\x92\x29", + "\xE3\x88\x8E" => "\x28\xEA\xB0\x80\x29", + "\xE3\x88\x8F" => "\x28\xEB\x82\x98\x29", + "\xE3\x88\x90" => "\x28\xEB\x8B\xA4\x29", + "\xE3\x88\x91" => "\x28\xEB\x9D\xBC\x29", + "\xE3\x88\x92" => "\x28\xEB\xA7\x88\x29", + "\xE3\x88\x93" => "\x28\xEB\xB0\x94\x29", + "\xE3\x88\x94" => "\x28\xEC\x82\xAC\x29", + "\xE3\x88\x95" => "\x28\xEC\x95\x84\x29", + "\xE3\x88\x96" => "\x28\xEC\x9E\x90\x29", + "\xE3\x88\x97" => "\x28\xEC\xB0\xA8\x29", + "\xE3\x88\x98" => "\x28\xEC\xB9\xB4\x29", + "\xE3\x88\x99" => "\x28\xED\x83\x80\x29", + "\xE3\x88\x9A" => "\x28\xED\x8C\x8C\x29", + "\xE3\x88\x9B" => "\x28\xED\x95\x98\x29", + "\xE3\x88\x9C" => "\x28\xEC\xA3\xBC\x29", + "\xE3\x88\x9D" => "\x28\xEC\x98\xA4\xEC\xA0\x84\x29", + "\xE3\x88\x9E" => "\x28\xEC\x98\xA4\xED\x9B\x84\x29", + "\xE3\x88\xA0" => "\x28\xE4\xB8\x80\x29", + "\xE3\x88\xA1" => "\x28\xE4\xBA\x8C\x29", + "\xE3\x88\xA2" => "\x28\xE4\xB8\x89\x29", + "\xE3\x88\xA3" => "\x28\xE5\x9B\x9B\x29", + "\xE3\x88\xA4" => "\x28\xE4\xBA\x94\x29", + "\xE3\x88\xA5" => "\x28\xE5\x85\xAD\x29", + "\xE3\x88\xA6" => "\x28\xE4\xB8\x83\x29", + "\xE3\x88\xA7" => "\x28\xE5\x85\xAB\x29", + "\xE3\x88\xA8" => "\x28\xE4\xB9\x9D\x29", + "\xE3\x88\xA9" => "\x28\xE5\x8D\x81\x29", + "\xE3\x88\xAA" => "\x28\xE6\x9C\x88\x29", + "\xE3\x88\xAB" => "\x28\xE7\x81\xAB\x29", + "\xE3\x88\xAC" => "\x28\xE6\xB0\xB4\x29", + "\xE3\x88\xAD" => "\x28\xE6\x9C\xA8\x29", + "\xE3\x88\xAE" => "\x28\xE9\x87\x91\x29", + "\xE3\x88\xAF" => "\x28\xE5\x9C\x9F\x29", + "\xE3\x88\xB0" => "\x28\xE6\x97\xA5\x29", + "\xE3\x88\xB1" => "\x28\xE6\xA0\xAA\x29", + "\xE3\x88\xB2" => "\x28\xE6\x9C\x89\x29", + "\xE3\x88\xB3" => "\x28\xE7\xA4\xBE\x29", + "\xE3\x88\xB4" => "\x28\xE5\x90\x8D\x29", + "\xE3\x88\xB5" => "\x28\xE7\x89\xB9\x29", + "\xE3\x88\xB6" => "\x28\xE8\xB2\xA1\x29", + "\xE3\x88\xB7" => "\x28\xE7\xA5\x9D\x29", + "\xE3\x88\xB8" => "\x28\xE5\x8A\xB4\x29", + "\xE3\x88\xB9" => "\x28\xE4\xBB\xA3\x29", + "\xE3\x88\xBA" => "\x28\xE5\x91\xBC\x29", + "\xE3\x88\xBB" => "\x28\xE5\xAD\xA6\x29", + "\xE3\x88\xBC" => "\x28\xE7\x9B\xA3\x29", + "\xE3\x88\xBD" => "\x28\xE4\xBC\x81\x29", + "\xE3\x88\xBE" => "\x28\xE8\xB3\x87\x29", + "\xE3\x88\xBF" => "\x28\xE5\x8D\x94\x29", + "\xE3\x89\x80" => "\x28\xE7\xA5\xAD\x29", + "\xE3\x89\x81" => "\x28\xE4\xBC\x91\x29", + "\xE3\x89\x82" => "\x28\xE8\x87\xAA\x29", + "\xE3\x89\x83" => "\x28\xE8\x87\xB3\x29", + "\xEF\xAC\xA9" => "\x2B", + "\xEF\xB1\x9E" => "\x20\xD9\x8C\xD9\x91", + "\xEF\xB1\x9F" => "\x20\xD9\x8D\xD9\x91", + "\xEF\xB1\xA0" => "\x20\xD9\x8E\xD9\x91", + "\xEF\xB1\xA1" => "\x20\xD9\x8F\xD9\x91", + "\xEF\xB1\xA2" => "\x20\xD9\x90\xD9\x91", + "\xEF\xB1\xA3" => "\x20\xD9\x91\xD9\xB0", + "\xEF\xB7\xBA" => "\xD8\xB5\xD9\x84\xD9\x89\x20\xD8\xA7\xD9\x84\xD9\x84\xD9\x87\x20\xD8\xB9\xD9\x84\xD9\x8A\xD9\x87\x20\xD9\x88\xD8\xB3\xD9\x84\xD9\x85", + "\xEF\xB7\xBB" => "\xD8\xAC\xD9\x84\x20\xD8\xAC\xD9\x84\xD8\xA7\xD9\x84\xD9\x87", + "\xEF\xB8\x90" => "\x2C", + "\xEF\xB8\x93" => "\x3A", + "\xEF\xB8\x94" => "\x3B", + "\xEF\xB8\x95" => "\x21", + "\xEF\xB8\x96" => "\x3F", + "\xEF\xB8\xB3" => "\x5F", + "\xEF\xB8\xB4" => "\x5F", + "\xEF\xB8\xB5" => "\x28", + "\xEF\xB8\xB6" => "\x29", + "\xEF\xB8\xB7" => "\x7B", + "\xEF\xB8\xB8" => "\x7D", + "\xEF\xB9\x87" => "\x5B", + "\xEF\xB9\x88" => "\x5D", + "\xEF\xB9\x89" => "\x20\xCC\x85", + "\xEF\xB9\x8A" => "\x20\xCC\x85", + "\xEF\xB9\x8B" => "\x20\xCC\x85", + "\xEF\xB9\x8C" => "\x20\xCC\x85", + "\xEF\xB9\x8D" => "\x5F", + "\xEF\xB9\x8E" => "\x5F", + "\xEF\xB9\x8F" => "\x5F", + "\xEF\xB9\x90" => "\x2C", + "\xEF\xB9\x94" => "\x3B", + "\xEF\xB9\x95" => "\x3A", + "\xEF\xB9\x96" => "\x3F", + "\xEF\xB9\x97" => "\x21", + "\xEF\xB9\x99" => "\x28", + "\xEF\xB9\x9A" => "\x29", + "\xEF\xB9\x9B" => "\x7B", + "\xEF\xB9\x9C" => "\x7D", + "\xEF\xB9\x9F" => "\x23", + "\xEF\xB9\xA0" => "\x26", + "\xEF\xB9\xA1" => "\x2A", + "\xEF\xB9\xA2" => "\x2B", + "\xEF\xB9\xA4" => "\x3C", + "\xEF\xB9\xA5" => "\x3E", + "\xEF\xB9\xA6" => "\x3D", + "\xEF\xB9\xA8" => "\x5C", + "\xEF\xB9\xA9" => "\x24", + "\xEF\xB9\xAA" => "\x25", + "\xEF\xB9\xAB" => "\x40", + "\xEF\xB9\xB0" => "\x20\xD9\x8B", + "\xEF\xB9\xB2" => "\x20\xD9\x8C", + "\xEF\xB9\xB4" => "\x20\xD9\x8D", + "\xEF\xB9\xB6" => "\x20\xD9\x8E", + "\xEF\xB9\xB8" => "\x20\xD9\x8F", + "\xEF\xB9\xBA" => "\x20\xD9\x90", + "\xEF\xB9\xBC" => "\x20\xD9\x91", + "\xEF\xB9\xBE" => "\x20\xD9\x92", + "\xEF\xBC\x81" => "\x21", + "\xEF\xBC\x82" => "\x22", + "\xEF\xBC\x83" => "\x23", + "\xEF\xBC\x84" => "\x24", + "\xEF\xBC\x85" => "\x25", + "\xEF\xBC\x86" => "\x26", + "\xEF\xBC\x87" => "\x27", + "\xEF\xBC\x88" => "\x28", + "\xEF\xBC\x89" => "\x29", + "\xEF\xBC\x8A" => "\x2A", + "\xEF\xBC\x8B" => "\x2B", + "\xEF\xBC\x8C" => "\x2C", + "\xEF\xBC\x8F" => "\x2F", + "\xEF\xBC\x9A" => "\x3A", + "\xEF\xBC\x9B" => "\x3B", + "\xEF\xBC\x9C" => "\x3C", + "\xEF\xBC\x9D" => "\x3D", + "\xEF\xBC\x9E" => "\x3E", + "\xEF\xBC\x9F" => "\x3F", + "\xEF\xBC\xA0" => "\x40", + "\xEF\xBC\xBB" => "\x5B", + "\xEF\xBC\xBC" => "\x5C", + "\xEF\xBC\xBD" => "\x5D", + "\xEF\xBC\xBE" => "\x5E", + "\xEF\xBC\xBF" => "\x5F", + "\xEF\xBD\x80" => "\x60", + "\xEF\xBD\x9B" => "\x7B", + "\xEF\xBD\x9C" => "\x7C", + "\xEF\xBD\x9D" => "\x7D", + "\xEF\xBD\x9E" => "\x7E", + "\xEF\xBF\xA3" => "\x20\xCC\x84", + "\xF0\x9F\x84\x81" => "\x30\x2C", + "\xF0\x9F\x84\x82" => "\x31\x2C", + "\xF0\x9F\x84\x83" => "\x32\x2C", + "\xF0\x9F\x84\x84" => "\x33\x2C", + "\xF0\x9F\x84\x85" => "\x34\x2C", + "\xF0\x9F\x84\x86" => "\x35\x2C", + "\xF0\x9F\x84\x87" => "\x36\x2C", + "\xF0\x9F\x84\x88" => "\x37\x2C", + "\xF0\x9F\x84\x89" => "\x38\x2C", + "\xF0\x9F\x84\x8A" => "\x39\x2C", + "\xF0\x9F\x84\x90" => "\x28\x61\x29", + "\xF0\x9F\x84\x91" => "\x28\x62\x29", + "\xF0\x9F\x84\x92" => "\x28\x63\x29", + "\xF0\x9F\x84\x93" => "\x28\x64\x29", + "\xF0\x9F\x84\x94" => "\x28\x65\x29", + "\xF0\x9F\x84\x95" => "\x28\x66\x29", + "\xF0\x9F\x84\x96" => "\x28\x67\x29", + "\xF0\x9F\x84\x97" => "\x28\x68\x29", + "\xF0\x9F\x84\x98" => "\x28\x69\x29", + "\xF0\x9F\x84\x99" => "\x28\x6A\x29", + "\xF0\x9F\x84\x9A" => "\x28\x6B\x29", + "\xF0\x9F\x84\x9B" => "\x28\x6C\x29", + "\xF0\x9F\x84\x9C" => "\x28\x6D\x29", + "\xF0\x9F\x84\x9D" => "\x28\x6E\x29", + "\xF0\x9F\x84\x9E" => "\x28\x6F\x29", + "\xF0\x9F\x84\x9F" => "\x28\x70\x29", + "\xF0\x9F\x84\xA0" => "\x28\x71\x29", + "\xF0\x9F\x84\xA1" => "\x28\x72\x29", + "\xF0\x9F\x84\xA2" => "\x28\x73\x29", + "\xF0\x9F\x84\xA3" => "\x28\x74\x29", + "\xF0\x9F\x84\xA4" => "\x28\x75\x29", + "\xF0\x9F\x84\xA5" => "\x28\x76\x29", + "\xF0\x9F\x84\xA6" => "\x28\x77\x29", + "\xF0\x9F\x84\xA7" => "\x28\x78\x29", + "\xF0\x9F\x84\xA8" => "\x28\x79\x29", + "\xF0\x9F\x84\xA9" => "\x28\x7A\x29", + ); +} + +/** + * Helper function for idn_to_* polyfills. + * + * Developers: Do not update the data in this function manually. Instead, + * run "php -f other/update_unicode_data.php" on the command line. + * + * @return array Regular expressions useful for IDNA processing. + */ +function idna_regex() +{ + return array( + 'disallowed_std3' => + '\x{0}-\x{2C}' . + '\x{2F}' . + '\x{3A}-\x{40}' . + '\x{5B}-\x{60}' . + '\x{7B}-\x{7F}' . + '\x{A0}' . + '\x{A8}' . + '\x{AF}' . + '\x{B4}' . + '\x{B8}' . + '\x{2D8}' . + '\x{2D9}' . + '\x{2DA}' . + '\x{2DB}' . + '\x{2DC}' . + '\x{2DD}' . + '\x{37A}' . + '\x{37E}' . + '\x{384}' . + '\x{385}' . + '\x{1FBD}' . + '\x{1FBF}' . + '\x{1FC0}' . + '\x{1FC1}' . + '\x{1FCD}' . + '\x{1FCE}' . + '\x{1FCF}' . + '\x{1FDD}' . + '\x{1FDE}' . + '\x{1FDF}' . + '\x{1FED}' . + '\x{1FEE}' . + '\x{1FEF}' . + '\x{1FFD}' . + '\x{1FFE}' . + '\x{2000}-\x{200A}' . + '\x{2017}' . + '\x{202F}' . + '\x{203C}' . + '\x{203E}' . + '\x{2047}' . + '\x{2048}' . + '\x{2049}' . + '\x{205F}' . + '\x{207A}' . + '\x{207C}' . + '\x{207D}' . + '\x{207E}' . + '\x{208A}' . + '\x{208C}' . + '\x{208D}' . + '\x{208E}' . + '\x{2100}' . + '\x{2101}' . + '\x{2105}' . + '\x{2106}' . + '\x{2260}' . + '\x{226E}-\x{226F}' . + '\x{2474}' . + '\x{2475}' . + '\x{2476}' . + '\x{2477}' . + '\x{2478}' . + '\x{2479}' . + '\x{247A}' . + '\x{247B}' . + '\x{247C}' . + '\x{247D}' . + '\x{247E}' . + '\x{247F}' . + '\x{2480}' . + '\x{2481}' . + '\x{2482}' . + '\x{2483}' . + '\x{2484}' . + '\x{2485}' . + '\x{2486}' . + '\x{2487}' . + '\x{249C}' . + '\x{249D}' . + '\x{249E}' . + '\x{249F}' . + '\x{24A0}' . + '\x{24A1}' . + '\x{24A2}' . + '\x{24A3}' . + '\x{24A4}' . + '\x{24A5}' . + '\x{24A6}' . + '\x{24A7}' . + '\x{24A8}' . + '\x{24A9}' . + '\x{24AA}' . + '\x{24AB}' . + '\x{24AC}' . + '\x{24AD}' . + '\x{24AE}' . + '\x{24AF}' . + '\x{24B0}' . + '\x{24B1}' . + '\x{24B2}' . + '\x{24B3}' . + '\x{24B4}' . + '\x{24B5}' . + '\x{2A74}' . + '\x{2A75}' . + '\x{2A76}' . + '\x{3000}' . + '\x{309B}' . + '\x{309C}' . + '\x{3200}' . + '\x{3201}' . + '\x{3202}' . + '\x{3203}' . + '\x{3204}' . + '\x{3205}' . + '\x{3206}' . + '\x{3207}' . + '\x{3208}' . + '\x{3209}' . + '\x{320A}' . + '\x{320B}' . + '\x{320C}' . + '\x{320D}' . + '\x{320E}' . + '\x{320F}' . + '\x{3210}' . + '\x{3211}' . + '\x{3212}' . + '\x{3213}' . + '\x{3214}' . + '\x{3215}' . + '\x{3216}' . + '\x{3217}' . + '\x{3218}' . + '\x{3219}' . + '\x{321A}' . + '\x{321B}' . + '\x{321C}' . + '\x{321D}' . + '\x{321E}' . + '\x{3220}' . + '\x{3221}' . + '\x{3222}' . + '\x{3223}' . + '\x{3224}' . + '\x{3225}' . + '\x{3226}' . + '\x{3227}' . + '\x{3228}' . + '\x{3229}' . + '\x{322A}' . + '\x{322B}' . + '\x{322C}' . + '\x{322D}' . + '\x{322E}' . + '\x{322F}' . + '\x{3230}' . + '\x{3231}' . + '\x{3232}' . + '\x{3233}' . + '\x{3234}' . + '\x{3235}' . + '\x{3236}' . + '\x{3237}' . + '\x{3238}' . + '\x{3239}' . + '\x{323A}' . + '\x{323B}' . + '\x{323C}' . + '\x{323D}' . + '\x{323E}' . + '\x{323F}' . + '\x{3240}' . + '\x{3241}' . + '\x{3242}' . + '\x{3243}' . + '\x{FB29}' . + '\x{FC5E}' . + '\x{FC5F}' . + '\x{FC60}' . + '\x{FC61}' . + '\x{FC62}' . + '\x{FC63}' . + '\x{FDFA}' . + '\x{FDFB}' . + '\x{FE10}' . + '\x{FE13}' . + '\x{FE14}' . + '\x{FE15}' . + '\x{FE16}' . + '\x{FE33}-\x{FE34}' . + '\x{FE35}' . + '\x{FE36}' . + '\x{FE37}' . + '\x{FE38}' . + '\x{FE47}' . + '\x{FE48}' . + '\x{FE49}-\x{FE4C}' . + '\x{FE4D}-\x{FE4F}' . + '\x{FE50}' . + '\x{FE54}' . + '\x{FE55}' . + '\x{FE56}' . + '\x{FE57}' . + '\x{FE59}' . + '\x{FE5A}' . + '\x{FE5B}' . + '\x{FE5C}' . + '\x{FE5F}' . + '\x{FE60}' . + '\x{FE61}' . + '\x{FE62}' . + '\x{FE64}' . + '\x{FE65}' . + '\x{FE66}' . + '\x{FE68}' . + '\x{FE69}' . + '\x{FE6A}' . + '\x{FE6B}' . + '\x{FE70}' . + '\x{FE72}' . + '\x{FE74}' . + '\x{FE76}' . + '\x{FE78}' . + '\x{FE7A}' . + '\x{FE7C}' . + '\x{FE7E}' . + '\x{FF01}' . + '\x{FF02}' . + '\x{FF03}' . + '\x{FF04}' . + '\x{FF05}' . + '\x{FF06}' . + '\x{FF07}' . + '\x{FF08}' . + '\x{FF09}' . + '\x{FF0A}' . + '\x{FF0B}' . + '\x{FF0C}' . + '\x{FF0F}' . + '\x{FF1A}' . + '\x{FF1B}' . + '\x{FF1C}' . + '\x{FF1D}' . + '\x{FF1E}' . + '\x{FF1F}' . + '\x{FF20}' . + '\x{FF3B}' . + '\x{FF3C}' . + '\x{FF3D}' . + '\x{FF3E}' . + '\x{FF3F}' . + '\x{FF40}' . + '\x{FF5B}' . + '\x{FF5C}' . + '\x{FF5D}' . + '\x{FF5E}' . + '\x{FFE3}' . + '\x{1F101}' . + '\x{1F102}' . + '\x{1F103}' . + '\x{1F104}' . + '\x{1F105}' . + '\x{1F106}' . + '\x{1F107}' . + '\x{1F108}' . + '\x{1F109}' . + '\x{1F10A}' . + '\x{1F110}' . + '\x{1F111}' . + '\x{1F112}' . + '\x{1F113}' . + '\x{1F114}' . + '\x{1F115}' . + '\x{1F116}' . + '\x{1F117}' . + '\x{1F118}' . + '\x{1F119}' . + '\x{1F11A}' . + '\x{1F11B}' . + '\x{1F11C}' . + '\x{1F11D}' . + '\x{1F11E}' . + '\x{1F11F}' . + '\x{1F120}' . + '\x{1F121}' . + '\x{1F122}' . + '\x{1F123}' . + '\x{1F124}' . + '\x{1F125}' . + '\x{1F126}' . + '\x{1F127}' . + '\x{1F128}' . + '\x{1F129}', + 'disallowed' => + '\x{80}-\x{9F}' . + '\x{378}-\x{379}' . + '\x{380}-\x{383}' . + '\x{38B}' . + '\x{38D}' . + '\x{3A2}' . + '\x{4C0}' . + '\x{530}' . + '\x{557}-\x{558}' . + '\x{58B}-\x{58C}' . + '\x{590}' . + '\x{5C8}-\x{5CF}' . + '\x{5EB}-\x{5EE}' . + '\x{5F5}-\x{5FF}' . + '\x{600}-\x{603}' . + '\x{604}' . + '\x{605}' . + '\x{61C}' . + '\x{6DD}' . + '\x{70E}' . + '\x{70F}' . + '\x{74B}-\x{74C}' . + '\x{7B2}-\x{7BF}' . + '\x{7FB}-\x{7FC}' . + '\x{82E}-\x{82F}' . + '\x{83F}' . + '\x{85C}-\x{85D}' . + '\x{85F}' . + '\x{86B}-\x{86F}' . + '\x{88F}' . + '\x{890}-\x{891}' . + '\x{892}-\x{897}' . + '\x{8E2}' . + '\x{984}' . + '\x{98D}-\x{98E}' . + '\x{991}-\x{992}' . + '\x{9A9}' . + '\x{9B1}' . + '\x{9B3}-\x{9B5}' . + '\x{9BA}-\x{9BB}' . + '\x{9C5}-\x{9C6}' . + '\x{9C9}-\x{9CA}' . + '\x{9CF}-\x{9D6}' . + '\x{9D8}-\x{9DB}' . + '\x{9DE}' . + '\x{9E4}-\x{9E5}' . + '\x{9FF}-\x{A00}' . + '\x{A04}' . + '\x{A0B}-\x{A0E}' . + '\x{A11}-\x{A12}' . + '\x{A29}' . + '\x{A31}' . + '\x{A34}' . + '\x{A37}' . + '\x{A3A}-\x{A3B}' . + '\x{A3D}' . + '\x{A43}-\x{A46}' . + '\x{A49}-\x{A4A}' . + '\x{A4E}-\x{A50}' . + '\x{A52}-\x{A58}' . + '\x{A5D}' . + '\x{A5F}-\x{A65}' . + '\x{A77}-\x{A80}' . + '\x{A84}' . + '\x{A8E}' . + '\x{A92}' . + '\x{AA9}' . + '\x{AB1}' . + '\x{AB4}' . + '\x{ABA}-\x{ABB}' . + '\x{AC6}' . + '\x{ACA}' . + '\x{ACE}-\x{ACF}' . + '\x{AD1}-\x{ADF}' . + '\x{AE4}-\x{AE5}' . + '\x{AF2}-\x{AF8}' . + '\x{B00}' . + '\x{B04}' . + '\x{B0D}-\x{B0E}' . + '\x{B11}-\x{B12}' . + '\x{B29}' . + '\x{B31}' . + '\x{B34}' . + '\x{B3A}-\x{B3B}' . + '\x{B45}-\x{B46}' . + '\x{B49}-\x{B4A}' . + '\x{B4E}-\x{B54}' . + '\x{B58}-\x{B5B}' . + '\x{B5E}' . + '\x{B64}-\x{B65}' . + '\x{B78}-\x{B81}' . + '\x{B84}' . + '\x{B8B}-\x{B8D}' . + '\x{B91}' . + '\x{B96}-\x{B98}' . + '\x{B9B}' . + '\x{B9D}' . + '\x{BA0}-\x{BA2}' . + '\x{BA5}-\x{BA7}' . + '\x{BAB}-\x{BAD}' . + '\x{BBA}-\x{BBD}' . + '\x{BC3}-\x{BC5}' . + '\x{BC9}' . + '\x{BCE}-\x{BCF}' . + '\x{BD1}-\x{BD6}' . + '\x{BD8}-\x{BE5}' . + '\x{BFB}-\x{BFF}' . + '\x{C0D}' . + '\x{C11}' . + '\x{C29}' . + '\x{C3A}-\x{C3B}' . + '\x{C45}' . + '\x{C49}' . + '\x{C4E}-\x{C54}' . + '\x{C57}' . + '\x{C5B}-\x{C5C}' . + '\x{C5E}-\x{C5F}' . + '\x{C64}-\x{C65}' . + '\x{C70}-\x{C76}' . + '\x{C8D}' . + '\x{C91}' . + '\x{CA9}' . + '\x{CB4}' . + '\x{CBA}-\x{CBB}' . + '\x{CC5}' . + '\x{CC9}' . + '\x{CCE}-\x{CD4}' . + '\x{CD7}-\x{CDC}' . + '\x{CDF}' . + '\x{CE4}-\x{CE5}' . + '\x{CF0}' . + '\x{CF4}-\x{CFF}' . + '\x{D0D}' . + '\x{D11}' . + '\x{D45}' . + '\x{D49}' . + '\x{D50}-\x{D53}' . + '\x{D64}-\x{D65}' . + '\x{D80}' . + '\x{D84}' . + '\x{D97}-\x{D99}' . + '\x{DB2}' . + '\x{DBC}' . + '\x{DBE}-\x{DBF}' . + '\x{DC7}-\x{DC9}' . + '\x{DCB}-\x{DCE}' . + '\x{DD5}' . + '\x{DD7}' . + '\x{DE0}-\x{DE5}' . + '\x{DF0}-\x{DF1}' . + '\x{DF5}-\x{E00}' . + '\x{E3B}-\x{E3E}' . + '\x{E5C}-\x{E80}' . + '\x{E83}' . + '\x{E85}' . + '\x{E8B}' . + '\x{EA4}' . + '\x{EA6}' . + '\x{EBE}-\x{EBF}' . + '\x{EC5}' . + '\x{EC7}' . + '\x{ECF}' . + '\x{EDA}-\x{EDB}' . + '\x{EE0}-\x{EFF}' . + '\x{F48}' . + '\x{F6D}-\x{F70}' . + '\x{F98}' . + '\x{FBD}' . + '\x{FCD}' . + '\x{FDB}-\x{FFF}' . + '\x{10A0}-\x{10C5}' . + '\x{10C6}' . + '\x{10C8}-\x{10CC}' . + '\x{10CE}-\x{10CF}' . + '\x{115F}-\x{1160}' . + '\x{1249}' . + '\x{124E}-\x{124F}' . + '\x{1257}' . + '\x{1259}' . + '\x{125E}-\x{125F}' . + '\x{1289}' . + '\x{128E}-\x{128F}' . + '\x{12B1}' . + '\x{12B6}-\x{12B7}' . + '\x{12BF}' . + '\x{12C1}' . + '\x{12C6}-\x{12C7}' . + '\x{12D7}' . + '\x{1311}' . + '\x{1316}-\x{1317}' . + '\x{135B}-\x{135C}' . + '\x{137D}-\x{137F}' . + '\x{139A}-\x{139F}' . + '\x{13F6}-\x{13F7}' . + '\x{13FE}-\x{13FF}' . + '\x{1680}' . + '\x{169D}-\x{169F}' . + '\x{16F9}-\x{16FF}' . + '\x{1716}-\x{171E}' . + '\x{1737}-\x{173F}' . + '\x{1754}-\x{175F}' . + '\x{176D}' . + '\x{1771}' . + '\x{1774}-\x{177F}' . + '\x{17B4}-\x{17B5}' . + '\x{17DE}-\x{17DF}' . + '\x{17EA}-\x{17EF}' . + '\x{17FA}-\x{17FF}' . + '\x{1806}' . + '\x{180E}' . + '\x{181A}-\x{181F}' . + '\x{1879}-\x{187F}' . + '\x{18AB}-\x{18AF}' . + '\x{18F6}-\x{18FF}' . + '\x{191F}' . + '\x{192C}-\x{192F}' . + '\x{193C}-\x{193F}' . + '\x{1941}-\x{1943}' . + '\x{196E}-\x{196F}' . + '\x{1975}-\x{197F}' . + '\x{19AC}-\x{19AF}' . + '\x{19CA}-\x{19CF}' . + '\x{19DB}-\x{19DD}' . + '\x{1A1C}-\x{1A1D}' . + '\x{1A5F}' . + '\x{1A7D}-\x{1A7E}' . + '\x{1A8A}-\x{1A8F}' . + '\x{1A9A}-\x{1A9F}' . + '\x{1AAE}-\x{1AAF}' . + '\x{1ACF}-\x{1AFF}' . + '\x{1B4D}-\x{1B4F}' . + '\x{1B7F}' . + '\x{1BF4}-\x{1BFB}' . + '\x{1C38}-\x{1C3A}' . + '\x{1C4A}-\x{1C4C}' . + '\x{1C89}-\x{1C8F}' . + '\x{1CBB}-\x{1CBC}' . + '\x{1CC8}-\x{1CCF}' . + '\x{1CFB}-\x{1CFF}' . + '\x{1F16}-\x{1F17}' . + '\x{1F1E}-\x{1F1F}' . + '\x{1F46}-\x{1F47}' . + '\x{1F4E}-\x{1F4F}' . + '\x{1F58}' . + '\x{1F5A}' . + '\x{1F5C}' . + '\x{1F5E}' . + '\x{1F7E}-\x{1F7F}' . + '\x{1FB5}' . + '\x{1FC5}' . + '\x{1FD4}-\x{1FD5}' . + '\x{1FDC}' . + '\x{1FF0}-\x{1FF1}' . + '\x{1FF5}' . + '\x{1FFF}' . + '\x{200E}-\x{200F}' . + '\x{2024}-\x{2026}' . + '\x{2028}-\x{202E}' . + '\x{2061}-\x{2063}' . + '\x{2065}' . + '\x{2066}-\x{2069}' . + '\x{206A}-\x{206F}' . + '\x{2072}-\x{2073}' . + '\x{208F}' . + '\x{209D}-\x{209F}' . + '\x{20C1}-\x{20CF}' . + '\x{20F1}-\x{20FF}' . + '\x{2132}' . + '\x{2183}' . + '\x{218C}-\x{218F}' . + '\x{2427}-\x{243F}' . + '\x{244B}-\x{245F}' . + '\x{2488}-\x{249B}' . + '\x{2B74}-\x{2B75}' . + '\x{2B96}' . + '\x{2CF4}-\x{2CF8}' . + '\x{2D26}' . + '\x{2D28}-\x{2D2C}' . + '\x{2D2E}-\x{2D2F}' . + '\x{2D68}-\x{2D6E}' . + '\x{2D71}-\x{2D7E}' . + '\x{2D97}-\x{2D9F}' . + '\x{2DA7}' . + '\x{2DAF}' . + '\x{2DB7}' . + '\x{2DBF}' . + '\x{2DC7}' . + '\x{2DCF}' . + '\x{2DD7}' . + '\x{2DDF}' . + '\x{2E5E}-\x{2E7F}' . + '\x{2E9A}' . + '\x{2EF4}-\x{2EFF}' . + '\x{2FD6}-\x{2FEF}' . + '\x{2FF0}-\x{2FFB}' . + '\x{2FFC}-\x{2FFF}' . + '\x{3040}' . + '\x{3097}-\x{3098}' . + '\x{3100}-\x{3104}' . + '\x{3130}' . + '\x{3164}' . + '\x{318F}' . + '\x{31E4}-\x{31EF}' . + '\x{321F}' . + '\x{33C2}' . + '\x{33C7}' . + '\x{33D8}' . + '\x{A48D}-\x{A48F}' . + '\x{A4C7}-\x{A4CF}' . + '\x{A62C}-\x{A63F}' . + '\x{A6F8}-\x{A6FF}' . + '\x{A7CB}-\x{A7CF}' . + '\x{A7D2}' . + '\x{A7D4}' . + '\x{A7DA}-\x{A7F1}' . + '\x{A82D}-\x{A82F}' . + '\x{A83A}-\x{A83F}' . + '\x{A878}-\x{A87F}' . + '\x{A8C6}-\x{A8CD}' . + '\x{A8DA}-\x{A8DF}' . + '\x{A954}-\x{A95E}' . + '\x{A97D}-\x{A97F}' . + '\x{A9CE}' . + '\x{A9DA}-\x{A9DD}' . + '\x{A9FF}' . + '\x{AA37}-\x{AA3F}' . + '\x{AA4E}-\x{AA4F}' . + '\x{AA5A}-\x{AA5B}' . + '\x{AAC3}-\x{AADA}' . + '\x{AAF7}-\x{AB00}' . + '\x{AB07}-\x{AB08}' . + '\x{AB0F}-\x{AB10}' . + '\x{AB17}-\x{AB1F}' . + '\x{AB27}' . + '\x{AB2F}' . + '\x{AB6C}-\x{AB6F}' . + '\x{ABEE}-\x{ABEF}' . + '\x{ABFA}-\x{ABFF}' . + '\x{D7A4}-\x{D7AF}' . + '\x{D7C7}-\x{D7CA}' . + '\x{D7FC}-\x{D7FF}' . + '\x{E000}-\x{F8FF}' . + '\x{FA6E}-\x{FA6F}' . + '\x{FADA}-\x{FAFF}' . + '\x{FB07}-\x{FB12}' . + '\x{FB18}-\x{FB1C}' . + '\x{FB37}' . + '\x{FB3D}' . + '\x{FB3F}' . + '\x{FB42}' . + '\x{FB45}' . + '\x{FBC3}-\x{FBD2}' . + '\x{FD90}-\x{FD91}' . + '\x{FDC8}-\x{FDCE}' . + '\x{FDD0}-\x{FDEF}' . + '\x{FE12}' . + '\x{FE19}' . + '\x{FE1A}-\x{FE1F}' . + '\x{FE30}' . + '\x{FE52}' . + '\x{FE53}' . + '\x{FE67}' . + '\x{FE6C}-\x{FE6F}' . + '\x{FE75}' . + '\x{FEFD}-\x{FEFE}' . + '\x{FF00}' . + '\x{FFA0}' . + '\x{FFBF}-\x{FFC1}' . + '\x{FFC8}-\x{FFC9}' . + '\x{FFD0}-\x{FFD1}' . + '\x{FFD8}-\x{FFD9}' . + '\x{FFDD}-\x{FFDF}' . + '\x{FFE7}' . + '\x{FFEF}-\x{FFF8}' . + '\x{FFF9}-\x{FFFB}' . + '\x{FFFC}' . + '\x{FFFD}' . + '\x{FFFE}-\x{FFFF}' . + '\x{1000C}' . + '\x{10027}' . + '\x{1003B}' . + '\x{1003E}' . + '\x{1004E}-\x{1004F}' . + '\x{1005E}-\x{1007F}' . + '\x{100FB}-\x{100FF}' . + '\x{10103}-\x{10106}' . + '\x{10134}-\x{10136}' . + '\x{1018F}' . + '\x{1019D}-\x{1019F}' . + '\x{101A1}-\x{101CF}' . + '\x{101FE}-\x{1027F}' . + '\x{1029D}-\x{1029F}' . + '\x{102D1}-\x{102DF}' . + '\x{102FC}-\x{102FF}' . + '\x{10324}-\x{1032C}' . + '\x{1034B}-\x{1034F}' . + '\x{1037B}-\x{1037F}' . + '\x{1039E}' . + '\x{103C4}-\x{103C7}' . + '\x{103D6}-\x{103FF}' . + '\x{1049E}-\x{1049F}' . + '\x{104AA}-\x{104AF}' . + '\x{104D4}-\x{104D7}' . + '\x{104FC}-\x{104FF}' . + '\x{10528}-\x{1052F}' . + '\x{10564}-\x{1056E}' . + '\x{1057B}' . + '\x{1058B}' . + '\x{10593}' . + '\x{10596}' . + '\x{105A2}' . + '\x{105B2}' . + '\x{105BA}' . + '\x{105BD}-\x{105FF}' . + '\x{10737}-\x{1073F}' . + '\x{10756}-\x{1075F}' . + '\x{10768}-\x{1077F}' . + '\x{10786}' . + '\x{107B1}' . + '\x{107BB}-\x{107FF}' . + '\x{10806}-\x{10807}' . + '\x{10809}' . + '\x{10836}' . + '\x{10839}-\x{1083B}' . + '\x{1083D}-\x{1083E}' . + '\x{10856}' . + '\x{1089F}-\x{108A6}' . + '\x{108B0}-\x{108DF}' . + '\x{108F3}' . + '\x{108F6}-\x{108FA}' . + '\x{1091C}-\x{1091E}' . + '\x{1093A}-\x{1093E}' . + '\x{10940}-\x{1097F}' . + '\x{109B8}-\x{109BB}' . + '\x{109D0}-\x{109D1}' . + '\x{10A04}' . + '\x{10A07}-\x{10A0B}' . + '\x{10A14}' . + '\x{10A18}' . + '\x{10A36}-\x{10A37}' . + '\x{10A3B}-\x{10A3E}' . + '\x{10A49}-\x{10A4F}' . + '\x{10A59}-\x{10A5F}' . + '\x{10AA0}-\x{10ABF}' . + '\x{10AE7}-\x{10AEA}' . + '\x{10AF7}-\x{10AFF}' . + '\x{10B36}-\x{10B38}' . + '\x{10B56}-\x{10B57}' . + '\x{10B73}-\x{10B77}' . + '\x{10B92}-\x{10B98}' . + '\x{10B9D}-\x{10BA8}' . + '\x{10BB0}-\x{10BFF}' . + '\x{10C49}-\x{10C7F}' . + '\x{10CB3}-\x{10CBF}' . + '\x{10CF3}-\x{10CF9}' . + '\x{10D28}-\x{10D2F}' . + '\x{10D3A}-\x{10E5F}' . + '\x{10E7F}' . + '\x{10EAA}' . + '\x{10EAE}-\x{10EAF}' . + '\x{10EB2}-\x{10EFC}' . + '\x{10F28}-\x{10F2F}' . + '\x{10F5A}-\x{10F6F}' . + '\x{10F8A}-\x{10FAF}' . + '\x{10FCC}-\x{10FDF}' . + '\x{10FF7}-\x{10FFF}' . + '\x{1104E}-\x{11051}' . + '\x{11076}-\x{1107E}' . + '\x{110BD}' . + '\x{110C3}-\x{110CC}' . + '\x{110CD}' . + '\x{110CE}-\x{110CF}' . + '\x{110E9}-\x{110EF}' . + '\x{110FA}-\x{110FF}' . + '\x{11135}' . + '\x{11148}-\x{1114F}' . + '\x{11177}-\x{1117F}' . + '\x{111E0}' . + '\x{111F5}-\x{111FF}' . + '\x{11212}' . + '\x{11242}-\x{1127F}' . + '\x{11287}' . + '\x{11289}' . + '\x{1128E}' . + '\x{1129E}' . + '\x{112AA}-\x{112AF}' . + '\x{112EB}-\x{112EF}' . + '\x{112FA}-\x{112FF}' . + '\x{11304}' . + '\x{1130D}-\x{1130E}' . + '\x{11311}-\x{11312}' . + '\x{11329}' . + '\x{11331}' . + '\x{11334}' . + '\x{1133A}' . + '\x{11345}-\x{11346}' . + '\x{11349}-\x{1134A}' . + '\x{1134E}-\x{1134F}' . + '\x{11351}-\x{11356}' . + '\x{11358}-\x{1135C}' . + '\x{11364}-\x{11365}' . + '\x{1136D}-\x{1136F}' . + '\x{11375}-\x{113FF}' . + '\x{1145C}' . + '\x{11462}-\x{1147F}' . + '\x{114C8}-\x{114CF}' . + '\x{114DA}-\x{1157F}' . + '\x{115B6}-\x{115B7}' . + '\x{115DE}-\x{115FF}' . + '\x{11645}-\x{1164F}' . + '\x{1165A}-\x{1165F}' . + '\x{1166D}-\x{1167F}' . + '\x{116BA}-\x{116BF}' . + '\x{116CA}-\x{116FF}' . + '\x{1171B}-\x{1171C}' . + '\x{1172C}-\x{1172F}' . + '\x{11747}-\x{117FF}' . + '\x{1183C}-\x{1189F}' . + '\x{118F3}-\x{118FE}' . + '\x{11907}-\x{11908}' . + '\x{1190A}-\x{1190B}' . + '\x{11914}' . + '\x{11917}' . + '\x{11936}' . + '\x{11939}-\x{1193A}' . + '\x{11947}-\x{1194F}' . + '\x{1195A}-\x{1199F}' . + '\x{119A8}-\x{119A9}' . + '\x{119D8}-\x{119D9}' . + '\x{119E5}-\x{119FF}' . + '\x{11A48}-\x{11A4F}' . + '\x{11AA3}-\x{11AAF}' . + '\x{11AF9}-\x{11AFF}' . + '\x{11B0A}-\x{11BFF}' . + '\x{11C09}' . + '\x{11C37}' . + '\x{11C46}-\x{11C4F}' . + '\x{11C6D}-\x{11C6F}' . + '\x{11C90}-\x{11C91}' . + '\x{11CA8}' . + '\x{11CB7}-\x{11CFF}' . + '\x{11D07}' . + '\x{11D0A}' . + '\x{11D37}-\x{11D39}' . + '\x{11D3B}' . + '\x{11D3E}' . + '\x{11D48}-\x{11D4F}' . + '\x{11D5A}-\x{11D5F}' . + '\x{11D66}' . + '\x{11D69}' . + '\x{11D8F}' . + '\x{11D92}' . + '\x{11D99}-\x{11D9F}' . + '\x{11DAA}-\x{11EDF}' . + '\x{11EF9}-\x{11EFF}' . + '\x{11F11}' . + '\x{11F3B}-\x{11F3D}' . + '\x{11F5A}-\x{11FAF}' . + '\x{11FB1}-\x{11FBF}' . + '\x{11FF2}-\x{11FFE}' . + '\x{1239A}-\x{123FF}' . + '\x{1246F}' . + '\x{12475}-\x{1247F}' . + '\x{12544}-\x{12F8F}' . + '\x{12FF3}-\x{12FFF}' . + '\x{13430}-\x{13438}' . + '\x{13439}-\x{1343F}' . + '\x{13456}-\x{143FF}' . + '\x{14647}-\x{167FF}' . + '\x{16A39}-\x{16A3F}' . + '\x{16A5F}' . + '\x{16A6A}-\x{16A6D}' . + '\x{16ABF}' . + '\x{16ACA}-\x{16ACF}' . + '\x{16AEE}-\x{16AEF}' . + '\x{16AF6}-\x{16AFF}' . + '\x{16B46}-\x{16B4F}' . + '\x{16B5A}' . + '\x{16B62}' . + '\x{16B78}-\x{16B7C}' . + '\x{16B90}-\x{16E3F}' . + '\x{16E9B}-\x{16EFF}' . + '\x{16F4B}-\x{16F4E}' . + '\x{16F88}-\x{16F8E}' . + '\x{16FA0}-\x{16FDF}' . + '\x{16FE5}-\x{16FEF}' . + '\x{16FF2}-\x{16FFF}' . + '\x{187F8}-\x{187FF}' . + '\x{18CD6}-\x{18CFF}' . + '\x{18D09}-\x{1AFEF}' . + '\x{1AFF4}' . + '\x{1AFFC}' . + '\x{1AFFF}' . + '\x{1B123}-\x{1B131}' . + '\x{1B133}-\x{1B14F}' . + '\x{1B153}-\x{1B154}' . + '\x{1B156}-\x{1B163}' . + '\x{1B168}-\x{1B16F}' . + '\x{1B2FC}-\x{1BBFF}' . + '\x{1BC6B}-\x{1BC6F}' . + '\x{1BC7D}-\x{1BC7F}' . + '\x{1BC89}-\x{1BC8F}' . + '\x{1BC9A}-\x{1BC9B}' . + '\x{1BCA4}-\x{1CEFF}' . + '\x{1CF2E}-\x{1CF2F}' . + '\x{1CF47}-\x{1CF4F}' . + '\x{1CFC4}-\x{1CFFF}' . + '\x{1D0F6}-\x{1D0FF}' . + '\x{1D127}-\x{1D128}' . + '\x{1D173}-\x{1D17A}' . + '\x{1D1EB}-\x{1D1FF}' . + '\x{1D246}-\x{1D2BF}' . + '\x{1D2D4}-\x{1D2DF}' . + '\x{1D2F4}-\x{1D2FF}' . + '\x{1D357}-\x{1D35F}' . + '\x{1D379}-\x{1D3FF}' . + '\x{1D455}' . + '\x{1D49D}' . + '\x{1D4A0}-\x{1D4A1}' . + '\x{1D4A3}-\x{1D4A4}' . + '\x{1D4A7}-\x{1D4A8}' . + '\x{1D4AD}' . + '\x{1D4BA}' . + '\x{1D4BC}' . + '\x{1D4C4}' . + '\x{1D506}' . + '\x{1D50B}-\x{1D50C}' . + '\x{1D515}' . + '\x{1D51D}' . + '\x{1D53A}' . + '\x{1D53F}' . + '\x{1D545}' . + '\x{1D547}-\x{1D549}' . + '\x{1D551}' . + '\x{1D6A6}-\x{1D6A7}' . + '\x{1D7CC}-\x{1D7CD}' . + '\x{1DA8C}-\x{1DA9A}' . + '\x{1DAA0}' . + '\x{1DAB0}-\x{1DEFF}' . + '\x{1DF1F}-\x{1DF24}' . + '\x{1DF2B}-\x{1DFFF}' . + '\x{1E007}' . + '\x{1E019}-\x{1E01A}' . + '\x{1E022}' . + '\x{1E025}' . + '\x{1E02B}-\x{1E02F}' . + '\x{1E06E}-\x{1E08E}' . + '\x{1E090}-\x{1E0FF}' . + '\x{1E12D}-\x{1E12F}' . + '\x{1E13E}-\x{1E13F}' . + '\x{1E14A}-\x{1E14D}' . + '\x{1E150}-\x{1E28F}' . + '\x{1E2AF}-\x{1E2BF}' . + '\x{1E2FA}-\x{1E2FE}' . + '\x{1E300}-\x{1E4CF}' . + '\x{1E4FA}-\x{1E7DF}' . + '\x{1E7E7}' . + '\x{1E7EC}' . + '\x{1E7EF}' . + '\x{1E7FF}' . + '\x{1E8C5}-\x{1E8C6}' . + '\x{1E8D7}-\x{1E8FF}' . + '\x{1E94C}-\x{1E94F}' . + '\x{1E95A}-\x{1E95D}' . + '\x{1E960}-\x{1EC70}' . + '\x{1ECB5}-\x{1ED00}' . + '\x{1ED3E}-\x{1EDFF}' . + '\x{1EE04}' . + '\x{1EE20}' . + '\x{1EE23}' . + '\x{1EE25}-\x{1EE26}' . + '\x{1EE28}' . + '\x{1EE33}' . + '\x{1EE38}' . + '\x{1EE3A}' . + '\x{1EE3C}-\x{1EE41}' . + '\x{1EE43}-\x{1EE46}' . + '\x{1EE48}' . + '\x{1EE4A}' . + '\x{1EE4C}' . + '\x{1EE50}' . + '\x{1EE53}' . + '\x{1EE55}-\x{1EE56}' . + '\x{1EE58}' . + '\x{1EE5A}' . + '\x{1EE5C}' . + '\x{1EE5E}' . + '\x{1EE60}' . + '\x{1EE63}' . + '\x{1EE65}-\x{1EE66}' . + '\x{1EE6B}' . + '\x{1EE73}' . + '\x{1EE78}' . + '\x{1EE7D}' . + '\x{1EE7F}' . + '\x{1EE8A}' . + '\x{1EE9C}-\x{1EEA0}' . + '\x{1EEA4}' . + '\x{1EEAA}' . + '\x{1EEBC}-\x{1EEEF}' . + '\x{1EEF2}-\x{1EFFF}' . + '\x{1F02C}-\x{1F02F}' . + '\x{1F094}-\x{1F09F}' . + '\x{1F0AF}-\x{1F0B0}' . + '\x{1F0C0}' . + '\x{1F0D0}' . + '\x{1F0F6}-\x{1F0FF}' . + '\x{1F100}' . + '\x{1F1AE}-\x{1F1E5}' . + '\x{1F203}-\x{1F20F}' . + '\x{1F23C}-\x{1F23F}' . + '\x{1F249}-\x{1F24F}' . + '\x{1F252}-\x{1F25F}' . + '\x{1F266}-\x{1F2FF}' . + '\x{1F6D8}-\x{1F6DB}' . + '\x{1F6ED}-\x{1F6EF}' . + '\x{1F6FD}-\x{1F6FF}' . + '\x{1F777}-\x{1F77A}' . + '\x{1F7DA}-\x{1F7DF}' . + '\x{1F7EC}-\x{1F7EF}' . + '\x{1F7F1}-\x{1F7FF}' . + '\x{1F80C}-\x{1F80F}' . + '\x{1F848}-\x{1F84F}' . + '\x{1F85A}-\x{1F85F}' . + '\x{1F888}-\x{1F88F}' . + '\x{1F8AE}-\x{1F8AF}' . + '\x{1F8B2}-\x{1F8FF}' . + '\x{1FA54}-\x{1FA5F}' . + '\x{1FA6E}-\x{1FA6F}' . + '\x{1FA7D}-\x{1FA7F}' . + '\x{1FA89}-\x{1FA8F}' . + '\x{1FABE}' . + '\x{1FAC6}-\x{1FACD}' . + '\x{1FADC}-\x{1FADF}' . + '\x{1FAE9}-\x{1FAEF}' . + '\x{1FAF9}-\x{1FAFF}' . + '\x{1FB93}' . + '\x{1FBCB}-\x{1FBEF}' . + '\x{1FBFA}-\x{1FFFD}' . + '\x{1FFFE}-\x{1FFFF}' . + '\x{2A6E0}-\x{2A6FF}' . + '\x{2B73A}-\x{2B73F}' . + '\x{2B81E}-\x{2B81F}' . + '\x{2CEA2}-\x{2CEAF}' . + '\x{2EBE1}-\x{2F7FF}' . + '\x{2F868}' . + '\x{2F874}' . + '\x{2F91F}' . + '\x{2F95F}' . + '\x{2F9BF}' . + '\x{2FA1E}-\x{2FFFD}' . + '\x{2FFFE}-\x{2FFFF}' . + '\x{3134B}-\x{3134F}' . + '\x{323B0}-\x{3FFFD}' . + '\x{3FFFE}-\x{3FFFF}' . + '\x{40000}-\x{4FFFD}' . + '\x{4FFFE}-\x{4FFFF}' . + '\x{50000}-\x{5FFFD}' . + '\x{5FFFE}-\x{5FFFF}' . + '\x{60000}-\x{6FFFD}' . + '\x{6FFFE}-\x{6FFFF}' . + '\x{70000}-\x{7FFFD}' . + '\x{7FFFE}-\x{7FFFF}' . + '\x{80000}-\x{8FFFD}' . + '\x{8FFFE}-\x{8FFFF}' . + '\x{90000}-\x{9FFFD}' . + '\x{9FFFE}-\x{9FFFF}' . + '\x{A0000}-\x{AFFFD}' . + '\x{AFFFE}-\x{AFFFF}' . + '\x{B0000}-\x{BFFFD}' . + '\x{BFFFE}-\x{BFFFF}' . + '\x{C0000}-\x{CFFFD}' . + '\x{CFFFE}-\x{CFFFF}' . + '\x{D0000}-\x{DFFFD}' . + '\x{DFFFE}-\x{DFFFF}' . + '\x{E0000}' . + '\x{E0001}' . + '\x{E0002}-\x{E001F}' . + '\x{E0020}-\x{E007F}' . + '\x{E0080}-\x{E00FF}' . + '\x{E01F0}-\x{EFFFD}' . + '\x{EFFFE}-\x{EFFFF}' . + '\x{F0000}-\x{FFFFD}' . + '\x{FFFFE}-\x{FFFFF}' . + '\x{100000}-\x{10FFFD}' . + '\x{10FFFE}-\x{10FFFF}', + 'ignored' => + '\x{AD}' . + '\x{34F}' . + '\x{180B}-\x{180D}' . + '\x{180F}' . + '\x{200B}' . + '\x{2060}' . + '\x{2064}' . + '\x{FE00}-\x{FE0F}' . + '\x{FEFF}' . + '\x{1BCA0}-\x{1BCA3}' . + '\x{E0100}-\x{E01EF}', + 'deviation' => + '\x{DF}' . + '\x{3C2}' . + '\x{200C}-\x{200D}', + ); +} + +?> \ No newline at end of file diff --git a/Sources/Unicode/Metadata.php b/Sources/Unicode/Metadata.php new file mode 100644 index 0000000..452c4fd --- /dev/null +++ b/Sources/Unicode/Metadata.php @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/Sources/Unicode/QuickCheck.php b/Sources/Unicode/QuickCheck.php new file mode 100644 index 0000000..168074a --- /dev/null +++ b/Sources/Unicode/QuickCheck.php @@ -0,0 +1,1919 @@ + + '\x{0340}-\x{0341}' . + '\x{0343}-\x{0344}' . + '\x{0374}' . + '\x{037E}' . + '\x{0387}' . + '\x{0958}-\x{095F}' . + '\x{09DC}-\x{09DD}' . + '\x{09DF}' . + '\x{0A33}' . + '\x{0A36}' . + '\x{0A59}-\x{0A5B}' . + '\x{0A5E}' . + '\x{0B5C}-\x{0B5D}' . + '\x{0F43}' . + '\x{0F4D}' . + '\x{0F52}' . + '\x{0F57}' . + '\x{0F5C}' . + '\x{0F69}' . + '\x{0F73}' . + '\x{0F75}-\x{0F76}' . + '\x{0F78}' . + '\x{0F81}' . + '\x{0F93}' . + '\x{0F9D}' . + '\x{0FA2}' . + '\x{0FA7}' . + '\x{0FAC}' . + '\x{0FB9}' . + '\x{1F71}' . + '\x{1F73}' . + '\x{1F75}' . + '\x{1F77}' . + '\x{1F79}' . + '\x{1F7B}' . + '\x{1F7D}' . + '\x{1FBB}' . + '\x{1FBE}' . + '\x{1FC9}' . + '\x{1FCB}' . + '\x{1FD3}' . + '\x{1FDB}' . + '\x{1FE3}' . + '\x{1FEB}' . + '\x{1FEE}-\x{1FEF}' . + '\x{1FF9}' . + '\x{1FFB}' . + '\x{1FFD}' . + '\x{2000}-\x{2001}' . + '\x{2126}' . + '\x{212A}-\x{212B}' . + '\x{2329}-\x{232A}' . + '\x{2ADC}' . + '\x{F900}-\x{FA0D}' . + '\x{FA10}' . + '\x{FA12}' . + '\x{FA15}-\x{FA1E}' . + '\x{FA20}' . + '\x{FA22}' . + '\x{FA25}-\x{FA26}' . + '\x{FA2A}-\x{FA6D}' . + '\x{FA70}-\x{FAD9}' . + '\x{FB1D}' . + '\x{FB1F}' . + '\x{FB2A}-\x{FB36}' . + '\x{FB38}-\x{FB3C}' . + '\x{FB3E}' . + '\x{FB40}-\x{FB41}' . + '\x{FB43}-\x{FB44}' . + '\x{FB46}-\x{FB4E}' . + '\x{1D15E}-\x{1D164}' . + '\x{1D1BB}-\x{1D1C0}' . + '\x{2F800}-\x{2FA1D}' . + '\x{0300}-\x{0304}' . + '\x{0306}-\x{030C}' . + '\x{030F}' . + '\x{0311}' . + '\x{0313}-\x{0314}' . + '\x{031B}' . + '\x{0323}-\x{0328}' . + '\x{032D}-\x{032E}' . + '\x{0330}-\x{0331}' . + '\x{0338}' . + '\x{0342}' . + '\x{0345}' . + '\x{0653}-\x{0655}' . + '\x{093C}' . + '\x{09BE}' . + '\x{09D7}' . + '\x{0B3E}' . + '\x{0B56}-\x{0B57}' . + '\x{0BBE}' . + '\x{0BD7}' . + '\x{0C56}' . + '\x{0CC2}' . + '\x{0CD5}-\x{0CD6}' . + '\x{0D3E}' . + '\x{0D57}' . + '\x{0DCA}' . + '\x{0DCF}' . + '\x{0DDF}' . + '\x{102E}' . + '\x{1161}-\x{1175}' . + '\x{11A8}-\x{11C2}' . + '\x{1B35}' . + '\x{3099}-\x{309A}' . + '\x{110BA}' . + '\x{11127}' . + '\x{1133E}' . + '\x{11357}' . + '\x{114B0}' . + '\x{114BA}' . + '\x{114BD}' . + '\x{115AF}' . + '\x{11930}', + 'NFKC_QC' => + '\x{00A0}' . + '\x{00A8}' . + '\x{00AA}' . + '\x{00AF}' . + '\x{00B2}-\x{00B5}' . + '\x{00B8}-\x{00BA}' . + '\x{00BC}-\x{00BE}' . + '\x{0132}-\x{0133}' . + '\x{013F}-\x{0140}' . + '\x{0149}' . + '\x{017F}' . + '\x{01C4}-\x{01CC}' . + '\x{01F1}-\x{01F3}' . + '\x{02B0}-\x{02B8}' . + '\x{02D8}-\x{02DD}' . + '\x{02E0}-\x{02E4}' . + '\x{0340}-\x{0341}' . + '\x{0343}-\x{0344}' . + '\x{0374}' . + '\x{037A}' . + '\x{037E}' . + '\x{0384}-\x{0385}' . + '\x{0387}' . + '\x{03D0}-\x{03D6}' . + '\x{03F0}-\x{03F2}' . + '\x{03F4}-\x{03F5}' . + '\x{03F9}' . + '\x{0587}' . + '\x{0675}-\x{0678}' . + '\x{0958}-\x{095F}' . + '\x{09DC}-\x{09DD}' . + '\x{09DF}' . + '\x{0A33}' . + '\x{0A36}' . + '\x{0A59}-\x{0A5B}' . + '\x{0A5E}' . + '\x{0B5C}-\x{0B5D}' . + '\x{0E33}' . + '\x{0EB3}' . + '\x{0EDC}-\x{0EDD}' . + '\x{0F0C}' . + '\x{0F43}' . + '\x{0F4D}' . + '\x{0F52}' . + '\x{0F57}' . + '\x{0F5C}' . + '\x{0F69}' . + '\x{0F73}' . + '\x{0F75}-\x{0F79}' . + '\x{0F81}' . + '\x{0F93}' . + '\x{0F9D}' . + '\x{0FA2}' . + '\x{0FA7}' . + '\x{0FAC}' . + '\x{0FB9}' . + '\x{10FC}' . + '\x{1D2C}-\x{1D2E}' . + '\x{1D30}-\x{1D3A}' . + '\x{1D3C}-\x{1D4D}' . + '\x{1D4F}-\x{1D6A}' . + '\x{1D78}' . + '\x{1D9B}-\x{1DBF}' . + '\x{1E9A}-\x{1E9B}' . + '\x{1F71}' . + '\x{1F73}' . + '\x{1F75}' . + '\x{1F77}' . + '\x{1F79}' . + '\x{1F7B}' . + '\x{1F7D}' . + '\x{1FBB}' . + '\x{1FBD}-\x{1FC1}' . + '\x{1FC9}' . + '\x{1FCB}' . + '\x{1FCD}-\x{1FCF}' . + '\x{1FD3}' . + '\x{1FDB}' . + '\x{1FDD}-\x{1FDF}' . + '\x{1FE3}' . + '\x{1FEB}' . + '\x{1FED}-\x{1FEF}' . + '\x{1FF9}' . + '\x{1FFB}' . + '\x{1FFD}-\x{1FFE}' . + '\x{2000}-\x{200A}' . + '\x{2011}' . + '\x{2017}' . + '\x{2024}-\x{2026}' . + '\x{202F}' . + '\x{2033}-\x{2034}' . + '\x{2036}-\x{2037}' . + '\x{203C}' . + '\x{203E}' . + '\x{2047}-\x{2049}' . + '\x{2057}' . + '\x{205F}' . + '\x{2070}-\x{2071}' . + '\x{2074}-\x{208E}' . + '\x{2090}-\x{209C}' . + '\x{20A8}' . + '\x{2100}-\x{2103}' . + '\x{2105}-\x{2107}' . + '\x{2109}-\x{2113}' . + '\x{2115}-\x{2116}' . + '\x{2119}-\x{211D}' . + '\x{2120}-\x{2122}' . + '\x{2124}' . + '\x{2126}' . + '\x{2128}' . + '\x{212A}-\x{212D}' . + '\x{212F}-\x{2131}' . + '\x{2133}-\x{2139}' . + '\x{213B}-\x{2140}' . + '\x{2145}-\x{2149}' . + '\x{2150}-\x{217F}' . + '\x{2189}' . + '\x{222C}-\x{222D}' . + '\x{222F}-\x{2230}' . + '\x{2329}-\x{232A}' . + '\x{2460}-\x{24EA}' . + '\x{2A0C}' . + '\x{2A74}-\x{2A76}' . + '\x{2ADC}' . + '\x{2C7C}-\x{2C7D}' . + '\x{2D6F}' . + '\x{2E9F}' . + '\x{2EF3}' . + '\x{2F00}-\x{2FD5}' . + '\x{3000}' . + '\x{3036}' . + '\x{3038}-\x{303A}' . + '\x{309B}-\x{309C}' . + '\x{309F}' . + '\x{30FF}' . + '\x{3131}-\x{318E}' . + '\x{3192}-\x{319F}' . + '\x{3200}-\x{321E}' . + '\x{3220}-\x{3247}' . + '\x{3250}-\x{327E}' . + '\x{3280}-\x{33FF}' . + '\x{A69C}-\x{A69D}' . + '\x{A770}' . + '\x{A7F2}-\x{A7F4}' . + '\x{A7F8}-\x{A7F9}' . + '\x{AB5C}-\x{AB5F}' . + '\x{AB69}' . + '\x{F900}-\x{FA0D}' . + '\x{FA10}' . + '\x{FA12}' . + '\x{FA15}-\x{FA1E}' . + '\x{FA20}' . + '\x{FA22}' . + '\x{FA25}-\x{FA26}' . + '\x{FA2A}-\x{FA6D}' . + '\x{FA70}-\x{FAD9}' . + '\x{FB00}-\x{FB06}' . + '\x{FB13}-\x{FB17}' . + '\x{FB1D}' . + '\x{FB1F}-\x{FB36}' . + '\x{FB38}-\x{FB3C}' . + '\x{FB3E}' . + '\x{FB40}-\x{FB41}' . + '\x{FB43}-\x{FB44}' . + '\x{FB46}-\x{FBB1}' . + '\x{FBD3}-\x{FD3D}' . + '\x{FD50}-\x{FD8F}' . + '\x{FD92}-\x{FDC7}' . + '\x{FDF0}-\x{FDFC}' . + '\x{FE10}-\x{FE19}' . + '\x{FE30}-\x{FE44}' . + '\x{FE47}-\x{FE52}' . + '\x{FE54}-\x{FE66}' . + '\x{FE68}-\x{FE6B}' . + '\x{FE70}-\x{FE72}' . + '\x{FE74}' . + '\x{FE76}-\x{FEFC}' . + '\x{FF01}-\x{FFBE}' . + '\x{FFC2}-\x{FFC7}' . + '\x{FFCA}-\x{FFCF}' . + '\x{FFD2}-\x{FFD7}' . + '\x{FFDA}-\x{FFDC}' . + '\x{FFE0}-\x{FFE6}' . + '\x{FFE8}-\x{FFEE}' . + '\x{10781}-\x{10785}' . + '\x{10787}-\x{107B0}' . + '\x{107B2}-\x{107BA}' . + '\x{1D15E}-\x{1D164}' . + '\x{1D1BB}-\x{1D1C0}' . + '\x{1D400}-\x{1D454}' . + '\x{1D456}-\x{1D49C}' . + '\x{1D49E}-\x{1D49F}' . + '\x{1D4A2}' . + '\x{1D4A5}-\x{1D4A6}' . + '\x{1D4A9}-\x{1D4AC}' . + '\x{1D4AE}-\x{1D4B9}' . + '\x{1D4BB}' . + '\x{1D4BD}-\x{1D4C3}' . + '\x{1D4C5}-\x{1D505}' . + '\x{1D507}-\x{1D50A}' . + '\x{1D50D}-\x{1D514}' . + '\x{1D516}-\x{1D51C}' . + '\x{1D51E}-\x{1D539}' . + '\x{1D53B}-\x{1D53E}' . + '\x{1D540}-\x{1D544}' . + '\x{1D546}' . + '\x{1D54A}-\x{1D550}' . + '\x{1D552}-\x{1D6A5}' . + '\x{1D6A8}-\x{1D7CB}' . + '\x{1D7CE}-\x{1D7FF}' . + '\x{1E030}-\x{1E06D}' . + '\x{1EE00}-\x{1EE03}' . + '\x{1EE05}-\x{1EE1F}' . + '\x{1EE21}-\x{1EE22}' . + '\x{1EE24}' . + '\x{1EE27}' . + '\x{1EE29}-\x{1EE32}' . + '\x{1EE34}-\x{1EE37}' . + '\x{1EE39}' . + '\x{1EE3B}' . + '\x{1EE42}' . + '\x{1EE47}' . + '\x{1EE49}' . + '\x{1EE4B}' . + '\x{1EE4D}-\x{1EE4F}' . + '\x{1EE51}-\x{1EE52}' . + '\x{1EE54}' . + '\x{1EE57}' . + '\x{1EE59}' . + '\x{1EE5B}' . + '\x{1EE5D}' . + '\x{1EE5F}' . + '\x{1EE61}-\x{1EE62}' . + '\x{1EE64}' . + '\x{1EE67}-\x{1EE6A}' . + '\x{1EE6C}-\x{1EE72}' . + '\x{1EE74}-\x{1EE77}' . + '\x{1EE79}-\x{1EE7C}' . + '\x{1EE7E}' . + '\x{1EE80}-\x{1EE89}' . + '\x{1EE8B}-\x{1EE9B}' . + '\x{1EEA1}-\x{1EEA3}' . + '\x{1EEA5}-\x{1EEA9}' . + '\x{1EEAB}-\x{1EEBB}' . + '\x{1F100}-\x{1F10A}' . + '\x{1F110}-\x{1F12E}' . + '\x{1F130}-\x{1F14F}' . + '\x{1F16A}-\x{1F16C}' . + '\x{1F190}' . + '\x{1F200}-\x{1F202}' . + '\x{1F210}-\x{1F23B}' . + '\x{1F240}-\x{1F248}' . + '\x{1F250}-\x{1F251}' . + '\x{1FBF0}-\x{1FBF9}' . + '\x{2F800}-\x{2FA1D}' . + '\x{0300}-\x{0304}' . + '\x{0306}-\x{030C}' . + '\x{030F}' . + '\x{0311}' . + '\x{0313}-\x{0314}' . + '\x{031B}' . + '\x{0323}-\x{0328}' . + '\x{032D}-\x{032E}' . + '\x{0330}-\x{0331}' . + '\x{0338}' . + '\x{0342}' . + '\x{0345}' . + '\x{0653}-\x{0655}' . + '\x{093C}' . + '\x{09BE}' . + '\x{09D7}' . + '\x{0B3E}' . + '\x{0B56}-\x{0B57}' . + '\x{0BBE}' . + '\x{0BD7}' . + '\x{0C56}' . + '\x{0CC2}' . + '\x{0CD5}-\x{0CD6}' . + '\x{0D3E}' . + '\x{0D57}' . + '\x{0DCA}' . + '\x{0DCF}' . + '\x{0DDF}' . + '\x{102E}' . + '\x{1161}-\x{1175}' . + '\x{11A8}-\x{11C2}' . + '\x{1B35}' . + '\x{3099}-\x{309A}' . + '\x{110BA}' . + '\x{11127}' . + '\x{1133E}' . + '\x{11357}' . + '\x{114B0}' . + '\x{114BA}' . + '\x{114BD}' . + '\x{115AF}' . + '\x{11930}', + 'NFD_QC' => + '\x{00C0}-\x{00C5}' . + '\x{00C7}-\x{00CF}' . + '\x{00D1}-\x{00D6}' . + '\x{00D9}-\x{00DD}' . + '\x{00E0}-\x{00E5}' . + '\x{00E7}-\x{00EF}' . + '\x{00F1}-\x{00F6}' . + '\x{00F9}-\x{00FD}' . + '\x{00FF}-\x{010F}' . + '\x{0112}-\x{0125}' . + '\x{0128}-\x{0130}' . + '\x{0134}-\x{0137}' . + '\x{0139}-\x{013E}' . + '\x{0143}-\x{0148}' . + '\x{014C}-\x{0151}' . + '\x{0154}-\x{0165}' . + '\x{0168}-\x{017E}' . + '\x{01A0}-\x{01A1}' . + '\x{01AF}-\x{01B0}' . + '\x{01CD}-\x{01DC}' . + '\x{01DE}-\x{01E3}' . + '\x{01E6}-\x{01F0}' . + '\x{01F4}-\x{01F5}' . + '\x{01F8}-\x{021B}' . + '\x{021E}-\x{021F}' . + '\x{0226}-\x{0233}' . + '\x{0340}-\x{0341}' . + '\x{0343}-\x{0344}' . + '\x{0374}' . + '\x{037E}' . + '\x{0385}-\x{038A}' . + '\x{038C}' . + '\x{038E}-\x{0390}' . + '\x{03AA}-\x{03B0}' . + '\x{03CA}-\x{03CE}' . + '\x{03D3}-\x{03D4}' . + '\x{0400}-\x{0401}' . + '\x{0403}' . + '\x{0407}' . + '\x{040C}-\x{040E}' . + '\x{0419}' . + '\x{0439}' . + '\x{0450}-\x{0451}' . + '\x{0453}' . + '\x{0457}' . + '\x{045C}-\x{045E}' . + '\x{0476}-\x{0477}' . + '\x{04C1}-\x{04C2}' . + '\x{04D0}-\x{04D3}' . + '\x{04D6}-\x{04D7}' . + '\x{04DA}-\x{04DF}' . + '\x{04E2}-\x{04E7}' . + '\x{04EA}-\x{04F5}' . + '\x{04F8}-\x{04F9}' . + '\x{0622}-\x{0626}' . + '\x{06C0}' . + '\x{06C2}' . + '\x{06D3}' . + '\x{0929}' . + '\x{0931}' . + '\x{0934}' . + '\x{0958}-\x{095F}' . + '\x{09CB}-\x{09CC}' . + '\x{09DC}-\x{09DD}' . + '\x{09DF}' . + '\x{0A33}' . + '\x{0A36}' . + '\x{0A59}-\x{0A5B}' . + '\x{0A5E}' . + '\x{0B48}' . + '\x{0B4B}-\x{0B4C}' . + '\x{0B5C}-\x{0B5D}' . + '\x{0B94}' . + '\x{0BCA}-\x{0BCC}' . + '\x{0C48}' . + '\x{0CC0}' . + '\x{0CC7}-\x{0CC8}' . + '\x{0CCA}-\x{0CCB}' . + '\x{0D4A}-\x{0D4C}' . + '\x{0DDA}' . + '\x{0DDC}-\x{0DDE}' . + '\x{0F43}' . + '\x{0F4D}' . + '\x{0F52}' . + '\x{0F57}' . + '\x{0F5C}' . + '\x{0F69}' . + '\x{0F73}' . + '\x{0F75}-\x{0F76}' . + '\x{0F78}' . + '\x{0F81}' . + '\x{0F93}' . + '\x{0F9D}' . + '\x{0FA2}' . + '\x{0FA7}' . + '\x{0FAC}' . + '\x{0FB9}' . + '\x{1026}' . + '\x{1B06}' . + '\x{1B08}' . + '\x{1B0A}' . + '\x{1B0C}' . + '\x{1B0E}' . + '\x{1B12}' . + '\x{1B3B}' . + '\x{1B3D}' . + '\x{1B40}-\x{1B41}' . + '\x{1B43}' . + '\x{1E00}-\x{1E99}' . + '\x{1E9B}' . + '\x{1EA0}-\x{1EF9}' . + '\x{1F00}-\x{1F15}' . + '\x{1F18}-\x{1F1D}' . + '\x{1F20}-\x{1F45}' . + '\x{1F48}-\x{1F4D}' . + '\x{1F50}-\x{1F57}' . + '\x{1F59}' . + '\x{1F5B}' . + '\x{1F5D}' . + '\x{1F5F}-\x{1F7D}' . + '\x{1F80}-\x{1FB4}' . + '\x{1FB6}-\x{1FBC}' . + '\x{1FBE}' . + '\x{1FC1}-\x{1FC4}' . + '\x{1FC6}-\x{1FD3}' . + '\x{1FD6}-\x{1FDB}' . + '\x{1FDD}-\x{1FEF}' . + '\x{1FF2}-\x{1FF4}' . + '\x{1FF6}-\x{1FFD}' . + '\x{2000}-\x{2001}' . + '\x{2126}' . + '\x{212A}-\x{212B}' . + '\x{219A}-\x{219B}' . + '\x{21AE}' . + '\x{21CD}-\x{21CF}' . + '\x{2204}' . + '\x{2209}' . + '\x{220C}' . + '\x{2224}' . + '\x{2226}' . + '\x{2241}' . + '\x{2244}' . + '\x{2247}' . + '\x{2249}' . + '\x{2260}' . + '\x{2262}' . + '\x{226D}-\x{2271}' . + '\x{2274}-\x{2275}' . + '\x{2278}-\x{2279}' . + '\x{2280}-\x{2281}' . + '\x{2284}-\x{2285}' . + '\x{2288}-\x{2289}' . + '\x{22AC}-\x{22AF}' . + '\x{22E0}-\x{22E3}' . + '\x{22EA}-\x{22ED}' . + '\x{2329}-\x{232A}' . + '\x{2ADC}' . + '\x{304C}' . + '\x{304E}' . + '\x{3050}' . + '\x{3052}' . + '\x{3054}' . + '\x{3056}' . + '\x{3058}' . + '\x{305A}' . + '\x{305C}' . + '\x{305E}' . + '\x{3060}' . + '\x{3062}' . + '\x{3065}' . + '\x{3067}' . + '\x{3069}' . + '\x{3070}-\x{3071}' . + '\x{3073}-\x{3074}' . + '\x{3076}-\x{3077}' . + '\x{3079}-\x{307A}' . + '\x{307C}-\x{307D}' . + '\x{3094}' . + '\x{309E}' . + '\x{30AC}' . + '\x{30AE}' . + '\x{30B0}' . + '\x{30B2}' . + '\x{30B4}' . + '\x{30B6}' . + '\x{30B8}' . + '\x{30BA}' . + '\x{30BC}' . + '\x{30BE}' . + '\x{30C0}' . + '\x{30C2}' . + '\x{30C5}' . + '\x{30C7}' . + '\x{30C9}' . + '\x{30D0}-\x{30D1}' . + '\x{30D3}-\x{30D4}' . + '\x{30D6}-\x{30D7}' . + '\x{30D9}-\x{30DA}' . + '\x{30DC}-\x{30DD}' . + '\x{30F4}' . + '\x{30F7}-\x{30FA}' . + '\x{30FE}' . + '\x{AC00}-\x{D7A3}' . + '\x{F900}-\x{FA0D}' . + '\x{FA10}' . + '\x{FA12}' . + '\x{FA15}-\x{FA1E}' . + '\x{FA20}' . + '\x{FA22}' . + '\x{FA25}-\x{FA26}' . + '\x{FA2A}-\x{FA6D}' . + '\x{FA70}-\x{FAD9}' . + '\x{FB1D}' . + '\x{FB1F}' . + '\x{FB2A}-\x{FB36}' . + '\x{FB38}-\x{FB3C}' . + '\x{FB3E}' . + '\x{FB40}-\x{FB41}' . + '\x{FB43}-\x{FB44}' . + '\x{FB46}-\x{FB4E}' . + '\x{1109A}' . + '\x{1109C}' . + '\x{110AB}' . + '\x{1112E}-\x{1112F}' . + '\x{1134B}-\x{1134C}' . + '\x{114BB}-\x{114BC}' . + '\x{114BE}' . + '\x{115BA}-\x{115BB}' . + '\x{11938}' . + '\x{1D15E}-\x{1D164}' . + '\x{1D1BB}-\x{1D1C0}' . + '\x{2F800}-\x{2FA1D}', + 'NFKD_QC' => + '\x{00A0}' . + '\x{00A8}' . + '\x{00AA}' . + '\x{00AF}' . + '\x{00B2}-\x{00B5}' . + '\x{00B8}-\x{00BA}' . + '\x{00BC}-\x{00BE}' . + '\x{00C0}-\x{00C5}' . + '\x{00C7}-\x{00CF}' . + '\x{00D1}-\x{00D6}' . + '\x{00D9}-\x{00DD}' . + '\x{00E0}-\x{00E5}' . + '\x{00E7}-\x{00EF}' . + '\x{00F1}-\x{00F6}' . + '\x{00F9}-\x{00FD}' . + '\x{00FF}-\x{010F}' . + '\x{0112}-\x{0125}' . + '\x{0128}-\x{0130}' . + '\x{0132}-\x{0137}' . + '\x{0139}-\x{0140}' . + '\x{0143}-\x{0149}' . + '\x{014C}-\x{0151}' . + '\x{0154}-\x{0165}' . + '\x{0168}-\x{017F}' . + '\x{01A0}-\x{01A1}' . + '\x{01AF}-\x{01B0}' . + '\x{01C4}-\x{01DC}' . + '\x{01DE}-\x{01E3}' . + '\x{01E6}-\x{01F5}' . + '\x{01F8}-\x{021B}' . + '\x{021E}-\x{021F}' . + '\x{0226}-\x{0233}' . + '\x{02B0}-\x{02B8}' . + '\x{02D8}-\x{02DD}' . + '\x{02E0}-\x{02E4}' . + '\x{0340}-\x{0341}' . + '\x{0343}-\x{0344}' . + '\x{0374}' . + '\x{037A}' . + '\x{037E}' . + '\x{0384}-\x{038A}' . + '\x{038C}' . + '\x{038E}-\x{0390}' . + '\x{03AA}-\x{03B0}' . + '\x{03CA}-\x{03CE}' . + '\x{03D0}-\x{03D6}' . + '\x{03F0}-\x{03F2}' . + '\x{03F4}-\x{03F5}' . + '\x{03F9}' . + '\x{0400}-\x{0401}' . + '\x{0403}' . + '\x{0407}' . + '\x{040C}-\x{040E}' . + '\x{0419}' . + '\x{0439}' . + '\x{0450}-\x{0451}' . + '\x{0453}' . + '\x{0457}' . + '\x{045C}-\x{045E}' . + '\x{0476}-\x{0477}' . + '\x{04C1}-\x{04C2}' . + '\x{04D0}-\x{04D3}' . + '\x{04D6}-\x{04D7}' . + '\x{04DA}-\x{04DF}' . + '\x{04E2}-\x{04E7}' . + '\x{04EA}-\x{04F5}' . + '\x{04F8}-\x{04F9}' . + '\x{0587}' . + '\x{0622}-\x{0626}' . + '\x{0675}-\x{0678}' . + '\x{06C0}' . + '\x{06C2}' . + '\x{06D3}' . + '\x{0929}' . + '\x{0931}' . + '\x{0934}' . + '\x{0958}-\x{095F}' . + '\x{09CB}-\x{09CC}' . + '\x{09DC}-\x{09DD}' . + '\x{09DF}' . + '\x{0A33}' . + '\x{0A36}' . + '\x{0A59}-\x{0A5B}' . + '\x{0A5E}' . + '\x{0B48}' . + '\x{0B4B}-\x{0B4C}' . + '\x{0B5C}-\x{0B5D}' . + '\x{0B94}' . + '\x{0BCA}-\x{0BCC}' . + '\x{0C48}' . + '\x{0CC0}' . + '\x{0CC7}-\x{0CC8}' . + '\x{0CCA}-\x{0CCB}' . + '\x{0D4A}-\x{0D4C}' . + '\x{0DDA}' . + '\x{0DDC}-\x{0DDE}' . + '\x{0E33}' . + '\x{0EB3}' . + '\x{0EDC}-\x{0EDD}' . + '\x{0F0C}' . + '\x{0F43}' . + '\x{0F4D}' . + '\x{0F52}' . + '\x{0F57}' . + '\x{0F5C}' . + '\x{0F69}' . + '\x{0F73}' . + '\x{0F75}-\x{0F79}' . + '\x{0F81}' . + '\x{0F93}' . + '\x{0F9D}' . + '\x{0FA2}' . + '\x{0FA7}' . + '\x{0FAC}' . + '\x{0FB9}' . + '\x{1026}' . + '\x{10FC}' . + '\x{1B06}' . + '\x{1B08}' . + '\x{1B0A}' . + '\x{1B0C}' . + '\x{1B0E}' . + '\x{1B12}' . + '\x{1B3B}' . + '\x{1B3D}' . + '\x{1B40}-\x{1B41}' . + '\x{1B43}' . + '\x{1D2C}-\x{1D2E}' . + '\x{1D30}-\x{1D3A}' . + '\x{1D3C}-\x{1D4D}' . + '\x{1D4F}-\x{1D6A}' . + '\x{1D78}' . + '\x{1D9B}-\x{1DBF}' . + '\x{1E00}-\x{1E9B}' . + '\x{1EA0}-\x{1EF9}' . + '\x{1F00}-\x{1F15}' . + '\x{1F18}-\x{1F1D}' . + '\x{1F20}-\x{1F45}' . + '\x{1F48}-\x{1F4D}' . + '\x{1F50}-\x{1F57}' . + '\x{1F59}' . + '\x{1F5B}' . + '\x{1F5D}' . + '\x{1F5F}-\x{1F7D}' . + '\x{1F80}-\x{1FB4}' . + '\x{1FB6}-\x{1FC4}' . + '\x{1FC6}-\x{1FD3}' . + '\x{1FD6}-\x{1FDB}' . + '\x{1FDD}-\x{1FEF}' . + '\x{1FF2}-\x{1FF4}' . + '\x{1FF6}-\x{1FFE}' . + '\x{2000}-\x{200A}' . + '\x{2011}' . + '\x{2017}' . + '\x{2024}-\x{2026}' . + '\x{202F}' . + '\x{2033}-\x{2034}' . + '\x{2036}-\x{2037}' . + '\x{203C}' . + '\x{203E}' . + '\x{2047}-\x{2049}' . + '\x{2057}' . + '\x{205F}' . + '\x{2070}-\x{2071}' . + '\x{2074}-\x{208E}' . + '\x{2090}-\x{209C}' . + '\x{20A8}' . + '\x{2100}-\x{2103}' . + '\x{2105}-\x{2107}' . + '\x{2109}-\x{2113}' . + '\x{2115}-\x{2116}' . + '\x{2119}-\x{211D}' . + '\x{2120}-\x{2122}' . + '\x{2124}' . + '\x{2126}' . + '\x{2128}' . + '\x{212A}-\x{212D}' . + '\x{212F}-\x{2131}' . + '\x{2133}-\x{2139}' . + '\x{213B}-\x{2140}' . + '\x{2145}-\x{2149}' . + '\x{2150}-\x{217F}' . + '\x{2189}' . + '\x{219A}-\x{219B}' . + '\x{21AE}' . + '\x{21CD}-\x{21CF}' . + '\x{2204}' . + '\x{2209}' . + '\x{220C}' . + '\x{2224}' . + '\x{2226}' . + '\x{222C}-\x{222D}' . + '\x{222F}-\x{2230}' . + '\x{2241}' . + '\x{2244}' . + '\x{2247}' . + '\x{2249}' . + '\x{2260}' . + '\x{2262}' . + '\x{226D}-\x{2271}' . + '\x{2274}-\x{2275}' . + '\x{2278}-\x{2279}' . + '\x{2280}-\x{2281}' . + '\x{2284}-\x{2285}' . + '\x{2288}-\x{2289}' . + '\x{22AC}-\x{22AF}' . + '\x{22E0}-\x{22E3}' . + '\x{22EA}-\x{22ED}' . + '\x{2329}-\x{232A}' . + '\x{2460}-\x{24EA}' . + '\x{2A0C}' . + '\x{2A74}-\x{2A76}' . + '\x{2ADC}' . + '\x{2C7C}-\x{2C7D}' . + '\x{2D6F}' . + '\x{2E9F}' . + '\x{2EF3}' . + '\x{2F00}-\x{2FD5}' . + '\x{3000}' . + '\x{3036}' . + '\x{3038}-\x{303A}' . + '\x{304C}' . + '\x{304E}' . + '\x{3050}' . + '\x{3052}' . + '\x{3054}' . + '\x{3056}' . + '\x{3058}' . + '\x{305A}' . + '\x{305C}' . + '\x{305E}' . + '\x{3060}' . + '\x{3062}' . + '\x{3065}' . + '\x{3067}' . + '\x{3069}' . + '\x{3070}-\x{3071}' . + '\x{3073}-\x{3074}' . + '\x{3076}-\x{3077}' . + '\x{3079}-\x{307A}' . + '\x{307C}-\x{307D}' . + '\x{3094}' . + '\x{309B}-\x{309C}' . + '\x{309E}-\x{309F}' . + '\x{30AC}' . + '\x{30AE}' . + '\x{30B0}' . + '\x{30B2}' . + '\x{30B4}' . + '\x{30B6}' . + '\x{30B8}' . + '\x{30BA}' . + '\x{30BC}' . + '\x{30BE}' . + '\x{30C0}' . + '\x{30C2}' . + '\x{30C5}' . + '\x{30C7}' . + '\x{30C9}' . + '\x{30D0}-\x{30D1}' . + '\x{30D3}-\x{30D4}' . + '\x{30D6}-\x{30D7}' . + '\x{30D9}-\x{30DA}' . + '\x{30DC}-\x{30DD}' . + '\x{30F4}' . + '\x{30F7}-\x{30FA}' . + '\x{30FE}-\x{30FF}' . + '\x{3131}-\x{318E}' . + '\x{3192}-\x{319F}' . + '\x{3200}-\x{321E}' . + '\x{3220}-\x{3247}' . + '\x{3250}-\x{327E}' . + '\x{3280}-\x{33FF}' . + '\x{A69C}-\x{A69D}' . + '\x{A770}' . + '\x{A7F2}-\x{A7F4}' . + '\x{A7F8}-\x{A7F9}' . + '\x{AB5C}-\x{AB5F}' . + '\x{AB69}' . + '\x{AC00}-\x{D7A3}' . + '\x{F900}-\x{FA0D}' . + '\x{FA10}' . + '\x{FA12}' . + '\x{FA15}-\x{FA1E}' . + '\x{FA20}' . + '\x{FA22}' . + '\x{FA25}-\x{FA26}' . + '\x{FA2A}-\x{FA6D}' . + '\x{FA70}-\x{FAD9}' . + '\x{FB00}-\x{FB06}' . + '\x{FB13}-\x{FB17}' . + '\x{FB1D}' . + '\x{FB1F}-\x{FB36}' . + '\x{FB38}-\x{FB3C}' . + '\x{FB3E}' . + '\x{FB40}-\x{FB41}' . + '\x{FB43}-\x{FB44}' . + '\x{FB46}-\x{FBB1}' . + '\x{FBD3}-\x{FD3D}' . + '\x{FD50}-\x{FD8F}' . + '\x{FD92}-\x{FDC7}' . + '\x{FDF0}-\x{FDFC}' . + '\x{FE10}-\x{FE19}' . + '\x{FE30}-\x{FE44}' . + '\x{FE47}-\x{FE52}' . + '\x{FE54}-\x{FE66}' . + '\x{FE68}-\x{FE6B}' . + '\x{FE70}-\x{FE72}' . + '\x{FE74}' . + '\x{FE76}-\x{FEFC}' . + '\x{FF01}-\x{FFBE}' . + '\x{FFC2}-\x{FFC7}' . + '\x{FFCA}-\x{FFCF}' . + '\x{FFD2}-\x{FFD7}' . + '\x{FFDA}-\x{FFDC}' . + '\x{FFE0}-\x{FFE6}' . + '\x{FFE8}-\x{FFEE}' . + '\x{10781}-\x{10785}' . + '\x{10787}-\x{107B0}' . + '\x{107B2}-\x{107BA}' . + '\x{1109A}' . + '\x{1109C}' . + '\x{110AB}' . + '\x{1112E}-\x{1112F}' . + '\x{1134B}-\x{1134C}' . + '\x{114BB}-\x{114BC}' . + '\x{114BE}' . + '\x{115BA}-\x{115BB}' . + '\x{11938}' . + '\x{1D15E}-\x{1D164}' . + '\x{1D1BB}-\x{1D1C0}' . + '\x{1D400}-\x{1D454}' . + '\x{1D456}-\x{1D49C}' . + '\x{1D49E}-\x{1D49F}' . + '\x{1D4A2}' . + '\x{1D4A5}-\x{1D4A6}' . + '\x{1D4A9}-\x{1D4AC}' . + '\x{1D4AE}-\x{1D4B9}' . + '\x{1D4BB}' . + '\x{1D4BD}-\x{1D4C3}' . + '\x{1D4C5}-\x{1D505}' . + '\x{1D507}-\x{1D50A}' . + '\x{1D50D}-\x{1D514}' . + '\x{1D516}-\x{1D51C}' . + '\x{1D51E}-\x{1D539}' . + '\x{1D53B}-\x{1D53E}' . + '\x{1D540}-\x{1D544}' . + '\x{1D546}' . + '\x{1D54A}-\x{1D550}' . + '\x{1D552}-\x{1D6A5}' . + '\x{1D6A8}-\x{1D7CB}' . + '\x{1D7CE}-\x{1D7FF}' . + '\x{1E030}-\x{1E06D}' . + '\x{1EE00}-\x{1EE03}' . + '\x{1EE05}-\x{1EE1F}' . + '\x{1EE21}-\x{1EE22}' . + '\x{1EE24}' . + '\x{1EE27}' . + '\x{1EE29}-\x{1EE32}' . + '\x{1EE34}-\x{1EE37}' . + '\x{1EE39}' . + '\x{1EE3B}' . + '\x{1EE42}' . + '\x{1EE47}' . + '\x{1EE49}' . + '\x{1EE4B}' . + '\x{1EE4D}-\x{1EE4F}' . + '\x{1EE51}-\x{1EE52}' . + '\x{1EE54}' . + '\x{1EE57}' . + '\x{1EE59}' . + '\x{1EE5B}' . + '\x{1EE5D}' . + '\x{1EE5F}' . + '\x{1EE61}-\x{1EE62}' . + '\x{1EE64}' . + '\x{1EE67}-\x{1EE6A}' . + '\x{1EE6C}-\x{1EE72}' . + '\x{1EE74}-\x{1EE77}' . + '\x{1EE79}-\x{1EE7C}' . + '\x{1EE7E}' . + '\x{1EE80}-\x{1EE89}' . + '\x{1EE8B}-\x{1EE9B}' . + '\x{1EEA1}-\x{1EEA3}' . + '\x{1EEA5}-\x{1EEA9}' . + '\x{1EEAB}-\x{1EEBB}' . + '\x{1F100}-\x{1F10A}' . + '\x{1F110}-\x{1F12E}' . + '\x{1F130}-\x{1F14F}' . + '\x{1F16A}-\x{1F16C}' . + '\x{1F190}' . + '\x{1F200}-\x{1F202}' . + '\x{1F210}-\x{1F23B}' . + '\x{1F240}-\x{1F248}' . + '\x{1F250}-\x{1F251}' . + '\x{1FBF0}-\x{1FBF9}' . + '\x{2F800}-\x{2FA1D}', + 'Changes_When_NFKC_Casefolded' => + '\x{0041}-\x{005A}' . + '\x{00A0}' . + '\x{00A8}' . + '\x{00AA}' . + '\x{00AD}' . + '\x{00AF}' . + '\x{00B2}-\x{00B5}' . + '\x{00B8}-\x{00BA}' . + '\x{00BC}-\x{00BE}' . + '\x{00C0}-\x{00D6}' . + '\x{00D8}-\x{00DF}' . + '\x{0100}' . + '\x{0102}' . + '\x{0104}' . + '\x{0106}' . + '\x{0108}' . + '\x{010A}' . + '\x{010C}' . + '\x{010E}' . + '\x{0110}' . + '\x{0112}' . + '\x{0114}' . + '\x{0116}' . + '\x{0118}' . + '\x{011A}' . + '\x{011C}' . + '\x{011E}' . + '\x{0120}' . + '\x{0122}' . + '\x{0124}' . + '\x{0126}' . + '\x{0128}' . + '\x{012A}' . + '\x{012C}' . + '\x{012E}' . + '\x{0130}' . + '\x{0132}-\x{0134}' . + '\x{0136}' . + '\x{0139}' . + '\x{013B}' . + '\x{013D}' . + '\x{013F}-\x{0141}' . + '\x{0143}' . + '\x{0145}' . + '\x{0147}' . + '\x{0149}-\x{014A}' . + '\x{014C}' . + '\x{014E}' . + '\x{0150}' . + '\x{0152}' . + '\x{0154}' . + '\x{0156}' . + '\x{0158}' . + '\x{015A}' . + '\x{015C}' . + '\x{015E}' . + '\x{0160}' . + '\x{0162}' . + '\x{0164}' . + '\x{0166}' . + '\x{0168}' . + '\x{016A}' . + '\x{016C}' . + '\x{016E}' . + '\x{0170}' . + '\x{0172}' . + '\x{0174}' . + '\x{0176}' . + '\x{0178}-\x{0179}' . + '\x{017B}' . + '\x{017D}' . + '\x{017F}' . + '\x{0181}-\x{0182}' . + '\x{0184}' . + '\x{0186}-\x{0187}' . + '\x{0189}-\x{018B}' . + '\x{018E}-\x{0191}' . + '\x{0193}-\x{0194}' . + '\x{0196}-\x{0198}' . + '\x{019C}-\x{019D}' . + '\x{019F}-\x{01A0}' . + '\x{01A2}' . + '\x{01A4}' . + '\x{01A6}-\x{01A7}' . + '\x{01A9}' . + '\x{01AC}' . + '\x{01AE}-\x{01AF}' . + '\x{01B1}-\x{01B3}' . + '\x{01B5}' . + '\x{01B7}-\x{01B8}' . + '\x{01BC}' . + '\x{01C4}-\x{01CD}' . + '\x{01CF}' . + '\x{01D1}' . + '\x{01D3}' . + '\x{01D5}' . + '\x{01D7}' . + '\x{01D9}' . + '\x{01DB}' . + '\x{01DE}' . + '\x{01E0}' . + '\x{01E2}' . + '\x{01E4}' . + '\x{01E6}' . + '\x{01E8}' . + '\x{01EA}' . + '\x{01EC}' . + '\x{01EE}' . + '\x{01F1}-\x{01F4}' . + '\x{01F6}-\x{01F8}' . + '\x{01FA}' . + '\x{01FC}' . + '\x{01FE}' . + '\x{0200}' . + '\x{0202}' . + '\x{0204}' . + '\x{0206}' . + '\x{0208}' . + '\x{020A}' . + '\x{020C}' . + '\x{020E}' . + '\x{0210}' . + '\x{0212}' . + '\x{0214}' . + '\x{0216}' . + '\x{0218}' . + '\x{021A}' . + '\x{021C}' . + '\x{021E}' . + '\x{0220}' . + '\x{0222}' . + '\x{0224}' . + '\x{0226}' . + '\x{0228}' . + '\x{022A}' . + '\x{022C}' . + '\x{022E}' . + '\x{0230}' . + '\x{0232}' . + '\x{023A}-\x{023B}' . + '\x{023D}-\x{023E}' . + '\x{0241}' . + '\x{0243}-\x{0246}' . + '\x{0248}' . + '\x{024A}' . + '\x{024C}' . + '\x{024E}' . + '\x{02B0}-\x{02B8}' . + '\x{02D8}-\x{02DD}' . + '\x{02E0}-\x{02E4}' . + '\x{0340}-\x{0341}' . + '\x{0343}-\x{0345}' . + '\x{034F}' . + '\x{0370}' . + '\x{0372}' . + '\x{0374}' . + '\x{0376}' . + '\x{037A}' . + '\x{037E}-\x{037F}' . + '\x{0384}-\x{038A}' . + '\x{038C}' . + '\x{038E}-\x{038F}' . + '\x{0391}-\x{03A1}' . + '\x{03A3}-\x{03AB}' . + '\x{03C2}' . + '\x{03CF}-\x{03D6}' . + '\x{03D8}' . + '\x{03DA}' . + '\x{03DC}' . + '\x{03DE}' . + '\x{03E0}' . + '\x{03E2}' . + '\x{03E4}' . + '\x{03E6}' . + '\x{03E8}' . + '\x{03EA}' . + '\x{03EC}' . + '\x{03EE}' . + '\x{03F0}-\x{03F2}' . + '\x{03F4}-\x{03F5}' . + '\x{03F7}' . + '\x{03F9}-\x{03FA}' . + '\x{03FD}-\x{042F}' . + '\x{0460}' . + '\x{0462}' . + '\x{0464}' . + '\x{0466}' . + '\x{0468}' . + '\x{046A}' . + '\x{046C}' . + '\x{046E}' . + '\x{0470}' . + '\x{0472}' . + '\x{0474}' . + '\x{0476}' . + '\x{0478}' . + '\x{047A}' . + '\x{047C}' . + '\x{047E}' . + '\x{0480}' . + '\x{048A}' . + '\x{048C}' . + '\x{048E}' . + '\x{0490}' . + '\x{0492}' . + '\x{0494}' . + '\x{0496}' . + '\x{0498}' . + '\x{049A}' . + '\x{049C}' . + '\x{049E}' . + '\x{04A0}' . + '\x{04A2}' . + '\x{04A4}' . + '\x{04A6}' . + '\x{04A8}' . + '\x{04AA}' . + '\x{04AC}' . + '\x{04AE}' . + '\x{04B0}' . + '\x{04B2}' . + '\x{04B4}' . + '\x{04B6}' . + '\x{04B8}' . + '\x{04BA}' . + '\x{04BC}' . + '\x{04BE}' . + '\x{04C0}-\x{04C1}' . + '\x{04C3}' . + '\x{04C5}' . + '\x{04C7}' . + '\x{04C9}' . + '\x{04CB}' . + '\x{04CD}' . + '\x{04D0}' . + '\x{04D2}' . + '\x{04D4}' . + '\x{04D6}' . + '\x{04D8}' . + '\x{04DA}' . + '\x{04DC}' . + '\x{04DE}' . + '\x{04E0}' . + '\x{04E2}' . + '\x{04E4}' . + '\x{04E6}' . + '\x{04E8}' . + '\x{04EA}' . + '\x{04EC}' . + '\x{04EE}' . + '\x{04F0}' . + '\x{04F2}' . + '\x{04F4}' . + '\x{04F6}' . + '\x{04F8}' . + '\x{04FA}' . + '\x{04FC}' . + '\x{04FE}' . + '\x{0500}' . + '\x{0502}' . + '\x{0504}' . + '\x{0506}' . + '\x{0508}' . + '\x{050A}' . + '\x{050C}' . + '\x{050E}' . + '\x{0510}' . + '\x{0512}' . + '\x{0514}' . + '\x{0516}' . + '\x{0518}' . + '\x{051A}' . + '\x{051C}' . + '\x{051E}' . + '\x{0520}' . + '\x{0522}' . + '\x{0524}' . + '\x{0526}' . + '\x{0528}' . + '\x{052A}' . + '\x{052C}' . + '\x{052E}' . + '\x{0531}-\x{0556}' . + '\x{0587}' . + '\x{061C}' . + '\x{0675}-\x{0678}' . + '\x{0958}-\x{095F}' . + '\x{09DC}-\x{09DD}' . + '\x{09DF}' . + '\x{0A33}' . + '\x{0A36}' . + '\x{0A59}-\x{0A5B}' . + '\x{0A5E}' . + '\x{0B5C}-\x{0B5D}' . + '\x{0E33}' . + '\x{0EB3}' . + '\x{0EDC}-\x{0EDD}' . + '\x{0F0C}' . + '\x{0F43}' . + '\x{0F4D}' . + '\x{0F52}' . + '\x{0F57}' . + '\x{0F5C}' . + '\x{0F69}' . + '\x{0F73}' . + '\x{0F75}-\x{0F79}' . + '\x{0F81}' . + '\x{0F93}' . + '\x{0F9D}' . + '\x{0FA2}' . + '\x{0FA7}' . + '\x{0FAC}' . + '\x{0FB9}' . + '\x{10A0}-\x{10C5}' . + '\x{10C7}' . + '\x{10CD}' . + '\x{10FC}' . + '\x{115F}-\x{1160}' . + '\x{13F8}-\x{13FD}' . + '\x{17B4}-\x{17B5}' . + '\x{180B}-\x{180F}' . + '\x{1C80}-\x{1C88}' . + '\x{1C90}-\x{1CBA}' . + '\x{1CBD}-\x{1CBF}' . + '\x{1D2C}-\x{1D2E}' . + '\x{1D30}-\x{1D3A}' . + '\x{1D3C}-\x{1D4D}' . + '\x{1D4F}-\x{1D6A}' . + '\x{1D78}' . + '\x{1D9B}-\x{1DBF}' . + '\x{1E00}' . + '\x{1E02}' . + '\x{1E04}' . + '\x{1E06}' . + '\x{1E08}' . + '\x{1E0A}' . + '\x{1E0C}' . + '\x{1E0E}' . + '\x{1E10}' . + '\x{1E12}' . + '\x{1E14}' . + '\x{1E16}' . + '\x{1E18}' . + '\x{1E1A}' . + '\x{1E1C}' . + '\x{1E1E}' . + '\x{1E20}' . + '\x{1E22}' . + '\x{1E24}' . + '\x{1E26}' . + '\x{1E28}' . + '\x{1E2A}' . + '\x{1E2C}' . + '\x{1E2E}' . + '\x{1E30}' . + '\x{1E32}' . + '\x{1E34}' . + '\x{1E36}' . + '\x{1E38}' . + '\x{1E3A}' . + '\x{1E3C}' . + '\x{1E3E}' . + '\x{1E40}' . + '\x{1E42}' . + '\x{1E44}' . + '\x{1E46}' . + '\x{1E48}' . + '\x{1E4A}' . + '\x{1E4C}' . + '\x{1E4E}' . + '\x{1E50}' . + '\x{1E52}' . + '\x{1E54}' . + '\x{1E56}' . + '\x{1E58}' . + '\x{1E5A}' . + '\x{1E5C}' . + '\x{1E5E}' . + '\x{1E60}' . + '\x{1E62}' . + '\x{1E64}' . + '\x{1E66}' . + '\x{1E68}' . + '\x{1E6A}' . + '\x{1E6C}' . + '\x{1E6E}' . + '\x{1E70}' . + '\x{1E72}' . + '\x{1E74}' . + '\x{1E76}' . + '\x{1E78}' . + '\x{1E7A}' . + '\x{1E7C}' . + '\x{1E7E}' . + '\x{1E80}' . + '\x{1E82}' . + '\x{1E84}' . + '\x{1E86}' . + '\x{1E88}' . + '\x{1E8A}' . + '\x{1E8C}' . + '\x{1E8E}' . + '\x{1E90}' . + '\x{1E92}' . + '\x{1E94}' . + '\x{1E9A}-\x{1E9B}' . + '\x{1E9E}' . + '\x{1EA0}' . + '\x{1EA2}' . + '\x{1EA4}' . + '\x{1EA6}' . + '\x{1EA8}' . + '\x{1EAA}' . + '\x{1EAC}' . + '\x{1EAE}' . + '\x{1EB0}' . + '\x{1EB2}' . + '\x{1EB4}' . + '\x{1EB6}' . + '\x{1EB8}' . + '\x{1EBA}' . + '\x{1EBC}' . + '\x{1EBE}' . + '\x{1EC0}' . + '\x{1EC2}' . + '\x{1EC4}' . + '\x{1EC6}' . + '\x{1EC8}' . + '\x{1ECA}' . + '\x{1ECC}' . + '\x{1ECE}' . + '\x{1ED0}' . + '\x{1ED2}' . + '\x{1ED4}' . + '\x{1ED6}' . + '\x{1ED8}' . + '\x{1EDA}' . + '\x{1EDC}' . + '\x{1EDE}' . + '\x{1EE0}' . + '\x{1EE2}' . + '\x{1EE4}' . + '\x{1EE6}' . + '\x{1EE8}' . + '\x{1EEA}' . + '\x{1EEC}' . + '\x{1EEE}' . + '\x{1EF0}' . + '\x{1EF2}' . + '\x{1EF4}' . + '\x{1EF6}' . + '\x{1EF8}' . + '\x{1EFA}' . + '\x{1EFC}' . + '\x{1EFE}' . + '\x{1F08}-\x{1F0F}' . + '\x{1F18}-\x{1F1D}' . + '\x{1F28}-\x{1F2F}' . + '\x{1F38}-\x{1F3F}' . + '\x{1F48}-\x{1F4D}' . + '\x{1F59}' . + '\x{1F5B}' . + '\x{1F5D}' . + '\x{1F5F}' . + '\x{1F68}-\x{1F6F}' . + '\x{1F71}' . + '\x{1F73}' . + '\x{1F75}' . + '\x{1F77}' . + '\x{1F79}' . + '\x{1F7B}' . + '\x{1F7D}' . + '\x{1F80}-\x{1FAF}' . + '\x{1FB2}-\x{1FB4}' . + '\x{1FB7}-\x{1FC4}' . + '\x{1FC7}-\x{1FCF}' . + '\x{1FD3}' . + '\x{1FD8}-\x{1FDB}' . + '\x{1FDD}-\x{1FDF}' . + '\x{1FE3}' . + '\x{1FE8}-\x{1FEF}' . + '\x{1FF2}-\x{1FF4}' . + '\x{1FF7}-\x{1FFE}' . + '\x{2000}-\x{200F}' . + '\x{2011}' . + '\x{2017}' . + '\x{2024}-\x{2026}' . + '\x{202A}-\x{202F}' . + '\x{2033}-\x{2034}' . + '\x{2036}-\x{2037}' . + '\x{203C}' . + '\x{203E}' . + '\x{2047}-\x{2049}' . + '\x{2057}' . + '\x{205F}-\x{2071}' . + '\x{2074}-\x{208E}' . + '\x{2090}-\x{209C}' . + '\x{20A8}' . + '\x{2100}-\x{2103}' . + '\x{2105}-\x{2107}' . + '\x{2109}-\x{2113}' . + '\x{2115}-\x{2116}' . + '\x{2119}-\x{211D}' . + '\x{2120}-\x{2122}' . + '\x{2124}' . + '\x{2126}' . + '\x{2128}' . + '\x{212A}-\x{212D}' . + '\x{212F}-\x{2139}' . + '\x{213B}-\x{2140}' . + '\x{2145}-\x{2149}' . + '\x{2150}-\x{217F}' . + '\x{2183}' . + '\x{2189}' . + '\x{222C}-\x{222D}' . + '\x{222F}-\x{2230}' . + '\x{2329}-\x{232A}' . + '\x{2460}-\x{24EA}' . + '\x{2A0C}' . + '\x{2A74}-\x{2A76}' . + '\x{2ADC}' . + '\x{2C00}-\x{2C2F}' . + '\x{2C60}' . + '\x{2C62}-\x{2C64}' . + '\x{2C67}' . + '\x{2C69}' . + '\x{2C6B}' . + '\x{2C6D}-\x{2C70}' . + '\x{2C72}' . + '\x{2C75}' . + '\x{2C7C}-\x{2C80}' . + '\x{2C82}' . + '\x{2C84}' . + '\x{2C86}' . + '\x{2C88}' . + '\x{2C8A}' . + '\x{2C8C}' . + '\x{2C8E}' . + '\x{2C90}' . + '\x{2C92}' . + '\x{2C94}' . + '\x{2C96}' . + '\x{2C98}' . + '\x{2C9A}' . + '\x{2C9C}' . + '\x{2C9E}' . + '\x{2CA0}' . + '\x{2CA2}' . + '\x{2CA4}' . + '\x{2CA6}' . + '\x{2CA8}' . + '\x{2CAA}' . + '\x{2CAC}' . + '\x{2CAE}' . + '\x{2CB0}' . + '\x{2CB2}' . + '\x{2CB4}' . + '\x{2CB6}' . + '\x{2CB8}' . + '\x{2CBA}' . + '\x{2CBC}' . + '\x{2CBE}' . + '\x{2CC0}' . + '\x{2CC2}' . + '\x{2CC4}' . + '\x{2CC6}' . + '\x{2CC8}' . + '\x{2CCA}' . + '\x{2CCC}' . + '\x{2CCE}' . + '\x{2CD0}' . + '\x{2CD2}' . + '\x{2CD4}' . + '\x{2CD6}' . + '\x{2CD8}' . + '\x{2CDA}' . + '\x{2CDC}' . + '\x{2CDE}' . + '\x{2CE0}' . + '\x{2CE2}' . + '\x{2CEB}' . + '\x{2CED}' . + '\x{2CF2}' . + '\x{2D6F}' . + '\x{2E9F}' . + '\x{2EF3}' . + '\x{2F00}-\x{2FD5}' . + '\x{3000}' . + '\x{3036}' . + '\x{3038}-\x{303A}' . + '\x{309B}-\x{309C}' . + '\x{309F}' . + '\x{30FF}' . + '\x{3131}-\x{318E}' . + '\x{3192}-\x{319F}' . + '\x{3200}-\x{321E}' . + '\x{3220}-\x{3247}' . + '\x{3250}-\x{327E}' . + '\x{3280}-\x{33FF}' . + '\x{A640}' . + '\x{A642}' . + '\x{A644}' . + '\x{A646}' . + '\x{A648}' . + '\x{A64A}' . + '\x{A64C}' . + '\x{A64E}' . + '\x{A650}' . + '\x{A652}' . + '\x{A654}' . + '\x{A656}' . + '\x{A658}' . + '\x{A65A}' . + '\x{A65C}' . + '\x{A65E}' . + '\x{A660}' . + '\x{A662}' . + '\x{A664}' . + '\x{A666}' . + '\x{A668}' . + '\x{A66A}' . + '\x{A66C}' . + '\x{A680}' . + '\x{A682}' . + '\x{A684}' . + '\x{A686}' . + '\x{A688}' . + '\x{A68A}' . + '\x{A68C}' . + '\x{A68E}' . + '\x{A690}' . + '\x{A692}' . + '\x{A694}' . + '\x{A696}' . + '\x{A698}' . + '\x{A69A}' . + '\x{A69C}-\x{A69D}' . + '\x{A722}' . + '\x{A724}' . + '\x{A726}' . + '\x{A728}' . + '\x{A72A}' . + '\x{A72C}' . + '\x{A72E}' . + '\x{A732}' . + '\x{A734}' . + '\x{A736}' . + '\x{A738}' . + '\x{A73A}' . + '\x{A73C}' . + '\x{A73E}' . + '\x{A740}' . + '\x{A742}' . + '\x{A744}' . + '\x{A746}' . + '\x{A748}' . + '\x{A74A}' . + '\x{A74C}' . + '\x{A74E}' . + '\x{A750}' . + '\x{A752}' . + '\x{A754}' . + '\x{A756}' . + '\x{A758}' . + '\x{A75A}' . + '\x{A75C}' . + '\x{A75E}' . + '\x{A760}' . + '\x{A762}' . + '\x{A764}' . + '\x{A766}' . + '\x{A768}' . + '\x{A76A}' . + '\x{A76C}' . + '\x{A76E}' . + '\x{A770}' . + '\x{A779}' . + '\x{A77B}' . + '\x{A77D}-\x{A77E}' . + '\x{A780}' . + '\x{A782}' . + '\x{A784}' . + '\x{A786}' . + '\x{A78B}' . + '\x{A78D}' . + '\x{A790}' . + '\x{A792}' . + '\x{A796}' . + '\x{A798}' . + '\x{A79A}' . + '\x{A79C}' . + '\x{A79E}' . + '\x{A7A0}' . + '\x{A7A2}' . + '\x{A7A4}' . + '\x{A7A6}' . + '\x{A7A8}' . + '\x{A7AA}-\x{A7AE}' . + '\x{A7B0}-\x{A7B4}' . + '\x{A7B6}' . + '\x{A7B8}' . + '\x{A7BA}' . + '\x{A7BC}' . + '\x{A7BE}' . + '\x{A7C0}' . + '\x{A7C2}' . + '\x{A7C4}-\x{A7C7}' . + '\x{A7C9}' . + '\x{A7D0}' . + '\x{A7D6}' . + '\x{A7D8}' . + '\x{A7F2}-\x{A7F5}' . + '\x{A7F8}-\x{A7F9}' . + '\x{AB5C}-\x{AB5F}' . + '\x{AB69}' . + '\x{AB70}-\x{ABBF}' . + '\x{F900}-\x{FA0D}' . + '\x{FA10}' . + '\x{FA12}' . + '\x{FA15}-\x{FA1E}' . + '\x{FA20}' . + '\x{FA22}' . + '\x{FA25}-\x{FA26}' . + '\x{FA2A}-\x{FA6D}' . + '\x{FA70}-\x{FAD9}' . + '\x{FB00}-\x{FB06}' . + '\x{FB13}-\x{FB17}' . + '\x{FB1D}' . + '\x{FB1F}-\x{FB36}' . + '\x{FB38}-\x{FB3C}' . + '\x{FB3E}' . + '\x{FB40}-\x{FB41}' . + '\x{FB43}-\x{FB44}' . + '\x{FB46}-\x{FBB1}' . + '\x{FBD3}-\x{FD3D}' . + '\x{FD50}-\x{FD8F}' . + '\x{FD92}-\x{FDC7}' . + '\x{FDF0}-\x{FDFC}' . + '\x{FE00}-\x{FE19}' . + '\x{FE30}-\x{FE44}' . + '\x{FE47}-\x{FE52}' . + '\x{FE54}-\x{FE66}' . + '\x{FE68}-\x{FE6B}' . + '\x{FE70}-\x{FE72}' . + '\x{FE74}' . + '\x{FE76}-\x{FEFC}' . + '\x{FEFF}' . + '\x{FF01}-\x{FFBE}' . + '\x{FFC2}-\x{FFC7}' . + '\x{FFCA}-\x{FFCF}' . + '\x{FFD2}-\x{FFD7}' . + '\x{FFDA}-\x{FFDC}' . + '\x{FFE0}-\x{FFE6}' . + '\x{FFE8}-\x{FFEE}' . + '\x{FFF0}-\x{FFF8}' . + '\x{10400}-\x{10427}' . + '\x{104B0}-\x{104D3}' . + '\x{10570}-\x{1057A}' . + '\x{1057C}-\x{1058A}' . + '\x{1058C}-\x{10592}' . + '\x{10594}-\x{10595}' . + '\x{10781}-\x{10785}' . + '\x{10787}-\x{107B0}' . + '\x{107B2}-\x{107BA}' . + '\x{10C80}-\x{10CB2}' . + '\x{118A0}-\x{118BF}' . + '\x{16E40}-\x{16E5F}' . + '\x{1BCA0}-\x{1BCA3}' . + '\x{1D15E}-\x{1D164}' . + '\x{1D173}-\x{1D17A}' . + '\x{1D1BB}-\x{1D1C0}' . + '\x{1D400}-\x{1D454}' . + '\x{1D456}-\x{1D49C}' . + '\x{1D49E}-\x{1D49F}' . + '\x{1D4A2}' . + '\x{1D4A5}-\x{1D4A6}' . + '\x{1D4A9}-\x{1D4AC}' . + '\x{1D4AE}-\x{1D4B9}' . + '\x{1D4BB}' . + '\x{1D4BD}-\x{1D4C3}' . + '\x{1D4C5}-\x{1D505}' . + '\x{1D507}-\x{1D50A}' . + '\x{1D50D}-\x{1D514}' . + '\x{1D516}-\x{1D51C}' . + '\x{1D51E}-\x{1D539}' . + '\x{1D53B}-\x{1D53E}' . + '\x{1D540}-\x{1D544}' . + '\x{1D546}' . + '\x{1D54A}-\x{1D550}' . + '\x{1D552}-\x{1D6A5}' . + '\x{1D6A8}-\x{1D7CB}' . + '\x{1D7CE}-\x{1D7FF}' . + '\x{1E030}-\x{1E06D}' . + '\x{1E900}-\x{1E921}' . + '\x{1EE00}-\x{1EE03}' . + '\x{1EE05}-\x{1EE1F}' . + '\x{1EE21}-\x{1EE22}' . + '\x{1EE24}' . + '\x{1EE27}' . + '\x{1EE29}-\x{1EE32}' . + '\x{1EE34}-\x{1EE37}' . + '\x{1EE39}' . + '\x{1EE3B}' . + '\x{1EE42}' . + '\x{1EE47}' . + '\x{1EE49}' . + '\x{1EE4B}' . + '\x{1EE4D}-\x{1EE4F}' . + '\x{1EE51}-\x{1EE52}' . + '\x{1EE54}' . + '\x{1EE57}' . + '\x{1EE59}' . + '\x{1EE5B}' . + '\x{1EE5D}' . + '\x{1EE5F}' . + '\x{1EE61}-\x{1EE62}' . + '\x{1EE64}' . + '\x{1EE67}-\x{1EE6A}' . + '\x{1EE6C}-\x{1EE72}' . + '\x{1EE74}-\x{1EE77}' . + '\x{1EE79}-\x{1EE7C}' . + '\x{1EE7E}' . + '\x{1EE80}-\x{1EE89}' . + '\x{1EE8B}-\x{1EE9B}' . + '\x{1EEA1}-\x{1EEA3}' . + '\x{1EEA5}-\x{1EEA9}' . + '\x{1EEAB}-\x{1EEBB}' . + '\x{1F100}-\x{1F10A}' . + '\x{1F110}-\x{1F12E}' . + '\x{1F130}-\x{1F14F}' . + '\x{1F16A}-\x{1F16C}' . + '\x{1F190}' . + '\x{1F200}-\x{1F202}' . + '\x{1F210}-\x{1F23B}' . + '\x{1F240}-\x{1F248}' . + '\x{1F250}-\x{1F251}' . + '\x{1FBF0}-\x{1FBF9}' . + '\x{2F800}-\x{2FA1D}' . + '\x{E0000}-\x{E0FFF}', + ); +} + +?> \ No newline at end of file diff --git a/Sources/Unicode/RegularExpressions.php b/Sources/Unicode/RegularExpressions.php new file mode 100644 index 0000000..7a360b7 --- /dev/null +++ b/Sources/Unicode/RegularExpressions.php @@ -0,0 +1,4524 @@ + + '\x{061C}' . + '\x{200E}-\x{200F}' . + '\x{202A}-\x{202E}' . + '\x{2066}-\x{2069}', + 'Case_Ignorable' => + '\x{0027}' . + '\x{002E}' . + '\x{003A}' . + '\x{005E}' . + '\x{0060}' . + '\x{00A8}' . + '\x{00AD}' . + '\x{00AF}' . + '\x{00B4}' . + '\x{00B7}' . + '\x{00B8}' . + '\x{02B0}-\x{02C1}' . + '\x{02C2}-\x{02C5}' . + '\x{02C6}-\x{02D1}' . + '\x{02D2}-\x{02DF}' . + '\x{02E0}-\x{02E4}' . + '\x{02E5}-\x{02EB}' . + '\x{02EC}' . + '\x{02ED}' . + '\x{02EE}' . + '\x{02EF}-\x{02FF}' . + '\x{0300}-\x{036F}' . + '\x{0374}' . + '\x{0375}' . + '\x{037A}' . + '\x{0384}-\x{0385}' . + '\x{0387}' . + '\x{0483}-\x{0487}' . + '\x{0488}-\x{0489}' . + '\x{0559}' . + '\x{055F}' . + '\x{0591}-\x{05BD}' . + '\x{05BF}' . + '\x{05C1}-\x{05C2}' . + '\x{05C4}-\x{05C5}' . + '\x{05C7}' . + '\x{05F4}' . + '\x{0600}-\x{0605}' . + '\x{0610}-\x{061A}' . + '\x{061C}' . + '\x{0640}' . + '\x{064B}-\x{065F}' . + '\x{0670}' . + '\x{06D6}-\x{06DC}' . + '\x{06DD}' . + '\x{06DF}-\x{06E4}' . + '\x{06E5}-\x{06E6}' . + '\x{06E7}-\x{06E8}' . + '\x{06EA}-\x{06ED}' . + '\x{070F}' . + '\x{0711}' . + '\x{0730}-\x{074A}' . + '\x{07A6}-\x{07B0}' . + '\x{07EB}-\x{07F3}' . + '\x{07F4}-\x{07F5}' . + '\x{07FA}' . + '\x{07FD}' . + '\x{0816}-\x{0819}' . + '\x{081A}' . + '\x{081B}-\x{0823}' . + '\x{0824}' . + '\x{0825}-\x{0827}' . + '\x{0828}' . + '\x{0829}-\x{082D}' . + '\x{0859}-\x{085B}' . + '\x{0888}' . + '\x{0890}-\x{0891}' . + '\x{0898}-\x{089F}' . + '\x{08C9}' . + '\x{08CA}-\x{08E1}' . + '\x{08E2}' . + '\x{08E3}-\x{0902}' . + '\x{093A}' . + '\x{093C}' . + '\x{0941}-\x{0948}' . + '\x{094D}' . + '\x{0951}-\x{0957}' . + '\x{0962}-\x{0963}' . + '\x{0971}' . + '\x{0981}' . + '\x{09BC}' . + '\x{09C1}-\x{09C4}' . + '\x{09CD}' . + '\x{09E2}-\x{09E3}' . + '\x{09FE}' . + '\x{0A01}-\x{0A02}' . + '\x{0A3C}' . + '\x{0A41}-\x{0A42}' . + '\x{0A47}-\x{0A48}' . + '\x{0A4B}-\x{0A4D}' . + '\x{0A51}' . + '\x{0A70}-\x{0A71}' . + '\x{0A75}' . + '\x{0A81}-\x{0A82}' . + '\x{0ABC}' . + '\x{0AC1}-\x{0AC5}' . + '\x{0AC7}-\x{0AC8}' . + '\x{0ACD}' . + '\x{0AE2}-\x{0AE3}' . + '\x{0AFA}-\x{0AFF}' . + '\x{0B01}' . + '\x{0B3C}' . + '\x{0B3F}' . + '\x{0B41}-\x{0B44}' . + '\x{0B4D}' . + '\x{0B55}-\x{0B56}' . + '\x{0B62}-\x{0B63}' . + '\x{0B82}' . + '\x{0BC0}' . + '\x{0BCD}' . + '\x{0C00}' . + '\x{0C04}' . + '\x{0C3C}' . + '\x{0C3E}-\x{0C40}' . + '\x{0C46}-\x{0C48}' . + '\x{0C4A}-\x{0C4D}' . + '\x{0C55}-\x{0C56}' . + '\x{0C62}-\x{0C63}' . + '\x{0C81}' . + '\x{0CBC}' . + '\x{0CBF}' . + '\x{0CC6}' . + '\x{0CCC}-\x{0CCD}' . + '\x{0CE2}-\x{0CE3}' . + '\x{0D00}-\x{0D01}' . + '\x{0D3B}-\x{0D3C}' . + '\x{0D41}-\x{0D44}' . + '\x{0D4D}' . + '\x{0D62}-\x{0D63}' . + '\x{0D81}' . + '\x{0DCA}' . + '\x{0DD2}-\x{0DD4}' . + '\x{0DD6}' . + '\x{0E31}' . + '\x{0E34}-\x{0E3A}' . + '\x{0E46}' . + '\x{0E47}-\x{0E4E}' . + '\x{0EB1}' . + '\x{0EB4}-\x{0EBC}' . + '\x{0EC6}' . + '\x{0EC8}-\x{0ECE}' . + '\x{0F18}-\x{0F19}' . + '\x{0F35}' . + '\x{0F37}' . + '\x{0F39}' . + '\x{0F71}-\x{0F7E}' . + '\x{0F80}-\x{0F84}' . + '\x{0F86}-\x{0F87}' . + '\x{0F8D}-\x{0F97}' . + '\x{0F99}-\x{0FBC}' . + '\x{0FC6}' . + '\x{102D}-\x{1030}' . + '\x{1032}-\x{1037}' . + '\x{1039}-\x{103A}' . + '\x{103D}-\x{103E}' . + '\x{1058}-\x{1059}' . + '\x{105E}-\x{1060}' . + '\x{1071}-\x{1074}' . + '\x{1082}' . + '\x{1085}-\x{1086}' . + '\x{108D}' . + '\x{109D}' . + '\x{10FC}' . + '\x{135D}-\x{135F}' . + '\x{1712}-\x{1714}' . + '\x{1732}-\x{1733}' . + '\x{1752}-\x{1753}' . + '\x{1772}-\x{1773}' . + '\x{17B4}-\x{17B5}' . + '\x{17B7}-\x{17BD}' . + '\x{17C6}' . + '\x{17C9}-\x{17D3}' . + '\x{17D7}' . + '\x{17DD}' . + '\x{180B}-\x{180D}' . + '\x{180E}' . + '\x{180F}' . + '\x{1843}' . + '\x{1885}-\x{1886}' . + '\x{18A9}' . + '\x{1920}-\x{1922}' . + '\x{1927}-\x{1928}' . + '\x{1932}' . + '\x{1939}-\x{193B}' . + '\x{1A17}-\x{1A18}' . + '\x{1A1B}' . + '\x{1A56}' . + '\x{1A58}-\x{1A5E}' . + '\x{1A60}' . + '\x{1A62}' . + '\x{1A65}-\x{1A6C}' . + '\x{1A73}-\x{1A7C}' . + '\x{1A7F}' . + '\x{1AA7}' . + '\x{1AB0}-\x{1ABD}' . + '\x{1ABE}' . + '\x{1ABF}-\x{1ACE}' . + '\x{1B00}-\x{1B03}' . + '\x{1B34}' . + '\x{1B36}-\x{1B3A}' . + '\x{1B3C}' . + '\x{1B42}' . + '\x{1B6B}-\x{1B73}' . + '\x{1B80}-\x{1B81}' . + '\x{1BA2}-\x{1BA5}' . + '\x{1BA8}-\x{1BA9}' . + '\x{1BAB}-\x{1BAD}' . + '\x{1BE6}' . + '\x{1BE8}-\x{1BE9}' . + '\x{1BED}' . + '\x{1BEF}-\x{1BF1}' . + '\x{1C2C}-\x{1C33}' . + '\x{1C36}-\x{1C37}' . + '\x{1C78}-\x{1C7D}' . + '\x{1CD0}-\x{1CD2}' . + '\x{1CD4}-\x{1CE0}' . + '\x{1CE2}-\x{1CE8}' . + '\x{1CED}' . + '\x{1CF4}' . + '\x{1CF8}-\x{1CF9}' . + '\x{1D2C}-\x{1D6A}' . + '\x{1D78}' . + '\x{1D9B}-\x{1DBF}' . + '\x{1DC0}-\x{1DFF}' . + '\x{1FBD}' . + '\x{1FBF}-\x{1FC1}' . + '\x{1FCD}-\x{1FCF}' . + '\x{1FDD}-\x{1FDF}' . + '\x{1FED}-\x{1FEF}' . + '\x{1FFD}-\x{1FFE}' . + '\x{200B}-\x{200F}' . + '\x{2018}' . + '\x{2019}' . + '\x{2024}' . + '\x{2027}' . + '\x{202A}-\x{202E}' . + '\x{2060}-\x{2064}' . + '\x{2066}-\x{206F}' . + '\x{2071}' . + '\x{207F}' . + '\x{2090}-\x{209C}' . + '\x{20D0}-\x{20DC}' . + '\x{20DD}-\x{20E0}' . + '\x{20E1}' . + '\x{20E2}-\x{20E4}' . + '\x{20E5}-\x{20F0}' . + '\x{2C7C}-\x{2C7D}' . + '\x{2CEF}-\x{2CF1}' . + '\x{2D6F}' . + '\x{2D7F}' . + '\x{2DE0}-\x{2DFF}' . + '\x{2E2F}' . + '\x{3005}' . + '\x{302A}-\x{302D}' . + '\x{3031}-\x{3035}' . + '\x{303B}' . + '\x{3099}-\x{309A}' . + '\x{309B}-\x{309C}' . + '\x{309D}-\x{309E}' . + '\x{30FC}-\x{30FE}' . + '\x{A015}' . + '\x{A4F8}-\x{A4FD}' . + '\x{A60C}' . + '\x{A66F}' . + '\x{A670}-\x{A672}' . + '\x{A674}-\x{A67D}' . + '\x{A67F}' . + '\x{A69C}-\x{A69D}' . + '\x{A69E}-\x{A69F}' . + '\x{A6F0}-\x{A6F1}' . + '\x{A700}-\x{A716}' . + '\x{A717}-\x{A71F}' . + '\x{A720}-\x{A721}' . + '\x{A770}' . + '\x{A788}' . + '\x{A789}-\x{A78A}' . + '\x{A7F2}-\x{A7F4}' . + '\x{A7F8}-\x{A7F9}' . + '\x{A802}' . + '\x{A806}' . + '\x{A80B}' . + '\x{A825}-\x{A826}' . + '\x{A82C}' . + '\x{A8C4}-\x{A8C5}' . + '\x{A8E0}-\x{A8F1}' . + '\x{A8FF}' . + '\x{A926}-\x{A92D}' . + '\x{A947}-\x{A951}' . + '\x{A980}-\x{A982}' . + '\x{A9B3}' . + '\x{A9B6}-\x{A9B9}' . + '\x{A9BC}-\x{A9BD}' . + '\x{A9CF}' . + '\x{A9E5}' . + '\x{A9E6}' . + '\x{AA29}-\x{AA2E}' . + '\x{AA31}-\x{AA32}' . + '\x{AA35}-\x{AA36}' . + '\x{AA43}' . + '\x{AA4C}' . + '\x{AA70}' . + '\x{AA7C}' . + '\x{AAB0}' . + '\x{AAB2}-\x{AAB4}' . + '\x{AAB7}-\x{AAB8}' . + '\x{AABE}-\x{AABF}' . + '\x{AAC1}' . + '\x{AADD}' . + '\x{AAEC}-\x{AAED}' . + '\x{AAF3}-\x{AAF4}' . + '\x{AAF6}' . + '\x{AB5B}' . + '\x{AB5C}-\x{AB5F}' . + '\x{AB69}' . + '\x{AB6A}-\x{AB6B}' . + '\x{ABE5}' . + '\x{ABE8}' . + '\x{ABED}' . + '\x{FB1E}' . + '\x{FBB2}-\x{FBC2}' . + '\x{FE00}-\x{FE0F}' . + '\x{FE13}' . + '\x{FE20}-\x{FE2F}' . + '\x{FE52}' . + '\x{FE55}' . + '\x{FEFF}' . + '\x{FF07}' . + '\x{FF0E}' . + '\x{FF1A}' . + '\x{FF3E}' . + '\x{FF40}' . + '\x{FF70}' . + '\x{FF9E}-\x{FF9F}' . + '\x{FFE3}' . + '\x{FFF9}-\x{FFFB}' . + '\x{101FD}' . + '\x{102E0}' . + '\x{10376}-\x{1037A}' . + '\x{10780}-\x{10785}' . + '\x{10787}-\x{107B0}' . + '\x{107B2}-\x{107BA}' . + '\x{10A01}-\x{10A03}' . + '\x{10A05}-\x{10A06}' . + '\x{10A0C}-\x{10A0F}' . + '\x{10A38}-\x{10A3A}' . + '\x{10A3F}' . + '\x{10AE5}-\x{10AE6}' . + '\x{10D24}-\x{10D27}' . + '\x{10EAB}-\x{10EAC}' . + '\x{10EFD}-\x{10EFF}' . + '\x{10F46}-\x{10F50}' . + '\x{10F82}-\x{10F85}' . + '\x{11001}' . + '\x{11038}-\x{11046}' . + '\x{11070}' . + '\x{11073}-\x{11074}' . + '\x{1107F}-\x{11081}' . + '\x{110B3}-\x{110B6}' . + '\x{110B9}-\x{110BA}' . + '\x{110BD}' . + '\x{110C2}' . + '\x{110CD}' . + '\x{11100}-\x{11102}' . + '\x{11127}-\x{1112B}' . + '\x{1112D}-\x{11134}' . + '\x{11173}' . + '\x{11180}-\x{11181}' . + '\x{111B6}-\x{111BE}' . + '\x{111C9}-\x{111CC}' . + '\x{111CF}' . + '\x{1122F}-\x{11231}' . + '\x{11234}' . + '\x{11236}-\x{11237}' . + '\x{1123E}' . + '\x{11241}' . + '\x{112DF}' . + '\x{112E3}-\x{112EA}' . + '\x{11300}-\x{11301}' . + '\x{1133B}-\x{1133C}' . + '\x{11340}' . + '\x{11366}-\x{1136C}' . + '\x{11370}-\x{11374}' . + '\x{11438}-\x{1143F}' . + '\x{11442}-\x{11444}' . + '\x{11446}' . + '\x{1145E}' . + '\x{114B3}-\x{114B8}' . + '\x{114BA}' . + '\x{114BF}-\x{114C0}' . + '\x{114C2}-\x{114C3}' . + '\x{115B2}-\x{115B5}' . + '\x{115BC}-\x{115BD}' . + '\x{115BF}-\x{115C0}' . + '\x{115DC}-\x{115DD}' . + '\x{11633}-\x{1163A}' . + '\x{1163D}' . + '\x{1163F}-\x{11640}' . + '\x{116AB}' . + '\x{116AD}' . + '\x{116B0}-\x{116B5}' . + '\x{116B7}' . + '\x{1171D}-\x{1171F}' . + '\x{11722}-\x{11725}' . + '\x{11727}-\x{1172B}' . + '\x{1182F}-\x{11837}' . + '\x{11839}-\x{1183A}' . + '\x{1193B}-\x{1193C}' . + '\x{1193E}' . + '\x{11943}' . + '\x{119D4}-\x{119D7}' . + '\x{119DA}-\x{119DB}' . + '\x{119E0}' . + '\x{11A01}-\x{11A0A}' . + '\x{11A33}-\x{11A38}' . + '\x{11A3B}-\x{11A3E}' . + '\x{11A47}' . + '\x{11A51}-\x{11A56}' . + '\x{11A59}-\x{11A5B}' . + '\x{11A8A}-\x{11A96}' . + '\x{11A98}-\x{11A99}' . + '\x{11C30}-\x{11C36}' . + '\x{11C38}-\x{11C3D}' . + '\x{11C3F}' . + '\x{11C92}-\x{11CA7}' . + '\x{11CAA}-\x{11CB0}' . + '\x{11CB2}-\x{11CB3}' . + '\x{11CB5}-\x{11CB6}' . + '\x{11D31}-\x{11D36}' . + '\x{11D3A}' . + '\x{11D3C}-\x{11D3D}' . + '\x{11D3F}-\x{11D45}' . + '\x{11D47}' . + '\x{11D90}-\x{11D91}' . + '\x{11D95}' . + '\x{11D97}' . + '\x{11EF3}-\x{11EF4}' . + '\x{11F00}-\x{11F01}' . + '\x{11F36}-\x{11F3A}' . + '\x{11F40}' . + '\x{11F42}' . + '\x{13430}-\x{1343F}' . + '\x{13440}' . + '\x{13447}-\x{13455}' . + '\x{16AF0}-\x{16AF4}' . + '\x{16B30}-\x{16B36}' . + '\x{16B40}-\x{16B43}' . + '\x{16F4F}' . + '\x{16F8F}-\x{16F92}' . + '\x{16F93}-\x{16F9F}' . + '\x{16FE0}-\x{16FE1}' . + '\x{16FE3}' . + '\x{16FE4}' . + '\x{1AFF0}-\x{1AFF3}' . + '\x{1AFF5}-\x{1AFFB}' . + '\x{1AFFD}-\x{1AFFE}' . + '\x{1BC9D}-\x{1BC9E}' . + '\x{1BCA0}-\x{1BCA3}' . + '\x{1CF00}-\x{1CF2D}' . + '\x{1CF30}-\x{1CF46}' . + '\x{1D167}-\x{1D169}' . + '\x{1D173}-\x{1D17A}' . + '\x{1D17B}-\x{1D182}' . + '\x{1D185}-\x{1D18B}' . + '\x{1D1AA}-\x{1D1AD}' . + '\x{1D242}-\x{1D244}' . + '\x{1DA00}-\x{1DA36}' . + '\x{1DA3B}-\x{1DA6C}' . + '\x{1DA75}' . + '\x{1DA84}' . + '\x{1DA9B}-\x{1DA9F}' . + '\x{1DAA1}-\x{1DAAF}' . + '\x{1E000}-\x{1E006}' . + '\x{1E008}-\x{1E018}' . + '\x{1E01B}-\x{1E021}' . + '\x{1E023}-\x{1E024}' . + '\x{1E026}-\x{1E02A}' . + '\x{1E030}-\x{1E06D}' . + '\x{1E08F}' . + '\x{1E130}-\x{1E136}' . + '\x{1E137}-\x{1E13D}' . + '\x{1E2AE}' . + '\x{1E2EC}-\x{1E2EF}' . + '\x{1E4EB}' . + '\x{1E4EC}-\x{1E4EF}' . + '\x{1E8D0}-\x{1E8D6}' . + '\x{1E944}-\x{1E94A}' . + '\x{1E94B}' . + '\x{1F3FB}-\x{1F3FF}' . + '\x{E0001}' . + '\x{E0020}-\x{E007F}' . + '\x{E0100}-\x{E01EF}', + 'Cn' => + '\x{0378}-\x{0379}' . + '\x{0380}-\x{0383}' . + '\x{038B}' . + '\x{038D}' . + '\x{03A2}' . + '\x{0530}' . + '\x{0557}-\x{0558}' . + '\x{058B}-\x{058C}' . + '\x{0590}' . + '\x{05C8}-\x{05CF}' . + '\x{05EB}-\x{05EE}' . + '\x{05F5}-\x{05FF}' . + '\x{070E}' . + '\x{074B}-\x{074C}' . + '\x{07B2}-\x{07BF}' . + '\x{07FB}-\x{07FC}' . + '\x{082E}-\x{082F}' . + '\x{083F}' . + '\x{085C}-\x{085D}' . + '\x{085F}' . + '\x{086B}-\x{086F}' . + '\x{088F}' . + '\x{0892}-\x{0897}' . + '\x{0984}' . + '\x{098D}-\x{098E}' . + '\x{0991}-\x{0992}' . + '\x{09A9}' . + '\x{09B1}' . + '\x{09B3}-\x{09B5}' . + '\x{09BA}-\x{09BB}' . + '\x{09C5}-\x{09C6}' . + '\x{09C9}-\x{09CA}' . + '\x{09CF}-\x{09D6}' . + '\x{09D8}-\x{09DB}' . + '\x{09DE}' . + '\x{09E4}-\x{09E5}' . + '\x{09FF}-\x{0A00}' . + '\x{0A04}' . + '\x{0A0B}-\x{0A0E}' . + '\x{0A11}-\x{0A12}' . + '\x{0A29}' . + '\x{0A31}' . + '\x{0A34}' . + '\x{0A37}' . + '\x{0A3A}-\x{0A3B}' . + '\x{0A3D}' . + '\x{0A43}-\x{0A46}' . + '\x{0A49}-\x{0A4A}' . + '\x{0A4E}-\x{0A50}' . + '\x{0A52}-\x{0A58}' . + '\x{0A5D}' . + '\x{0A5F}-\x{0A65}' . + '\x{0A77}-\x{0A80}' . + '\x{0A84}' . + '\x{0A8E}' . + '\x{0A92}' . + '\x{0AA9}' . + '\x{0AB1}' . + '\x{0AB4}' . + '\x{0ABA}-\x{0ABB}' . + '\x{0AC6}' . + '\x{0ACA}' . + '\x{0ACE}-\x{0ACF}' . + '\x{0AD1}-\x{0ADF}' . + '\x{0AE4}-\x{0AE5}' . + '\x{0AF2}-\x{0AF8}' . + '\x{0B00}' . + '\x{0B04}' . + '\x{0B0D}-\x{0B0E}' . + '\x{0B11}-\x{0B12}' . + '\x{0B29}' . + '\x{0B31}' . + '\x{0B34}' . + '\x{0B3A}-\x{0B3B}' . + '\x{0B45}-\x{0B46}' . + '\x{0B49}-\x{0B4A}' . + '\x{0B4E}-\x{0B54}' . + '\x{0B58}-\x{0B5B}' . + '\x{0B5E}' . + '\x{0B64}-\x{0B65}' . + '\x{0B78}-\x{0B81}' . + '\x{0B84}' . + '\x{0B8B}-\x{0B8D}' . + '\x{0B91}' . + '\x{0B96}-\x{0B98}' . + '\x{0B9B}' . + '\x{0B9D}' . + '\x{0BA0}-\x{0BA2}' . + '\x{0BA5}-\x{0BA7}' . + '\x{0BAB}-\x{0BAD}' . + '\x{0BBA}-\x{0BBD}' . + '\x{0BC3}-\x{0BC5}' . + '\x{0BC9}' . + '\x{0BCE}-\x{0BCF}' . + '\x{0BD1}-\x{0BD6}' . + '\x{0BD8}-\x{0BE5}' . + '\x{0BFB}-\x{0BFF}' . + '\x{0C0D}' . + '\x{0C11}' . + '\x{0C29}' . + '\x{0C3A}-\x{0C3B}' . + '\x{0C45}' . + '\x{0C49}' . + '\x{0C4E}-\x{0C54}' . + '\x{0C57}' . + '\x{0C5B}-\x{0C5C}' . + '\x{0C5E}-\x{0C5F}' . + '\x{0C64}-\x{0C65}' . + '\x{0C70}-\x{0C76}' . + '\x{0C8D}' . + '\x{0C91}' . + '\x{0CA9}' . + '\x{0CB4}' . + '\x{0CBA}-\x{0CBB}' . + '\x{0CC5}' . + '\x{0CC9}' . + '\x{0CCE}-\x{0CD4}' . + '\x{0CD7}-\x{0CDC}' . + '\x{0CDF}' . + '\x{0CE4}-\x{0CE5}' . + '\x{0CF0}' . + '\x{0CF4}-\x{0CFF}' . + '\x{0D0D}' . + '\x{0D11}' . + '\x{0D45}' . + '\x{0D49}' . + '\x{0D50}-\x{0D53}' . + '\x{0D64}-\x{0D65}' . + '\x{0D80}' . + '\x{0D84}' . + '\x{0D97}-\x{0D99}' . + '\x{0DB2}' . + '\x{0DBC}' . + '\x{0DBE}-\x{0DBF}' . + '\x{0DC7}-\x{0DC9}' . + '\x{0DCB}-\x{0DCE}' . + '\x{0DD5}' . + '\x{0DD7}' . + '\x{0DE0}-\x{0DE5}' . + '\x{0DF0}-\x{0DF1}' . + '\x{0DF5}-\x{0E00}' . + '\x{0E3B}-\x{0E3E}' . + '\x{0E5C}-\x{0E80}' . + '\x{0E83}' . + '\x{0E85}' . + '\x{0E8B}' . + '\x{0EA4}' . + '\x{0EA6}' . + '\x{0EBE}-\x{0EBF}' . + '\x{0EC5}' . + '\x{0EC7}' . + '\x{0ECF}' . + '\x{0EDA}-\x{0EDB}' . + '\x{0EE0}-\x{0EFF}' . + '\x{0F48}' . + '\x{0F6D}-\x{0F70}' . + '\x{0F98}' . + '\x{0FBD}' . + '\x{0FCD}' . + '\x{0FDB}-\x{0FFF}' . + '\x{10C6}' . + '\x{10C8}-\x{10CC}' . + '\x{10CE}-\x{10CF}' . + '\x{1249}' . + '\x{124E}-\x{124F}' . + '\x{1257}' . + '\x{1259}' . + '\x{125E}-\x{125F}' . + '\x{1289}' . + '\x{128E}-\x{128F}' . + '\x{12B1}' . + '\x{12B6}-\x{12B7}' . + '\x{12BF}' . + '\x{12C1}' . + '\x{12C6}-\x{12C7}' . + '\x{12D7}' . + '\x{1311}' . + '\x{1316}-\x{1317}' . + '\x{135B}-\x{135C}' . + '\x{137D}-\x{137F}' . + '\x{139A}-\x{139F}' . + '\x{13F6}-\x{13F7}' . + '\x{13FE}-\x{13FF}' . + '\x{169D}-\x{169F}' . + '\x{16F9}-\x{16FF}' . + '\x{1716}-\x{171E}' . + '\x{1737}-\x{173F}' . + '\x{1754}-\x{175F}' . + '\x{176D}' . + '\x{1771}' . + '\x{1774}-\x{177F}' . + '\x{17DE}-\x{17DF}' . + '\x{17EA}-\x{17EF}' . + '\x{17FA}-\x{17FF}' . + '\x{181A}-\x{181F}' . + '\x{1879}-\x{187F}' . + '\x{18AB}-\x{18AF}' . + '\x{18F6}-\x{18FF}' . + '\x{191F}' . + '\x{192C}-\x{192F}' . + '\x{193C}-\x{193F}' . + '\x{1941}-\x{1943}' . + '\x{196E}-\x{196F}' . + '\x{1975}-\x{197F}' . + '\x{19AC}-\x{19AF}' . + '\x{19CA}-\x{19CF}' . + '\x{19DB}-\x{19DD}' . + '\x{1A1C}-\x{1A1D}' . + '\x{1A5F}' . + '\x{1A7D}-\x{1A7E}' . + '\x{1A8A}-\x{1A8F}' . + '\x{1A9A}-\x{1A9F}' . + '\x{1AAE}-\x{1AAF}' . + '\x{1ACF}-\x{1AFF}' . + '\x{1B4D}-\x{1B4F}' . + '\x{1B7F}' . + '\x{1BF4}-\x{1BFB}' . + '\x{1C38}-\x{1C3A}' . + '\x{1C4A}-\x{1C4C}' . + '\x{1C89}-\x{1C8F}' . + '\x{1CBB}-\x{1CBC}' . + '\x{1CC8}-\x{1CCF}' . + '\x{1CFB}-\x{1CFF}' . + '\x{1F16}-\x{1F17}' . + '\x{1F1E}-\x{1F1F}' . + '\x{1F46}-\x{1F47}' . + '\x{1F4E}-\x{1F4F}' . + '\x{1F58}' . + '\x{1F5A}' . + '\x{1F5C}' . + '\x{1F5E}' . + '\x{1F7E}-\x{1F7F}' . + '\x{1FB5}' . + '\x{1FC5}' . + '\x{1FD4}-\x{1FD5}' . + '\x{1FDC}' . + '\x{1FF0}-\x{1FF1}' . + '\x{1FF5}' . + '\x{1FFF}' . + '\x{2065}' . + '\x{2072}-\x{2073}' . + '\x{208F}' . + '\x{209D}-\x{209F}' . + '\x{20C1}-\x{20CF}' . + '\x{20F1}-\x{20FF}' . + '\x{218C}-\x{218F}' . + '\x{2427}-\x{243F}' . + '\x{244B}-\x{245F}' . + '\x{2B74}-\x{2B75}' . + '\x{2B96}' . + '\x{2CF4}-\x{2CF8}' . + '\x{2D26}' . + '\x{2D28}-\x{2D2C}' . + '\x{2D2E}-\x{2D2F}' . + '\x{2D68}-\x{2D6E}' . + '\x{2D71}-\x{2D7E}' . + '\x{2D97}-\x{2D9F}' . + '\x{2DA7}' . + '\x{2DAF}' . + '\x{2DB7}' . + '\x{2DBF}' . + '\x{2DC7}' . + '\x{2DCF}' . + '\x{2DD7}' . + '\x{2DDF}' . + '\x{2E5E}-\x{2E7F}' . + '\x{2E9A}' . + '\x{2EF4}-\x{2EFF}' . + '\x{2FD6}-\x{2FEF}' . + '\x{2FFC}-\x{2FFF}' . + '\x{3040}' . + '\x{3097}-\x{3098}' . + '\x{3100}-\x{3104}' . + '\x{3130}' . + '\x{318F}' . + '\x{31E4}-\x{31EF}' . + '\x{321F}' . + '\x{A48D}-\x{A48F}' . + '\x{A4C7}-\x{A4CF}' . + '\x{A62C}-\x{A63F}' . + '\x{A6F8}-\x{A6FF}' . + '\x{A7CB}-\x{A7CF}' . + '\x{A7D2}' . + '\x{A7D4}' . + '\x{A7DA}-\x{A7F1}' . + '\x{A82D}-\x{A82F}' . + '\x{A83A}-\x{A83F}' . + '\x{A878}-\x{A87F}' . + '\x{A8C6}-\x{A8CD}' . + '\x{A8DA}-\x{A8DF}' . + '\x{A954}-\x{A95E}' . + '\x{A97D}-\x{A97F}' . + '\x{A9CE}' . + '\x{A9DA}-\x{A9DD}' . + '\x{A9FF}' . + '\x{AA37}-\x{AA3F}' . + '\x{AA4E}-\x{AA4F}' . + '\x{AA5A}-\x{AA5B}' . + '\x{AAC3}-\x{AADA}' . + '\x{AAF7}-\x{AB00}' . + '\x{AB07}-\x{AB08}' . + '\x{AB0F}-\x{AB10}' . + '\x{AB17}-\x{AB1F}' . + '\x{AB27}' . + '\x{AB2F}' . + '\x{AB6C}-\x{AB6F}' . + '\x{ABEE}-\x{ABEF}' . + '\x{ABFA}-\x{ABFF}' . + '\x{D7A4}-\x{D7AF}' . + '\x{D7C7}-\x{D7CA}' . + '\x{D7FC}-\x{D7FF}' . + '\x{FA6E}-\x{FA6F}' . + '\x{FADA}-\x{FAFF}' . + '\x{FB07}-\x{FB12}' . + '\x{FB18}-\x{FB1C}' . + '\x{FB37}' . + '\x{FB3D}' . + '\x{FB3F}' . + '\x{FB42}' . + '\x{FB45}' . + '\x{FBC3}-\x{FBD2}' . + '\x{FD90}-\x{FD91}' . + '\x{FDC8}-\x{FDCE}' . + '\x{FDD0}-\x{FDEF}' . + '\x{FE1A}-\x{FE1F}' . + '\x{FE53}' . + '\x{FE67}' . + '\x{FE6C}-\x{FE6F}' . + '\x{FE75}' . + '\x{FEFD}-\x{FEFE}' . + '\x{FF00}' . + '\x{FFBF}-\x{FFC1}' . + '\x{FFC8}-\x{FFC9}' . + '\x{FFD0}-\x{FFD1}' . + '\x{FFD8}-\x{FFD9}' . + '\x{FFDD}-\x{FFDF}' . + '\x{FFE7}' . + '\x{FFEF}-\x{FFF8}' . + '\x{FFFE}-\x{FFFF}' . + '\x{1000C}' . + '\x{10027}' . + '\x{1003B}' . + '\x{1003E}' . + '\x{1004E}-\x{1004F}' . + '\x{1005E}-\x{1007F}' . + '\x{100FB}-\x{100FF}' . + '\x{10103}-\x{10106}' . + '\x{10134}-\x{10136}' . + '\x{1018F}' . + '\x{1019D}-\x{1019F}' . + '\x{101A1}-\x{101CF}' . + '\x{101FE}-\x{1027F}' . + '\x{1029D}-\x{1029F}' . + '\x{102D1}-\x{102DF}' . + '\x{102FC}-\x{102FF}' . + '\x{10324}-\x{1032C}' . + '\x{1034B}-\x{1034F}' . + '\x{1037B}-\x{1037F}' . + '\x{1039E}' . + '\x{103C4}-\x{103C7}' . + '\x{103D6}-\x{103FF}' . + '\x{1049E}-\x{1049F}' . + '\x{104AA}-\x{104AF}' . + '\x{104D4}-\x{104D7}' . + '\x{104FC}-\x{104FF}' . + '\x{10528}-\x{1052F}' . + '\x{10564}-\x{1056E}' . + '\x{1057B}' . + '\x{1058B}' . + '\x{10593}' . + '\x{10596}' . + '\x{105A2}' . + '\x{105B2}' . + '\x{105BA}' . + '\x{105BD}-\x{105FF}' . + '\x{10737}-\x{1073F}' . + '\x{10756}-\x{1075F}' . + '\x{10768}-\x{1077F}' . + '\x{10786}' . + '\x{107B1}' . + '\x{107BB}-\x{107FF}' . + '\x{10806}-\x{10807}' . + '\x{10809}' . + '\x{10836}' . + '\x{10839}-\x{1083B}' . + '\x{1083D}-\x{1083E}' . + '\x{10856}' . + '\x{1089F}-\x{108A6}' . + '\x{108B0}-\x{108DF}' . + '\x{108F3}' . + '\x{108F6}-\x{108FA}' . + '\x{1091C}-\x{1091E}' . + '\x{1093A}-\x{1093E}' . + '\x{10940}-\x{1097F}' . + '\x{109B8}-\x{109BB}' . + '\x{109D0}-\x{109D1}' . + '\x{10A04}' . + '\x{10A07}-\x{10A0B}' . + '\x{10A14}' . + '\x{10A18}' . + '\x{10A36}-\x{10A37}' . + '\x{10A3B}-\x{10A3E}' . + '\x{10A49}-\x{10A4F}' . + '\x{10A59}-\x{10A5F}' . + '\x{10AA0}-\x{10ABF}' . + '\x{10AE7}-\x{10AEA}' . + '\x{10AF7}-\x{10AFF}' . + '\x{10B36}-\x{10B38}' . + '\x{10B56}-\x{10B57}' . + '\x{10B73}-\x{10B77}' . + '\x{10B92}-\x{10B98}' . + '\x{10B9D}-\x{10BA8}' . + '\x{10BB0}-\x{10BFF}' . + '\x{10C49}-\x{10C7F}' . + '\x{10CB3}-\x{10CBF}' . + '\x{10CF3}-\x{10CF9}' . + '\x{10D28}-\x{10D2F}' . + '\x{10D3A}-\x{10E5F}' . + '\x{10E7F}' . + '\x{10EAA}' . + '\x{10EAE}-\x{10EAF}' . + '\x{10EB2}-\x{10EFC}' . + '\x{10F28}-\x{10F2F}' . + '\x{10F5A}-\x{10F6F}' . + '\x{10F8A}-\x{10FAF}' . + '\x{10FCC}-\x{10FDF}' . + '\x{10FF7}-\x{10FFF}' . + '\x{1104E}-\x{11051}' . + '\x{11076}-\x{1107E}' . + '\x{110C3}-\x{110CC}' . + '\x{110CE}-\x{110CF}' . + '\x{110E9}-\x{110EF}' . + '\x{110FA}-\x{110FF}' . + '\x{11135}' . + '\x{11148}-\x{1114F}' . + '\x{11177}-\x{1117F}' . + '\x{111E0}' . + '\x{111F5}-\x{111FF}' . + '\x{11212}' . + '\x{11242}-\x{1127F}' . + '\x{11287}' . + '\x{11289}' . + '\x{1128E}' . + '\x{1129E}' . + '\x{112AA}-\x{112AF}' . + '\x{112EB}-\x{112EF}' . + '\x{112FA}-\x{112FF}' . + '\x{11304}' . + '\x{1130D}-\x{1130E}' . + '\x{11311}-\x{11312}' . + '\x{11329}' . + '\x{11331}' . + '\x{11334}' . + '\x{1133A}' . + '\x{11345}-\x{11346}' . + '\x{11349}-\x{1134A}' . + '\x{1134E}-\x{1134F}' . + '\x{11351}-\x{11356}' . + '\x{11358}-\x{1135C}' . + '\x{11364}-\x{11365}' . + '\x{1136D}-\x{1136F}' . + '\x{11375}-\x{113FF}' . + '\x{1145C}' . + '\x{11462}-\x{1147F}' . + '\x{114C8}-\x{114CF}' . + '\x{114DA}-\x{1157F}' . + '\x{115B6}-\x{115B7}' . + '\x{115DE}-\x{115FF}' . + '\x{11645}-\x{1164F}' . + '\x{1165A}-\x{1165F}' . + '\x{1166D}-\x{1167F}' . + '\x{116BA}-\x{116BF}' . + '\x{116CA}-\x{116FF}' . + '\x{1171B}-\x{1171C}' . + '\x{1172C}-\x{1172F}' . + '\x{11747}-\x{117FF}' . + '\x{1183C}-\x{1189F}' . + '\x{118F3}-\x{118FE}' . + '\x{11907}-\x{11908}' . + '\x{1190A}-\x{1190B}' . + '\x{11914}' . + '\x{11917}' . + '\x{11936}' . + '\x{11939}-\x{1193A}' . + '\x{11947}-\x{1194F}' . + '\x{1195A}-\x{1199F}' . + '\x{119A8}-\x{119A9}' . + '\x{119D8}-\x{119D9}' . + '\x{119E5}-\x{119FF}' . + '\x{11A48}-\x{11A4F}' . + '\x{11AA3}-\x{11AAF}' . + '\x{11AF9}-\x{11AFF}' . + '\x{11B0A}-\x{11BFF}' . + '\x{11C09}' . + '\x{11C37}' . + '\x{11C46}-\x{11C4F}' . + '\x{11C6D}-\x{11C6F}' . + '\x{11C90}-\x{11C91}' . + '\x{11CA8}' . + '\x{11CB7}-\x{11CFF}' . + '\x{11D07}' . + '\x{11D0A}' . + '\x{11D37}-\x{11D39}' . + '\x{11D3B}' . + '\x{11D3E}' . + '\x{11D48}-\x{11D4F}' . + '\x{11D5A}-\x{11D5F}' . + '\x{11D66}' . + '\x{11D69}' . + '\x{11D8F}' . + '\x{11D92}' . + '\x{11D99}-\x{11D9F}' . + '\x{11DAA}-\x{11EDF}' . + '\x{11EF9}-\x{11EFF}' . + '\x{11F11}' . + '\x{11F3B}-\x{11F3D}' . + '\x{11F5A}-\x{11FAF}' . + '\x{11FB1}-\x{11FBF}' . + '\x{11FF2}-\x{11FFE}' . + '\x{1239A}-\x{123FF}' . + '\x{1246F}' . + '\x{12475}-\x{1247F}' . + '\x{12544}-\x{12F8F}' . + '\x{12FF3}-\x{12FFF}' . + '\x{13456}-\x{143FF}' . + '\x{14647}-\x{167FF}' . + '\x{16A39}-\x{16A3F}' . + '\x{16A5F}' . + '\x{16A6A}-\x{16A6D}' . + '\x{16ABF}' . + '\x{16ACA}-\x{16ACF}' . + '\x{16AEE}-\x{16AEF}' . + '\x{16AF6}-\x{16AFF}' . + '\x{16B46}-\x{16B4F}' . + '\x{16B5A}' . + '\x{16B62}' . + '\x{16B78}-\x{16B7C}' . + '\x{16B90}-\x{16E3F}' . + '\x{16E9B}-\x{16EFF}' . + '\x{16F4B}-\x{16F4E}' . + '\x{16F88}-\x{16F8E}' . + '\x{16FA0}-\x{16FDF}' . + '\x{16FE5}-\x{16FEF}' . + '\x{16FF2}-\x{16FFF}' . + '\x{187F8}-\x{187FF}' . + '\x{18CD6}-\x{18CFF}' . + '\x{18D09}-\x{1AFEF}' . + '\x{1AFF4}' . + '\x{1AFFC}' . + '\x{1AFFF}' . + '\x{1B123}-\x{1B131}' . + '\x{1B133}-\x{1B14F}' . + '\x{1B153}-\x{1B154}' . + '\x{1B156}-\x{1B163}' . + '\x{1B168}-\x{1B16F}' . + '\x{1B2FC}-\x{1BBFF}' . + '\x{1BC6B}-\x{1BC6F}' . + '\x{1BC7D}-\x{1BC7F}' . + '\x{1BC89}-\x{1BC8F}' . + '\x{1BC9A}-\x{1BC9B}' . + '\x{1BCA4}-\x{1CEFF}' . + '\x{1CF2E}-\x{1CF2F}' . + '\x{1CF47}-\x{1CF4F}' . + '\x{1CFC4}-\x{1CFFF}' . + '\x{1D0F6}-\x{1D0FF}' . + '\x{1D127}-\x{1D128}' . + '\x{1D1EB}-\x{1D1FF}' . + '\x{1D246}-\x{1D2BF}' . + '\x{1D2D4}-\x{1D2DF}' . + '\x{1D2F4}-\x{1D2FF}' . + '\x{1D357}-\x{1D35F}' . + '\x{1D379}-\x{1D3FF}' . + '\x{1D455}' . + '\x{1D49D}' . + '\x{1D4A0}-\x{1D4A1}' . + '\x{1D4A3}-\x{1D4A4}' . + '\x{1D4A7}-\x{1D4A8}' . + '\x{1D4AD}' . + '\x{1D4BA}' . + '\x{1D4BC}' . + '\x{1D4C4}' . + '\x{1D506}' . + '\x{1D50B}-\x{1D50C}' . + '\x{1D515}' . + '\x{1D51D}' . + '\x{1D53A}' . + '\x{1D53F}' . + '\x{1D545}' . + '\x{1D547}-\x{1D549}' . + '\x{1D551}' . + '\x{1D6A6}-\x{1D6A7}' . + '\x{1D7CC}-\x{1D7CD}' . + '\x{1DA8C}-\x{1DA9A}' . + '\x{1DAA0}' . + '\x{1DAB0}-\x{1DEFF}' . + '\x{1DF1F}-\x{1DF24}' . + '\x{1DF2B}-\x{1DFFF}' . + '\x{1E007}' . + '\x{1E019}-\x{1E01A}' . + '\x{1E022}' . + '\x{1E025}' . + '\x{1E02B}-\x{1E02F}' . + '\x{1E06E}-\x{1E08E}' . + '\x{1E090}-\x{1E0FF}' . + '\x{1E12D}-\x{1E12F}' . + '\x{1E13E}-\x{1E13F}' . + '\x{1E14A}-\x{1E14D}' . + '\x{1E150}-\x{1E28F}' . + '\x{1E2AF}-\x{1E2BF}' . + '\x{1E2FA}-\x{1E2FE}' . + '\x{1E300}-\x{1E4CF}' . + '\x{1E4FA}-\x{1E7DF}' . + '\x{1E7E7}' . + '\x{1E7EC}' . + '\x{1E7EF}' . + '\x{1E7FF}' . + '\x{1E8C5}-\x{1E8C6}' . + '\x{1E8D7}-\x{1E8FF}' . + '\x{1E94C}-\x{1E94F}' . + '\x{1E95A}-\x{1E95D}' . + '\x{1E960}-\x{1EC70}' . + '\x{1ECB5}-\x{1ED00}' . + '\x{1ED3E}-\x{1EDFF}' . + '\x{1EE04}' . + '\x{1EE20}' . + '\x{1EE23}' . + '\x{1EE25}-\x{1EE26}' . + '\x{1EE28}' . + '\x{1EE33}' . + '\x{1EE38}' . + '\x{1EE3A}' . + '\x{1EE3C}-\x{1EE41}' . + '\x{1EE43}-\x{1EE46}' . + '\x{1EE48}' . + '\x{1EE4A}' . + '\x{1EE4C}' . + '\x{1EE50}' . + '\x{1EE53}' . + '\x{1EE55}-\x{1EE56}' . + '\x{1EE58}' . + '\x{1EE5A}' . + '\x{1EE5C}' . + '\x{1EE5E}' . + '\x{1EE60}' . + '\x{1EE63}' . + '\x{1EE65}-\x{1EE66}' . + '\x{1EE6B}' . + '\x{1EE73}' . + '\x{1EE78}' . + '\x{1EE7D}' . + '\x{1EE7F}' . + '\x{1EE8A}' . + '\x{1EE9C}-\x{1EEA0}' . + '\x{1EEA4}' . + '\x{1EEAA}' . + '\x{1EEBC}-\x{1EEEF}' . + '\x{1EEF2}-\x{1EFFF}' . + '\x{1F02C}-\x{1F02F}' . + '\x{1F094}-\x{1F09F}' . + '\x{1F0AF}-\x{1F0B0}' . + '\x{1F0C0}' . + '\x{1F0D0}' . + '\x{1F0F6}-\x{1F0FF}' . + '\x{1F1AE}-\x{1F1E5}' . + '\x{1F203}-\x{1F20F}' . + '\x{1F23C}-\x{1F23F}' . + '\x{1F249}-\x{1F24F}' . + '\x{1F252}-\x{1F25F}' . + '\x{1F266}-\x{1F2FF}' . + '\x{1F6D8}-\x{1F6DB}' . + '\x{1F6ED}-\x{1F6EF}' . + '\x{1F6FD}-\x{1F6FF}' . + '\x{1F777}-\x{1F77A}' . + '\x{1F7DA}-\x{1F7DF}' . + '\x{1F7EC}-\x{1F7EF}' . + '\x{1F7F1}-\x{1F7FF}' . + '\x{1F80C}-\x{1F80F}' . + '\x{1F848}-\x{1F84F}' . + '\x{1F85A}-\x{1F85F}' . + '\x{1F888}-\x{1F88F}' . + '\x{1F8AE}-\x{1F8AF}' . + '\x{1F8B2}-\x{1F8FF}' . + '\x{1FA54}-\x{1FA5F}' . + '\x{1FA6E}-\x{1FA6F}' . + '\x{1FA7D}-\x{1FA7F}' . + '\x{1FA89}-\x{1FA8F}' . + '\x{1FABE}' . + '\x{1FAC6}-\x{1FACD}' . + '\x{1FADC}-\x{1FADF}' . + '\x{1FAE9}-\x{1FAEF}' . + '\x{1FAF9}-\x{1FAFF}' . + '\x{1FB93}' . + '\x{1FBCB}-\x{1FBEF}' . + '\x{1FBFA}-\x{1FFFF}' . + '\x{2A6E0}-\x{2A6FF}' . + '\x{2B73A}-\x{2B73F}' . + '\x{2B81E}-\x{2B81F}' . + '\x{2CEA2}-\x{2CEAF}' . + '\x{2EBE1}-\x{2F7FF}' . + '\x{2FA1E}-\x{2FFFF}' . + '\x{3134B}-\x{3134F}' . + '\x{323B0}-\x{E0000}' . + '\x{E0002}-\x{E001F}' . + '\x{E0080}-\x{E00FF}' . + '\x{E01F0}-\x{EFFFF}' . + '\x{FFFFE}-\x{FFFFF}' . + '\x{10FFFE}-\x{10FFFF}', + 'Default_Ignorable_Code_Point' => + '\x{00AD}' . + '\x{034F}' . + '\x{061C}' . + '\x{115F}-\x{1160}' . + '\x{17B4}-\x{17B5}' . + '\x{180B}-\x{180D}' . + '\x{180E}' . + '\x{180F}' . + '\x{200B}-\x{200F}' . + '\x{202A}-\x{202E}' . + '\x{2060}-\x{2064}' . + '\x{2065}' . + '\x{2066}-\x{206F}' . + '\x{3164}' . + '\x{FE00}-\x{FE0F}' . + '\x{FEFF}' . + '\x{FFA0}' . + '\x{FFF0}-\x{FFF8}' . + '\x{1BCA0}-\x{1BCA3}' . + '\x{1D173}-\x{1D17A}' . + '\x{E0000}' . + '\x{E0001}' . + '\x{E0002}-\x{E001F}' . + '\x{E0020}-\x{E007F}' . + '\x{E0080}-\x{E00FF}' . + '\x{E0100}-\x{E01EF}' . + '\x{E01F0}-\x{E0FFF}', + 'Emoji' => + '\x{0023}' . + '\x{002A}' . + '\x{0030}-\x{0039}' . + '\x{00A9}' . + '\x{00AE}' . + '\x{203C}' . + '\x{2049}' . + '\x{2122}' . + '\x{2139}' . + '\x{2194}-\x{2199}' . + '\x{21A9}-\x{21AA}' . + '\x{231A}-\x{231B}' . + '\x{2328}' . + '\x{23CF}' . + '\x{23E9}-\x{23EC}' . + '\x{23ED}-\x{23EE}' . + '\x{23EF}' . + '\x{23F0}' . + '\x{23F1}-\x{23F2}' . + '\x{23F3}' . + '\x{23F8}-\x{23FA}' . + '\x{24C2}' . + '\x{25AA}-\x{25AB}' . + '\x{25B6}' . + '\x{25C0}' . + '\x{25FB}-\x{25FE}' . + '\x{2600}-\x{2601}' . + '\x{2602}-\x{2603}' . + '\x{2604}' . + '\x{260E}' . + '\x{2611}' . + '\x{2614}-\x{2615}' . + '\x{2618}' . + '\x{261D}' . + '\x{2620}' . + '\x{2622}-\x{2623}' . + '\x{2626}' . + '\x{262A}' . + '\x{262E}' . + '\x{262F}' . + '\x{2638}-\x{2639}' . + '\x{263A}' . + '\x{2640}' . + '\x{2642}' . + '\x{2648}-\x{2653}' . + '\x{265F}' . + '\x{2660}' . + '\x{2663}' . + '\x{2665}-\x{2666}' . + '\x{2668}' . + '\x{267B}' . + '\x{267E}' . + '\x{267F}' . + '\x{2692}' . + '\x{2693}' . + '\x{2694}' . + '\x{2695}' . + '\x{2696}-\x{2697}' . + '\x{2699}' . + '\x{269B}-\x{269C}' . + '\x{26A0}-\x{26A1}' . + '\x{26A7}' . + '\x{26AA}-\x{26AB}' . + '\x{26B0}-\x{26B1}' . + '\x{26BD}-\x{26BE}' . + '\x{26C4}-\x{26C5}' . + '\x{26C8}' . + '\x{26CE}' . + '\x{26CF}' . + '\x{26D1}' . + '\x{26D3}' . + '\x{26D4}' . + '\x{26E9}' . + '\x{26EA}' . + '\x{26F0}-\x{26F1}' . + '\x{26F2}-\x{26F3}' . + '\x{26F4}' . + '\x{26F5}' . + '\x{26F7}-\x{26F9}' . + '\x{26FA}' . + '\x{26FD}' . + '\x{2702}' . + '\x{2705}' . + '\x{2708}-\x{270C}' . + '\x{270D}' . + '\x{270F}' . + '\x{2712}' . + '\x{2714}' . + '\x{2716}' . + '\x{271D}' . + '\x{2721}' . + '\x{2728}' . + '\x{2733}-\x{2734}' . + '\x{2744}' . + '\x{2747}' . + '\x{274C}' . + '\x{274E}' . + '\x{2753}-\x{2755}' . + '\x{2757}' . + '\x{2763}' . + '\x{2764}' . + '\x{2795}-\x{2797}' . + '\x{27A1}' . + '\x{27B0}' . + '\x{27BF}' . + '\x{2934}-\x{2935}' . + '\x{2B05}-\x{2B07}' . + '\x{2B1B}-\x{2B1C}' . + '\x{2B50}' . + '\x{2B55}' . + '\x{3030}' . + '\x{303D}' . + '\x{3297}' . + '\x{3299}' . + '\x{1F004}' . + '\x{1F0CF}' . + '\x{1F170}-\x{1F171}' . + '\x{1F17E}-\x{1F17F}' . + '\x{1F18E}' . + '\x{1F191}-\x{1F19A}' . + '\x{1F1E6}-\x{1F1FF}' . + '\x{1F201}-\x{1F202}' . + '\x{1F21A}' . + '\x{1F22F}' . + '\x{1F232}-\x{1F23A}' . + '\x{1F250}-\x{1F251}' . + '\x{1F300}-\x{1F30C}' . + '\x{1F30D}-\x{1F30E}' . + '\x{1F30F}' . + '\x{1F310}' . + '\x{1F311}' . + '\x{1F312}' . + '\x{1F313}-\x{1F315}' . + '\x{1F316}-\x{1F318}' . + '\x{1F319}' . + '\x{1F31A}' . + '\x{1F31B}' . + '\x{1F31C}' . + '\x{1F31D}-\x{1F31E}' . + '\x{1F31F}-\x{1F320}' . + '\x{1F321}' . + '\x{1F324}-\x{1F32C}' . + '\x{1F32D}-\x{1F32F}' . + '\x{1F330}-\x{1F331}' . + '\x{1F332}-\x{1F333}' . + '\x{1F334}-\x{1F335}' . + '\x{1F336}' . + '\x{1F337}-\x{1F34A}' . + '\x{1F34B}' . + '\x{1F34C}-\x{1F34F}' . + '\x{1F350}' . + '\x{1F351}-\x{1F37B}' . + '\x{1F37C}' . + '\x{1F37D}' . + '\x{1F37E}-\x{1F37F}' . + '\x{1F380}-\x{1F393}' . + '\x{1F396}-\x{1F397}' . + '\x{1F399}-\x{1F39B}' . + '\x{1F39E}-\x{1F39F}' . + '\x{1F3A0}-\x{1F3C4}' . + '\x{1F3C5}' . + '\x{1F3C6}' . + '\x{1F3C7}' . + '\x{1F3C8}' . + '\x{1F3C9}' . + '\x{1F3CA}' . + '\x{1F3CB}-\x{1F3CE}' . + '\x{1F3CF}-\x{1F3D3}' . + '\x{1F3D4}-\x{1F3DF}' . + '\x{1F3E0}-\x{1F3E3}' . + '\x{1F3E4}' . + '\x{1F3E5}-\x{1F3F0}' . + '\x{1F3F3}' . + '\x{1F3F4}' . + '\x{1F3F5}' . + '\x{1F3F7}' . + '\x{1F3F8}-\x{1F407}' . + '\x{1F408}' . + '\x{1F409}-\x{1F40B}' . + '\x{1F40C}-\x{1F40E}' . + '\x{1F40F}-\x{1F410}' . + '\x{1F411}-\x{1F412}' . + '\x{1F413}' . + '\x{1F414}' . + '\x{1F415}' . + '\x{1F416}' . + '\x{1F417}-\x{1F429}' . + '\x{1F42A}' . + '\x{1F42B}-\x{1F43E}' . + '\x{1F43F}' . + '\x{1F440}' . + '\x{1F441}' . + '\x{1F442}-\x{1F464}' . + '\x{1F465}' . + '\x{1F466}-\x{1F46B}' . + '\x{1F46C}-\x{1F46D}' . + '\x{1F46E}-\x{1F4AC}' . + '\x{1F4AD}' . + '\x{1F4AE}-\x{1F4B5}' . + '\x{1F4B6}-\x{1F4B7}' . + '\x{1F4B8}-\x{1F4EB}' . + '\x{1F4EC}-\x{1F4ED}' . + '\x{1F4EE}' . + '\x{1F4EF}' . + '\x{1F4F0}-\x{1F4F4}' . + '\x{1F4F5}' . + '\x{1F4F6}-\x{1F4F7}' . + '\x{1F4F8}' . + '\x{1F4F9}-\x{1F4FC}' . + '\x{1F4FD}' . + '\x{1F4FF}-\x{1F502}' . + '\x{1F503}' . + '\x{1F504}-\x{1F507}' . + '\x{1F508}' . + '\x{1F509}' . + '\x{1F50A}-\x{1F514}' . + '\x{1F515}' . + '\x{1F516}-\x{1F52B}' . + '\x{1F52C}-\x{1F52D}' . + '\x{1F52E}-\x{1F53D}' . + '\x{1F549}-\x{1F54A}' . + '\x{1F54B}-\x{1F54E}' . + '\x{1F550}-\x{1F55B}' . + '\x{1F55C}-\x{1F567}' . + '\x{1F56F}-\x{1F570}' . + '\x{1F573}-\x{1F579}' . + '\x{1F57A}' . + '\x{1F587}' . + '\x{1F58A}-\x{1F58D}' . + '\x{1F590}' . + '\x{1F595}-\x{1F596}' . + '\x{1F5A4}' . + '\x{1F5A5}' . + '\x{1F5A8}' . + '\x{1F5B1}-\x{1F5B2}' . + '\x{1F5BC}' . + '\x{1F5C2}-\x{1F5C4}' . + '\x{1F5D1}-\x{1F5D3}' . + '\x{1F5DC}-\x{1F5DE}' . + '\x{1F5E1}' . + '\x{1F5E3}' . + '\x{1F5E8}' . + '\x{1F5EF}' . + '\x{1F5F3}' . + '\x{1F5FA}' . + '\x{1F5FB}-\x{1F5FF}' . + '\x{1F600}' . + '\x{1F601}-\x{1F606}' . + '\x{1F607}-\x{1F608}' . + '\x{1F609}-\x{1F60D}' . + '\x{1F60E}' . + '\x{1F60F}' . + '\x{1F610}' . + '\x{1F611}' . + '\x{1F612}-\x{1F614}' . + '\x{1F615}' . + '\x{1F616}' . + '\x{1F617}' . + '\x{1F618}' . + '\x{1F619}' . + '\x{1F61A}' . + '\x{1F61B}' . + '\x{1F61C}-\x{1F61E}' . + '\x{1F61F}' . + '\x{1F620}-\x{1F625}' . + '\x{1F626}-\x{1F627}' . + '\x{1F628}-\x{1F62B}' . + '\x{1F62C}' . + '\x{1F62D}' . + '\x{1F62E}-\x{1F62F}' . + '\x{1F630}-\x{1F633}' . + '\x{1F634}' . + '\x{1F635}' . + '\x{1F636}' . + '\x{1F637}-\x{1F640}' . + '\x{1F641}-\x{1F644}' . + '\x{1F645}-\x{1F64F}' . + '\x{1F680}' . + '\x{1F681}-\x{1F682}' . + '\x{1F683}-\x{1F685}' . + '\x{1F686}' . + '\x{1F687}' . + '\x{1F688}' . + '\x{1F689}' . + '\x{1F68A}-\x{1F68B}' . + '\x{1F68C}' . + '\x{1F68D}' . + '\x{1F68E}' . + '\x{1F68F}' . + '\x{1F690}' . + '\x{1F691}-\x{1F693}' . + '\x{1F694}' . + '\x{1F695}' . + '\x{1F696}' . + '\x{1F697}' . + '\x{1F698}' . + '\x{1F699}-\x{1F69A}' . + '\x{1F69B}-\x{1F6A1}' . + '\x{1F6A2}' . + '\x{1F6A3}' . + '\x{1F6A4}-\x{1F6A5}' . + '\x{1F6A6}' . + '\x{1F6A7}-\x{1F6AD}' . + '\x{1F6AE}-\x{1F6B1}' . + '\x{1F6B2}' . + '\x{1F6B3}-\x{1F6B5}' . + '\x{1F6B6}' . + '\x{1F6B7}-\x{1F6B8}' . + '\x{1F6B9}-\x{1F6BE}' . + '\x{1F6BF}' . + '\x{1F6C0}' . + '\x{1F6C1}-\x{1F6C5}' . + '\x{1F6CB}' . + '\x{1F6CC}' . + '\x{1F6CD}-\x{1F6CF}' . + '\x{1F6D0}' . + '\x{1F6D1}-\x{1F6D2}' . + '\x{1F6D5}' . + '\x{1F6D6}-\x{1F6D7}' . + '\x{1F6DC}' . + '\x{1F6DD}-\x{1F6DF}' . + '\x{1F6E0}-\x{1F6E5}' . + '\x{1F6E9}' . + '\x{1F6EB}-\x{1F6EC}' . + '\x{1F6F0}' . + '\x{1F6F3}' . + '\x{1F6F4}-\x{1F6F6}' . + '\x{1F6F7}-\x{1F6F8}' . + '\x{1F6F9}' . + '\x{1F6FA}' . + '\x{1F6FB}-\x{1F6FC}' . + '\x{1F7E0}-\x{1F7EB}' . + '\x{1F7F0}' . + '\x{1F90C}' . + '\x{1F90D}-\x{1F90F}' . + '\x{1F910}-\x{1F918}' . + '\x{1F919}-\x{1F91E}' . + '\x{1F91F}' . + '\x{1F920}-\x{1F927}' . + '\x{1F928}-\x{1F92F}' . + '\x{1F930}' . + '\x{1F931}-\x{1F932}' . + '\x{1F933}-\x{1F93A}' . + '\x{1F93C}-\x{1F93E}' . + '\x{1F93F}' . + '\x{1F940}-\x{1F945}' . + '\x{1F947}-\x{1F94B}' . + '\x{1F94C}' . + '\x{1F94D}-\x{1F94F}' . + '\x{1F950}-\x{1F95E}' . + '\x{1F95F}-\x{1F96B}' . + '\x{1F96C}-\x{1F970}' . + '\x{1F971}' . + '\x{1F972}' . + '\x{1F973}-\x{1F976}' . + '\x{1F977}-\x{1F978}' . + '\x{1F979}' . + '\x{1F97A}' . + '\x{1F97B}' . + '\x{1F97C}-\x{1F97F}' . + '\x{1F980}-\x{1F984}' . + '\x{1F985}-\x{1F991}' . + '\x{1F992}-\x{1F997}' . + '\x{1F998}-\x{1F9A2}' . + '\x{1F9A3}-\x{1F9A4}' . + '\x{1F9A5}-\x{1F9AA}' . + '\x{1F9AB}-\x{1F9AD}' . + '\x{1F9AE}-\x{1F9AF}' . + '\x{1F9B0}-\x{1F9B9}' . + '\x{1F9BA}-\x{1F9BF}' . + '\x{1F9C0}' . + '\x{1F9C1}-\x{1F9C2}' . + '\x{1F9C3}-\x{1F9CA}' . + '\x{1F9CB}' . + '\x{1F9CC}' . + '\x{1F9CD}-\x{1F9CF}' . + '\x{1F9D0}-\x{1F9E6}' . + '\x{1F9E7}-\x{1F9FF}' . + '\x{1FA70}-\x{1FA73}' . + '\x{1FA74}' . + '\x{1FA75}-\x{1FA77}' . + '\x{1FA78}-\x{1FA7A}' . + '\x{1FA7B}-\x{1FA7C}' . + '\x{1FA80}-\x{1FA82}' . + '\x{1FA83}-\x{1FA86}' . + '\x{1FA87}-\x{1FA88}' . + '\x{1FA90}-\x{1FA95}' . + '\x{1FA96}-\x{1FAA8}' . + '\x{1FAA9}-\x{1FAAC}' . + '\x{1FAAD}-\x{1FAAF}' . + '\x{1FAB0}-\x{1FAB6}' . + '\x{1FAB7}-\x{1FABA}' . + '\x{1FABB}-\x{1FABD}' . + '\x{1FABF}' . + '\x{1FAC0}-\x{1FAC2}' . + '\x{1FAC3}-\x{1FAC5}' . + '\x{1FACE}-\x{1FACF}' . + '\x{1FAD0}-\x{1FAD6}' . + '\x{1FAD7}-\x{1FAD9}' . + '\x{1FADA}-\x{1FADB}' . + '\x{1FAE0}-\x{1FAE7}' . + '\x{1FAE8}' . + '\x{1FAF0}-\x{1FAF6}' . + '\x{1FAF7}-\x{1FAF8}', + 'Emoji_Modifier' => + '\x{1F3FB}-\x{1F3FF}', + 'Ideographic' => + '\x{3006}' . + '\x{3007}' . + '\x{3021}-\x{3029}' . + '\x{3038}-\x{303A}' . + '\x{3400}-\x{4DBF}' . + '\x{4E00}-\x{9FFF}' . + '\x{F900}-\x{FA6D}' . + '\x{FA70}-\x{FAD9}' . + '\x{16FE4}' . + '\x{17000}-\x{187F7}' . + '\x{18800}-\x{18CD5}' . + '\x{18D00}-\x{18D08}' . + '\x{1B170}-\x{1B2FB}' . + '\x{20000}-\x{2A6DF}' . + '\x{2A700}-\x{2B739}' . + '\x{2B740}-\x{2B81D}' . + '\x{2B820}-\x{2CEA1}' . + '\x{2CEB0}-\x{2EBE0}' . + '\x{2F800}-\x{2FA1D}' . + '\x{30000}-\x{3134A}' . + '\x{31350}-\x{323AF}', + 'Join_Control' => + '\x{200C}-\x{200D}', + 'Regional_Indicator' => + '\x{1F1E6}-\x{1F1FF}', + 'Variation_Selector' => + '\x{180B}-\x{180D}' . + '\x{180F}' . + '\x{FE00}-\x{FE0F}' . + '\x{E0100}-\x{E01EF}', + ); +} + +/** + * Helper function for utf8_sanitize_invisibles. + * + * Character class lists compiled from: + * https://unicode.org/Public/UNIDATA/StandardizedVariants.txt + * https://unicode.org/Public/UNIDATA/emoji/emoji-variation-sequences.txt + * + * Developers: Do not update the data in this function manually. Instead, + * run "php -f other/update_unicode_data.php" on the command line. + * + * @return array Character classes for filtering variation selectors. + */ +function utf8_regex_variation_selectors() +{ + return array( + '\x{FE0E}\x{FE0F}' => + '\x{0023}' . + '\x{002A}' . + '\x{0030}-\x{0039}' . + '\x{00A9}' . + '\x{00AE}' . + '\x{203C}' . + '\x{2049}' . + '\x{2122}' . + '\x{2139}' . + '\x{2194}-\x{2199}' . + '\x{21A9}-\x{21AA}' . + '\x{231A}-\x{231B}' . + '\x{2328}' . + '\x{23CF}' . + '\x{23E9}-\x{23EA}' . + '\x{23ED}-\x{23EF}' . + '\x{23F1}-\x{23F3}' . + '\x{23F8}-\x{23FA}' . + '\x{24C2}' . + '\x{25AA}-\x{25AB}' . + '\x{25B6}' . + '\x{25C0}' . + '\x{25FB}-\x{25FE}' . + '\x{2600}-\x{2604}' . + '\x{260E}' . + '\x{2611}' . + '\x{2614}-\x{2615}' . + '\x{2618}' . + '\x{261D}' . + '\x{2620}' . + '\x{2622}-\x{2623}' . + '\x{2626}' . + '\x{262A}' . + '\x{262E}-\x{262F}' . + '\x{2638}-\x{263A}' . + '\x{2640}' . + '\x{2642}' . + '\x{2648}-\x{2653}' . + '\x{265F}-\x{2660}' . + '\x{2663}' . + '\x{2665}-\x{2666}' . + '\x{2668}' . + '\x{267B}' . + '\x{267E}-\x{267F}' . + '\x{2692}-\x{2697}' . + '\x{2699}' . + '\x{269B}-\x{269C}' . + '\x{26A0}-\x{26A1}' . + '\x{26A7}' . + '\x{26AA}-\x{26AB}' . + '\x{26B0}-\x{26B1}' . + '\x{26BD}-\x{26BE}' . + '\x{26C4}-\x{26C5}' . + '\x{26C8}' . + '\x{26CF}' . + '\x{26D1}' . + '\x{26D3}-\x{26D4}' . + '\x{26E9}-\x{26EA}' . + '\x{26F0}-\x{26F5}' . + '\x{26F7}-\x{26FA}' . + '\x{26FD}' . + '\x{2702}' . + '\x{2708}-\x{2709}' . + '\x{270C}-\x{270D}' . + '\x{270F}' . + '\x{2712}' . + '\x{2714}' . + '\x{2716}' . + '\x{271D}' . + '\x{2721}' . + '\x{2733}-\x{2734}' . + '\x{2744}' . + '\x{2747}' . + '\x{2753}' . + '\x{2757}' . + '\x{2763}-\x{2764}' . + '\x{27A1}' . + '\x{2934}-\x{2935}' . + '\x{2B05}-\x{2B07}' . + '\x{2B1B}-\x{2B1C}' . + '\x{2B50}' . + '\x{2B55}' . + '\x{3030}' . + '\x{303D}' . + '\x{3297}' . + '\x{3299}' . + '\x{1F004}' . + '\x{1F170}-\x{1F171}' . + '\x{1F17E}-\x{1F17F}' . + '\x{1F202}' . + '\x{1F21A}' . + '\x{1F22F}' . + '\x{1F237}' . + '\x{1F30D}-\x{1F30F}' . + '\x{1F315}' . + '\x{1F31C}' . + '\x{1F321}' . + '\x{1F324}-\x{1F32C}' . + '\x{1F336}' . + '\x{1F378}' . + '\x{1F37D}' . + '\x{1F393}' . + '\x{1F396}-\x{1F397}' . + '\x{1F399}-\x{1F39B}' . + '\x{1F39E}-\x{1F39F}' . + '\x{1F3A7}' . + '\x{1F3AC}-\x{1F3AE}' . + '\x{1F3C2}' . + '\x{1F3C4}' . + '\x{1F3C6}' . + '\x{1F3CA}-\x{1F3CE}' . + '\x{1F3D4}-\x{1F3E0}' . + '\x{1F3ED}' . + '\x{1F3F3}' . + '\x{1F3F5}' . + '\x{1F3F7}' . + '\x{1F408}' . + '\x{1F415}' . + '\x{1F41F}' . + '\x{1F426}' . + '\x{1F43F}' . + '\x{1F441}-\x{1F442}' . + '\x{1F446}-\x{1F449}' . + '\x{1F44D}-\x{1F44E}' . + '\x{1F453}' . + '\x{1F46A}' . + '\x{1F47D}' . + '\x{1F4A3}' . + '\x{1F4B0}' . + '\x{1F4B3}' . + '\x{1F4BB}' . + '\x{1F4BF}' . + '\x{1F4CB}' . + '\x{1F4DA}' . + '\x{1F4DF}' . + '\x{1F4E4}-\x{1F4E6}' . + '\x{1F4EA}-\x{1F4ED}' . + '\x{1F4F7}' . + '\x{1F4F9}-\x{1F4FB}' . + '\x{1F4FD}' . + '\x{1F508}' . + '\x{1F50D}' . + '\x{1F512}-\x{1F513}' . + '\x{1F549}-\x{1F54A}' . + '\x{1F550}-\x{1F567}' . + '\x{1F56F}-\x{1F570}' . + '\x{1F573}-\x{1F579}' . + '\x{1F587}' . + '\x{1F58A}-\x{1F58D}' . + '\x{1F590}' . + '\x{1F5A5}' . + '\x{1F5A8}' . + '\x{1F5B1}-\x{1F5B2}' . + '\x{1F5BC}' . + '\x{1F5C2}-\x{1F5C4}' . + '\x{1F5D1}-\x{1F5D3}' . + '\x{1F5DC}-\x{1F5DE}' . + '\x{1F5E1}' . + '\x{1F5E3}' . + '\x{1F5E8}' . + '\x{1F5EF}' . + '\x{1F5F3}' . + '\x{1F5FA}' . + '\x{1F610}' . + '\x{1F687}' . + '\x{1F68D}' . + '\x{1F691}' . + '\x{1F694}' . + '\x{1F698}' . + '\x{1F6AD}' . + '\x{1F6B2}' . + '\x{1F6B9}-\x{1F6BA}' . + '\x{1F6BC}' . + '\x{1F6CB}' . + '\x{1F6CD}-\x{1F6CF}' . + '\x{1F6E0}-\x{1F6E5}' . + '\x{1F6E9}' . + '\x{1F6F0}' . + '\x{1F6F3}', + '\x{FE02}' => + '\x{13117}' . + '\x{13139}' . + '\x{13183}' . + '\x{131A0}' . + '\x{131BA}' . + '\x{131EE}' . + '\x{13216}' . + '\x{1327B}' . + '\x{132A4}' . + '\x{132E7}' . + '\x{132E9}' . + '\x{132F8}' . + '\x{132FD}' . + '\x{13302}-\x{13303}' . + '\x{13310}-\x{13314}' . + '\x{1331C}' . + '\x{13321}' . + '\x{13331}' . + '\x{1334A}' . + '\x{13361}' . + '\x{13373}' . + '\x{1337D}' . + '\x{13385}' . + '\x{133AF}-\x{133B0}' . + '\x{133BF}' . + '\x{133DD}' . + '\x{13419}' . + '\x{1342C}' . + '\x{1342E}' . + '\x{537F}' . + '\x{5BE7}' . + '\x{618E}' . + '\x{61F2}' . + '\x{6717}' . + '\x{6A02}' . + '\x{6BBA}' . + '\x{6D41}' . + '\x{7DF4}' . + '\x{8005}' . + '\x{980B}' . + '\x{9F9C}', + '\x{FE01}' => + '\x{1D49C}' . + '\x{212C}' . + '\x{1D49E}-\x{1D49F}' . + '\x{2130}-\x{2131}' . + '\x{1D4A2}' . + '\x{210B}' . + '\x{2110}' . + '\x{1D4A5}-\x{1D4A6}' . + '\x{2112}' . + '\x{2133}' . + '\x{1D4A9}-\x{1D4AC}' . + '\x{211B}' . + '\x{1D4AE}-\x{1D4B5}' . + '\x{3001}-\x{3002}' . + '\x{FF01}' . + '\x{FF0C}' . + '\x{FF0E}' . + '\x{FF1A}-\x{FF1B}' . + '\x{FF1F}' . + '\x{13093}' . + '\x{130A9}' . + '\x{13187}' . + '\x{131B1}' . + '\x{131EE}' . + '\x{131F8}-\x{131FA}' . + '\x{13257}' . + '\x{1327F}' . + '\x{132A4}' . + '\x{13308}' . + '\x{13312}-\x{13314}' . + '\x{1331B}' . + '\x{13321}-\x{13322}' . + '\x{13331}' . + '\x{13419}' . + '\x{3B9D}' . + '\x{3EB8}' . + '\x{4039}' . + '\x{4FAE}' . + '\x{50E7}' . + '\x{514D}' . + '\x{51B5}' . + '\x{5207}' . + '\x{52C7}' . + '\x{52C9}' . + '\x{52E4}' . + '\x{52FA}' . + '\x{5317}' . + '\x{5351}' . + '\x{537F}' . + '\x{5584}' . + '\x{5599}' . + '\x{559D}' . + '\x{5606}' . + '\x{585A}' . + '\x{5B3E}' . + '\x{5BE7}' . + '\x{5C6E}' . + '\x{5ECA}' . + '\x{5F22}' . + '\x{6094}' . + '\x{614C}' . + '\x{614E}' . + '\x{618E}' . + '\x{61F2}' . + '\x{61F6}' . + '\x{654F}' . + '\x{6674}' . + '\x{6691}' . + '\x{6717}' . + '\x{671B}' . + '\x{6885}' . + '\x{6A02}' . + '\x{6BBA}' . + '\x{6D41}' . + '\x{6D77}' . + '\x{6ECB}' . + '\x{6F22}' . + '\x{701E}' . + '\x{716E}' . + '\x{7235}' . + '\x{732A}' . + '\x{7387}' . + '\x{7471}' . + '\x{7570}' . + '\x{76CA}' . + '\x{76F4}' . + '\x{771F}' . + '\x{774A}' . + '\x{788C}' . + '\x{78CC}' . + '\x{7956}' . + '\x{798F}' . + '\x{7A40}' . + '\x{7BC0}' . + '\x{7DF4}' . + '\x{8005}' . + '\x{8201}' . + '\x{8279}' . + '\x{82E5}' . + '\x{8457}' . + '\x{865C}' . + '\x{8779}' . + '\x{8996}' . + '\x{8AAA}' . + '\x{8AED}' . + '\x{8AF8}' . + '\x{8AFE}' . + '\x{8B01}' . + '\x{8B39}' . + '\x{8B8A}' . + '\x{8D08}' . + '\x{8F38}' . + '\x{9038}' . + '\x{96E3}' . + '\x{9756}' . + '\x{97FF}' . + '\x{980B}' . + '\x{983B}' . + '\x{9B12}' . + '\x{9F9C}' . + '\x{22331}' . + '\x{25AA7}', + '\x{FE00}' => + '\x{0030}' . + '\x{2205}' . + '\x{2229}-\x{222A}' . + '\x{2268}-\x{2269}' . + '\x{2272}-\x{2273}' . + '\x{228A}-\x{228B}' . + '\x{2293}-\x{2295}' . + '\x{2297}' . + '\x{229C}' . + '\x{22DA}-\x{22DB}' . + '\x{2A3C}-\x{2A3D}' . + '\x{2A9D}-\x{2A9E}' . + '\x{2AAC}-\x{2AAD}' . + '\x{2ACB}-\x{2ACC}' . + '\x{FF10}' . + '\x{1D49C}' . + '\x{212C}' . + '\x{1D49E}-\x{1D49F}' . + '\x{2130}-\x{2131}' . + '\x{1D4A2}' . + '\x{210B}' . + '\x{2110}' . + '\x{1D4A5}-\x{1D4A6}' . + '\x{2112}' . + '\x{2133}' . + '\x{1D4A9}-\x{1D4AC}' . + '\x{211B}' . + '\x{1D4AE}-\x{1D4B5}' . + '\x{3001}-\x{3002}' . + '\x{FF01}' . + '\x{FF0C}' . + '\x{FF0E}' . + '\x{FF1A}-\x{FF1B}' . + '\x{FF1F}' . + '\x{1000}' . + '\x{1002}' . + '\x{1004}' . + '\x{1010}-\x{1011}' . + '\x{1015}' . + '\x{1019}-\x{101A}' . + '\x{101C}-\x{101D}' . + '\x{1022}' . + '\x{1031}' . + '\x{1075}' . + '\x{1078}' . + '\x{107A}' . + '\x{1080}' . + '\x{AA60}-\x{AA66}' . + '\x{AA6B}-\x{AA6C}' . + '\x{AA6F}' . + '\x{AA7A}' . + '\x{A856}' . + '\x{A85C}' . + '\x{A85E}-\x{A860}' . + '\x{A868}' . + '\x{10AC5}-\x{10AC6}' . + '\x{10AD6}-\x{10AD7}' . + '\x{10AE1}' . + '\x{13091}-\x{13092}' . + '\x{1310F}' . + '\x{1311C}' . + '\x{13121}' . + '\x{13127}' . + '\x{13139}' . + '\x{131A0}' . + '\x{131B1}' . + '\x{131B8}-\x{131B9}' . + '\x{131CB}' . + '\x{131F9}-\x{131FA}' . + '\x{1327F}' . + '\x{13285}' . + '\x{1328C}' . + '\x{132AA}' . + '\x{132CB}' . + '\x{132DC}' . + '\x{132E7}' . + '\x{13307}' . + '\x{1331B}' . + '\x{13322}' . + '\x{1333B}-\x{1333C}' . + '\x{13377}-\x{13378}' . + '\x{13399}-\x{1339A}' . + '\x{133D3}' . + '\x{133F2}' . + '\x{133F5}-\x{133F6}' . + '\x{13403}' . + '\x{13416}' . + '\x{13419}-\x{1341A}' . + '\x{13423}' . + '\x{13443}-\x{13446}' . + '\x{349E}' . + '\x{34B9}' . + '\x{34BB}' . + '\x{34DF}' . + '\x{3515}' . + '\x{36EE}' . + '\x{36FC}' . + '\x{3781}' . + '\x{382F}' . + '\x{3862}' . + '\x{387C}' . + '\x{38C7}' . + '\x{38E3}' . + '\x{391C}' . + '\x{393A}' . + '\x{3A2E}' . + '\x{3A6C}' . + '\x{3AE4}' . + '\x{3B08}' . + '\x{3B19}' . + '\x{3B49}' . + '\x{3B9D}' . + '\x{3C18}' . + '\x{3C4E}' . + '\x{3D33}' . + '\x{3D96}' . + '\x{3EAC}' . + '\x{3EB8}' . + '\x{3F1B}' . + '\x{3FFC}' . + '\x{4008}' . + '\x{4018}' . + '\x{4039}' . + '\x{4046}' . + '\x{4096}' . + '\x{40E3}' . + '\x{412F}' . + '\x{4202}' . + '\x{4227}' . + '\x{42A0}' . + '\x{4301}' . + '\x{4334}' . + '\x{4359}' . + '\x{43D5}' . + '\x{43D9}' . + '\x{440B}' . + '\x{446B}' . + '\x{452B}' . + '\x{455D}' . + '\x{4561}' . + '\x{456B}' . + '\x{45D7}' . + '\x{45F9}' . + '\x{4635}' . + '\x{46BE}' . + '\x{46C7}' . + '\x{4995}' . + '\x{49E6}' . + '\x{4A6E}' . + '\x{4A76}' . + '\x{4AB2}' . + '\x{4B33}' . + '\x{4BCE}' . + '\x{4CCE}' . + '\x{4CED}' . + '\x{4CF8}' . + '\x{4D56}' . + '\x{4E0D}' . + '\x{4E26}' . + '\x{4E32}' . + '\x{4E38}-\x{4E39}' . + '\x{4E3D}' . + '\x{4E41}' . + '\x{4E82}' . + '\x{4E86}' . + '\x{4EAE}' . + '\x{4EC0}' . + '\x{4ECC}' . + '\x{4EE4}' . + '\x{4F60}' . + '\x{4F80}' . + '\x{4F86}' . + '\x{4F8B}' . + '\x{4FAE}' . + '\x{4FBB}' . + '\x{4FBF}' . + '\x{5002}' . + '\x{502B}' . + '\x{507A}' . + '\x{5099}' . + '\x{50CF}' . + '\x{50DA}' . + '\x{50E7}' . + '\x{5140}' . + '\x{5145}' . + '\x{514D}' . + '\x{5154}' . + '\x{5164}' . + '\x{5167}-\x{5169}' . + '\x{516D}' . + '\x{5177}' . + '\x{5180}' . + '\x{518D}' . + '\x{5192}' . + '\x{5195}' . + '\x{5197}' . + '\x{51A4}' . + '\x{51AC}' . + '\x{51B5}' . + '\x{51B7}' . + '\x{51C9}' . + '\x{51CC}' . + '\x{51DC}' . + '\x{51DE}' . + '\x{51F5}' . + '\x{5203}' . + '\x{5207}' . + '\x{5217}' . + '\x{5229}' . + '\x{523A}-\x{523B}' . + '\x{5246}' . + '\x{5272}' . + '\x{5277}' . + '\x{5289}' . + '\x{529B}' . + '\x{52A3}' . + '\x{52B3}' . + '\x{52C7}' . + '\x{52C9}' . + '\x{52D2}' . + '\x{52DE}' . + '\x{52E4}' . + '\x{52F5}' . + '\x{52FA}' . + '\x{5305}-\x{5306}' . + '\x{5317}' . + '\x{533F}' . + '\x{5349}' . + '\x{5351}' . + '\x{535A}' . + '\x{5373}' . + '\x{5375}' . + '\x{537D}' . + '\x{537F}' . + '\x{53C3}' . + '\x{53CA}' . + '\x{53DF}' . + '\x{53E5}' . + '\x{53EB}' . + '\x{53F1}' . + '\x{5406}' . + '\x{540F}' . + '\x{541D}' . + '\x{5438}' . + '\x{5442}' . + '\x{5448}' . + '\x{5468}' . + '\x{549E}' . + '\x{54A2}' . + '\x{54BD}' . + '\x{54F6}' . + '\x{5510}' . + '\x{5553}' . + '\x{5555}' . + '\x{5563}' . + '\x{5584}' . + '\x{5587}' . + '\x{5599}' . + '\x{559D}' . + '\x{55AB}' . + '\x{55B3}' . + '\x{55C0}' . + '\x{55C2}' . + '\x{55E2}' . + '\x{5606}' . + '\x{5651}' . + '\x{5668}' . + '\x{5674}' . + '\x{56F9}' . + '\x{5716}-\x{5717}' . + '\x{578B}' . + '\x{57CE}' . + '\x{57F4}' . + '\x{580D}' . + '\x{5831}-\x{5832}' . + '\x{5840}' . + '\x{585A}' . + '\x{585E}' . + '\x{58A8}' . + '\x{58AC}' . + '\x{58B3}' . + '\x{58D8}' . + '\x{58DF}' . + '\x{58EE}' . + '\x{58F2}' . + '\x{58F7}' . + '\x{5906}' . + '\x{591A}' . + '\x{5922}' . + '\x{5944}' . + '\x{5948}' . + '\x{5951}' . + '\x{5954}' . + '\x{5962}' . + '\x{5973}' . + '\x{59D8}' . + '\x{59EC}' . + '\x{5A1B}' . + '\x{5A27}' . + '\x{5A62}' . + '\x{5A66}' . + '\x{5AB5}' . + '\x{5B08}' . + '\x{5B28}' . + '\x{5B3E}' . + '\x{5B85}' . + '\x{5BC3}' . + '\x{5BD8}' . + '\x{5BE7}' . + '\x{5BEE}' . + '\x{5BF3}' . + '\x{5BFF}' . + '\x{5C06}' . + '\x{5C22}' . + '\x{5C3F}' . + '\x{5C60}' . + '\x{5C62}' . + '\x{5C64}-\x{5C65}' . + '\x{5C6E}' . + '\x{5C8D}' . + '\x{5CC0}' . + '\x{5D19}' . + '\x{5D43}' . + '\x{5D50}' . + '\x{5D6B}' . + '\x{5D6E}' . + '\x{5D7C}' . + '\x{5DB2}' . + '\x{5DBA}' . + '\x{5DE1}-\x{5DE2}' . + '\x{5DFD}' . + '\x{5E28}' . + '\x{5E3D}' . + '\x{5E69}' . + '\x{5E74}' . + '\x{5EA6}' . + '\x{5EB0}' . + '\x{5EB3}' . + '\x{5EB6}' . + '\x{5EC9}-\x{5ECA}' . + '\x{5ED2}-\x{5ED3}' . + '\x{5ED9}' . + '\x{5EEC}' . + '\x{5EFE}' . + '\x{5F04}' . + '\x{5F22}' . + '\x{5F53}' . + '\x{5F62}' . + '\x{5F69}' . + '\x{5F6B}' . + '\x{5F8B}' . + '\x{5F9A}' . + '\x{5FA9}' . + '\x{5FAD}' . + '\x{5FCD}' . + '\x{5FD7}' . + '\x{5FF5}' . + '\x{5FF9}' . + '\x{6012}' . + '\x{601C}' . + '\x{6075}' . + '\x{6081}' . + '\x{6094}' . + '\x{60C7}' . + '\x{60D8}' . + '\x{60E1}' . + '\x{6108}' . + '\x{6144}' . + '\x{6148}' . + '\x{614C}' . + '\x{614E}' . + '\x{6160}' . + '\x{6168}' . + '\x{617A}' . + '\x{618E}' . + '\x{6190}' . + '\x{61A4}' . + '\x{61AF}' . + '\x{61B2}' . + '\x{61DE}' . + '\x{61F2}' . + '\x{61F6}' . + '\x{6200}' . + '\x{6210}' . + '\x{621B}' . + '\x{622E}' . + '\x{6234}' . + '\x{625D}' . + '\x{62B1}' . + '\x{62C9}' . + '\x{62CF}' . + '\x{62D3}-\x{62D4}' . + '\x{62FC}' . + '\x{62FE}' . + '\x{633D}' . + '\x{6350}' . + '\x{6368}' . + '\x{637B}' . + '\x{6383}' . + '\x{63A0}' . + '\x{63A9}' . + '\x{63C4}-\x{63C5}' . + '\x{63E4}' . + '\x{641C}' . + '\x{6422}' . + '\x{6452}' . + '\x{6469}' . + '\x{6477}' . + '\x{647E}' . + '\x{649A}' . + '\x{649D}' . + '\x{64C4}' . + '\x{654F}' . + '\x{6556}' . + '\x{656C}' . + '\x{6578}' . + '\x{6599}' . + '\x{65C5}' . + '\x{65E2}-\x{65E3}' . + '\x{6613}' . + '\x{6649}' . + '\x{6674}' . + '\x{6688}' . + '\x{6691}' . + '\x{669C}' . + '\x{66B4}' . + '\x{66C6}' . + '\x{66F4}' . + '\x{66F8}' . + '\x{6700}' . + '\x{6717}' . + '\x{671B}' . + '\x{6721}' . + '\x{674E}' . + '\x{6753}' . + '\x{6756}' . + '\x{675E}' . + '\x{677B}' . + '\x{6785}' . + '\x{6797}' . + '\x{67F3}' . + '\x{67FA}' . + '\x{6817}' . + '\x{681F}' . + '\x{6852}' . + '\x{6881}' . + '\x{6885}' . + '\x{688E}' . + '\x{68A8}' . + '\x{6914}' . + '\x{6942}' . + '\x{69A3}' . + '\x{69EA}' . + '\x{6A02}' . + '\x{6A13}' . + '\x{6AA8}' . + '\x{6AD3}' . + '\x{6ADB}' . + '\x{6B04}' . + '\x{6B21}' . + '\x{6B54}' . + '\x{6B72}' . + '\x{6B77}' . + '\x{6B79}' . + '\x{6B9F}' . + '\x{6BAE}' . + '\x{6BBA}-\x{6BBB}' . + '\x{6C4E}' . + '\x{6C67}' . + '\x{6C88}' . + '\x{6CBF}' . + '\x{6CCC}-\x{6CCD}' . + '\x{6CE5}' . + '\x{6D16}' . + '\x{6D1B}' . + '\x{6D1E}' . + '\x{6D34}' . + '\x{6D3E}' . + '\x{6D41}' . + '\x{6D69}-\x{6D6A}' . + '\x{6D77}-\x{6D78}' . + '\x{6D85}' . + '\x{6DCB}' . + '\x{6DDA}' . + '\x{6DEA}' . + '\x{6DF9}' . + '\x{6E1A}' . + '\x{6E2F}' . + '\x{6E6E}' . + '\x{6E9C}' . + '\x{6EBA}' . + '\x{6EC7}' . + '\x{6ECB}' . + '\x{6ED1}' . + '\x{6EDB}' . + '\x{6F0F}' . + '\x{6F22}-\x{6F23}' . + '\x{6F6E}' . + '\x{6FC6}' . + '\x{6FEB}' . + '\x{6FFE}' . + '\x{701B}' . + '\x{701E}' . + '\x{7039}' . + '\x{704A}' . + '\x{7070}' . + '\x{7077}' . + '\x{707D}' . + '\x{7099}' . + '\x{70AD}' . + '\x{70C8}' . + '\x{70D9}' . + '\x{7145}' . + '\x{7149}' . + '\x{716E}' . + '\x{719C}' . + '\x{71CE}' . + '\x{71D0}' . + '\x{7210}' . + '\x{721B}' . + '\x{7228}' . + '\x{722B}' . + '\x{7235}' . + '\x{7250}' . + '\x{7262}' . + '\x{7280}' . + '\x{7295}' . + '\x{72AF}' . + '\x{72C0}' . + '\x{72FC}' . + '\x{732A}' . + '\x{7375}' . + '\x{737A}' . + '\x{7387}' . + '\x{738B}' . + '\x{73A5}' . + '\x{73B2}' . + '\x{73DE}' . + '\x{7406}' . + '\x{7409}' . + '\x{7422}' . + '\x{7447}' . + '\x{745C}' . + '\x{7469}' . + '\x{7471}' . + '\x{7485}' . + '\x{7489}' . + '\x{7498}' . + '\x{74CA}' . + '\x{7506}' . + '\x{7524}' . + '\x{753B}' . + '\x{753E}' . + '\x{7559}' . + '\x{7565}' . + '\x{7570}' . + '\x{75E2}' . + '\x{7610}' . + '\x{761D}' . + '\x{761F}' . + '\x{7642}' . + '\x{7669}' . + '\x{76CA}' . + '\x{76DB}' . + '\x{76E7}' . + '\x{76F4}' . + '\x{7701}' . + '\x{771E}-\x{771F}' . + '\x{7740}' . + '\x{774A}' . + '\x{778B}' . + '\x{77A7}' . + '\x{784E}' . + '\x{786B}' . + '\x{788C}' . + '\x{7891}' . + '\x{78CA}' . + '\x{78CC}' . + '\x{78FB}' . + '\x{792A}' . + '\x{793C}' . + '\x{793E}' . + '\x{7948}-\x{7949}' . + '\x{7950}' . + '\x{7956}' . + '\x{795D}-\x{795E}' . + '\x{7965}' . + '\x{797F}' . + '\x{798D}-\x{798F}' . + '\x{79AE}' . + '\x{79CA}' . + '\x{79EB}' . + '\x{7A1C}' . + '\x{7A40}' . + '\x{7A4A}' . + '\x{7A4F}' . + '\x{7A81}' . + '\x{7AB1}' . + '\x{7ACB}' . + '\x{7AEE}' . + '\x{7B20}' . + '\x{7BC0}' . + '\x{7BC6}' . + '\x{7BC9}' . + '\x{7C3E}' . + '\x{7C60}' . + '\x{7C7B}' . + '\x{7C92}' . + '\x{7CBE}' . + '\x{7CD2}' . + '\x{7CD6}' . + '\x{7CE3}' . + '\x{7CE7}-\x{7CE8}' . + '\x{7D00}' . + '\x{7D10}' . + '\x{7D22}' . + '\x{7D2F}' . + '\x{7D5B}' . + '\x{7D63}' . + '\x{7DA0}' . + '\x{7DBE}' . + '\x{7DC7}' . + '\x{7DF4}' . + '\x{7E02}' . + '\x{7E09}' . + '\x{7E37}' . + '\x{7E41}' . + '\x{7E45}' . + '\x{7F3E}' . + '\x{7F72}' . + '\x{7F79}-\x{7F7A}' . + '\x{7F85}' . + '\x{7F95}' . + '\x{7F9A}' . + '\x{7FBD}' . + '\x{7FFA}' . + '\x{8001}' . + '\x{8005}' . + '\x{8046}' . + '\x{8060}' . + '\x{806F}-\x{8070}' . + '\x{807E}' . + '\x{808B}' . + '\x{80AD}' . + '\x{80B2}' . + '\x{8103}' . + '\x{813E}' . + '\x{81D8}' . + '\x{81E8}' . + '\x{81ED}' . + '\x{8201}' . + '\x{8204}' . + '\x{8218}' . + '\x{826F}' . + '\x{8279}' . + '\x{828B}' . + '\x{8291}' . + '\x{829D}' . + '\x{82B1}' . + '\x{82B3}' . + '\x{82BD}' . + '\x{82E5}-\x{82E6}' . + '\x{831D}' . + '\x{8323}' . + '\x{8336}' . + '\x{8352}-\x{8353}' . + '\x{8363}' . + '\x{83AD}' . + '\x{83BD}' . + '\x{83C9}-\x{83CA}' . + '\x{83CC}' . + '\x{83DC}' . + '\x{83E7}' . + '\x{83EF}' . + '\x{83F1}' . + '\x{843D}' . + '\x{8449}' . + '\x{8457}' . + '\x{84EE}' . + '\x{84F1}' . + '\x{84F3}' . + '\x{84FC}' . + '\x{8516}' . + '\x{8564}' . + '\x{85CD}' . + '\x{85FA}' . + '\x{8606}' . + '\x{8612}' . + '\x{862D}' . + '\x{863F}' . + '\x{8650}' . + '\x{865C}' . + '\x{8667}' . + '\x{8669}' . + '\x{8688}' . + '\x{86A9}' . + '\x{86E2}' . + '\x{870E}' . + '\x{8728}' . + '\x{876B}' . + '\x{8779}' . + '\x{8786}' . + '\x{87BA}' . + '\x{87E1}' . + '\x{8801}' . + '\x{881F}' . + '\x{884C}' . + '\x{8860}' . + '\x{8863}' . + '\x{88C2}' . + '\x{88CF}' . + '\x{88D7}' . + '\x{88DE}' . + '\x{88E1}' . + '\x{88F8}' . + '\x{88FA}' . + '\x{8910}' . + '\x{8941}' . + '\x{8964}' . + '\x{8986}' . + '\x{898B}' . + '\x{8996}' . + '\x{8AA0}' . + '\x{8AAA}' . + '\x{8ABF}' . + '\x{8ACB}' . + '\x{8AD2}' . + '\x{8AD6}' . + '\x{8AED}' . + '\x{8AF8}' . + '\x{8AFE}' . + '\x{8B01}' . + '\x{8B39}' . + '\x{8B58}' . + '\x{8B80}' . + '\x{8B8A}' . + '\x{8C48}' . + '\x{8C55}' . + '\x{8CAB}' . + '\x{8CC1}-\x{8CC2}' . + '\x{8CC8}' . + '\x{8CD3}' . + '\x{8D08}' . + '\x{8D1B}' . + '\x{8D77}' . + '\x{8DBC}' . + '\x{8DCB}' . + '\x{8DEF}-\x{8DF0}' . + '\x{8ECA}' . + '\x{8ED4}' . + '\x{8F26}' . + '\x{8F2A}' . + '\x{8F38}' . + '\x{8F3B}' . + '\x{8F62}' . + '\x{8F9E}' . + '\x{8FB0}' . + '\x{8FB6}' . + '\x{9023}' . + '\x{9038}' . + '\x{9072}' . + '\x{907C}' . + '\x{908F}' . + '\x{9094}' . + '\x{90CE}' . + '\x{90DE}' . + '\x{90F1}' . + '\x{90FD}' . + '\x{9111}' . + '\x{911B}' . + '\x{916A}' . + '\x{9199}' . + '\x{91B4}' . + '\x{91CC}' . + '\x{91CF}' . + '\x{91D1}' . + '\x{9234}' . + '\x{9238}' . + '\x{9276}' . + '\x{927C}' . + '\x{92D7}-\x{92D8}' . + '\x{9304}' . + '\x{934A}' . + '\x{93F9}' . + '\x{9415}' . + '\x{958B}' . + '\x{95AD}' . + '\x{95B7}' . + '\x{962E}' . + '\x{964B}' . + '\x{964D}' . + '\x{9675}' . + '\x{9678}' . + '\x{967C}' . + '\x{9686}' . + '\x{96A3}' . + '\x{96B7}-\x{96B8}' . + '\x{96C3}' . + '\x{96E2}-\x{96E3}' . + '\x{96F6}-\x{96F7}' . + '\x{9723}' . + '\x{9732}' . + '\x{9748}' . + '\x{9756}' . + '\x{97DB}' . + '\x{97E0}' . + '\x{97FF}' . + '\x{980B}' . + '\x{9818}' . + '\x{9829}' . + '\x{983B}' . + '\x{985E}' . + '\x{98E2}' . + '\x{98EF}' . + '\x{98FC}' . + '\x{9928}-\x{9929}' . + '\x{99A7}' . + '\x{99C2}' . + '\x{99F1}' . + '\x{99FE}' . + '\x{9A6A}' . + '\x{9B12}' . + '\x{9B6F}' . + '\x{9C40}' . + '\x{9C57}' . + '\x{9CFD}' . + '\x{9D67}' . + '\x{9DB4}' . + '\x{9DFA}' . + '\x{9E1E}' . + '\x{9E7F}' . + '\x{9E97}' . + '\x{9E9F}' . + '\x{9EBB}' . + '\x{9ECE}' . + '\x{9EF9}' . + '\x{9EFE}' . + '\x{9F05}' . + '\x{9F0F}' . + '\x{9F16}' . + '\x{9F3B}' . + '\x{9F43}' . + '\x{9F8D}-\x{9F8E}' . + '\x{9F9C}' . + '\x{20122}' . + '\x{2051C}' . + '\x{20525}' . + '\x{2054B}' . + '\x{2063A}' . + '\x{20804}' . + '\x{208DE}' . + '\x{20A2C}' . + '\x{20B63}' . + '\x{214E4}' . + '\x{216A8}' . + '\x{216EA}' . + '\x{219C8}' . + '\x{21B18}' . + '\x{21D0B}' . + '\x{21DE4}' . + '\x{21DE6}' . + '\x{22183}' . + '\x{2219F}' . + '\x{22331}' . + '\x{226D4}' . + '\x{22844}' . + '\x{2284A}' . + '\x{22B0C}' . + '\x{22BF1}' . + '\x{2300A}' . + '\x{232B8}' . + '\x{2335F}' . + '\x{23393}' . + '\x{2339C}' . + '\x{233C3}' . + '\x{233D5}' . + '\x{2346D}' . + '\x{236A3}' . + '\x{238A7}' . + '\x{23A8D}' . + '\x{23AFA}' . + '\x{23CBC}' . + '\x{23D1E}' . + '\x{23ED1}' . + '\x{23F5E}' . + '\x{23F8E}' . + '\x{24263}' . + '\x{242EE}' . + '\x{243AB}' . + '\x{24608}' . + '\x{24735}' . + '\x{24814}' . + '\x{24C36}' . + '\x{24C92}' . + '\x{24FA1}' . + '\x{24FB8}' . + '\x{25044}' . + '\x{250F2}-\x{250F3}' . + '\x{25119}' . + '\x{25133}' . + '\x{25249}' . + '\x{2541D}' . + '\x{25626}' . + '\x{2569A}' . + '\x{256C5}' . + '\x{2597C}' . + '\x{25AA7}' . + '\x{25BAB}' . + '\x{25C80}' . + '\x{25CD0}' . + '\x{25F86}' . + '\x{261DA}' . + '\x{26228}' . + '\x{26247}' . + '\x{262D9}' . + '\x{2633E}' . + '\x{264DA}' . + '\x{26523}' . + '\x{265A8}' . + '\x{267A7}' . + '\x{267B5}' . + '\x{26B3C}' . + '\x{26C36}' . + '\x{26CD5}' . + '\x{26D6B}' . + '\x{26F2C}' . + '\x{26FB1}' . + '\x{270D2}' . + '\x{273CA}' . + '\x{27667}' . + '\x{278AE}' . + '\x{27966}' . + '\x{27CA8}' . + '\x{27ED3}' . + '\x{27F2F}' . + '\x{285D2}' . + '\x{285ED}' . + '\x{2872E}' . + '\x{28BFA}' . + '\x{28D77}' . + '\x{29145}' . + '\x{291DF}' . + '\x{2921A}' . + '\x{2940A}' . + '\x{29496}' . + '\x{295B6}' . + '\x{29B30}' . + '\x{2A0CE}' . + '\x{2A105}' . + '\x{2A20E}' . + '\x{2A291}' . + '\x{2A392}' . + '\x{2A600}', + '\x{180D}' => + '\x{1828}' . + '\x{182C}-\x{182D}' . + '\x{1873}-\x{1874}' . + '\x{1887}', + '\x{180C}' => + '\x{1820}' . + '\x{1825}-\x{1826}' . + '\x{1828}' . + '\x{182C}-\x{182D}' . + '\x{1830}' . + '\x{1836}' . + '\x{1847}' . + '\x{185E}' . + '\x{1868}' . + '\x{1873}-\x{1874}' . + '\x{1887}', + '\x{180B}' => + '\x{1820}-\x{1826}' . + '\x{1828}' . + '\x{182A}' . + '\x{182C}-\x{182D}' . + '\x{1830}' . + '\x{1832}-\x{1833}' . + '\x{1835}-\x{1836}' . + '\x{1838}' . + '\x{1844}-\x{1849}' . + '\x{184D}-\x{184E}' . + '\x{185D}-\x{185E}' . + '\x{1860}' . + '\x{1863}' . + '\x{1868}-\x{1869}' . + '\x{186F}' . + '\x{1873}-\x{1874}' . + '\x{1876}' . + '\x{1880}-\x{1881}' . + '\x{1887}-\x{1888}' . + '\x{188A}', + ); +} + +/** + * Helper function for utf8_sanitize_invisibles. + * + * Character class lists compiled from: + * https://unicode.org/Public/UNIDATA/extracted/DerivedJoiningType.txt + * + * Developers: Do not update the data in this function manually. Instead, + * run "php -f other/update_unicode_data.php" on the command line. + * + * @return array Character classes for joining characters in certain scripts. + */ +function utf8_regex_joining_type() +{ + return array( + 'Arabic' => array( + 'Join_Causing' => + '\x{0640}' . + '\x{0883}-\x{0885}', + 'Dual_Joining' => + '\x{0620}' . + '\x{0626}' . + '\x{0628}' . + '\x{062A}-\x{062E}' . + '\x{0633}-\x{063F}' . + '\x{0641}-\x{0647}' . + '\x{0649}-\x{064A}' . + '\x{066E}-\x{066F}' . + '\x{0678}-\x{0687}' . + '\x{069A}-\x{06BF}' . + '\x{06C1}-\x{06C2}' . + '\x{06CC}' . + '\x{06CE}' . + '\x{06D0}-\x{06D1}' . + '\x{06FA}-\x{06FC}' . + '\x{06FF}' . + '\x{075C}-\x{076A}' . + '\x{076D}-\x{0770}' . + '\x{0772}' . + '\x{0775}-\x{0777}' . + '\x{077A}-\x{077F}' . + '\x{0886}' . + '\x{0889}-\x{088D}' . + '\x{08A0}-\x{08A9}' . + '\x{08AF}-\x{08B0}' . + '\x{08B3}-\x{08B8}' . + '\x{08BA}-\x{08C8}', + 'Right_Joining' => + '\x{0622}-\x{0625}' . + '\x{0627}' . + '\x{0629}' . + '\x{062F}-\x{0632}' . + '\x{0648}' . + '\x{0671}-\x{0673}' . + '\x{0675}-\x{0677}' . + '\x{0688}-\x{0699}' . + '\x{06C0}' . + '\x{06C3}-\x{06CB}' . + '\x{06CD}' . + '\x{06CF}' . + '\x{06D2}-\x{06D3}' . + '\x{06D5}' . + '\x{06EE}-\x{06EF}' . + '\x{0759}-\x{075B}' . + '\x{076B}-\x{076C}' . + '\x{0771}' . + '\x{0773}-\x{0774}' . + '\x{0778}-\x{0779}' . + '\x{0870}-\x{0882}' . + '\x{088E}' . + '\x{08AA}-\x{08AC}' . + '\x{08AE}' . + '\x{08B1}-\x{08B2}' . + '\x{08B9}', + 'Transparent' => + '\x{0610}-\x{061A}' . + '\x{061C}' . + '\x{061C}' . + '\x{064B}-\x{065F}' . + '\x{0670}' . + '\x{06D6}-\x{06DC}' . + '\x{06DF}-\x{06E4}' . + '\x{06E7}-\x{06E8}' . + '\x{06EA}-\x{06ED}' . + '\x{0898}-\x{089F}' . + '\x{08CA}-\x{08E1}' . + '\x{08E3}-\x{0902}' . + '\x{102E0}' . + '\x{10EFD}-\x{10EFF}', + ), + 'Syriac' => array( + 'Join_Causing' => + '\x{0640}', + 'Dual_Joining' => + '\x{0712}-\x{0714}' . + '\x{071A}-\x{071D}' . + '\x{071F}-\x{0727}' . + '\x{0729}' . + '\x{072B}' . + '\x{072D}-\x{072E}' . + '\x{074E}-\x{0758}' . + '\x{0860}' . + '\x{0862}-\x{0865}' . + '\x{0868}', + 'Right_Joining' => + '\x{0710}' . + '\x{0715}-\x{0719}' . + '\x{071E}' . + '\x{0728}' . + '\x{072A}' . + '\x{072C}' . + '\x{072F}' . + '\x{074D}' . + '\x{0867}' . + '\x{0869}-\x{086A}', + 'Transparent' => + '\x{061C}' . + '\x{0670}' . + '\x{070F}' . + '\x{0711}' . + '\x{0730}-\x{074A}', + ), + 'Adlam' => array( + 'Join_Causing' => + '\x{0640}', + 'Dual_Joining' => + '\x{1E900}-\x{1E943}', + 'Transparent' => + '\x{1E944}-\x{1E94A}' . + '\x{1E94B}', + ), + 'Tirhuta' => array( + 'Dual_Joining' => + '\x{A840}-\x{A871}', + 'Transparent' => + '\x{0951}-\x{0957}' . + '\x{114B3}-\x{114B8}' . + '\x{114BA}' . + '\x{114BF}-\x{114C0}' . + '\x{114C2}-\x{114C3}', + ), + 'Nko' => array( + 'Join_Causing' => + '\x{07FA}', + 'Dual_Joining' => + '\x{07CA}-\x{07EA}', + 'Transparent' => + '\x{07EB}-\x{07F3}' . + '\x{07FD}', + ), + 'Hanifi_Rohingya' => array( + 'Join_Causing' => + '\x{0640}', + 'Dual_Joining' => + '\x{10D01}-\x{10D21}' . + '\x{10D23}', + 'Right_Joining' => + '\x{10D22}', + 'Left_Joining' => + '\x{10D00}', + 'Transparent' => + '\x{10D24}-\x{10D27}', + ), + 'Manichaean' => array( + 'Join_Causing' => + '\x{0640}', + 'Dual_Joining' => + '\x{10AC0}-\x{10AC4}' . + '\x{10AD3}-\x{10AD6}' . + '\x{10AD8}-\x{10ADC}' . + '\x{10ADE}-\x{10AE0}' . + '\x{10AEB}-\x{10AEE}', + 'Right_Joining' => + '\x{10AC5}' . + '\x{10AC7}' . + '\x{10AC9}-\x{10ACA}' . + '\x{10ACE}-\x{10AD2}' . + '\x{10ADD}' . + '\x{10AE1}' . + '\x{10AE4}' . + '\x{10AEF}', + 'Left_Joining' => + '\x{10ACD}' . + '\x{10AD7}', + 'Transparent' => + '\x{10AE5}-\x{10AE6}', + ), + 'Sogdian' => array( + 'Join_Causing' => + '\x{0640}', + 'Dual_Joining' => + '\x{10F30}-\x{10F32}' . + '\x{10F34}-\x{10F44}' . + '\x{10F51}-\x{10F53}', + 'Right_Joining' => + '\x{10F33}' . + '\x{10F54}', + 'Transparent' => + '\x{10F46}-\x{10F50}', + ), + 'Mandaic' => array( + 'Join_Causing' => + '\x{0640}', + 'Dual_Joining' => + '\x{0841}-\x{0845}' . + '\x{0848}' . + '\x{084A}-\x{0853}' . + '\x{0855}', + 'Right_Joining' => + '\x{0840}' . + '\x{0846}-\x{0847}' . + '\x{0849}' . + '\x{0854}' . + '\x{0856}-\x{0858}', + 'Transparent' => + '\x{0859}-\x{085B}', + ), + 'Psalter_Pahlavi' => array( + 'Join_Causing' => + '\x{0640}', + 'Dual_Joining' => + '\x{10B80}' . + '\x{10B82}' . + '\x{10B86}-\x{10B88}' . + '\x{10B8A}-\x{10B8B}' . + '\x{10B8D}' . + '\x{10B90}' . + '\x{10BAD}-\x{10BAE}', + 'Right_Joining' => + '\x{10B81}' . + '\x{10B83}-\x{10B85}' . + '\x{10B89}' . + '\x{10B8C}' . + '\x{10B8E}-\x{10B8F}' . + '\x{10B91}' . + '\x{10BA9}-\x{10BAC}', + ), + 'Old_Uyghur' => array( + 'Join_Causing' => + '\x{0640}', + 'Dual_Joining' => + '\x{10F70}-\x{10F73}' . + '\x{10F76}-\x{10F81}', + 'Right_Joining' => + '\x{10F74}-\x{10F75}', + 'Transparent' => + '\x{10F82}-\x{10F85}', + ), + 'Mongolian' => array( + 'Join_Causing' => + '\x{180A}', + 'Dual_Joining' => + '\x{1807}' . + '\x{1820}-\x{1842}' . + '\x{1843}' . + '\x{1844}-\x{1878}' . + '\x{1887}-\x{18A8}' . + '\x{18AA}', + 'Transparent' => + '\x{180B}-\x{180D}' . + '\x{180F}' . + '\x{1885}-\x{1886}' . + '\x{18A9}', + ), + 'Phags_Pa' => array( + 'Dual_Joining' => + '\x{A840}-\x{A871}', + 'Left_Joining' => + '\x{A872}', + ), + 'Chorasmian' => array( + 'Dual_Joining' => + '\x{10FB0}' . + '\x{10FB2}-\x{10FB3}' . + '\x{10FB8}' . + '\x{10FBB}-\x{10FBC}' . + '\x{10FBE}-\x{10FBF}' . + '\x{10FC1}' . + '\x{10FC4}' . + '\x{10FCA}', + 'Right_Joining' => + '\x{10FB4}-\x{10FB6}' . + '\x{10FB9}-\x{10FBA}' . + '\x{10FBD}' . + '\x{10FC2}-\x{10FC3}' . + '\x{10FC9}', + 'Left_Joining' => + '\x{10FCB}', + ), + ); +} + +/** + * Helper function for utf8_sanitize_invisibles. + * + * Character class lists compiled from: + * https://unicode.org/Public/UNIDATA/extracted/DerivedCombiningClass.txt + * https://unicode.org/Public/UNIDATA/IndicSyllabicCategory.txt + * + * Developers: Do not update the data in this function manually. Instead, + * run "php -f other/update_unicode_data.php" on the command line. + * + * @return array Character classes for Indic scripts that use viramas. + */ +function utf8_regex_indic() +{ + return array( + 'Devanagari' => array( + 'All' => + '\x{0900}-\x{0952}' . + '\x{0955}-\x{0966}' . + '\x{0966}-\x{096A}' . + '\x{096A}-\x{096E}' . + '\x{096E}-\x{097F}' . + '\x{1CD0}-\x{1CD4}' . + '\x{1CD6}-\x{1CDC}' . + '\x{1CDE}-\x{1CF4}' . + '\x{1CF6}' . + '\x{1CF8}' . + '\x{20F0}' . + '\x{A830}' . + '\x{A833}' . + '\x{A836}' . + '\x{A838}-\x{A839}' . + '\x{A8E0}-\x{A8F1}' . + '\x{A8F1}-\x{A8F3}' . + '\x{A8F3}-\x{A8FF}' . + '\x{11B00}-\x{11B09}', + 'Letter' => + '\x{0904}-\x{0939}' . + '\x{093D}' . + '\x{0950}' . + '\x{0958}-\x{0961}' . + '\x{0971}-\x{097F}' . + '\x{1CE9}-\x{1CEC}' . + '\x{1CEE}-\x{1CF3}' . + '\x{1CF6}' . + '\x{A8F2}-\x{A8F3}' . + '\x{A8F3}-\x{A8F7}' . + '\x{A8FB}' . + '\x{A8FD}-\x{A8FE}', + 'Nonspacing_Combining_Mark' => + '\x{093C}' . + '\x{094D}' . + '\x{0951}-\x{0952}' . + '\x{1CD0}-\x{1CD2}' . + '\x{1CD4}' . + '\x{1CD6}-\x{1CDC}' . + '\x{1CDE}-\x{1CE0}' . + '\x{1CE2}-\x{1CE8}' . + '\x{1CED}' . + '\x{1CF4}' . + '\x{1CF8}' . + '\x{20F0}' . + '\x{A8E0}-\x{A8F1}' . + '\x{A8F1}', + 'Nonspacing_Mark' => + '\x{0900}-\x{0902}' . + '\x{093A}' . + '\x{093C}' . + '\x{0941}-\x{0948}' . + '\x{094D}' . + '\x{0951}-\x{0952}' . + '\x{0955}-\x{0957}' . + '\x{0962}-\x{0963}' . + '\x{1CD0}-\x{1CD2}' . + '\x{1CD4}' . + '\x{1CD6}-\x{1CDC}' . + '\x{1CDE}-\x{1CE0}' . + '\x{1CE2}-\x{1CE8}' . + '\x{1CED}' . + '\x{1CF4}' . + '\x{1CF8}' . + '\x{20F0}' . + '\x{A8E0}-\x{A8F1}' . + '\x{A8F1}' . + '\x{A8FF}', + 'Virama' => + '\x{094D}', + 'Vowel_Dependent' => + '\x{093A}' . + '\x{093B}' . + '\x{093E}-\x{0940}' . + '\x{0941}-\x{0948}' . + '\x{0949}-\x{094C}' . + '\x{094E}-\x{094F}' . + '\x{0955}-\x{0957}' . + '\x{0962}-\x{0963}' . + '\x{A8FF}', + ), + 'Tamil' => array( + 'All' => + '\x{0951}-\x{0952}' . + '\x{0964}-\x{0965}' . + '\x{0B82}-\x{0B83}' . + '\x{0B85}-\x{0B8A}' . + '\x{0B8E}-\x{0B90}' . + '\x{0B92}-\x{0B95}' . + '\x{0B99}-\x{0B9A}' . + '\x{0B9C}' . + '\x{0B9E}-\x{0B9F}' . + '\x{0BA3}-\x{0BA4}' . + '\x{0BA8}-\x{0BAA}' . + '\x{0BAE}-\x{0BB9}' . + '\x{0BBE}-\x{0BC2}' . + '\x{0BC6}-\x{0BC8}' . + '\x{0BCA}-\x{0BCD}' . + '\x{0BD0}' . + '\x{0BD7}' . + '\x{0BE6}-\x{0BE7}' . + '\x{0BE7}-\x{0BE9}' . + '\x{0BE9}-\x{0BEB}' . + '\x{0BEB}-\x{0BED}' . + '\x{0BED}-\x{0BEF}' . + '\x{0BEF}-\x{0BF1}' . + '\x{0BF1}-\x{0BF3}' . + '\x{0BF3}' . + '\x{0BF3}-\x{0BFA}' . + '\x{1CDA}' . + '\x{A8F3}' . + '\x{11301}' . + '\x{11303}' . + '\x{1133C}' . + '\x{11FC0}-\x{11FD1}' . + '\x{11FD1}-\x{11FD3}' . + '\x{11FD3}-\x{11FF1}' . + '\x{11FFF}', + 'Letter' => + '\x{0B83}' . + '\x{0B85}-\x{0B8A}' . + '\x{0B8E}-\x{0B90}' . + '\x{0B92}-\x{0B95}' . + '\x{0B99}-\x{0B9A}' . + '\x{0B9C}' . + '\x{0B9E}-\x{0B9F}' . + '\x{0BA3}-\x{0BA4}' . + '\x{0BA8}-\x{0BAA}' . + '\x{0BAE}-\x{0BB9}' . + '\x{0BD0}' . + '\x{A8F3}', + 'Nonspacing_Combining_Mark' => + '\x{0951}-\x{0952}' . + '\x{0BCD}' . + '\x{1CDA}' . + '\x{1133C}', + 'Nonspacing_Mark' => + '\x{0951}-\x{0952}' . + '\x{0B82}' . + '\x{0BC0}' . + '\x{0BCD}' . + '\x{1CDA}' . + '\x{11301}' . + '\x{1133C}', + 'Virama' => + '\x{0BCD}', + 'Vowel_Dependent' => + '\x{0BBE}-\x{0BBF}' . + '\x{0BC0}' . + '\x{0BC1}-\x{0BC2}' . + '\x{0BC6}-\x{0BC8}' . + '\x{0BCA}-\x{0BCC}' . + '\x{0BD7}', + ), + 'Malayalam' => array( + 'All' => + '\x{0951}-\x{0952}' . + '\x{0964}-\x{0965}' . + '\x{0D00}-\x{0D0C}' . + '\x{0D0E}-\x{0D10}' . + '\x{0D12}-\x{0D44}' . + '\x{0D46}-\x{0D48}' . + '\x{0D4A}-\x{0D4F}' . + '\x{0D54}-\x{0D63}' . + '\x{0D66}-\x{0D7F}' . + '\x{1CDA}' . + '\x{A838}', + 'Letter' => + '\x{0D04}-\x{0D0C}' . + '\x{0D0E}-\x{0D10}' . + '\x{0D12}-\x{0D3A}' . + '\x{0D3D}' . + '\x{0D4E}' . + '\x{0D54}-\x{0D56}' . + '\x{0D5F}-\x{0D61}' . + '\x{0D7A}-\x{0D7F}', + 'Nonspacing_Combining_Mark' => + '\x{0951}-\x{0952}' . + '\x{0D3B}-\x{0D3C}' . + '\x{0D4D}' . + '\x{1CDA}', + 'Nonspacing_Mark' => + '\x{0951}-\x{0952}' . + '\x{0D00}-\x{0D01}' . + '\x{0D3B}-\x{0D3C}' . + '\x{0D41}-\x{0D44}' . + '\x{0D4D}' . + '\x{0D62}-\x{0D63}' . + '\x{1CDA}', + 'Virama' => + '\x{0D4D}', + 'Vowel_Dependent' => + '\x{0D3E}-\x{0D40}' . + '\x{0D41}-\x{0D44}' . + '\x{0D46}-\x{0D48}' . + '\x{0D4A}-\x{0D4C}' . + '\x{0D57}' . + '\x{0D62}-\x{0D63}', + ), + 'Bengali' => array( + 'All' => + '\x{0951}-\x{0952}' . + '\x{0964}-\x{0965}' . + '\x{0980}-\x{0983}' . + '\x{0985}-\x{098C}' . + '\x{098F}-\x{0990}' . + '\x{0993}-\x{09A8}' . + '\x{09AA}-\x{09B0}' . + '\x{09B2}' . + '\x{09B6}-\x{09B9}' . + '\x{09BC}-\x{09C4}' . + '\x{09C7}-\x{09C8}' . + '\x{09CB}-\x{09CE}' . + '\x{09D7}' . + '\x{09DC}-\x{09DD}' . + '\x{09DF}-\x{09E3}' . + '\x{09E6}' . + '\x{09E6}-\x{09E9}' . + '\x{09E9}-\x{09EC}' . + '\x{09EC}-\x{09EF}' . + '\x{09EF}-\x{09FE}' . + '\x{1CD0}' . + '\x{1CD2}' . + '\x{1CD5}' . + '\x{1CD8}' . + '\x{1CE1}' . + '\x{1CEA}' . + '\x{1CED}' . + '\x{1CF2}' . + '\x{1CF5}' . + '\x{1CF7}' . + '\x{A8F1}', + 'Letter' => + '\x{0980}' . + '\x{0985}-\x{098C}' . + '\x{098F}-\x{0990}' . + '\x{0993}-\x{09A8}' . + '\x{09AA}-\x{09B0}' . + '\x{09B2}' . + '\x{09B6}-\x{09B9}' . + '\x{09BD}' . + '\x{09CE}' . + '\x{09DC}-\x{09DD}' . + '\x{09DF}-\x{09E1}' . + '\x{09F0}-\x{09F1}' . + '\x{09FC}' . + '\x{1CEA}' . + '\x{1CF2}' . + '\x{1CF5}', + 'Nonspacing_Combining_Mark' => + '\x{0951}-\x{0952}' . + '\x{09BC}' . + '\x{09CD}' . + '\x{09FE}' . + '\x{1CD0}' . + '\x{1CD2}' . + '\x{1CD5}' . + '\x{1CD8}' . + '\x{1CED}' . + '\x{A8F1}', + 'Nonspacing_Mark' => + '\x{0951}-\x{0952}' . + '\x{0981}' . + '\x{09BC}' . + '\x{09C1}-\x{09C4}' . + '\x{09CD}' . + '\x{09E2}-\x{09E3}' . + '\x{09FE}' . + '\x{1CD0}' . + '\x{1CD2}' . + '\x{1CD5}' . + '\x{1CD8}' . + '\x{1CED}' . + '\x{A8F1}', + 'Virama' => + '\x{09CD}', + 'Vowel_Dependent' => + '\x{09BE}-\x{09C0}' . + '\x{09C1}-\x{09C4}' . + '\x{09C7}-\x{09C8}' . + '\x{09CB}-\x{09CC}' . + '\x{09D7}' . + '\x{09E2}-\x{09E3}', + ), + 'Sinhala' => array( + 'All' => + '\x{0964}-\x{0965}' . + '\x{0D81}-\x{0D83}' . + '\x{0D85}-\x{0D96}' . + '\x{0D9A}-\x{0DB1}' . + '\x{0DB3}-\x{0DBB}' . + '\x{0DBD}' . + '\x{0DC0}-\x{0DC6}' . + '\x{0DCA}' . + '\x{0DCF}-\x{0DD4}' . + '\x{0DD6}' . + '\x{0DD8}-\x{0DDF}' . + '\x{0DE6}-\x{0DEF}' . + '\x{0DF2}-\x{0DF4}' . + '\x{111E1}-\x{111F4}', + 'Letter' => + '\x{0D85}-\x{0D96}' . + '\x{0D9A}-\x{0DB1}' . + '\x{0DB3}-\x{0DBB}' . + '\x{0DBD}' . + '\x{0DC0}-\x{0DC6}', + 'Nonspacing_Combining_Mark' => + '\x{0DCA}', + 'Nonspacing_Mark' => + '\x{0D81}' . + '\x{0DCA}' . + '\x{0DD2}-\x{0DD4}' . + '\x{0DD6}', + 'Virama' => + '\x{0DCA}', + 'Vowel_Dependent' => + '\x{0DCF}-\x{0DD1}' . + '\x{0DD2}-\x{0DD4}' . + '\x{0DD6}' . + '\x{0DD8}-\x{0DDF}' . + '\x{0DF2}-\x{0DF3}', + ), + 'Grantha' => array( + 'All' => + '\x{0951}-\x{0952}' . + '\x{0964}-\x{0965}' . + '\x{0BE6}' . + '\x{0BE8}' . + '\x{0BEA}' . + '\x{0BEC}' . + '\x{0BEE}' . + '\x{0BF0}' . + '\x{0BF2}-\x{0BF3}' . + '\x{1CD0}' . + '\x{1CD2}-\x{1CD3}' . + '\x{1CF2}-\x{1CF4}' . + '\x{1CF9}' . + '\x{20F0}' . + '\x{11300}-\x{11301}' . + '\x{11301}-\x{11303}' . + '\x{11303}' . + '\x{11305}-\x{1130C}' . + '\x{1130F}-\x{11310}' . + '\x{11313}-\x{11328}' . + '\x{1132A}-\x{11330}' . + '\x{11332}-\x{11333}' . + '\x{11335}-\x{11339}' . + '\x{1133B}-\x{11344}' . + '\x{11347}-\x{11348}' . + '\x{1134B}-\x{1134D}' . + '\x{11350}' . + '\x{11357}' . + '\x{1135D}-\x{11363}' . + '\x{11366}-\x{1136C}' . + '\x{11370}-\x{11374}' . + '\x{11FD0}' . + '\x{11FD3}', + 'Letter' => + '\x{1CF2}-\x{1CF3}' . + '\x{11305}-\x{1130C}' . + '\x{1130F}-\x{11310}' . + '\x{11313}-\x{11328}' . + '\x{1132A}-\x{11330}' . + '\x{11332}-\x{11333}' . + '\x{11335}-\x{11339}' . + '\x{1133D}' . + '\x{11350}' . + '\x{1135D}-\x{11361}', + 'Nonspacing_Combining_Mark' => + '\x{0951}-\x{0952}' . + '\x{1CD0}' . + '\x{1CD2}' . + '\x{1CF4}' . + '\x{1CF9}' . + '\x{20F0}' . + '\x{1133B}-\x{1133C}' . + '\x{11366}-\x{1136C}' . + '\x{11370}-\x{11374}', + 'Nonspacing_Mark' => + '\x{0951}-\x{0952}' . + '\x{1CD0}' . + '\x{1CD2}' . + '\x{1CF4}' . + '\x{1CF9}' . + '\x{20F0}' . + '\x{11300}-\x{11301}' . + '\x{11301}' . + '\x{1133B}-\x{1133C}' . + '\x{11340}' . + '\x{11366}-\x{1136C}' . + '\x{11370}-\x{11374}', + 'Virama' => + '\x{1134D}', + 'Vowel_Dependent' => + '\x{1133E}-\x{1133F}' . + '\x{11340}' . + '\x{11341}-\x{11344}' . + '\x{11347}-\x{11348}' . + '\x{1134B}-\x{1134C}' . + '\x{11357}' . + '\x{11362}-\x{11363}', + ), + 'Kannada' => array( + 'All' => + '\x{0951}-\x{0952}' . + '\x{0964}-\x{0965}' . + '\x{0C80}-\x{0C8C}' . + '\x{0C8E}-\x{0C90}' . + '\x{0C92}-\x{0CA8}' . + '\x{0CAA}-\x{0CB3}' . + '\x{0CB5}-\x{0CB9}' . + '\x{0CBC}-\x{0CC4}' . + '\x{0CC6}-\x{0CC8}' . + '\x{0CCA}-\x{0CCD}' . + '\x{0CD5}-\x{0CD6}' . + '\x{0CDD}-\x{0CDE}' . + '\x{0CE0}-\x{0CE3}' . + '\x{0CE6}' . + '\x{0CE6}-\x{0CE8}' . + '\x{0CE8}-\x{0CEA}' . + '\x{0CEA}-\x{0CEC}' . + '\x{0CEC}-\x{0CEE}' . + '\x{0CEE}-\x{0CEF}' . + '\x{0CF1}-\x{0CF3}' . + '\x{1CD0}' . + '\x{1CD2}' . + '\x{1CDA}' . + '\x{1CF2}' . + '\x{1CF4}' . + '\x{A835}' . + '\x{A838}', + 'Letter' => + '\x{0C80}' . + '\x{0C85}-\x{0C8C}' . + '\x{0C8E}-\x{0C90}' . + '\x{0C92}-\x{0CA8}' . + '\x{0CAA}-\x{0CB3}' . + '\x{0CB5}-\x{0CB9}' . + '\x{0CBD}' . + '\x{0CDD}-\x{0CDE}' . + '\x{0CE0}-\x{0CE1}' . + '\x{0CF1}-\x{0CF2}' . + '\x{1CF2}', + 'Nonspacing_Combining_Mark' => + '\x{0951}-\x{0952}' . + '\x{0CBC}' . + '\x{0CCD}' . + '\x{1CD0}' . + '\x{1CD2}' . + '\x{1CDA}' . + '\x{1CF4}', + 'Nonspacing_Mark' => + '\x{0951}-\x{0952}' . + '\x{0C81}' . + '\x{0CBC}' . + '\x{0CBF}' . + '\x{0CC6}' . + '\x{0CCC}-\x{0CCD}' . + '\x{0CE2}-\x{0CE3}' . + '\x{1CD0}' . + '\x{1CD2}' . + '\x{1CDA}' . + '\x{1CF4}', + 'Virama' => + '\x{0CCD}', + 'Vowel_Dependent' => + '\x{0CBE}' . + '\x{0CBF}' . + '\x{0CC0}-\x{0CC4}' . + '\x{0CC6}' . + '\x{0CC7}-\x{0CC8}' . + '\x{0CCA}-\x{0CCB}' . + '\x{0CCC}' . + '\x{0CD5}-\x{0CD6}' . + '\x{0CE2}-\x{0CE3}', + ), + 'Telugu' => array( + 'All' => + '\x{0951}-\x{0952}' . + '\x{0964}-\x{0965}' . + '\x{0C00}-\x{0C0C}' . + '\x{0C0E}-\x{0C10}' . + '\x{0C12}-\x{0C28}' . + '\x{0C2A}-\x{0C39}' . + '\x{0C3C}-\x{0C44}' . + '\x{0C46}-\x{0C48}' . + '\x{0C4A}-\x{0C4D}' . + '\x{0C55}-\x{0C56}' . + '\x{0C58}-\x{0C5A}' . + '\x{0C5D}' . + '\x{0C60}-\x{0C63}' . + '\x{0C66}-\x{0C6F}' . + '\x{0C77}-\x{0C7F}' . + '\x{1CDA}' . + '\x{1CF2}', + 'Letter' => + '\x{0C05}-\x{0C0C}' . + '\x{0C0E}-\x{0C10}' . + '\x{0C12}-\x{0C28}' . + '\x{0C2A}-\x{0C39}' . + '\x{0C3D}' . + '\x{0C58}-\x{0C5A}' . + '\x{0C5D}' . + '\x{0C60}-\x{0C61}' . + '\x{1CF2}', + 'Nonspacing_Combining_Mark' => + '\x{0951}-\x{0952}' . + '\x{0C3C}' . + '\x{0C4D}' . + '\x{0C55}-\x{0C56}' . + '\x{1CDA}', + 'Nonspacing_Mark' => + '\x{0951}-\x{0952}' . + '\x{0C00}' . + '\x{0C04}' . + '\x{0C3C}' . + '\x{0C3E}-\x{0C40}' . + '\x{0C46}-\x{0C48}' . + '\x{0C4A}-\x{0C4D}' . + '\x{0C55}-\x{0C56}' . + '\x{0C62}-\x{0C63}' . + '\x{1CDA}', + 'Virama' => + '\x{0C4D}', + 'Vowel_Dependent' => + '\x{0C3E}-\x{0C40}' . + '\x{0C41}-\x{0C44}' . + '\x{0C46}-\x{0C48}' . + '\x{0C4A}-\x{0C4C}' . + '\x{0C55}-\x{0C56}' . + '\x{0C62}-\x{0C63}', + ), + 'Gujarati' => array( + 'All' => + '\x{0951}-\x{0952}' . + '\x{0964}-\x{0965}' . + '\x{0A81}-\x{0A83}' . + '\x{0A85}-\x{0A8D}' . + '\x{0A8F}-\x{0A91}' . + '\x{0A93}-\x{0AA8}' . + '\x{0AAA}-\x{0AB0}' . + '\x{0AB2}-\x{0AB3}' . + '\x{0AB5}-\x{0AB9}' . + '\x{0ABC}-\x{0AC5}' . + '\x{0AC7}-\x{0AC9}' . + '\x{0ACB}-\x{0ACD}' . + '\x{0AD0}' . + '\x{0AE0}-\x{0AE3}' . + '\x{0AE6}' . + '\x{0AE6}-\x{0AE8}' . + '\x{0AE8}-\x{0AEA}' . + '\x{0AEA}-\x{0AEC}' . + '\x{0AEC}-\x{0AEE}' . + '\x{0AEE}-\x{0AF1}' . + '\x{0AF9}-\x{0AFF}' . + '\x{A832}' . + '\x{A835}' . + '\x{A838}' . + '\x{A838}-\x{A839}', + 'Letter' => + '\x{0A85}-\x{0A8D}' . + '\x{0A8F}-\x{0A91}' . + '\x{0A93}-\x{0AA8}' . + '\x{0AAA}-\x{0AB0}' . + '\x{0AB2}-\x{0AB3}' . + '\x{0AB5}-\x{0AB9}' . + '\x{0ABD}' . + '\x{0AD0}' . + '\x{0AE0}-\x{0AE1}' . + '\x{0AF9}', + 'Nonspacing_Combining_Mark' => + '\x{0951}-\x{0952}' . + '\x{0ABC}' . + '\x{0ACD}', + 'Nonspacing_Mark' => + '\x{0951}-\x{0952}' . + '\x{0A81}-\x{0A82}' . + '\x{0ABC}' . + '\x{0AC1}-\x{0AC5}' . + '\x{0AC7}-\x{0AC8}' . + '\x{0ACD}' . + '\x{0AE2}-\x{0AE3}' . + '\x{0AFA}-\x{0AFF}', + 'Virama' => + '\x{0ACD}', + 'Vowel_Dependent' => + '\x{0ABE}-\x{0AC0}' . + '\x{0AC1}-\x{0AC5}' . + '\x{0AC7}-\x{0AC8}' . + '\x{0AC9}' . + '\x{0ACB}-\x{0ACC}' . + '\x{0AE2}-\x{0AE3}', + ), + 'Sharada' => array( + 'All' => + '\x{0951}' . + '\x{1CD7}' . + '\x{1CD9}' . + '\x{1CDD}' . + '\x{1CE0}' . + '\x{11180}-\x{111DF}', + 'Letter' => + '\x{11183}-\x{111B2}' . + '\x{111C1}-\x{111C4}' . + '\x{111DA}' . + '\x{111DC}', + 'Nonspacing_Combining_Mark' => + '\x{0951}' . + '\x{1CD7}' . + '\x{1CD9}' . + '\x{1CDD}' . + '\x{1CE0}' . + '\x{111CA}', + 'Nonspacing_Mark' => + '\x{0951}' . + '\x{1CD7}' . + '\x{1CD9}' . + '\x{1CDD}' . + '\x{1CE0}' . + '\x{11180}-\x{11181}' . + '\x{111B6}-\x{111BE}' . + '\x{111C9}-\x{111CC}' . + '\x{111CF}', + 'Virama' => + '\x{111C0}', + 'Vowel_Dependent' => + '\x{111B3}-\x{111B5}' . + '\x{111B6}-\x{111BE}' . + '\x{111BF}' . + '\x{111CB}-\x{111CC}' . + '\x{111CE}', + ), + 'Oriya' => array( + 'All' => + '\x{0951}-\x{0952}' . + '\x{0964}-\x{0965}' . + '\x{0B01}-\x{0B03}' . + '\x{0B05}-\x{0B0C}' . + '\x{0B0F}-\x{0B10}' . + '\x{0B13}-\x{0B28}' . + '\x{0B2A}-\x{0B30}' . + '\x{0B32}-\x{0B33}' . + '\x{0B35}-\x{0B39}' . + '\x{0B3C}-\x{0B44}' . + '\x{0B47}-\x{0B48}' . + '\x{0B4B}-\x{0B4D}' . + '\x{0B55}-\x{0B57}' . + '\x{0B5C}-\x{0B5D}' . + '\x{0B5F}-\x{0B63}' . + '\x{0B66}-\x{0B77}' . + '\x{1CDA}' . + '\x{1CF2}', + 'Letter' => + '\x{0B05}-\x{0B0C}' . + '\x{0B0F}-\x{0B10}' . + '\x{0B13}-\x{0B28}' . + '\x{0B2A}-\x{0B30}' . + '\x{0B32}-\x{0B33}' . + '\x{0B35}-\x{0B39}' . + '\x{0B3D}' . + '\x{0B5C}-\x{0B5D}' . + '\x{0B5F}-\x{0B61}' . + '\x{0B71}' . + '\x{1CF2}', + 'Nonspacing_Combining_Mark' => + '\x{0951}-\x{0952}' . + '\x{0B3C}' . + '\x{0B4D}' . + '\x{1CDA}', + 'Nonspacing_Mark' => + '\x{0951}-\x{0952}' . + '\x{0B01}' . + '\x{0B3C}' . + '\x{0B3F}' . + '\x{0B41}-\x{0B44}' . + '\x{0B4D}' . + '\x{0B55}-\x{0B56}' . + '\x{0B62}-\x{0B63}' . + '\x{1CDA}', + 'Virama' => + '\x{0B4D}', + 'Vowel_Dependent' => + '\x{0B3E}' . + '\x{0B3F}' . + '\x{0B40}' . + '\x{0B41}-\x{0B44}' . + '\x{0B47}-\x{0B48}' . + '\x{0B4B}-\x{0B4C}' . + '\x{0B55}-\x{0B56}' . + '\x{0B57}' . + '\x{0B62}-\x{0B63}', + ), + 'Gurmukhi' => array( + 'All' => + '\x{0951}-\x{0952}' . + '\x{0964}-\x{0965}' . + '\x{0A01}-\x{0A03}' . + '\x{0A05}-\x{0A0A}' . + '\x{0A0F}-\x{0A10}' . + '\x{0A13}-\x{0A28}' . + '\x{0A2A}-\x{0A30}' . + '\x{0A32}-\x{0A33}' . + '\x{0A35}-\x{0A36}' . + '\x{0A38}-\x{0A39}' . + '\x{0A3C}' . + '\x{0A3E}-\x{0A42}' . + '\x{0A47}-\x{0A48}' . + '\x{0A4B}-\x{0A4D}' . + '\x{0A51}' . + '\x{0A59}-\x{0A5C}' . + '\x{0A5E}' . + '\x{0A66}' . + '\x{0A66}-\x{0A68}' . + '\x{0A68}-\x{0A6A}' . + '\x{0A6A}-\x{0A6C}' . + '\x{0A6C}-\x{0A6E}' . + '\x{0A6E}-\x{0A76}' . + '\x{A833}' . + '\x{A836}' . + '\x{A838}-\x{A839}' . + '\x{A839}', + 'Letter' => + '\x{0A05}-\x{0A0A}' . + '\x{0A0F}-\x{0A10}' . + '\x{0A13}-\x{0A28}' . + '\x{0A2A}-\x{0A30}' . + '\x{0A32}-\x{0A33}' . + '\x{0A35}-\x{0A36}' . + '\x{0A38}-\x{0A39}' . + '\x{0A59}-\x{0A5C}' . + '\x{0A5E}' . + '\x{0A72}-\x{0A74}', + 'Nonspacing_Combining_Mark' => + '\x{0951}-\x{0952}' . + '\x{0A3C}' . + '\x{0A4D}', + 'Nonspacing_Mark' => + '\x{0951}-\x{0952}' . + '\x{0A01}-\x{0A02}' . + '\x{0A3C}' . + '\x{0A41}-\x{0A42}' . + '\x{0A47}-\x{0A48}' . + '\x{0A4B}-\x{0A4D}' . + '\x{0A51}' . + '\x{0A70}-\x{0A71}' . + '\x{0A75}', + 'Virama' => + '\x{0A4D}', + 'Vowel_Dependent' => + '\x{0A3E}-\x{0A40}' . + '\x{0A41}-\x{0A42}' . + '\x{0A47}-\x{0A48}' . + '\x{0A4B}-\x{0A4C}', + ), + 'Tirhuta' => array( + 'All' => + '\x{0951}-\x{0952}' . + '\x{0964}-\x{0965}' . + '\x{1CF2}' . + '\x{A838}-\x{A839}' . + '\x{A83D}' . + '\x{A83F}-\x{A840}' . + '\x{11480}-\x{114C7}' . + '\x{114D0}-\x{114D9}', + 'Letter' => + '\x{1CF2}' . + '\x{A840}' . + '\x{11480}-\x{114AF}' . + '\x{114C4}-\x{114C5}' . + '\x{114C7}', + 'Nonspacing_Combining_Mark' => + '\x{0951}-\x{0952}' . + '\x{114C2}-\x{114C3}', + 'Nonspacing_Mark' => + '\x{0951}-\x{0952}' . + '\x{114B3}-\x{114B8}' . + '\x{114BA}' . + '\x{114BF}-\x{114C0}' . + '\x{114C2}-\x{114C3}', + 'Virama' => + '\x{114C2}', + 'Vowel_Dependent' => + '\x{114B0}-\x{114B2}' . + '\x{114B3}-\x{114B8}' . + '\x{114B9}' . + '\x{114BA}' . + '\x{114BB}-\x{114BE}', + ), + 'Kaithi' => array( + 'All' => + '\x{0968}' . + '\x{096C}' . + '\x{0970}' . + '\x{A836}' . + '\x{A838}-\x{A839}' . + '\x{A839}' . + '\x{A83B}' . + '\x{11080}-\x{110C2}' . + '\x{110CD}', + 'Letter' => + '\x{11083}-\x{110AF}', + 'Nonspacing_Combining_Mark' => + '\x{110B9}-\x{110BA}', + 'Nonspacing_Mark' => + '\x{11080}-\x{11081}' . + '\x{110B3}-\x{110B6}' . + '\x{110B9}-\x{110BA}' . + '\x{110C2}', + 'Virama' => + '\x{110B9}', + 'Vowel_Dependent' => + '\x{110B0}-\x{110B2}' . + '\x{110B3}-\x{110B6}' . + '\x{110B7}-\x{110B8}' . + '\x{110C2}', + ), + 'Nandinagari' => array( + 'All' => + '\x{0964}-\x{0965}' . + '\x{0CE7}' . + '\x{0CE9}' . + '\x{0CEB}' . + '\x{0CED}' . + '\x{0CEF}' . + '\x{1CE9}' . + '\x{1CF2}' . + '\x{1CFA}' . + '\x{A83A}' . + '\x{A83C}' . + '\x{119A0}-\x{119A7}' . + '\x{119AA}-\x{119D7}' . + '\x{119DA}-\x{119E4}', + 'Letter' => + '\x{1CE9}' . + '\x{1CF2}' . + '\x{1CFA}' . + '\x{119A0}-\x{119A7}' . + '\x{119AA}-\x{119D0}' . + '\x{119E1}' . + '\x{119E3}', + 'Nonspacing_Combining_Mark' => + '\x{119E0}', + 'Nonspacing_Mark' => + '\x{119D4}-\x{119D7}' . + '\x{119DA}-\x{119DB}' . + '\x{119E0}', + 'Virama' => + '\x{119E0}', + 'Vowel_Dependent' => + '\x{119D1}-\x{119D3}' . + '\x{119D4}-\x{119D7}' . + '\x{119DA}-\x{119DB}' . + '\x{119DC}-\x{119DD}' . + '\x{119E4}', + ), + 'Khojki' => array( + 'All' => + '\x{0AE7}' . + '\x{0AE9}' . + '\x{0AEB}' . + '\x{0AED}' . + '\x{0AEF}' . + '\x{A834}' . + '\x{A837}-\x{A83A}' . + '\x{11200}-\x{11211}' . + '\x{11213}-\x{11241}', + 'Letter' => + '\x{11200}-\x{11211}' . + '\x{11213}-\x{1122B}' . + '\x{1123F}-\x{11240}', + 'Nonspacing_Combining_Mark' => + '\x{11236}', + 'Nonspacing_Mark' => + '\x{1122F}-\x{11231}' . + '\x{11234}' . + '\x{11236}-\x{11237}' . + '\x{1123E}' . + '\x{11241}', + 'Virama' => + '\x{11235}', + 'Vowel_Dependent' => + '\x{1122C}-\x{1122E}' . + '\x{1122F}-\x{11231}' . + '\x{11232}-\x{11233}' . + '\x{11241}', + ), + 'Takri' => array( + 'All' => + '\x{0964}-\x{0965}' . + '\x{A838}-\x{A839}' . + '\x{A83C}' . + '\x{A83E}-\x{A83F}' . + '\x{11680}-\x{116B9}' . + '\x{116C0}-\x{116C9}', + 'Letter' => + '\x{11680}-\x{116AA}' . + '\x{116B8}', + 'Nonspacing_Combining_Mark' => + '\x{116B7}', + 'Nonspacing_Mark' => + '\x{116AB}' . + '\x{116AD}' . + '\x{116B0}-\x{116B5}' . + '\x{116B7}', + 'Virama' => + '\x{116B6}', + 'Vowel_Dependent' => + '\x{116AD}' . + '\x{116AE}-\x{116AF}' . + '\x{116B0}-\x{116B5}', + ), + 'Dogra' => array( + 'All' => + '\x{0964}-\x{0965}' . + '\x{0967}' . + '\x{096B}' . + '\x{096F}' . + '\x{A831}' . + '\x{A834}' . + '\x{A837}-\x{A839}' . + '\x{11800}-\x{1183B}', + 'Letter' => + '\x{11800}-\x{1182B}', + 'Nonspacing_Combining_Mark' => + '\x{11839}-\x{1183A}', + 'Nonspacing_Mark' => + '\x{1182F}-\x{11837}' . + '\x{11839}-\x{1183A}', + 'Virama' => + '\x{11839}', + 'Vowel_Dependent' => + '\x{1182C}-\x{1182E}' . + '\x{1182F}-\x{11836}', + ), + 'Syloti_Nagri' => array( + 'All' => + '\x{0964}-\x{0965}' . + '\x{09E8}' . + '\x{09EB}' . + '\x{09EE}' . + '\x{09F1}' . + '\x{A800}-\x{A82C}', + 'Letter' => + '\x{09F1}' . + '\x{A800}-\x{A801}' . + '\x{A803}-\x{A805}' . + '\x{A807}-\x{A80A}' . + '\x{A80C}-\x{A822}', + 'Nonspacing_Combining_Mark' => + '\x{A806}' . + '\x{A82C}', + 'Nonspacing_Mark' => + '\x{A802}' . + '\x{A806}' . + '\x{A80B}' . + '\x{A825}-\x{A826}' . + '\x{A82C}', + 'Virama' => + '\x{A806}', + 'Vowel_Dependent' => + '\x{A802}' . + '\x{A823}-\x{A824}' . + '\x{A825}-\x{A826}' . + '\x{A827}', + ), + 'Balinese' => array( + 'All' => + '\x{1B00}-\x{1B4C}' . + '\x{1B50}-\x{1B7E}', + 'Letter' => + '\x{1B05}-\x{1B33}' . + '\x{1B45}-\x{1B4C}', + 'Nonspacing_Combining_Mark' => + '\x{1B34}' . + '\x{1B6B}-\x{1B73}', + 'Nonspacing_Mark' => + '\x{1B00}-\x{1B03}' . + '\x{1B34}' . + '\x{1B36}-\x{1B3A}' . + '\x{1B3C}' . + '\x{1B42}' . + '\x{1B6B}-\x{1B73}', + 'Virama' => + '\x{1B44}', + 'Vowel_Dependent' => + '\x{1B35}' . + '\x{1B36}-\x{1B3A}' . + '\x{1B3B}' . + '\x{1B3C}' . + '\x{1B3D}-\x{1B41}' . + '\x{1B42}' . + '\x{1B43}', + ), + 'Saurashtra' => array( + 'All' => + '\x{A880}-\x{A8C5}' . + '\x{A8CE}-\x{A8D9}', + 'Letter' => + '\x{A882}-\x{A8B3}', + 'Nonspacing_Combining_Mark' => + '\x{A8C4}', + 'Nonspacing_Mark' => + '\x{A8C4}-\x{A8C5}', + 'Virama' => + '\x{A8C4}', + 'Vowel_Dependent' => + '\x{A8B5}-\x{A8C3}', + ), + 'Javanese' => array( + 'All' => + '\x{A980}-\x{A9CD}' . + '\x{A9CF}-\x{A9D9}' . + '\x{A9DE}-\x{A9DF}', + 'Letter' => + '\x{A984}-\x{A9B2}' . + '\x{A9CF}', + 'Nonspacing_Combining_Mark' => + '\x{A9B3}', + 'Nonspacing_Mark' => + '\x{A980}-\x{A982}' . + '\x{A9B3}' . + '\x{A9B6}-\x{A9B9}' . + '\x{A9BC}-\x{A9BD}', + 'Virama' => + '\x{A9C0}', + 'Vowel_Dependent' => + '\x{A9B4}-\x{A9B5}' . + '\x{A9B6}-\x{A9B9}' . + '\x{A9BA}-\x{A9BB}' . + '\x{A9BC}', + ), + 'Brahmi' => array( + 'All' => + '\x{11000}-\x{1104D}' . + '\x{11052}-\x{11075}' . + '\x{1107F}', + 'Letter' => + '\x{11003}-\x{11037}' . + '\x{11071}-\x{11072}' . + '\x{11075}', + 'Nonspacing_Combining_Mark' => + '\x{11046}' . + '\x{11070}' . + '\x{1107F}', + 'Nonspacing_Mark' => + '\x{11001}' . + '\x{11038}-\x{11046}' . + '\x{11070}' . + '\x{11073}-\x{11074}' . + '\x{1107F}', + 'Virama' => + '\x{11046}', + 'Vowel_Dependent' => + '\x{11038}-\x{11045}' . + '\x{11073}-\x{11074}', + ), + 'Modi' => array( + 'All' => + '\x{A838}-\x{A839}' . + '\x{A839}' . + '\x{A83B}' . + '\x{A83D}' . + '\x{11600}-\x{11644}' . + '\x{11650}-\x{11659}', + 'Letter' => + '\x{11600}-\x{1162F}' . + '\x{11644}', + 'Nonspacing_Combining_Mark' => + '\x{1163F}', + 'Nonspacing_Mark' => + '\x{11633}-\x{1163A}' . + '\x{1163D}' . + '\x{1163F}-\x{11640}', + 'Virama' => + '\x{1163F}', + 'Vowel_Dependent' => + '\x{11630}-\x{11632}' . + '\x{11633}-\x{1163A}' . + '\x{1163B}-\x{1163C}' . + '\x{11640}', + ), + 'Siddham' => array( + 'All' => + '\x{11580}-\x{115B5}' . + '\x{115B8}-\x{115DD}', + 'Letter' => + '\x{11580}-\x{115AE}' . + '\x{115D8}-\x{115DB}', + 'Nonspacing_Combining_Mark' => + '\x{115BF}-\x{115C0}', + 'Nonspacing_Mark' => + '\x{115B2}-\x{115B5}' . + '\x{115BC}-\x{115BD}' . + '\x{115BF}-\x{115C0}' . + '\x{115DC}-\x{115DD}', + 'Virama' => + '\x{115BF}', + 'Vowel_Dependent' => + '\x{115AF}-\x{115B1}' . + '\x{115B2}-\x{115B5}' . + '\x{115B8}-\x{115BB}' . + '\x{115DC}-\x{115DD}', + ), + 'Newa' => array( + 'All' => + '\x{11400}-\x{1145B}' . + '\x{1145D}-\x{11461}', + 'Letter' => + '\x{11400}-\x{11434}' . + '\x{11447}-\x{1144A}' . + '\x{1145F}-\x{11461}', + 'Nonspacing_Combining_Mark' => + '\x{11442}' . + '\x{11446}' . + '\x{1145E}', + 'Nonspacing_Mark' => + '\x{11438}-\x{1143F}' . + '\x{11442}-\x{11444}' . + '\x{11446}' . + '\x{1145E}', + 'Virama' => + '\x{11442}', + 'Vowel_Dependent' => + '\x{11435}-\x{11437}' . + '\x{11438}-\x{1143F}' . + '\x{11440}-\x{11441}', + ), + 'Bhaiksuki' => array( + 'All' => + '\x{11C00}-\x{11C08}' . + '\x{11C0A}-\x{11C36}' . + '\x{11C38}-\x{11C45}' . + '\x{11C50}-\x{11C6C}', + 'Letter' => + '\x{11C00}-\x{11C08}' . + '\x{11C0A}-\x{11C2E}' . + '\x{11C40}', + 'Nonspacing_Combining_Mark' => + '\x{11C3F}', + 'Nonspacing_Mark' => + '\x{11C30}-\x{11C36}' . + '\x{11C38}-\x{11C3D}' . + '\x{11C3F}', + 'Virama' => + '\x{11C3F}', + 'Vowel_Dependent' => + '\x{11C2F}' . + '\x{11C30}-\x{11C36}' . + '\x{11C38}-\x{11C3B}', + ), + ); +} + +?> \ No newline at end of file diff --git a/Sources/Unicode/index.php b/Sources/Unicode/index.php new file mode 100644 index 0000000..69278ce --- /dev/null +++ b/Sources/Unicode/index.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/Sources/ViewQuery.php b/Sources/ViewQuery.php new file mode 100644 index 0000000..81595a7 --- /dev/null +++ b/Sources/ViewQuery.php @@ -0,0 +1,190 @@ + + + + ', $context['forum_name_html_safe'], ' + + + + +
    '; + + foreach ($_SESSION['debug'] as $q => $query_data) + { + // Fix the indentation.... + $query_data['q'] = ltrim(str_replace("\r", '', $query_data['q']), "\n"); + $query = explode("\n", $query_data['q']); + $min_indent = 0; + foreach ($query as $line) + { + preg_match('/^(\t*)/', $line, $temp); + if (strlen($temp[0]) < $min_indent || $min_indent == 0) + $min_indent = strlen($temp[0]); + } + foreach ($query as $l => $dummy) + $query[$l] = substr($dummy, $min_indent); + $query_data['q'] = implode("\n", $query); + + // Make the filenames look a bit better. + if (isset($query_data['f'])) + $query_data['f'] = preg_replace('~^' . preg_quote($boarddir, '~') . '~', '...', $query_data['f']); + + $is_select_query = substr(trim($query_data['q']), 0, 6) == 'SELECT' || substr(trim($query_data['q']), 0, 4) == 'WITH'; + if ($is_select_query) + $select = $query_data['q']; + elseif (preg_match('~^INSERT(?: IGNORE)? INTO \w+(?:\s+\([^)]+\))?\s+(SELECT .+)$~s', trim($query_data['q']), $matches) != 0) + { + $is_select_query = true; + $select = $matches[1]; + } + elseif (preg_match('~^CREATE TEMPORARY TABLE .+?(SELECT .+)$~s', trim($query_data['q']), $matches) != 0) + { + $is_select_query = true; + $select = $matches[1]; + } + // Temporary tables created in earlier queries are not explainable. + if ($is_select_query) + { + foreach (array('log_topics_unread', 'topics_posted_in', 'tmp_log_search_topics', 'tmp_log_search_messages') as $tmp) + if (strpos($select, $tmp) !== false) + { + $is_select_query = false; + break; + } + } + + echo ' +
    + + ', nl2br(str_replace("\t", '   ', $smcFunc['htmlspecialchars']($query_data['q']))), ' +
    '; + + if (!empty($query_data['f']) && !empty($query_data['l'])) + echo sprintf($txt['debug_query_in_line'], $query_data['f'], $query_data['l']); + + if (isset($query_data['s'], $query_data['t']) && isset($txt['debug_query_which_took_at'])) + echo sprintf($txt['debug_query_which_took_at'], round($query_data['t'], 8), round($query_data['s'], 8)); + else + echo sprintf($txt['debug_query_which_took'], round($query_data['t'], 8)); + + echo ' +
    '; + + // Explain the query. + if ($query_id == $q && $is_select_query) + { + $result = $smcFunc['db_query']('', ' + EXPLAIN ' . ($smcFunc['db_title'] === POSTGRE_TITLE ? 'ANALYZE ' : '') . $select, + array( + ) + ); + if ($result === false) + { + echo ' + + +
    ', $smcFunc['db_error']($db_connection), '
    '; + continue; + } + + echo ' + '; + + $row = $smcFunc['db_fetch_assoc']($result); + + echo ' + + + + '; + + $smcFunc['db_data_seek']($result, 0); + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + echo ' + + + + '; + } + $smcFunc['db_free_result']($result); + + echo ' +
    ' . implode('', array_keys($row)) . '
    ' . implode('', $row) . '
    '; + } + } + + echo ' +
    + +'; + + obExit(false); +} + +?> \ No newline at end of file diff --git a/Sources/Who.php b/Sources/Who.php new file mode 100644 index 0000000..9dca7d0 --- /dev/null +++ b/Sources/Who.php @@ -0,0 +1,922 @@ + 'mem.real_name', + 'time' => 'lo.log_time' + ); + + $show_methods = array( + 'members' => '(lo.id_member != 0)', + 'guests' => '(lo.id_member = 0)', + 'all' => '1=1', + ); + + // Store the sort methods and the show types for use in the template. + $context['sort_methods'] = array( + 'user' => $txt['who_user'], + 'time' => $txt['who_time'], + ); + $context['show_methods'] = array( + 'all' => $txt['who_show_all'], + 'members' => $txt['who_show_members_only'], + 'guests' => $txt['who_show_guests_only'], + ); + + // Can they see spiders too? + if (!empty($modSettings['show_spider_online']) && ($modSettings['show_spider_online'] == 2 || allowedTo('admin_forum')) && !empty($modSettings['spider_name_cache'])) + { + $show_methods['spiders'] = '(lo.id_member = 0 AND lo.id_spider > 0)'; + $show_methods['guests'] = '(lo.id_member = 0 AND lo.id_spider = 0)'; + $context['show_methods']['spiders'] = $txt['who_show_spiders_only']; + } + elseif (empty($modSettings['show_spider_online']) && isset($_SESSION['who_online_filter']) && $_SESSION['who_online_filter'] == 'spiders') + unset($_SESSION['who_online_filter']); + + // Does the user prefer a different sort direction? + if (isset($_REQUEST['sort']) && isset($sort_methods[$_REQUEST['sort']])) + { + $context['sort_by'] = $_SESSION['who_online_sort_by'] = $_REQUEST['sort']; + $sort_method = $sort_methods[$_REQUEST['sort']]; + } + // Did we set a preferred sort order earlier in the session? + elseif (isset($_SESSION['who_online_sort_by'])) + { + $context['sort_by'] = $_SESSION['who_online_sort_by']; + $sort_method = $sort_methods[$_SESSION['who_online_sort_by']]; + } + // Default to last time online. + else + { + $context['sort_by'] = $_SESSION['who_online_sort_by'] = 'time'; + $sort_method = 'lo.log_time'; + } + + $context['sort_direction'] = isset($_REQUEST['asc']) || (isset($_REQUEST['sort_dir']) && $_REQUEST['sort_dir'] == 'asc') ? 'up' : 'down'; + + $conditions = array(); + if (!allowedTo('moderate_forum')) + $conditions[] = '(COALESCE(mem.show_online, 1) = 1)'; + + // Fallback to top filter? + if (isset($_REQUEST['submit_top']) && isset($_REQUEST['show_top'])) + $_REQUEST['show'] = $_REQUEST['show_top']; + // Does the user wish to apply a filter? + if (isset($_REQUEST['show']) && isset($show_methods[$_REQUEST['show']])) + $context['show_by'] = $_SESSION['who_online_filter'] = $_REQUEST['show']; + // Perhaps we saved a filter earlier in the session? + elseif (isset($_SESSION['who_online_filter'])) + $context['show_by'] = $_SESSION['who_online_filter']; + else + $context['show_by'] = 'members'; + + $conditions[] = $show_methods[$context['show_by']]; + + // Get the total amount of members online. + $request = $smcFunc['db_query']('', ' + SELECT COUNT(*) + FROM {db_prefix}log_online AS lo + LEFT JOIN {db_prefix}members AS mem ON (lo.id_member = mem.id_member)' . (!empty($conditions) ? ' + WHERE ' . implode(' AND ', $conditions) : ''), + array( + ) + ); + list ($totalMembers) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // Prepare some page index variables. + $context['page_index'] = constructPageIndex($scripturl . '?action=who;sort=' . $context['sort_by'] . ($context['sort_direction'] == 'up' ? ';asc' : '') . ';show=' . $context['show_by'], $_REQUEST['start'], $totalMembers, $modSettings['defaultMaxMembers']); + $context['start'] = $_REQUEST['start']; + + // Look for people online, provided they don't mind if you see they are. + $request = $smcFunc['db_query']('', ' + SELECT + lo.log_time, lo.id_member, lo.url, lo.ip AS ip, mem.real_name, + lo.session, mg.online_color, COALESCE(mem.show_online, 1) AS show_online, + lo.id_spider + FROM {db_prefix}log_online AS lo + LEFT JOIN {db_prefix}members AS mem ON (lo.id_member = mem.id_member) + LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = CASE WHEN mem.id_group = {int:regular_member} THEN mem.id_post_group ELSE mem.id_group END)' . (!empty($conditions) ? ' + WHERE ' . implode(' AND ', $conditions) : '') . ' + ORDER BY {raw:sort_method} {raw:sort_direction} + LIMIT {int:offset}, {int:limit}', + array( + 'regular_member' => 0, + 'sort_method' => $sort_method, + 'sort_direction' => $context['sort_direction'] == 'up' ? 'ASC' : 'DESC', + 'offset' => $context['start'], + 'limit' => $modSettings['defaultMaxMembers'], + ) + ); + $context['members'] = array(); + $member_ids = array(); + $url_data = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $actions = $smcFunc['json_decode']($row['url'], true); + if ($actions === array()) + continue; + + // Send the information to the template. + $context['members'][$row['session']] = array( + 'id' => $row['id_member'], + 'ip' => allowedTo('moderate_forum') ? inet_dtop($row['ip']) : '', + // It is *going* to be today or yesterday, so why keep that information in there? + 'time' => strtr(timeformat($row['log_time']), array($txt['today'] => '', $txt['yesterday'] => '')), + 'timestamp' => $row['log_time'], + 'query' => $actions, + 'is_hidden' => $row['show_online'] == 0, + 'id_spider' => $row['id_spider'], + 'color' => empty($row['online_color']) ? '' : $row['online_color'] + ); + + $url_data[$row['session']] = array($row['url'], $row['id_member']); + $member_ids[] = $row['id_member']; + } + $smcFunc['db_free_result']($request); + + // Load the user data for these members. + loadMemberData($member_ids); + + // Load up the guest user. + $memberContext[0] = array( + 'id' => 0, + 'name' => $txt['guest_title'], + 'group' => $txt['guest_title'], + 'href' => '', + 'link' => $txt['guest_title'], + 'email' => $txt['guest_title'], + 'is_guest' => true + ); + + // Are we showing spiders? + $spiderContext = array(); + if (!empty($modSettings['show_spider_online']) && ($modSettings['show_spider_online'] == 2 || allowedTo('admin_forum')) && !empty($modSettings['spider_name_cache'])) + { + foreach ($smcFunc['json_decode']($modSettings['spider_name_cache'], true) as $id => $name) + $spiderContext[$id] = array( + 'id' => 0, + 'name' => $name, + 'group' => $txt['spiders'], + 'href' => '', + 'link' => $name, + 'email' => $name, + 'is_guest' => true + ); + } + + $url_data = determineActions($url_data); + + // Setup the linktree and page title (do it down here because the language files are now loaded..) + $context['page_title'] = $txt['who_title']; + $context['linktree'][] = array( + 'url' => $scripturl . '?action=who', + 'name' => $txt['who_title'] + ); + + // Put it in the context variables. + foreach ($context['members'] as $i => $member) + { + if ($member['id'] != 0) + $member['id'] = loadMemberContext($member['id']) ? $member['id'] : 0; + + // Keep the IP that came from the database. + $memberContext[$member['id']]['ip'] = $member['ip']; + $context['members'][$i]['action'] = isset($url_data[$i]) ? $url_data[$i] : array('label' => 'who_hidden', 'class' => 'em'); + if ($member['id'] == 0 && isset($spiderContext[$member['id_spider']])) + $context['members'][$i] += $spiderContext[$member['id_spider']]; + else + $context['members'][$i] += $memberContext[$member['id']]; + } + + // Some people can't send personal messages... + $context['can_send_pm'] = allowedTo('pm_send'); + $context['can_send_email'] = allowedTo('send_email_to_members'); + + // any profile fields disabled? + $context['disabled_fields'] = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : array(); + +} + +/** + * This function determines the actions of the members passed in urls. + * + * Adding actions to the Who's Online list: + * Adding actions to this list is actually relatively easy... + * - for actions anyone should be able to see, just add a string named whoall_ACTION. + * (where ACTION is the action used in index.php.) + * - for actions that have a subaction which should be represented differently, use whoall_ACTION_SUBACTION. + * - for actions that include a topic, and should be restricted, use whotopic_ACTION. + * - for actions that use a message, by msg or quote, use whopost_ACTION. + * - for administrator-only actions, use whoadmin_ACTION. + * - for actions that should be viewable only with certain permissions, + * use whoallow_ACTION and add a list of possible permissions to the + * $allowedActions array, using ACTION as the key. + * + * @param mixed $urls a single url (string) or an array of arrays, each inner array being (JSON-encoded request data, id_member) + * @param string|bool $preferred_prefix = false + * @return array an array of descriptions if you passed an array, otherwise the string describing their current location. + */ +function determineActions($urls, $preferred_prefix = false) +{ + global $txt, $user_info, $modSettings, $smcFunc, $scripturl, $context; + + if (!allowedTo('who_view')) + return array(); + loadLanguage('Who'); + + // Actions that require a specific permission level. + $allowedActions = array( + 'admin' => array('moderate_forum', 'manage_membergroups', 'manage_bans', 'admin_forum', 'manage_permissions', 'send_mail', 'manage_attachments', 'manage_smileys', 'manage_boards', 'edit_news'), + 'ban' => array('manage_bans'), + 'boardrecount' => array('admin_forum'), + 'calendar' => array('calendar_view'), + 'corefeatures' => array('admin_forum'), + 'editnews' => array('edit_news'), + 'featuresettings' => array('admin_forum'), + 'languages' => array('admin_forum'), + 'logs' => array('admin_forum'), + 'mailing' => array('send_mail'), + 'mailqueue' => array('admin_forum'), + 'maintain' => array('admin_forum'), + 'manageattachments' => array('manage_attachments'), + 'manageboards' => array('manage_boards'), + 'managecalendar' => array('admin_forum'), + 'managesearch' => array('admin_forum'), + 'managesmileys' => array('manage_smileys'), + 'membergroups' => array('manage_membergroups'), + 'mlist' => array('view_mlist'), + 'moderate' => array('access_mod_center', 'moderate_forum', 'manage_membergroups'), + 'modsettings' => array('admin_forum'), + 'news' => array('edit_news', 'send_mail', 'admin_forum'), + 'optimizetables' => array('admin_forum'), + 'packages' => array('admin_forum'), + 'paidsubscribe' => array('admin_forum'), + 'permissions' => array('manage_permissions'), + 'postsettings' => array('admin_forum'), + 'regcenter' => array('admin_forum', 'moderate_forum'), + 'repairboards' => array('admin_forum'), + 'reports' => array('admin_forum'), + 'scheduledtasks' => array('admin_forum'), + 'search' => array('search_posts'), + 'search2' => array('search_posts'), + 'securitysettings' => array('admin_forum'), + 'sengines' => array('admin_forum'), + 'serversettings' => array('admin_forum'), + 'setcensor' => array('moderate_forum'), + 'setreserve' => array('moderate_forum'), + 'stats' => array('view_stats'), + 'theme' => array('admin_forum'), + 'viewerrorlog' => array('admin_forum'), + 'viewmembers' => array('moderate_forum'), + ); + call_integration_hook('who_allowed', array(&$allowedActions)); + + if (!is_array($urls)) + $url_list = array(array($urls, $user_info['id'])); + else + $url_list = $urls; + + // These are done to later query these in large chunks. (instead of one by one.) + $topic_ids = array(); + $profile_ids = array(); + $board_ids = array(); + + $data = array(); + foreach ($url_list as $k => $url) + { + // Get the request parameters.. + $actions = $smcFunc['json_decode']($url[0], true); + if ($actions === array()) + continue; + + // If it's the admin or moderation center, and there is an area set, use that instead. + if (isset($actions['action']) && ($actions['action'] == 'admin' || $actions['action'] == 'moderate') && isset($actions['area'])) + $actions['action'] = $actions['area']; + + // Check if there was no action or the action is display. + if (!isset($actions['action']) || $actions['action'] == 'display') + { + // It's a topic! Must be! + if (isset($actions['topic'])) + { + // Assume they can't view it, and queue it up for later. + $data[$k] = array('label' => 'who_hidden', 'class' => 'em'); + $topic_ids[(int) $actions['topic']][$k] = $txt['who_topic']; + } + // It's a board! + elseif (isset($actions['board'])) + { + // Hide first, show later. + $data[$k] = array('label' => 'who_hidden', 'class' => 'em'); + $board_ids[$actions['board']][$k] = $txt['who_board']; + } + // It's the board index!! It must be! + else + $data[$k] = sprintf($txt['who_index'], $scripturl, $context['forum_name_html_safe']); + } + // Probably an error or some goon? + elseif ($actions['action'] == '') + $data[$k] = sprintf($txt['who_index'], $scripturl, $context['forum_name_html_safe']); + // Some other normal action...? + else + { + // Viewing/editing a profile. + if ($actions['action'] == 'profile') + { + // Whose? Their own? + if (empty($actions['u'])) + $actions['u'] = $url[1]; + + $data[$k] = array('label' => 'who_hidden', 'class' => 'em'); + $profile_ids[(int) $actions['u']][$k] = $actions['u'] == $url[1] ? $txt['who_viewownprofile'] : $txt['who_viewprofile']; + } + elseif (($actions['action'] == 'post' || $actions['action'] == 'post2') && empty($actions['topic']) && isset($actions['board'])) + { + $data[$k] = array('label' => 'who_hidden', 'class' => 'em'); + $board_ids[(int) $actions['board']][$k] = isset($actions['poll']) ? $txt['who_poll'] : $txt['who_post']; + } + // A subaction anyone can view... if the language string is there, show it. + elseif (isset($actions['sa']) && isset($txt['whoall_' . $actions['action'] . '_' . $actions['sa']])) + $data[$k] = $preferred_prefix && isset($txt[$preferred_prefix . $actions['action'] . '_' . $actions['sa']]) ? $txt[$preferred_prefix . $actions['action'] . '_' . $actions['sa']] : sprintf($txt['whoall_' . $actions['action'] . '_' . $actions['sa']], $scripturl); + // An action any old fellow can look at. (if ['whoall_' . $action] exists, we know everyone can see it.) + elseif (isset($txt['whoall_' . $actions['action']])) + $data[$k] = $preferred_prefix && isset($txt[$preferred_prefix . $actions['action']]) ? $txt[$preferred_prefix . $actions['action']] : sprintf($txt['whoall_' . $actions['action']], $scripturl); + // Viewable if and only if they can see the board... + elseif (isset($txt['whotopic_' . $actions['action']])) + { + // Find out what topic they are accessing. + $topic = (int) (isset($actions['topic']) ? $actions['topic'] : (isset($actions['from']) ? $actions['from'] : 0)); + + $data[$k] = array('label' => 'who_hidden', 'class' => 'em'); + $topic_ids[$topic][$k] = $txt['whotopic_' . $actions['action']]; + } + elseif (isset($txt['whopost_' . $actions['action']])) + { + // Find out what message they are accessing. + $msgid = (int) (isset($actions['msg']) ? $actions['msg'] : (isset($actions['quote']) ? $actions['quote'] : 0)); + + $result = $smcFunc['db_query']('', ' + SELECT m.id_topic, m.subject + FROM {db_prefix}messages AS m + ' . ($modSettings['postmod_active'] ? 'INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic AND t.approved = {int:is_approved})' : '') . ' + WHERE m.id_msg = {int:id_msg} + AND {query_see_message_board}' . ($modSettings['postmod_active'] ? ' + AND m.approved = {int:is_approved}' : '') . ' + LIMIT 1', + array( + 'is_approved' => 1, + 'id_msg' => $msgid, + ) + ); + list ($id_topic, $subject) = $smcFunc['db_fetch_row']($result); + $data[$k] = sprintf($txt['whopost_' . $actions['action']], $id_topic, $subject, $scripturl); + $smcFunc['db_free_result']($result); + + if (empty($id_topic)) + $data[$k] = array('label' => 'who_hidden', 'class' => 'em'); + } + // Viewable only by administrators.. (if it starts with whoadmin, it's admin only!) + elseif (allowedTo('moderate_forum') && isset($txt['whoadmin_' . $actions['action']])) + $data[$k] = sprintf($txt['whoadmin_' . $actions['action']], $scripturl); + // Viewable by permission level. + elseif (isset($allowedActions[$actions['action']])) + { + if (allowedTo($allowedActions[$actions['action']]) && !empty($txt['whoallow_' . $actions['action']])) + $data[$k] = sprintf($txt['whoallow_' . $actions['action']], $scripturl); + elseif (in_array('moderate_forum', $allowedActions[$actions['action']])) + $data[$k] = $txt['who_moderate']; + elseif (in_array('admin_forum', $allowedActions[$actions['action']])) + $data[$k] = $txt['who_admin']; + else + $data[$k] = array('label' => 'who_hidden', 'class' => 'em'); + } + elseif (!empty($actions['action'])) + $data[$k] = $txt['who_generic'] . ' ' . $actions['action']; + else + $data[$k] = array('label' => 'who_unknown', 'class' => 'em'); + } + + if (isset($actions['error'])) + { + loadLanguage('Errors'); + + if (isset($txt[$actions['error']])) + $error_message = str_replace('"', '"', empty($actions['error_params']) ? $txt[$actions['error']] : vsprintf($txt[$actions['error']], (array) $actions['error_params'])); + elseif ($actions['error'] == 'guest_login') + $error_message = str_replace('"', '"', $txt['who_guest_login']); + else + $error_message = str_replace('"', '"', $actions['error']); + + if (!empty($error_message)) + { + $error_message = ' '; + + if (is_array($data[$k])) + $data[$k]['error_message'] = $error_message; + else + $data[$k] .= $error_message; + } + } + + // Maybe the action is integrated into another system? + if (count($integrate_actions = call_integration_hook('integrate_whos_online', array($actions))) > 0) + { + foreach ($integrate_actions as $integrate_action) + { + if (!empty($integrate_action)) + { + $data[$k] = $integrate_action; + if (isset($actions['topic']) && isset($topic_ids[(int) $actions['topic']][$k])) + $topic_ids[(int) $actions['topic']][$k] = $integrate_action; + if (isset($actions['board']) && isset($board_ids[(int) $actions['board']][$k])) + $board_ids[(int) $actions['board']][$k] = $integrate_action; + if (isset($actions['u']) && isset($profile_ids[(int) $actions['u']][$k])) + $profile_ids[(int) $actions['u']][$k] = $integrate_action; + break; + } + } + } + } + + // Load topic names. + if (!empty($topic_ids)) + { + $result = $smcFunc['db_query']('', ' + SELECT t.id_topic, m.subject + FROM {db_prefix}topics AS t + INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg) + WHERE {query_see_topic_board} + AND t.id_topic IN ({array_int:topic_list})' . ($modSettings['postmod_active'] ? ' + AND t.approved = {int:is_approved}' : '') . ' + LIMIT {int:limit}', + array( + 'topic_list' => array_keys($topic_ids), + 'is_approved' => 1, + 'limit' => count($topic_ids), + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + // Show the topic's subject for each of the actions. + foreach ($topic_ids[$row['id_topic']] as $k => $session_text) + $data[$k] = sprintf($session_text, $row['id_topic'], censorText($row['subject']), $scripturl); + } + $smcFunc['db_free_result']($result); + } + + // Load board names. + if (!empty($board_ids)) + { + $result = $smcFunc['db_query']('', ' + SELECT b.id_board, b.name + FROM {db_prefix}boards AS b + WHERE {query_see_board} + AND b.id_board IN ({array_int:board_list}) + LIMIT {int:limit}', + array( + 'board_list' => array_keys($board_ids), + 'limit' => count($board_ids), + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + // Put the board name into the string for each member... + foreach ($board_ids[$row['id_board']] as $k => $session_text) + $data[$k] = sprintf($session_text, $row['id_board'], $row['name'], $scripturl); + } + $smcFunc['db_free_result']($result); + } + + // Load member names for the profile. (is_not_guest permission for viewing their own profile) + $allow_view_own = allowedTo('is_not_guest'); + $allow_view_any = allowedTo('profile_view'); + if (!empty($profile_ids) && ($allow_view_any || $allow_view_own)) + { + $result = $smcFunc['db_query']('', ' + SELECT id_member, real_name + FROM {db_prefix}members + WHERE id_member IN ({array_int:member_list}) + LIMIT ' . count($profile_ids), + array( + 'member_list' => array_keys($profile_ids), + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + // If they aren't allowed to view this person's profile, skip it. + if (!$allow_view_any && ($user_info['id'] != $row['id_member'])) + continue; + + // Set their action on each - session/text to sprintf. + foreach ($profile_ids[$row['id_member']] as $k => $session_text) + $data[$k] = sprintf($session_text, $row['id_member'], $row['real_name'], $scripturl); + } + $smcFunc['db_free_result']($result); + } + + call_integration_hook('whos_online_after', array(&$urls, &$data)); + + if (!is_array($urls)) + return isset($data[0]) ? $data[0] : false; + else + return $data; +} + +/** + * It prepares credit and copyright information for the credits page or the admin page + * + * @param bool $in_admin = false, if parameter is true the it will not load the sub-template nor the template file + */ +function Credits($in_admin = false) +{ + global $context, $smcFunc, $forum_copyright, $txt, $user_info, $scripturl; + + // Don't blink. Don't even blink. Blink and you're dead. + loadLanguage('Who'); + + // Discourage robots from indexing this page. + $context['robot_no_index'] = true; + + if ($in_admin) + { + $context[$context['admin_menu_name']]['tab_data'] = array( + 'title' => $txt['support_credits_title'], + 'help' => '', + 'description' => '', + ); + } + + $context['credits'] = array( + array( + 'pretext' => $txt['credits_intro'], + 'title' => $txt['credits_team'], + 'groups' => array( + array( + 'title' => $txt['credits_groups_pm'], + 'members' => array( + 'Aleksi "Lex" Kilpinen', + // Former Project Managers + 'Michele "Illori" Davis', + 'Jessica "Suki" González', + 'Will "Kindred" Wagner', + ), + ), + array( + 'title' => $txt['credits_groups_dev'], + 'members' => array( + // Lead Developer + 'Shawn Bulen', + // Developers + 'John "live627" Rayes', + 'Oscar "Ozp" Rydhé', + + // Former Developers + 'Aaron van Geffen', + 'Antechinus', + 'Bjoern "Bloc" Kristiansen', + 'Brad "IchBin™" Grow', + 'Colin Schoen', + 'emanuele', + 'Hendrik Jan "Compuart" Visser', + 'Jessica "Suki" González', + 'Jon "Sesquipedalian" Stovell', + 'Juan "JayBachatero" Hernandez', + 'Karl "RegularExpression" Benson', + 'Matthew "Labradoodle-360" Kerle', + $user_info['is_admin'] ? 'Matt "Grudge" Wolf' : 'Grudge', + 'Michael "Oldiesmann" Eshom', + 'Michael "Thantos" Miller', + 'Norv', + 'Peter "Arantor" Spicer', + 'Selman "[SiNaN]" Eser', + 'Shitiz "Dragooon" Garg', + // 'Spuds', // Doesn't want to be listed here + // 'Steven "Fustrate" Hoffman', + 'Theodore "Orstio" Hildebrandt', + 'Thorsten "TE" Eurich', + 'winrules', + ), + ), + array( + 'title' => $txt['credits_groups_support'], + 'members' => array( + // Lead Support Specialist + 'Will "Kindred" Wagner', + // Support Specialists + 'Doug Heffernan', + 'lurkalot', + 'Steve', + + // Former Support Specialists + 'Aleksi "Lex" Kilpinen', + 'br360', + 'GigaWatt', + 'ziycon', + 'Adam Tallon', + 'Bigguy', + 'Bruno "margarett" Alves', + 'CapadY', + 'ChalkCat', + 'Chas Large', + 'Duncan85', + 'gbsothere', + 'JimM', + 'Justyne', + 'Kat', + 'Kevin "greyknight17" Hou', + 'Krash', + 'Mashby', + 'Michael Colin Blaber', + 'Old Fossil', + 'S-Ace', + 'shadav', + 'Storman™', + 'Wade "sησω" Poulsen', + 'xenovanis', + ), + ), + array( + 'title' => $txt['credits_groups_customize'], + 'members' => array( + // Lead Customizer + 'Diego Andrés', + // Customizers + 'GL700Wing', + 'Johnnie "TwitchisMental" Ballew', + 'Jonathan "vbgamer45" Valentin', + + // Former Customizers + 'Sami "SychO" Mazouz', + 'Brannon "B" Hall', + 'Gary M. Gadsdon', + 'Jack "akabugeyes" Thorsen', + 'Jason "JBlaze" Clemons', + 'Joey "Tyrsson" Smith', + 'Kays', + 'Michael "Mick." Gomez', + 'NanoSector', + 'Ricky.', + 'Russell "NEND" Najar', + 'SA™', + ), + ), + array( + 'title' => $txt['credits_groups_docs'], + 'members' => array( + // Doc Coordinator + 'Michele "Illori" Davis', + // Doc Writers + 'Irisado', + + // Former Doc Writers + 'AngelinaBelle', + 'Chainy', + 'Graeme Spence', + 'Joshua "groundup" Dickerson', + ), + ), + array( + 'title' => $txt['credits_groups_internationalizers'], + 'members' => array( + // Lead Localizer + 'Nikola "Dzonny" Novaković', + // Localizers + 'm4z', + // Former Localizers + 'Francisco "d3vcho" Domínguez', + 'Robert Monden', + 'Relyana', + ), + ), + array( + 'title' => $txt['credits_groups_marketing'], + 'members' => array( + // Marketing Coordinator + + // Marketing + + // Former Marketing + 'Adish "(F.L.A.M.E.R)" Patel', + 'Bryan "Runic" Deakin', + 'Marcus "cσσкιє мσηѕтєя" Forsberg', + 'Ralph "[n3rve]" Otowo', + ), + ), + array( + 'title' => $txt['credits_groups_site'], + 'members' => array( + 'Jeremy "SleePy" Darwood', + ), + ), + array( + 'title' => $txt['credits_groups_servers'], + 'members' => array( + 'Derek Schwab', + 'Michael Johnson', + 'Liroy van Hoewijk', + ), + ), + ), + ), + ); + + // Give the translators some credit for their hard work. + if (!is_array($txt['translation_credits'])) + $txt['translation_credits'] = array_filter(array_map('trim', explode(',', $txt['translation_credits']))); + + if (!empty($txt['translation_credits'])) + $context['credits'][] = array( + 'title' => $txt['credits_groups_translation'], + 'groups' => array( + array( + 'title' => $txt['credits_groups_translation'], + 'members' => $txt['translation_credits'], + ), + ), + ); + + $context['credits'][] = array( + 'title' => $txt['credits_special'], + 'posttext' => $txt['credits_anyone'], + 'groups' => array( + array( + 'title' => $txt['credits_groups_consultants'], + 'members' => array( + 'albertlast', + 'Brett Flannigan', + 'Mark Rose', + 'René-Gilles "Nao 尚" Deberdt', + 'tinoest', + $txt['credits_code_contributors'], + ), + ), + array( + 'title' => $txt['credits_groups_beta'], + 'members' => array( + $txt['credits_beta_message'], + ), + ), + array( + 'title' => $txt['credits_groups_translators'], + 'members' => array( + $txt['credits_translators_message'], + ), + ), + array( + 'title' => $txt['credits_groups_founder'], + 'members' => array( + 'Unknown W. "[Unknown]" Brackets', + ), + ), + array( + 'title' => $txt['credits_groups_orignal_pm'], + 'members' => array( + 'Jeff Lewis', + 'Joseph Fung', + 'David Recordon', + ), + ), + array( + 'title' => $txt['credits_in_memoriam'], + 'members' => array( + 'Crip', + 'K@', + 'metallica48423', + 'Paul_Pauline', + ), + ), + ), + ); + + // Give credit to any graphic library's, software library's, plugins etc + $context['credits_software_graphics'] = array( + 'graphics' => array( + 'Fugue Icons | © 2012 Yusuke Kamiyamane | These icons are licensed under a Creative Commons Attribution 3.0 License', + 'Oxygen Icons | These icons are licensed under GNU LGPLv3', + ), + 'software' => array( + 'JQuery | © John Resig | Licensed under The MIT License (MIT)', + 'hoverIntent | © Brian Cherne | Licensed under The MIT License (MIT)', + 'SCEditor | © Sam Clarke | Licensed under The MIT License (MIT)', + 'animaDrag | © Abel Mohler | Licensed under The MIT License (MIT)', + 'jQuery Custom Scrollbar | © Maciej Zubala | Licensed under The MIT License (MIT)', + 'jQuery Responsive Slider | © booncon ROCKETS | Licensed under The MIT License (MIT)', + 'At.js | © chord.luo@gmail.com | Licensed under The MIT License (MIT)', + 'HTML5 Desktop Notifications | © Tsvetan Tsvetkov | Licensed under The Apache License Version 2.0', + 'GAuth Code Generator/Validator | © Chris Cornutt | Licensed under The MIT License (MIT)', + 'Dropzone.js | © Matias Meno | Licensed under The MIT License (MIT)', + 'Minify | © Matthias Mullie | Licensed under The MIT License (MIT)', + 'PHP-Punycode | © True B.V. | Licensed under The MIT License (MIT)', + ), + 'fonts' => array( + ' Anonymous Pro | © 2009 | This font is licensed under the SIL Open Font License, Version 1.1', + ' ConsolaMono | © 2012 | This font is licensed under the SIL Open Font License, Version 1.1', + ' Phennig | © 2009-2012 | This font is licensed under the SIL Open Font License, Version 1.1', + ), + ); + + // Support for mods that use the tag via the package manager + $context['credits_modifications'] = array(); + if (($mods = cache_get_data('mods_credits', 86400)) === null) + { + $mods = array(); + $request = $smcFunc['db_query']('substring', ' + SELECT version, name, credits + FROM {db_prefix}log_packages + WHERE install_state = {int:installed_mods} + AND credits != {string:empty} + AND SUBSTRING(filename, 1, 9) != {string:patch_name}', + array( + 'installed_mods' => 1, + 'patch_name' => 'smf_patch', + 'empty' => '', + ) + ); + + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $credit_info = $smcFunc['json_decode']($row['credits'], true); + + $copyright = empty($credit_info['copyright']) ? '' : $txt['credits_copyright'] . ' © ' . $smcFunc['htmlspecialchars']($credit_info['copyright']); + $license = empty($credit_info['license']) ? '' : $txt['credits_license'] . ': ' . (!empty($credit_info['licenseurl']) ? '' . $smcFunc['htmlspecialchars']($credit_info['license']) . '' : $smcFunc['htmlspecialchars']($credit_info['license'])); + $version = $txt['credits_version'] . ' ' . $row['version']; + $title = (empty($credit_info['title']) ? $row['name'] : $smcFunc['htmlspecialchars']($credit_info['title'])) . ': ' . $version; + + // build this one out and stash it away + $mod_name = empty($credit_info['url']) ? $title : '' . $title . ''; + $mods[] = $mod_name . (!empty($license) ? ' | ' . $license : '') . (!empty($copyright) ? ' | ' . $copyright : ''); + } + cache_put_data('mods_credits', $mods, 86400); + } + $context['credits_modifications'] = $mods; + + $context['copyrights'] = array( + 'smf' => sprintf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR, $scripturl), + /* Modification Authors: You may add a copyright statement to this array for your mods. + Copyright statements should be in the form of a value only without a array key. I.E.: + 'Some Mod by Thantos © 2010', + $txt['some_mod_copyright'], + */ + 'mods' => array( + ), + ); + + // Support for those that want to use a hook as well + call_integration_hook('integrate_credits'); + + if (!$in_admin) + { + loadTemplate('Who'); + $context['sub_template'] = 'credits'; + $context['robot_no_index'] = true; + $context['page_title'] = $txt['credits']; + } +} + +?> \ No newline at end of file diff --git a/Sources/Xml.php b/Sources/Xml.php new file mode 100644 index 0000000..6d27f22 --- /dev/null +++ b/Sources/Xml.php @@ -0,0 +1,310 @@ + 'GetJumpTo', + 'messageicons' => 'ListMessageIcons', + 'previews' => 'RetrievePreview', + ); + + // Easy adding of sub actions. + call_integration_hook('integrate_XMLhttpMain_subActions', array(&$subActions)); + + if (!isset($_REQUEST['sa'], $subActions[$_REQUEST['sa']])) + fatal_lang_error('no_access', false); + + call_helper($subActions[$_REQUEST['sa']]); +} + +/** + * Get a list of boards and categories used for the jumpto dropdown. + */ +function GetJumpTo() +{ + global $context, $sourcedir; + + // Find the boards/categories they can see. + require_once($sourcedir . '/Subs-MessageIndex.php'); + $boardListOptions = array( + 'use_permissions' => true, + 'selected_board' => isset($context['current_board']) ? $context['current_board'] : 0, + ); + $context['jump_to'] = getBoardList($boardListOptions); + + // Make the board safe for display. + foreach ($context['jump_to'] as $id_cat => $cat) + { + $context['jump_to'][$id_cat]['name'] = un_htmlspecialchars(strip_tags($cat['name'])); + foreach ($cat['boards'] as $id_board => $board) + $context['jump_to'][$id_cat]['boards'][$id_board]['name'] = un_htmlspecialchars(strip_tags($board['name'])); + } + + $context['sub_template'] = 'jump_to'; +} + +/** + * Gets a list of available message icons and sends the info to the template for display + */ +function ListMessageIcons() +{ + global $context, $sourcedir, $board; + + require_once($sourcedir . '/Subs-Editor.php'); + $context['icons'] = getMessageIcons($board); + + $context['sub_template'] = 'message_icons'; +} + +/** + * Handles retrieving previews of news items, newsletters, signatures and warnings. + * Calls the appropriate function based on $_POST['item'] + * + * @return void|bool Returns false if $_POST['item'] isn't set or isn't valid + */ +function RetrievePreview() +{ + global $context; + + $items = array( + 'newspreview', + 'newsletterpreview', + 'sig_preview', + 'warning_preview', + ); + + $context['sub_template'] = 'generic_xml'; + + if (!isset($_POST['item']) || !in_array($_POST['item'], $items)) + return false; + + $_POST['item'](); +} + +/** + * Handles previewing news items + */ +function newspreview() +{ + global $context, $sourcedir, $smcFunc; + + require_once($sourcedir . '/Subs-Post.php'); + + $errors = array(); + $news = !isset($_POST['news']) ? '' : $smcFunc['htmlspecialchars']($_POST['news'], ENT_QUOTES); + if (empty($news)) + $errors[] = array('value' => 'no_news'); + else + preparsecode($news); + + $context['xml_data'] = array( + 'news' => array( + 'identifier' => 'parsedNews', + 'children' => array( + array( + 'value' => parse_bbc($news), + ), + ), + ), + 'errors' => array( + 'identifier' => 'error', + 'children' => $errors + ), + ); +} + +/** + * Handles previewing newsletters + */ +function newsletterpreview() +{ + global $context, $sourcedir, $txt; + + require_once($sourcedir . '/Subs-Post.php'); + require_once($sourcedir . '/ManageNews.php'); + loadLanguage('Errors'); + + $context['post_error']['messages'] = array(); + $context['send_pm'] = !empty($_POST['send_pm']) ? 1 : 0; + $context['send_html'] = !empty($_POST['send_html']) ? 1 : 0; + + if (empty($_POST['subject'])) + $context['post_error']['messages'][] = $txt['error_no_subject']; + if (empty($_POST['message'])) + $context['post_error']['messages'][] = $txt['error_no_message']; + + prepareMailingForPreview(); + + $context['sub_template'] = 'pm'; +} + +/** + * Handles previewing signatures + */ +function sig_preview() +{ + global $context, $sourcedir, $smcFunc, $txt, $user_info; + + require_once($sourcedir . '/Profile-Modify.php'); + loadLanguage('Profile'); + loadLanguage('Errors'); + + $user = isset($_POST['user']) ? (int) $_POST['user'] : 0; + $is_owner = $user == $user_info['id']; + + // @todo Temporary + // Borrowed from loadAttachmentContext in Display.php + $can_change = $is_owner ? allowedTo(array('profile_extra_any', 'profile_extra_own')) : allowedTo('profile_extra_any'); + + $errors = array(); + if (!empty($user) && $can_change) + { + $request = $smcFunc['db_query']('', ' + SELECT signature + FROM {db_prefix}members + WHERE id_member = {int:id_member} + LIMIT 1', + array( + 'id_member' => $user, + ) + ); + list($current_signature) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + censorText($current_signature); + $allowedTags = get_signature_allowed_bbc_tags(); + $current_signature = !empty($current_signature) ? parse_bbc($current_signature, true, 'sig' . $user, $allowedTags) : $txt['no_signature_set']; + + $preview_signature = !empty($_POST['signature']) ? $smcFunc['htmlspecialchars']($_POST['signature']) : $txt['no_signature_preview']; + $validation = profileValidateSignature($preview_signature); + + if ($validation !== true && $validation !== false) + $errors[] = array('value' => $txt['profile_error_' . $validation], 'attributes' => array('type' => 'error')); + + censorText($preview_signature); + $preview_signature = parse_bbc($preview_signature, true, 'sig' . $user, $allowedTags); + } + elseif (!$can_change) + { + if ($is_owner) + $errors[] = array('value' => $txt['cannot_profile_extra_own'], 'attributes' => array('type' => 'error')); + else + $errors[] = array('value' => $txt['cannot_profile_extra_any'], 'attributes' => array('type' => 'error')); + } + else + $errors[] = array('value' => $txt['no_user_selected'], 'attributes' => array('type' => 'error')); + + $context['xml_data']['signatures'] = array( + 'identifier' => 'signature', + 'children' => array() + ); + if (isset($current_signature)) + $context['xml_data']['signatures']['children'][] = array( + 'value' => $current_signature, + 'attributes' => array('type' => 'current'), + ); + if (isset($preview_signature)) + $context['xml_data']['signatures']['children'][] = array( + 'value' => $preview_signature, + 'attributes' => array('type' => 'preview'), + ); + if (!empty($errors)) + $context['xml_data']['errors'] = array( + 'identifier' => 'error', + 'children' => array_merge( + array( + array( + 'value' => $txt['profile_errors_occurred'], + 'attributes' => array('type' => 'errors_occurred'), + ), + ), + $errors + ), + ); +} + +/** + * Handles previewing user warnings + */ +function warning_preview() +{ + global $context, $sourcedir, $smcFunc, $txt, $user_info, $scripturl, $mbname; + + require_once($sourcedir . '/Subs-Post.php'); + loadLanguage('Errors'); + loadLanguage('ModerationCenter'); + + $context['post_error']['messages'] = array(); + if (allowedTo('issue_warning')) + { + $warning_body = !empty($_POST['body']) ? trim(censorText($_POST['body'])) : ''; + $context['preview_subject'] = !empty($_POST['title']) ? trim($smcFunc['htmlspecialchars']($_POST['title'])) : ''; + if (isset($_POST['issuing'])) + { + if (empty($_POST['title']) || empty($_POST['body'])) + $context['post_error']['messages'][] = $txt['warning_notify_blank']; + } + else + { + if (empty($_POST['title'])) + $context['post_error']['messages'][] = $txt['mc_warning_template_error_no_title']; + if (empty($_POST['body'])) + $context['post_error']['messages'][] = $txt['mc_warning_template_error_no_body']; + // Add in few replacements. + /** + * These are the defaults: + * - {MEMBER} - Member Name. => current user for review + * - {MESSAGE} - Link to Offending Post. (If Applicable) => not applicable here, so not replaced + * - {FORUMNAME} - Forum Name. + * - {SCRIPTURL} - Web address of forum. + * - {REGARDS} - Standard email sign-off. + */ + $find = array( + '{MEMBER}', + '{FORUMNAME}', + '{SCRIPTURL}', + '{REGARDS}', + ); + $replace = array( + $user_info['name'], + $mbname, + $scripturl, + sprintf($txt['regards_team'], $context['forum_name']), + ); + $warning_body = str_replace($find, $replace, $warning_body); + } + + if (!empty($_POST['body'])) + { + preparsecode($warning_body); + $warning_body = parse_bbc($warning_body, true); + } + $context['preview_message'] = $warning_body; + } + else + $context['post_error']['messages'][] = array('value' => $txt['cannot_issue_warning'], 'attributes' => array('type' => 'error')); + + $context['sub_template'] = 'warning'; +} + +?> \ No newline at end of file diff --git a/Sources/index.php b/Sources/index.php new file mode 100644 index 0000000..01bf5c3 --- /dev/null +++ b/Sources/index.php @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/Sources/minify/data/index.php b/Sources/minify/data/index.php new file mode 100644 index 0000000..69278ce --- /dev/null +++ b/Sources/minify/data/index.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/Sources/minify/data/js/index.php b/Sources/minify/data/js/index.php new file mode 100644 index 0000000..69278ce --- /dev/null +++ b/Sources/minify/data/js/index.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/Sources/minify/data/js/keywords_after.txt b/Sources/minify/data/js/keywords_after.txt new file mode 100644 index 0000000..5c8cba7 --- /dev/null +++ b/Sources/minify/data/js/keywords_after.txt @@ -0,0 +1,7 @@ +in +public +extends +private +protected +implements +instanceof \ No newline at end of file diff --git a/Sources/minify/data/js/keywords_before.txt b/Sources/minify/data/js/keywords_before.txt new file mode 100644 index 0000000..40b4ec0 --- /dev/null +++ b/Sources/minify/data/js/keywords_before.txt @@ -0,0 +1,27 @@ +do +in +let +new +var +case +else +enum +void +with +class +const +yield +delete +export +import +public +static +typeof +extends +package +private +continue +function +protected +implements +instanceof \ No newline at end of file diff --git a/Sources/minify/data/js/keywords_reserved.txt b/Sources/minify/data/js/keywords_reserved.txt new file mode 100644 index 0000000..2a3ad3c --- /dev/null +++ b/Sources/minify/data/js/keywords_reserved.txt @@ -0,0 +1,63 @@ +do +if +in +for +let +new +try +var +case +else +enum +eval +null +this +true +void +with +break +catch +class +const +false +super +throw +while +yield +delete +export +import +public +return +static +switch +typeof +default +extends +finally +package +private +continue +debugger +function +arguments +interface +protected +implements +instanceof +abstract +boolean +byte +char +double +final +float +goto +int +long +native +short +synchronized +throws +transient +volatile \ No newline at end of file diff --git a/Sources/minify/data/js/operators.txt b/Sources/minify/data/js/operators.txt new file mode 100644 index 0000000..e66229a --- /dev/null +++ b/Sources/minify/data/js/operators.txt @@ -0,0 +1,46 @@ ++ +- +* +/ +% += ++= +-= +*= +/= +%= +<<= +>>= +>>>= +&= +^= +|= +& +| +^ +~ +<< +>> +>>> +== +=== +!= +!== +> +< +>= +<= +&& +|| +! +. +[ +] +? +: +, +; +( +) +{ +} \ No newline at end of file diff --git a/Sources/minify/data/js/operators_after.txt b/Sources/minify/data/js/operators_after.txt new file mode 100644 index 0000000..7592656 --- /dev/null +++ b/Sources/minify/data/js/operators_after.txt @@ -0,0 +1,44 @@ ++ +- +* +/ +% += ++= +-= +*= +/= +%= +<<= +>>= +>>>= +&= +^= +|= +& +| +^ +~ +<< +>> +>>> +== +=== +!= +!== +> +< +>= +<= +&& +|| +. +[ +] +? +: +, +; +( +) +} \ No newline at end of file diff --git a/Sources/minify/data/js/operators_before.txt b/Sources/minify/data/js/operators_before.txt new file mode 100644 index 0000000..ff50d87 --- /dev/null +++ b/Sources/minify/data/js/operators_before.txt @@ -0,0 +1,43 @@ ++ +- +* +/ +% += ++= +-= +*= +/= +%= +<<= +>>= +>>>= +&= +^= +|= +& +| +^ +~ +<< +>> +>>> +== +=== +!= +!== +> +< +>= +<= +&& +|| +! +. +[ +? +: +, +; +( +{ diff --git a/Sources/minify/index.php b/Sources/minify/index.php new file mode 100644 index 0000000..69278ce --- /dev/null +++ b/Sources/minify/index.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/Sources/minify/path-converter/index.php b/Sources/minify/path-converter/index.php new file mode 100644 index 0000000..69278ce --- /dev/null +++ b/Sources/minify/path-converter/index.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/Sources/minify/path-converter/src/Converter.php b/Sources/minify/path-converter/src/Converter.php new file mode 100644 index 0000000..f5ee3ae --- /dev/null +++ b/Sources/minify/path-converter/src/Converter.php @@ -0,0 +1,195 @@ + + * @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved. + * @license MIT License + */ +class Converter +{ + /** + * @var string + */ + protected $from; + + /** + * @var string + */ + protected $to; + + /** + * @param string $from The original base path (directory, not file!) + * @param string $to The new base path (directory, not file!) + */ + public function __construct($from, $to) + { + $shared = $this->shared($from, $to); + if ($shared === '') { + // when both paths have nothing in common, one of them is probably + // absolute while the other is relative + $cwd = getcwd(); + $from = strpos($from, $cwd) === 0 ? $from : $cwd.'/'.$from; + $to = strpos($to, $cwd) === 0 ? $to : $cwd.'/'.$to; + + // or traveling the tree via `..` + // attempt to resolve path, or assume it's fine if it doesn't exist + $from = realpath($from) ?: $from; + $to = realpath($to) ?: $to; + } + + $from = $this->dirname($from); + $to = $this->dirname($to); + + $from = $this->normalize($from); + $to = $this->normalize($to); + + $this->from = $from; + $this->to = $to; + } + + /** + * Normalize path. + * + * @param string $path + * + * @return string + */ + protected function normalize($path) + { + // deal with different operating systems' directory structure + $path = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $path), '/'); + + /* + * Example: + * /home/forkcms/frontend/cache/compiled_templates/../../core/layout/css/../images/img.gif + * to + * /home/forkcms/frontend/core/layout/images/img.gif + */ + do { + $path = preg_replace('/[^\/]+(? $chunk) { + if (isset($path2[$i]) && $path1[$i] == $path2[$i]) { + $shared[] = $chunk; + } else { + break; + } + } + + return implode('/', $shared); + } + + /** + * Convert paths relative from 1 file to another. + * + * E.g. + * ../images/img.gif relative to /home/forkcms/frontend/core/layout/css + * should become: + * ../../core/layout/images/img.gif relative to + * /home/forkcms/frontend/cache/minified_css + * + * @param string $path The relative path that needs to be converted. + * + * @return string The new relative path. + */ + public function convert($path) + { + // quit early if conversion makes no sense + if ($this->from === $this->to) { + return $path; + } + + $path = $this->normalize($path); + // if we're not dealing with a relative path, just return absolute + if (strpos($path, '/') === 0) { + return $path; + } + + // normalize paths + $path = $this->normalize($this->from.'/'.$path); + + // strip shared ancestor paths + $shared = $this->shared($path, $this->to); + $path = mb_substr($path, mb_strlen($shared)); + $to = mb_substr($this->to, mb_strlen($shared)); + + // add .. for every directory that needs to be traversed to new path + $to = str_repeat('../', mb_substr_count($to, '/')); + + return $to.ltrim($path, '/'); + } + + /** + * Attempt to get the directory name from a path. + * + * @param string $path + * + * @return string + */ + public function dirname($path) + { + if (is_file($path)) { + return dirname($path); + } + + if (is_dir($path)) { + return rtrim($path, '/'); + } + + // no known file/dir, start making assumptions + + // ends in / = dir + if (mb_substr($path, -1) === '/') { + return rtrim($path, '/'); + } + + // has a dot in the name, likely a file + if (preg_match('/.*\..*$/', basename($path)) !== 0) { + return dirname($path); + } + + // you're on your own here! + return $path; + } +} diff --git a/Sources/minify/path-converter/src/index.php b/Sources/minify/path-converter/src/index.php new file mode 100644 index 0000000..69278ce --- /dev/null +++ b/Sources/minify/path-converter/src/index.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/Sources/minify/src/CSS.php b/Sources/minify/src/CSS.php new file mode 100644 index 0000000..89fb9be --- /dev/null +++ b/Sources/minify/src/CSS.php @@ -0,0 +1,674 @@ + + * @author Tijs Verkoyen + * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved + * @license MIT License + */ +class CSS extends Minify +{ + /** + * @var int + */ + protected $maxImportSize = 5; + + /** + * @var string[] + */ + protected $importExtensions = array( + 'gif' => 'data:image/gif', + 'png' => 'data:image/png', + 'jpe' => 'data:image/jpeg', + 'jpg' => 'data:image/jpeg', + 'jpeg' => 'data:image/jpeg', + 'svg' => 'data:image/svg+xml', + 'woff' => 'data:application/x-font-woff', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'xbm' => 'image/x-xbitmap', + ); + + /** + * Set the maximum size if files to be imported. + * + * Files larger than this size (in kB) will not be imported into the CSS. + * Importing files into the CSS as data-uri will save you some connections, + * but we should only import relatively small decorative images so that our + * CSS file doesn't get too bulky. + * + * @param int $size Size in kB + */ + public function setMaxImportSize($size) + { + $this->maxImportSize = $size; + } + + /** + * Set the type of extensions to be imported into the CSS (to save network + * connections). + * Keys of the array should be the file extensions & respective values + * should be the data type. + * + * @param string[] $extensions Array of file extensions + */ + public function setImportExtensions(array $extensions) + { + $this->importExtensions = $extensions; + } + + /** + * Move any import statements to the top. + * + * @param string $content Nearly finished CSS content + * + * @return string + */ + protected function moveImportsToTop($content) + { + if (preg_match_all('/@import[^;]+;/', $content, $matches)) { + // remove from content + foreach ($matches[0] as $import) { + $content = str_replace($import, '', $content); + } + + // add to top + $content = implode('', $matches[0]).$content; + } + + return $content; + } + + /** + * Combine CSS from import statements. + * + * @import's will be loaded and their content merged into the original file, + * to save HTTP requests. + * + * @param string $source The file to combine imports for + * @param string $content The CSS content to combine imports for + * @param string[] $parents Parent paths, for circular reference checks + * + * @return string + * + * @throws FileImportException + */ + protected function combineImports($source, $content, $parents) + { + $importRegexes = array( + // @import url(xxx) + '/ + # import statement + @import + + # whitespace + \s+ + + # open url() + url\( + + # (optional) open path enclosure + (?P["\']?) + + # fetch path + (?P + + # do not fetch data uris or external sources + (?!( + ["\']? + (data|https?): + )) + + .+? + ) + + # (optional) close path enclosure + (?P=quotes) + + # close url() + \) + + # (optional) trailing whitespace + \s* + + # (optional) media statement(s) + (?P[^;]*) + + # (optional) trailing whitespace + \s* + + # (optional) closing semi-colon + ;? + + /ix', + + // @import 'xxx' + '/ + + # import statement + @import + + # whitespace + \s+ + + # open path enclosure + (?P["\']) + + # fetch path + (?P + + # do not fetch data uris or external sources + (?!( + ["\']? + (data|https?): + )) + + .+? + ) + + # close path enclosure + (?P=quotes) + + # (optional) trailing whitespace + \s* + + # (optional) media statement(s) + (?P[^;]*) + + # (optional) trailing whitespace + \s* + + # (optional) closing semi-colon + ;? + + /ix', + ); + + // find all relative imports in css + $matches = array(); + foreach ($importRegexes as $importRegex) { + if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) { + $matches = array_merge($matches, $regexMatches); + } + } + + $search = array(); + $replace = array(); + + // loop the matches + foreach ($matches as $match) { + // get the path for the file that will be imported + $importPath = dirname($source).'/'.$match['path']; + + // only replace the import with the content if we can grab the + // content of the file + if ($this->canImportFile($importPath)) { + // check if current file was not imported previously in the same + // import chain. + if (in_array($importPath, $parents)) { + throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.'); + } + + // grab referenced file & minify it (which may include importing + // yet other @import statements recursively) + $minifier = new static($importPath); + $importContent = $minifier->execute($source, $parents); + + // check if this is only valid for certain media + if (!empty($match['media'])) { + $importContent = '@media '.$match['media'].'{'.$importContent.'}'; + } + + // add to replacement array + $search[] = $match[0]; + $replace[] = $importContent; + } + } + + // replace the import statements + $content = str_replace($search, $replace, $content); + + return $content; + } + + /** + * Import files into the CSS, base64-ized. + * + * @url(image.jpg) images will be loaded and their content merged into the + * original file, to save HTTP requests. + * + * @param string $source The file to import files for + * @param string $content The CSS content to import files for + * + * @return string + */ + protected function importFiles($source, $content) + { + $extensions = array_keys($this->importExtensions); + $regex = '/url\((["\']?)((?!["\']?data:).*?\.('.implode('|', $extensions).'))\\1\)/i'; + if ($extensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { + $search = array(); + $replace = array(); + + // loop the matches + foreach ($matches as $match) { + // get the path for the file that will be imported + $path = $match[2]; + $path = dirname($source).'/'.$path; + $extension = $match[3]; + + // only replace the import with the content if we're able to get + // the content of the file, and it's relatively small + if ($this->canImportFile($path) && $this->canImportBySize($path)) { + // grab content && base64-ize + $importContent = $this->load($path); + $importContent = base64_encode($importContent); + + // build replacement + $search[] = $match[0]; + $replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')'; + } + } + + // replace the import statements + $content = str_replace($search, $replace, $content); + } + + return $content; + } + + /** + * Minify the data. + * Perform CSS optimizations. + * + * @param string[optional] $path Path to write the data to + * @param string[] $parents Parent paths, for circular reference checks + * + * @return string The minified data + */ + public function execute($path = null, $parents = array()) + { + $content = ''; + + // loop css data (raw data and files) + foreach ($this->data as $source => $css) { + /* + * Let's first take out strings & comments, since we can't just remove + * whitespace anywhere. If whitespace occurs inside a string, we should + * leave it alone. E.g.: + * p { content: "a test" } + */ + $this->extractStrings(); + $this->stripComments(); + $css = $this->replace($css); + + $css = $this->stripWhitespace($css); + $css = $this->shortenHex($css); + $css = $this->shortenZeroes($css); + $css = $this->shortenFontWeights($css); + $css = $this->stripEmptyTags($css); + + // restore the string we've extracted earlier + $css = $this->restoreExtractedData($css); + + $source = is_int($source) ? '' : $source; + $parents = $source ? array_merge($parents, array($source)) : $parents; + $css = $this->combineImports($source, $css, $parents); + $css = $this->importFiles($source, $css); + + /* + * If we'll save to a new path, we'll have to fix the relative paths + * to be relative no longer to the source file, but to the new path. + * If we don't write to a file, fall back to same path so no + * conversion happens (because we still want it to go through most + * of the move code...) + */ + $converter = new Converter($source, $path ?: $source); + $css = $this->move($converter, $css); + + // combine css + $content .= $css; + } + + $content = $this->moveImportsToTop($content); + + return $content; + } + + /** + * Moving a css file should update all relative urls. + * Relative references (e.g. ../images/image.gif) in a certain css file, + * will have to be updated when a file is being saved at another location + * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper). + * + * @param Converter $converter Relative path converter + * @param string $content The CSS content to update relative urls for + * + * @return string + */ + protected function move(Converter $converter, $content) + { + /* + * Relative path references will usually be enclosed by url(). @import + * is an exception, where url() is not necessary around the path (but is + * allowed). + * This *could* be 1 regular expression, where both regular expressions + * in this array are on different sides of a |. But we're using named + * patterns in both regexes, the same name on both regexes. This is only + * possible with a (?J) modifier, but that only works after a fairly + * recent PCRE version. That's why I'm doing 2 separate regular + * expressions & combining the matches after executing of both. + */ + $relativeRegexes = array( + // url(xxx) + '/ + # open url() + url\( + + \s* + + # open path enclosure + (?P["\'])? + + # fetch path + (?P + + # do not fetch data uris or external sources + (?!( + \s? + ["\']? + (data|https?): + )) + + .+? + ) + + # close path enclosure + (?(quotes)(?P=quotes)) + + \s* + + # close url() + \) + + /ix', + + // @import "xxx" + '/ + # import statement + @import + + # whitespace + \s+ + + # we don\'t have to check for @import url(), because the + # condition above will already catch these + + # open path enclosure + (?P["\']) + + # fetch path + (?P + + # do not fetch data uris or external sources + (?!( + ["\']? + (data|https?): + )) + + .+? + ) + + # close path enclosure + (?P=quotes) + + /ix', + ); + + // find all relative urls in css + $matches = array(); + foreach ($relativeRegexes as $relativeRegex) { + if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) { + $matches = array_merge($matches, $regexMatches); + } + } + + $search = array(); + $replace = array(); + + // loop all urls + foreach ($matches as $match) { + // determine if it's a url() or an @import match + $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url'); + + // attempting to interpret GET-params makes no sense, so let's discard them for awhile + $params = strrchr($match['path'], '?'); + $url = $params ? substr($match['path'], 0, -strlen($params)) : $match['path']; + + // fix relative url + $url = $converter->convert($url); + + // now that the path has been converted, re-apply GET-params + $url .= $params; + + // build replacement + $search[] = $match[0]; + if ($type == 'url') { + $replace[] = 'url('.$url.')'; + } elseif ($type == 'import') { + $replace[] = '@import "'.$url.'"'; + } + } + + // replace urls + $content = str_replace($search, $replace, $content); + + return $content; + } + + /** + * Shorthand hex color codes. + * #FF0000 -> #F00. + * + * @param string $content The CSS content to shorten the hex color codes for + * + * @return string + */ + protected function shortenHex($content) + { + $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?=[; }])/i', '#$1$2$3', $content); + + // we can shorten some even more by replacing them with their color name + $colors = array( + '#F0FFFF' => 'azure', + '#F5F5DC' => 'beige', + '#A52A2A' => 'brown', + '#FF7F50' => 'coral', + '#FFD700' => 'gold', + '#808080' => 'gray', + '#008000' => 'green', + '#4B0082' => 'indigo', + '#FFFFF0' => 'ivory', + '#F0E68C' => 'khaki', + '#FAF0E6' => 'linen', + '#800000' => 'maroon', + '#000080' => 'navy', + '#808000' => 'olive', + '#CD853F' => 'peru', + '#FFC0CB' => 'pink', + '#DDA0DD' => 'plum', + '#800080' => 'purple', + '#F00' => 'red', + '#FA8072' => 'salmon', + '#A0522D' => 'sienna', + '#C0C0C0' => 'silver', + '#FFFAFA' => 'snow', + '#D2B48C' => 'tan', + '#FF6347' => 'tomato', + '#EE82EE' => 'violet', + '#F5DEB3' => 'wheat', + ); + + return preg_replace_callback( + '/(?<=[: ])('.implode('|', array_keys($colors)).')(?=[; }])/i', + function ($match) use ($colors) { + return $colors[strtoupper($match[0])]; + }, + $content + ); + } + + /** + * Shorten CSS font weights. + * + * @param string $content The CSS content to shorten the font weights for + * + * @return string + */ + protected function shortenFontWeights($content) + { + $weights = array( + 'normal' => 400, + 'bold' => 700, + ); + + $callback = function ($match) use ($weights) { + return $match[1].$weights[$match[2]]; + }; + + return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content); + } + + /** + * Shorthand 0 values to plain 0, instead of e.g. -0em. + * + * @param string $content The CSS content to shorten the zero values for + * + * @return string + */ + protected function shortenZeroes($content) + { + // reusable bits of code throughout these regexes: + // before & after are used to make sure we don't match lose unintended + // 0-like values (e.g. in #000, or in http://url/1.0) + // units can be stripped from 0 values, or used to recognize non 0 + // values (where wa may be able to strip a .0 suffix) + $before = '(?<=[:(, ])'; + $after = '(?=[ ,);}])'; + $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)'; + + // strip units after zeroes (0px -> 0) + // NOTE: it should be safe to remove all units for a 0 value, but in + // practice, Webkit (especially Safari) seems to stumble over at least + // 0%, potentially other units as well. Only stripping 'px' for now. + // @see https://github.com/matthiasmullie/minify/issues/60 + $content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content); + + // strip 0-digits (.0 -> 0) + $content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content); + // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px + $content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content); + // strip trailing 0: 50.00 -> 50, 50.00px -> 50px + $content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content); + // strip leading 0: 0.1 -> .1, 01.1 -> 1.1 + $content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content); + + // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0) + $content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content); + + // remove zeroes where they make no sense in calc: e.g. calc(100px - 0) + // the 0 doesn't have any effect, and this isn't even valid without unit + // strip all `+ 0` or `- 0` occurrences: calc(10% + 0) -> calc(10%) + // looped because there may be multiple 0s inside 1 group of parentheses + do { + $previous = $content; + $content = preg_replace('/\(([^\(\)]+)\s+[\+\-]\s+0(\s+[^\(\)]+)?\)/', '(\\1\\2)', $content); + } while ($content !== $previous); + // strip all `0 +` occurrences: calc(0 + 10%) -> calc(10%) + $content = preg_replace('/\(\s*0\s+\+\s+([^\(\)]+)\)/', '(\\1)', $content); + // strip all `0 -` occurrences: calc(0 - 10%) -> calc(-10%) + $content = preg_replace('/\(\s*0\s+\-\s+([^\(\)]+)\)/', '(-\\1)', $content); + // I'm not going to attempt to optimize away `x * 0` instances: + // it's dumb enough code already that it likely won't occur, and it's + // too complex to do right (order of operations would have to be + // respected etc) + // what I cared about most here was fixing incorrectly truncated units + + return $content; + } + + /** + * Strip comments from source code. + * + * @param string $content + * + * @return string + */ + protected function stripEmptyTags($content) + { + return preg_replace('/(^|\}|;)[^\{\};]+\{\s*\}/', '\\1', $content); + } + + /** + * Strip comments from source code. + */ + protected function stripComments() + { + $this->registerPattern('/\/\*.*?\*\//s', ''); + } + + /** + * Strip whitespace. + * + * @param string $content The CSS content to strip the whitespace for + * + * @return string + */ + protected function stripWhitespace($content) + { + // remove leading & trailing whitespace + $content = preg_replace('/^\s*/m', '', $content); + $content = preg_replace('/\s*$/m', '', $content); + + // replace newlines with a single space + $content = preg_replace('/\s+/', ' ', $content); + + // remove whitespace around meta characters + // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex + $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content); + $content = preg_replace('/([\[(:])\s+/', '$1', $content); + $content = preg_replace('/\s+([\]\)])/', '$1', $content); + $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content); + + // whitespace around + and - can only be stripped in selectors, like + // :nth-child(3+2n), not in things like calc(3px + 2px) or shorthands + // like 3px -2px + $content = preg_replace('/\s*([+-])\s*(?=[^}]*{)/', '$1', $content); + + // remove semicolon/whitespace followed by closing bracket + $content = str_replace(';}', '}', $content); + + return trim($content); + } + + /** + * Check if file is small enough to be imported. + * + * @param string $path The path to the file + * + * @return bool + */ + protected function canImportBySize($path) + { + return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024; + } +} diff --git a/Sources/minify/src/Exception.php b/Sources/minify/src/Exception.php new file mode 100644 index 0000000..0cb3fab --- /dev/null +++ b/Sources/minify/src/Exception.php @@ -0,0 +1,12 @@ + + */ +abstract class Exception extends \Exception +{ +} diff --git a/Sources/minify/src/Exceptions/BasicException.php b/Sources/minify/src/Exceptions/BasicException.php new file mode 100644 index 0000000..a6328f4 --- /dev/null +++ b/Sources/minify/src/Exceptions/BasicException.php @@ -0,0 +1,12 @@ + + */ +abstract class BasicException extends Exception +{ +} diff --git a/Sources/minify/src/Exceptions/FileImportException.php b/Sources/minify/src/Exceptions/FileImportException.php new file mode 100644 index 0000000..98fc341 --- /dev/null +++ b/Sources/minify/src/Exceptions/FileImportException.php @@ -0,0 +1,10 @@ + + */ +class FileImportException extends BasicException +{ +} diff --git a/Sources/minify/src/Exceptions/IOException.php b/Sources/minify/src/Exceptions/IOException.php new file mode 100644 index 0000000..9b59074 --- /dev/null +++ b/Sources/minify/src/Exceptions/IOException.php @@ -0,0 +1,10 @@ + + */ +class IOException extends BasicException +{ +} diff --git a/Sources/minify/src/Exceptions/index.php b/Sources/minify/src/Exceptions/index.php new file mode 100644 index 0000000..69278ce --- /dev/null +++ b/Sources/minify/src/Exceptions/index.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/Sources/minify/src/JS.php b/Sources/minify/src/JS.php new file mode 100644 index 0000000..739e169 --- /dev/null +++ b/Sources/minify/src/JS.php @@ -0,0 +1,515 @@ + + * @author Tijs Verkoyen + * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved + * @license MIT License + */ +class JS extends Minify +{ + /** + * Var-matching regex based on https://stackoverflow.com/a/9337047/802993. + * + * Note that regular expressions using that bit must have the PCRE_UTF8 + * pattern modifier (/u) set. + * + * @var string + */ + const REGEX_VARIABLE = '\b[$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}][$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}0-9\x{0300}-\x{036f}\x{0483}-\x{0487}\x{0591}-\x{05bd}\x{05bf}\x{05c1}\x{05c2}\x{05c4}\x{05c5}\x{05c7}\x{0610}-\x{061a}\x{064b}-\x{0669}\x{0670}\x{06d6}-\x{06dc}\x{06df}-\x{06e4}\x{06e7}\x{06e8}\x{06ea}-\x{06ed}\x{06f0}-\x{06f9}\x{0711}\x{0730}-\x{074a}\x{07a6}-\x{07b0}\x{07c0}-\x{07c9}\x{07eb}-\x{07f3}\x{0816}-\x{0819}\x{081b}-\x{0823}\x{0825}-\x{0827}\x{0829}-\x{082d}\x{0859}-\x{085b}\x{08e4}-\x{08fe}\x{0900}-\x{0903}\x{093a}-\x{093c}\x{093e}-\x{094f}\x{0951}-\x{0957}\x{0962}\x{0963}\x{0966}-\x{096f}\x{0981}-\x{0983}\x{09bc}\x{09be}-\x{09c4}\x{09c7}\x{09c8}\x{09cb}-\x{09cd}\x{09d7}\x{09e2}\x{09e3}\x{09e6}-\x{09ef}\x{0a01}-\x{0a03}\x{0a3c}\x{0a3e}-\x{0a42}\x{0a47}\x{0a48}\x{0a4b}-\x{0a4d}\x{0a51}\x{0a66}-\x{0a71}\x{0a75}\x{0a81}-\x{0a83}\x{0abc}\x{0abe}-\x{0ac5}\x{0ac7}-\x{0ac9}\x{0acb}-\x{0acd}\x{0ae2}\x{0ae3}\x{0ae6}-\x{0aef}\x{0b01}-\x{0b03}\x{0b3c}\x{0b3e}-\x{0b44}\x{0b47}\x{0b48}\x{0b4b}-\x{0b4d}\x{0b56}\x{0b57}\x{0b62}\x{0b63}\x{0b66}-\x{0b6f}\x{0b82}\x{0bbe}-\x{0bc2}\x{0bc6}-\x{0bc8}\x{0bca}-\x{0bcd}\x{0bd7}\x{0be6}-\x{0bef}\x{0c01}-\x{0c03}\x{0c3e}-\x{0c44}\x{0c46}-\x{0c48}\x{0c4a}-\x{0c4d}\x{0c55}\x{0c56}\x{0c62}\x{0c63}\x{0c66}-\x{0c6f}\x{0c82}\x{0c83}\x{0cbc}\x{0cbe}-\x{0cc4}\x{0cc6}-\x{0cc8}\x{0cca}-\x{0ccd}\x{0cd5}\x{0cd6}\x{0ce2}\x{0ce3}\x{0ce6}-\x{0cef}\x{0d02}\x{0d03}\x{0d3e}-\x{0d44}\x{0d46}-\x{0d48}\x{0d4a}-\x{0d4d}\x{0d57}\x{0d62}\x{0d63}\x{0d66}-\x{0d6f}\x{0d82}\x{0d83}\x{0dca}\x{0dcf}-\x{0dd4}\x{0dd6}\x{0dd8}-\x{0ddf}\x{0df2}\x{0df3}\x{0e31}\x{0e34}-\x{0e3a}\x{0e47}-\x{0e4e}\x{0e50}-\x{0e59}\x{0eb1}\x{0eb4}-\x{0eb9}\x{0ebb}\x{0ebc}\x{0ec8}-\x{0ecd}\x{0ed0}-\x{0ed9}\x{0f18}\x{0f19}\x{0f20}-\x{0f29}\x{0f35}\x{0f37}\x{0f39}\x{0f3e}\x{0f3f}\x{0f71}-\x{0f84}\x{0f86}\x{0f87}\x{0f8d}-\x{0f97}\x{0f99}-\x{0fbc}\x{0fc6}\x{102b}-\x{103e}\x{1040}-\x{1049}\x{1056}-\x{1059}\x{105e}-\x{1060}\x{1062}-\x{1064}\x{1067}-\x{106d}\x{1071}-\x{1074}\x{1082}-\x{108d}\x{108f}-\x{109d}\x{135d}-\x{135f}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}\x{1753}\x{1772}\x{1773}\x{17b4}-\x{17d3}\x{17dd}\x{17e0}-\x{17e9}\x{180b}-\x{180d}\x{1810}-\x{1819}\x{18a9}\x{1920}-\x{192b}\x{1930}-\x{193b}\x{1946}-\x{194f}\x{19b0}-\x{19c0}\x{19c8}\x{19c9}\x{19d0}-\x{19d9}\x{1a17}-\x{1a1b}\x{1a55}-\x{1a5e}\x{1a60}-\x{1a7c}\x{1a7f}-\x{1a89}\x{1a90}-\x{1a99}\x{1b00}-\x{1b04}\x{1b34}-\x{1b44}\x{1b50}-\x{1b59}\x{1b6b}-\x{1b73}\x{1b80}-\x{1b82}\x{1ba1}-\x{1bad}\x{1bb0}-\x{1bb9}\x{1be6}-\x{1bf3}\x{1c24}-\x{1c37}\x{1c40}-\x{1c49}\x{1c50}-\x{1c59}\x{1cd0}-\x{1cd2}\x{1cd4}-\x{1ce8}\x{1ced}\x{1cf2}-\x{1cf4}\x{1dc0}-\x{1de6}\x{1dfc}-\x{1dff}\x{200c}\x{200d}\x{203f}\x{2040}\x{2054}\x{20d0}-\x{20dc}\x{20e1}\x{20e5}-\x{20f0}\x{2cef}-\x{2cf1}\x{2d7f}\x{2de0}-\x{2dff}\x{302a}-\x{302f}\x{3099}\x{309a}\x{a620}-\x{a629}\x{a66f}\x{a674}-\x{a67d}\x{a69f}\x{a6f0}\x{a6f1}\x{a802}\x{a806}\x{a80b}\x{a823}-\x{a827}\x{a880}\x{a881}\x{a8b4}-\x{a8c4}\x{a8d0}-\x{a8d9}\x{a8e0}-\x{a8f1}\x{a900}-\x{a909}\x{a926}-\x{a92d}\x{a947}-\x{a953}\x{a980}-\x{a983}\x{a9b3}-\x{a9c0}\x{a9d0}-\x{a9d9}\x{aa29}-\x{aa36}\x{aa43}\x{aa4c}\x{aa4d}\x{aa50}-\x{aa59}\x{aa7b}\x{aab0}\x{aab2}-\x{aab4}\x{aab7}\x{aab8}\x{aabe}\x{aabf}\x{aac1}\x{aaeb}-\x{aaef}\x{aaf5}\x{aaf6}\x{abe3}-\x{abea}\x{abec}\x{abed}\x{abf0}-\x{abf9}\x{fb1e}\x{fe00}-\x{fe0f}\x{fe20}-\x{fe26}\x{fe33}\x{fe34}\x{fe4d}-\x{fe4f}\x{ff10}-\x{ff19}\x{ff3f}]*\b'; + + /** + * Full list of JavaScript reserved words. + * Will be loaded from /data/js/keywords_reserved.txt. + * + * @see https://mathiasbynens.be/notes/reserved-keywords + * + * @var string[] + */ + protected $keywordsReserved = array(); + + /** + * List of JavaScript reserved words that accept a + * after them. Some end of lines are not the end of a statement, like with + * these keywords. + * + * E.g.: we shouldn't insert a ; after this else + * else + * console.log('this is quite fine') + * + * Will be loaded from /data/js/keywords_before.txt + * + * @var string[] + */ + protected $keywordsBefore = array(); + + /** + * List of JavaScript reserved words that accept a + * before them. Some end of lines are not the end of a statement, like when + * continued by one of these keywords on the newline. + * + * E.g.: we shouldn't insert a ; before this instanceof + * variable + * instanceof String + * + * Will be loaded from /data/js/keywords_after.txt + * + * @var string[] + */ + protected $keywordsAfter = array(); + + /** + * List of all JavaScript operators. + * + * Will be loaded from /data/js/operators.txt + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators + * + * @var string[] + */ + protected $operators = array(); + + /** + * List of JavaScript operators that accept a after + * them. Some end of lines are not the end of a statement, like with these + * operators. + * + * Note: Most operators are fine, we've only removed !, ++ and --. + * There can't be a newline separating ! and whatever it is negating. + * ++ & -- have to be joined with the value they're in-/decrementing. + * + * Will be loaded from /data/js/operators_before.txt + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators + * + * @var string[] + */ + protected $operatorsBefore = array(); + + /** + * List of JavaScript operators that accept a before + * them. Some end of lines are not the end of a statement, like when + * continued by one of these operators on the newline. + * + * Note: Most operators are fine, we've only removed ), ], ++ and --. + * ++ & -- have to be joined with the value they're in-/decrementing. + * ) & ] are "special" in that they have lots or usecases. () for example + * is used for function calls, for grouping, in if () and for (), ... + * + * Will be loaded from /data/js/operators_after.txt + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators + * + * @var string[] + */ + protected $operatorsAfter = array(); + + /** + * {@inheritdoc} + */ + public function __construct() + { + call_user_func_array(array('parent', '__construct'), func_get_args()); + + $dataDir = __DIR__.'/../data/js/'; + $options = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES; + $this->keywordsReserved = file($dataDir.'keywords_reserved.txt', $options); + $this->keywordsBefore = file($dataDir.'keywords_before.txt', $options); + $this->keywordsAfter = file($dataDir.'keywords_after.txt', $options); + $this->operators = file($dataDir.'operators.txt', $options); + $this->operatorsBefore = file($dataDir.'operators_before.txt', $options); + $this->operatorsAfter = file($dataDir.'operators_after.txt', $options); + } + + /** + * Minify the data. + * Perform JS optimizations. + * + * @param string[optional] $path Path to write the data to + * + * @return string The minified data + */ + public function execute($path = null) + { + $content = ''; + + // loop files + foreach ($this->data as $source => $js) { + /* + * Combine js: separating the scripts by a ; + * I'm also adding a newline: it will be eaten when whitespace is + * stripped, but we need to make sure we're not just appending + * a new script right after a previous script that ended with a + * singe-line comment on the last line (in which case it would also + * be seen as part of that comment) + */ + $content .= $js."\n;"; + } + + /* + * Let's first take out strings, comments and regular expressions. + * All of these can contain JS code-like characters, and we should make + * sure any further magic ignores anything inside of these. + * + * Consider this example, where we should not strip any whitespace: + * var str = "a test"; + * + * Comments will be removed altogether, strings and regular expressions + * will be replaced by placeholder text, which we'll restore later. + */ + $this->extractStrings('\'"`'); + $this->stripComments(); + $this->extractRegex(); + $content = $this->replace($content); + + $content = $this->propertyNotation($content); + $content = $this->shortenBools($content); + $content = $this->stripWhitespace($content); + + /* + * Earlier, we extracted strings & regular expressions and replaced them + * with placeholder text. This will restore them. + */ + $content = $this->restoreExtractedData($content); + + return $content; + } + + /** + * Strip comments from source code. + */ + protected function stripComments() + { + // single-line comments + $this->registerPattern('/\/\/.*$/m', ''); + + // multi-line comments + $this->registerPattern('/\/\*.*?\*\//s', ''); + } + + /** + * JS can have /-delimited regular expressions, like: /ab+c/.match(string). + * + * The content inside the regex can contain characters that may be confused + * for JS code: e.g. it could contain whitespace it needs to match & we + * don't want to strip whitespace in there. + * + * The regex can be pretty simple: we don't have to care about comments, + * (which also use slashes) because stripComments() will have stripped those + * already. + * + * This method will replace all string content with simple REGEX# + * placeholder text, so we've rid all regular expressions from characters + * that may be misinterpreted. Original regex content will be saved in + * $this->extracted and after doing all other minifying, we can restore the + * original content via restoreRegex() + */ + protected function extractRegex() + { + // PHP only supports $this inside anonymous functions since 5.4 + $minifier = $this; + $callback = function ($match) use ($minifier) { + $count = count($minifier->extracted); + $placeholder = '/'.$count.'/'; + $minifier->extracted[$placeholder] = $match[0]; + + return $placeholder; + }; + + $pattern = '\/.+?(?registerPattern('/'.$pattern.'(?=\s*('.$after.'|'.$methods.'))/', $callback); + + // 1 more edge case: a regex can be followed by a lot more operators or + // keywords if there's a newline (ASI) in between, where the operator + // actually starts a new statement + // (https://github.com/matthiasmullie/minify/issues/56) + $operators = $this->getOperatorsForRegex($this->operatorsBefore, '/'); + $operators += $this->getOperatorsForRegex($this->keywordsReserved, '/'); + $this->registerPattern('/'.$pattern.'\s*\n(?=\s*('.implode('|', $operators).'))/', $callback); + } + + /** + * Strip whitespace. + * + * We won't strip *all* whitespace, but as much as possible. The thing that + * we'll preserve are newlines we're unsure about. + * JavaScript doesn't require statements to be terminated with a semicolon. + * It will automatically fix missing semicolons with ASI (automatic semi- + * colon insertion) at the end of line causing errors (without semicolon.) + * + * Because it's sometimes hard to tell if a newline is part of a statement + * that should be terminated or not, we'll just leave some of them alone. + * + * @param string $content The content to strip the whitespace for + * + * @return string + */ + protected function stripWhitespace($content) + { + // uniform line endings, make them all line feed + $content = str_replace(array("\r\n", "\r"), "\n", $content); + + // collapse all non-line feed whitespace into a single space + $content = preg_replace('/[^\S\n]+/', ' ', $content); + + // strip leading & trailing whitespace + $content = str_replace(array(" \n", "\n "), "\n", $content); + + // collapse consecutive line feeds into just 1 + $content = preg_replace('/\n+/', "\n", $content); + + $operatorsBefore = $this->getOperatorsForRegex($this->operatorsBefore, '/'); + $operatorsAfter = $this->getOperatorsForRegex($this->operatorsAfter, '/'); + $operators = $this->getOperatorsForRegex($this->operators, '/'); + $keywordsBefore = $this->getKeywordsForRegex($this->keywordsBefore, '/'); + $keywordsAfter = $this->getKeywordsForRegex($this->keywordsAfter, '/'); + + // strip whitespace that ends in (or next line begin with) an operator + // that allows statements to be broken up over multiple lines + unset($operatorsBefore['+'], $operatorsBefore['-'], $operatorsAfter['+'], $operatorsAfter['-']); + $content = preg_replace( + array( + '/('.implode('|', $operatorsBefore).')\s+/', + '/\s+('.implode('|', $operatorsAfter).')/', + ), '\\1', $content + ); + + // make sure + and - can't be mistaken for, or joined into ++ and -- + $content = preg_replace( + array( + '/(?%&|'); + $operators['='] = '(?keywordsReserved; + $callback = function ($match) use ($minifier, $keywords) { + $property = trim($minifier->extracted[$match[1]], '\'"'); + + /* + * Check if the property is a reserved keyword. In this context (as + * property of an object literal/array) it shouldn't matter, but IE8 + * freaks out with "Expected identifier". + */ + if (in_array($property, $keywords)) { + return $match[0]; + } + + /* + * See if the property is in a variable-like format (e.g. + * array['key-here'] can't be replaced by array.key-here since '-' + * is not a valid character there. + */ + if (!preg_match('/^'.$minifier::REGEX_VARIABLE.'$/u', $property)) { + return $match[0]; + } + + return '.'.$property; + }; + + /* + * Figure out if previous character is a variable name (of the array + * we want to use property notation on) - this is to make sure + * standalone ['value'] arrays aren't confused for keys-of-an-array. + * We can (and only have to) check the last character, because PHP's + * regex implementation doesn't allow unfixed-length look-behind + * assertions. + */ + preg_match('/(\[[^\]]+\])[^\]]*$/', static::REGEX_VARIABLE, $previousChar); + $previousChar = $previousChar[1]; + + /* + * Make sure word preceding the ['value'] is not a keyword, e.g. + * return['x']. Because -again- PHP's regex implementation doesn't allow + * unfixed-length look-behind assertions, I'm just going to do a lot of + * separate look-behind assertions, one for each keyword. + */ + $keywords = $this->getKeywordsForRegex($keywords); + $keywords = '(? + * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved + * @license MIT License + */ +abstract class Minify +{ + /** + * The data to be minified. + * + * @var string[] + */ + protected $data = array(); + + /** + * Array of patterns to match. + * + * @var string[] + */ + protected $patterns = array(); + + /** + * This array will hold content of strings and regular expressions that have + * been extracted from the JS source code, so we can reliably match "code", + * without having to worry about potential "code-like" characters inside. + * + * @var string[] + */ + public $extracted = array(); + + /** + * Init the minify class - optionally, code may be passed along already. + */ + public function __construct(/* $data = null, ... */) + { + // it's possible to add the source through the constructor as well ;) + if (func_num_args()) { + call_user_func_array(array($this, 'add'), func_get_args()); + } + } + + /** + * Add a file or straight-up code to be minified. + * + * @param string|string[] $data + */ + public function add($data /* $data = null, ... */) + { + // bogus "usage" of parameter $data: scrutinizer warns this variable is + // not used (we're using func_get_args instead to support overloading), + // but it still needs to be defined because it makes no sense to have + // this function without argument :) + $args = array($data) + func_get_args(); + + // this method can be overloaded + foreach ($args as $data) { + if (is_array($data)) { + call_user_func_array(array($this, 'add'), $data); + continue; + } + + // redefine var + $data = (string) $data; + + // load data + $value = $this->load($data); + $key = ($data != $value) ? $data : count($this->data); + + // replace CR linefeeds etc. + // @see https://github.com/matthiasmullie/minify/pull/139 + $value = str_replace(array("\r\n", "\r"), "\n", $value); + + // store data + $this->data[$key] = $value; + } + } + + /** + * Minify the data & (optionally) saves it to a file. + * + * @param string[optional] $path Path to write the data to + * + * @return string The minified data + */ + public function minify($path = null) + { + $content = $this->execute($path); + + // These are not the droids you're looking for + $content = "/* Any changes to this file will be overwritten. To change the content\nof this file, edit the source files from which it was compiled. */\n" . $content; + + // save to path + if ($path !== null) { + $this->save($content, $path); + } + + return $content; + } + + /** + * Minify & gzip the data & (optionally) saves it to a file. + * + * @param string[optional] $path Path to write the data to + * @param int[optional] $level Compression level, from 0 to 9 + * + * @return string The minified & gzipped data + */ + public function gzip($path = null, $level = 9) + { + $content = $this->execute($path); + $content = gzencode($content, $level, FORCE_GZIP); + + // save to path + if ($path !== null) { + $this->save($content, $path); + } + + return $content; + } + + /** + * Minify the data & write it to a CacheItemInterface object. + * + * @param CacheItemInterface $item Cache item to write the data to + * + * @return CacheItemInterface Cache item with the minifier data + */ + public function cache(CacheItemInterface $item) + { + $content = $this->execute(); + $item->set($content); + + return $item; + } + + /** + * Minify the data. + * + * @param string[optional] $path Path to write the data to + * + * @return string The minified data + */ + abstract public function execute($path = null); + + /** + * Load data. + * + * @param string $data Either a path to a file or the content itself + * + * @return string + */ + protected function load($data) + { + // check if the data is a file + if ($this->canImportFile($data)) { + $data = file_get_contents($data); + + // strip BOM, if any + if (substr($data, 0, 3) == "\xef\xbb\xbf") { + $data = substr($data, 3); + } + } + + return $data; + } + + /** + * Save to file. + * + * @param string $content The minified data + * @param string $path The path to save the minified data to + * + * @throws IOException + */ + protected function save($content, $path) + { + $handler = $this->openFileForWriting($path); + + $this->writeToFile($handler, $content); + + @fclose($handler); + } + + /** + * Register a pattern to execute against the source content. + * + * @param string $pattern PCRE pattern + * @param string|callable $replacement Replacement value for matched pattern + */ + protected function registerPattern($pattern, $replacement = '') + { + // study the pattern, we'll execute it more than once + $pattern .= 'S'; + + $this->patterns[] = array($pattern, $replacement); + } + + /** + * We can't "just" run some regular expressions against JavaScript: it's a + * complex language. E.g. having an occurrence of // xyz would be a comment, + * unless it's used within a string. Of you could have something that looks + * like a 'string', but inside a comment. + * The only way to accurately replace these pieces is to traverse the JS one + * character at a time and try to find whatever starts first. + * + * @param string $content The content to replace patterns in + * + * @return string The (manipulated) content + */ + protected function replace($content) + { + $processed = ''; + $positions = array_fill(0, count($this->patterns), -1); + $matches = array(); + + while ($content) { + // find first match for all patterns + foreach ($this->patterns as $i => $pattern) { + list($pattern, $replacement) = $pattern; + + // no need to re-run matches that are still in the part of the + // content that hasn't been processed + if ($positions[$i] >= 0) { + continue; + } + + $match = null; + if (preg_match($pattern, $content, $match)) { + $matches[$i] = $match; + + // we'll store the match position as well; that way, we + // don't have to redo all preg_matches after changing only + // the first (we'll still know where those others are) + $positions[$i] = strpos($content, $match[0]); + } else { + // if the pattern couldn't be matched, there's no point in + // executing it again in later runs on this same content; + // ignore this one until we reach end of content + unset($matches[$i]); + $positions[$i] = strlen($content); + } + } + + // no more matches to find: everything's been processed, break out + if (!$matches) { + $processed .= $content; + break; + } + + // see which of the patterns actually found the first thing (we'll + // only want to execute that one, since we're unsure if what the + // other found was not inside what the first found) + $discardLength = min($positions); + $firstPattern = array_search($discardLength, $positions); + $match = $matches[$firstPattern][0]; + + // execute the pattern that matches earliest in the content string + list($pattern, $replacement) = $this->patterns[$firstPattern]; + $replacement = $this->replacePattern($pattern, $replacement, $content); + + // figure out which part of the string was unmatched; that's the + // part we'll execute the patterns on again next + $content = substr($content, $discardLength); + $unmatched = (string) substr($content, strpos($content, $match) + strlen($match)); + + // move the replaced part to $processed and prepare $content to + // again match batch of patterns against + $processed .= substr($replacement, 0, strlen($replacement) - strlen($unmatched)); + $content = $unmatched; + + // first match has been replaced & that content is to be left alone, + // the next matches will start after this replacement, so we should + // fix their offsets + foreach ($positions as $i => $position) { + $positions[$i] -= $discardLength + strlen($match); + } + } + + return $processed; + } + + /** + * This is where a pattern is matched against $content and the matches + * are replaced by their respective value. + * This function will be called plenty of times, where $content will always + * move up 1 character. + * + * @param string $pattern Pattern to match + * @param string|callable $replacement Replacement value + * @param string $content Content to match pattern against + * + * @return string + */ + protected function replacePattern($pattern, $replacement, $content) + { + if (is_callable($replacement)) { + return preg_replace_callback($pattern, $replacement, $content, 1, $count); + } else { + return preg_replace($pattern, $replacement, $content, 1, $count); + } + } + + /** + * Strings are a pattern we need to match, in order to ignore potential + * code-like content inside them, but we just want all of the string + * content to remain untouched. + * + * This method will replace all string content with simple STRING# + * placeholder text, so we've rid all strings from characters that may be + * misinterpreted. Original string content will be saved in $this->extracted + * and after doing all other minifying, we can restore the original content + * via restoreStrings(). + * + * @param string[optional] $chars + */ + protected function extractStrings($chars = '\'"') + { + // PHP only supports $this inside anonymous functions since 5.4 + $minifier = $this; + $callback = function ($match) use ($minifier) { + // check the second index here, because the first always contains a quote + if ($match[2] === '') { + /* + * Empty strings need no placeholder; they can't be confused for + * anything else anyway. + * But we still needed to match them, for the extraction routine + * to skip over this particular string. + */ + return $match[0]; + } + + $count = count($minifier->extracted); + $placeholder = $match[1].$count.$match[1]; + $minifier->extracted[$placeholder] = $match[1].$match[2].$match[1]; + + return $placeholder; + }; + + /* + * The \\ messiness explained: + * * Don't count ' or " as end-of-string if it's escaped (has backslash + * in front of it) + * * Unless... that backslash itself is escaped (another leading slash), + * in which case it's no longer escaping the ' or " + * * So there can be either no backslash, or an even number + * * multiply all of that times 4, to account for the escaping that has + * to be done to pass the backslash into the PHP string without it being + * considered as escape-char (times 2) and to get it in the regex, + * escaped (times 2) + */ + $this->registerPattern('/(['.$chars.'])(.*?(?extracted. + * + * @param string $content + * + * @return string + */ + protected function restoreExtractedData($content) + { + if (!$this->extracted) { + // nothing was extracted, nothing to restore + return $content; + } + + $content = strtr($content, $this->extracted); + + $this->extracted = array(); + + return $content; + } + + /** + * Check if the path is a regular file and can be read. + * + * @param string $path + * + * @return bool + */ + protected function canImportFile($path) + { + return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path); + } + + /** + * Attempts to open file specified by $path for writing. + * + * @param string $path The path to the file + * + * @return resource Specifier for the target file + * + * @throws IOException + */ + protected function openFileForWriting($path) + { + if (($handler = @fopen($path, 'w')) === false) { + throw new IOException('The file "'.$path.'" could not be opened for writing. Check if PHP has enough permissions.'); + } + + return $handler; + } + + /** + * Attempts to write $content to the file specified by $handler. $path is used for printing exceptions. + * + * @param resource $handler The resource to write to + * @param string $content The content to write + * @param string $path The path to the file (for exception printing only) + * + * @throws IOException + */ + protected function writeToFile($handler, $content, $path = '') + { + if (($result = @fwrite($handler, $content)) === false || ($result < strlen($content))) { + throw new IOException('The file "'.$path.'" could not be written to. Check your disk space and file permissions.'); + } + } +} diff --git a/Sources/minify/src/index.php b/Sources/minify/src/index.php new file mode 100644 index 0000000..69278ce --- /dev/null +++ b/Sources/minify/src/index.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/Sources/random_compat/byte_safe_strings.php b/Sources/random_compat/byte_safe_strings.php new file mode 100644 index 0000000..ef24488 --- /dev/null +++ b/Sources/random_compat/byte_safe_strings.php @@ -0,0 +1,195 @@ + RandomCompat_strlen($binary_string)) { + return ''; + } + + return (string) mb_substr( + (string) $binary_string, + (int) $start, + (int) $length, + '8bit' + ); + } + + } else { + + /** + * substr() implementation that isn't brittle to mbstring.func_overload + * + * This version just uses the default substr() + * + * @param string $binary_string + * @param int $start + * @param int|null $length (optional) + * + * @throws TypeError + * + * @return string + */ + function RandomCompat_substr($binary_string, $start, $length = null) + { + if (!is_string($binary_string)) { + throw new TypeError( + 'RandomCompat_substr(): First argument should be a string' + ); + } + + if (!is_int($start)) { + throw new TypeError( + 'RandomCompat_substr(): Second argument should be an integer' + ); + } + + if ($length !== null) { + if (!is_int($length)) { + throw new TypeError( + 'RandomCompat_substr(): Third argument should be an integer, or omitted' + ); + } + + return (string) substr( + (string )$binary_string, + (int) $start, + (int) $length + ); + } + + return (string) substr( + (string) $binary_string, + (int) $start + ); + } + } +} diff --git a/Sources/random_compat/cast_to_int.php b/Sources/random_compat/cast_to_int.php new file mode 100644 index 0000000..1b1bbfe --- /dev/null +++ b/Sources/random_compat/cast_to_int.php @@ -0,0 +1,77 @@ + operators might accidentally let a float + * through. + * + * @param int|float $number The number we want to convert to an int + * @param bool $fail_open Set to true to not throw an exception + * + * @return float|int + * @psalm-suppress InvalidReturnType + * + * @throws TypeError + */ + function RandomCompat_intval($number, $fail_open = false) + { + if (is_int($number) || is_float($number)) { + $number += 0; + } elseif (is_numeric($number)) { + /** @psalm-suppress InvalidOperand */ + $number += 0; + } + /** @var int|float $number */ + + if ( + is_float($number) + && + $number > ~PHP_INT_MAX + && + $number < PHP_INT_MAX + ) { + $number = (int) $number; + } + + if (is_int($number)) { + return (int) $number; + } elseif (!$fail_open) { + throw new TypeError( + 'Expected an integer.' + ); + } + return $number; + } +} diff --git a/Sources/random_compat/error_polyfill.php b/Sources/random_compat/error_polyfill.php new file mode 100644 index 0000000..c02c5c8 --- /dev/null +++ b/Sources/random_compat/error_polyfill.php @@ -0,0 +1,49 @@ + \ No newline at end of file diff --git a/Sources/random_compat/random.php b/Sources/random_compat/random.php new file mode 100644 index 0000000..36245f5 --- /dev/null +++ b/Sources/random_compat/random.php @@ -0,0 +1,225 @@ += 70000) { + return; +} + +if (!defined('RANDOM_COMPAT_READ_BUFFER')) { + define('RANDOM_COMPAT_READ_BUFFER', 8); +} + +$RandomCompatDIR = dirname(__FILE__); + +require_once $RandomCompatDIR . DIRECTORY_SEPARATOR . 'byte_safe_strings.php'; +require_once $RandomCompatDIR . DIRECTORY_SEPARATOR . 'cast_to_int.php'; +require_once $RandomCompatDIR . DIRECTORY_SEPARATOR . 'error_polyfill.php'; + +if (!is_callable('random_bytes')) { + /** + * PHP 5.2.0 - 5.6.x way to implement random_bytes() + * + * We use conditional statements here to define the function in accordance + * to the operating environment. It's a micro-optimization. + * + * In order of preference: + * 1. Use libsodium if available. + * 2. fread() /dev/urandom if available (never on Windows) + * 3. mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM) + * 4. COM('CAPICOM.Utilities.1')->GetRandom() + * + * See RATIONALE.md for our reasoning behind this particular order + */ + if (extension_loaded('libsodium')) { + // See random_bytes_libsodium.php + if (PHP_VERSION_ID >= 50300 && is_callable('\\Sodium\\randombytes_buf')) { + require_once $RandomCompatDIR . DIRECTORY_SEPARATOR . 'random_bytes_libsodium.php'; + } elseif (method_exists('Sodium', 'randombytes_buf')) { + require_once $RandomCompatDIR . DIRECTORY_SEPARATOR . 'random_bytes_libsodium_legacy.php'; + } + } + + /** + * Reading directly from /dev/urandom: + */ + if (DIRECTORY_SEPARATOR === '/') { + // DIRECTORY_SEPARATOR === '/' on Unix-like OSes -- this is a fast + // way to exclude Windows. + $RandomCompatUrandom = true; + $RandomCompat_basedir = ini_get('open_basedir'); + + if (!empty($RandomCompat_basedir)) { + $RandomCompat_open_basedir = explode( + PATH_SEPARATOR, + strtolower($RandomCompat_basedir) + ); + $RandomCompatUrandom = (array() !== array_intersect( + array('/dev', '/dev/', '/dev/urandom'), + $RandomCompat_open_basedir + )); + $RandomCompat_open_basedir = null; + } + + if ( + !is_callable('random_bytes') + && + $RandomCompatUrandom + && + @is_readable('/dev/urandom') + ) { + // Error suppression on is_readable() in case of an open_basedir + // or safe_mode failure. All we care about is whether or not we + // can read it at this point. If the PHP environment is going to + // panic over trying to see if the file can be read in the first + // place, that is not helpful to us here. + + // See random_bytes_dev_urandom.php + require_once $RandomCompatDIR . DIRECTORY_SEPARATOR . 'random_bytes_dev_urandom.php'; + } + // Unset variables after use + $RandomCompat_basedir = null; + } else { + $RandomCompatUrandom = false; + } + + /** + * mcrypt_create_iv() + * + * We only want to use mcypt_create_iv() if: + * + * - random_bytes() hasn't already been defined + * - the mcrypt extensions is loaded + * - One of these two conditions is true: + * - We're on Windows (DIRECTORY_SEPARATOR !== '/') + * - We're not on Windows and /dev/urandom is readabale + * (i.e. we're not in a chroot jail) + * - Special case: + * - If we're not on Windows, but the PHP version is between + * 5.6.10 and 5.6.12, we don't want to use mcrypt. It will + * hang indefinitely. This is bad. + * - If we're on Windows, we want to use PHP >= 5.3.7 or else + * we get insufficient entropy errors. + */ + if ( + !is_callable('random_bytes') + && + // Windows on PHP < 5.3.7 is broken, but non-Windows is not known to be. + (DIRECTORY_SEPARATOR === '/' || PHP_VERSION_ID >= 50307) + && + // Prevent this code from hanging indefinitely on non-Windows; + // see https://bugs.php.net/bug.php?id=69833 + ( + DIRECTORY_SEPARATOR !== '/' || + (PHP_VERSION_ID <= 50609 || PHP_VERSION_ID >= 50613) + ) + && + extension_loaded('mcrypt') + ) { + // See random_bytes_mcrypt.php + require_once $RandomCompatDIR . DIRECTORY_SEPARATOR . 'random_bytes_mcrypt.php'; + } + $RandomCompatUrandom = null; + + /** + * This is a Windows-specific fallback, for when the mcrypt extension + * isn't loaded. + */ + if ( + !is_callable('random_bytes') + && + extension_loaded('com_dotnet') + && + class_exists('COM') + ) { + $RandomCompat_disabled_classes = preg_split( + '#\s*,\s*#', + strtolower(ini_get('disable_classes')) + ); + + if (!in_array('com', $RandomCompat_disabled_classes)) { + try { + $RandomCompatCOMtest = new COM('CAPICOM.Utilities.1'); + if (method_exists($RandomCompatCOMtest, 'GetRandom')) { + // See random_bytes_com_dotnet.php + require_once $RandomCompatDIR . DIRECTORY_SEPARATOR . 'random_bytes_com_dotnet.php'; + } + } catch (com_exception $e) { + // Don't try to use it. + } + } + $RandomCompat_disabled_classes = null; + $RandomCompatCOMtest = null; + } + + /** + * throw new Exception + */ + if (!is_callable('random_bytes')) { + /** + * We don't have any more options, so let's throw an exception right now + * and hope the developer won't let it fail silently. + * + * @param mixed $length + * @psalm-suppress InvalidReturnType + * @throws Exception + * @return string + */ + function random_bytes($length) + { + unset($length); // Suppress "variable not used" warnings. + throw new Exception( + 'There is no suitable CSPRNG installed on your system' + ); + return ''; + } + } +} + +if (!is_callable('random_int')) { + require_once $RandomCompatDIR . DIRECTORY_SEPARATOR . 'random_int.php'; +} + +$RandomCompatDIR = null; diff --git a/Sources/random_compat/random_bytes_com_dotnet.php b/Sources/random_compat/random_bytes_com_dotnet.php new file mode 100644 index 0000000..537d02b --- /dev/null +++ b/Sources/random_compat/random_bytes_com_dotnet.php @@ -0,0 +1,91 @@ +GetRandom($bytes, 0)); + if (RandomCompat_strlen($buf) >= $bytes) { + /** + * Return our random entropy buffer here: + */ + return (string) RandomCompat_substr($buf, 0, $bytes); + } + ++$execCount; + } while ($execCount < $bytes); + + /** + * If we reach here, PHP has failed us. + */ + throw new Exception( + 'Could not gather sufficient random data' + ); + } +} diff --git a/Sources/random_compat/random_bytes_dev_urandom.php b/Sources/random_compat/random_bytes_dev_urandom.php new file mode 100644 index 0000000..c4e31cc --- /dev/null +++ b/Sources/random_compat/random_bytes_dev_urandom.php @@ -0,0 +1,190 @@ + $st */ + $st = fstat($fp); + if (($st['mode'] & 0170000) !== 020000) { + fclose($fp); + $fp = false; + } + } + } + + if (is_resource($fp)) { + /** + * stream_set_read_buffer() does not exist in HHVM + * + * If we don't set the stream's read buffer to 0, PHP will + * internally buffer 8192 bytes, which can waste entropy + * + * stream_set_read_buffer returns 0 on success + */ + if (is_callable('stream_set_read_buffer')) { + stream_set_read_buffer($fp, RANDOM_COMPAT_READ_BUFFER); + } + if (is_callable('stream_set_chunk_size')) { + stream_set_chunk_size($fp, RANDOM_COMPAT_READ_BUFFER); + } + } + } + + try { + /** @var int $bytes */ + $bytes = RandomCompat_intval($bytes); + } catch (TypeError $ex) { + throw new TypeError( + 'random_bytes(): $bytes must be an integer' + ); + } + + if ($bytes < 1) { + throw new Error( + 'Length must be greater than 0' + ); + } + + /** + * This if() block only runs if we managed to open a file handle + * + * It does not belong in an else {} block, because the above + * if (empty($fp)) line is logic that should only be run once per + * page load. + */ + if (is_resource($fp)) { + /** + * @var int + */ + $remaining = $bytes; + + /** + * @var string|bool + */ + $buf = ''; + + /** + * We use fread() in a loop to protect against partial reads + */ + do { + /** + * @var string|bool + */ + $read = fread($fp, $remaining); + if (!is_string($read)) { + /** + * We cannot safely read from the file. Exit the + * do-while loop and trigger the exception condition + * + * @var string|bool + */ + $buf = false; + break; + } + /** + * Decrease the number of bytes returned from remaining + */ + $remaining -= RandomCompat_strlen($read); + /** + * @var string $buf + */ + $buf .= $read; + } while ($remaining > 0); + + /** + * Is our result valid? + * @var string|bool $buf + */ + if (is_string($buf)) { + if (RandomCompat_strlen($buf) === $bytes) { + /** + * Return our random entropy buffer here: + */ + return $buf; + } + } + } + + /** + * If we reach here, PHP has failed us. + */ + throw new Exception( + 'Error reading from source device' + ); + } +} diff --git a/Sources/random_compat/random_bytes_libsodium.php b/Sources/random_compat/random_bytes_libsodium.php new file mode 100644 index 0000000..2e56290 --- /dev/null +++ b/Sources/random_compat/random_bytes_libsodium.php @@ -0,0 +1,91 @@ + 2147483647) { + $buf = ''; + for ($i = 0; $i < $bytes; $i += 1073741824) { + $n = ($bytes - $i) > 1073741824 + ? 1073741824 + : $bytes - $i; + $buf .= \Sodium\randombytes_buf($n); + } + } else { + /** @var string|bool $buf */ + $buf = \Sodium\randombytes_buf($bytes); + } + + if (is_string($buf)) { + if (RandomCompat_strlen($buf) === $bytes) { + return $buf; + } + } + + /** + * If we reach here, PHP has failed us. + */ + throw new Exception( + 'Could not gather sufficient random data' + ); + } +} diff --git a/Sources/random_compat/random_bytes_libsodium_legacy.php b/Sources/random_compat/random_bytes_libsodium_legacy.php new file mode 100644 index 0000000..f78b219 --- /dev/null +++ b/Sources/random_compat/random_bytes_libsodium_legacy.php @@ -0,0 +1,93 @@ + 2147483647) { + for ($i = 0; $i < $bytes; $i += 1073741824) { + $n = ($bytes - $i) > 1073741824 + ? 1073741824 + : $bytes - $i; + $buf .= Sodium::randombytes_buf((int) $n); + } + } else { + $buf .= Sodium::randombytes_buf((int) $bytes); + } + + if (is_string($buf)) { + if (RandomCompat_strlen($buf) === $bytes) { + return $buf; + } + } + + /** + * If we reach here, PHP has failed us. + */ + throw new Exception( + 'Could not gather sufficient random data' + ); + } +} diff --git a/Sources/random_compat/random_bytes_mcrypt.php b/Sources/random_compat/random_bytes_mcrypt.php new file mode 100644 index 0000000..0b13fa7 --- /dev/null +++ b/Sources/random_compat/random_bytes_mcrypt.php @@ -0,0 +1,79 @@ + operators might accidentally let a float + * through. + */ + + try { + /** @var int $min */ + $min = RandomCompat_intval($min); + } catch (TypeError $ex) { + throw new TypeError( + 'random_int(): $min must be an integer' + ); + } + + try { + /** @var int $max */ + $max = RandomCompat_intval($max); + } catch (TypeError $ex) { + throw new TypeError( + 'random_int(): $max must be an integer' + ); + } + + /** + * Now that we've verified our weak typing system has given us an integer, + * let's validate the logic then we can move forward with generating random + * integers along a given range. + */ + if ($min > $max) { + throw new Error( + 'Minimum value must be less than or equal to the maximum value' + ); + } + + if ($max === $min) { + return (int) $min; + } + + /** + * Initialize variables to 0 + * + * We want to store: + * $bytes => the number of random bytes we need + * $mask => an integer bitmask (for use with the &) operator + * so we can minimize the number of discards + */ + $attempts = $bits = $bytes = $mask = $valueShift = 0; + /** @var int $attempts */ + /** @var int $bits */ + /** @var int $bytes */ + /** @var int $mask */ + /** @var int $valueShift */ + + /** + * At this point, $range is a positive number greater than 0. It might + * overflow, however, if $max - $min > PHP_INT_MAX. PHP will cast it to + * a float and we will lose some precision. + * + * @var int|float $range + */ + $range = $max - $min; + + /** + * Test for integer overflow: + */ + if (!is_int($range)) { + + /** + * Still safely calculate wider ranges. + * Provided by @CodesInChaos, @oittaa + * + * @ref https://gist.github.com/CodesInChaos/03f9ea0b58e8b2b8d435 + * + * We use ~0 as a mask in this case because it generates all 1s + * + * @ref https://eval.in/400356 (32-bit) + * @ref http://3v4l.org/XX9r5 (64-bit) + */ + $bytes = PHP_INT_SIZE; + /** @var int $mask */ + $mask = ~0; + + } else { + + /** + * $bits is effectively ceil(log($range, 2)) without dealing with + * type juggling + */ + while ($range > 0) { + if ($bits % 8 === 0) { + ++$bytes; + } + ++$bits; + $range >>= 1; + /** @var int $mask */ + $mask = $mask << 1 | 1; + } + $valueShift = $min; + } + + /** @var int $val */ + $val = 0; + /** + * Now that we have our parameters set up, let's begin generating + * random integers until one falls between $min and $max + */ + /** @psalm-suppress RedundantCondition */ + do { + /** + * The rejection probability is at most 0.5, so this corresponds + * to a failure probability of 2^-128 for a working RNG + */ + if ($attempts > 128) { + throw new Exception( + 'random_int: RNG is broken - too many rejections' + ); + } + + /** + * Let's grab the necessary number of random bytes + */ + $randomByteString = random_bytes($bytes); + + /** + * Let's turn $randomByteString into an integer + * + * This uses bitwise operators (<< and |) to build an integer + * out of the values extracted from ord() + * + * Example: [9F] | [6D] | [32] | [0C] => + * 159 + 27904 + 3276800 + 201326592 => + * 204631455 + */ + $val &= 0; + for ($i = 0; $i < $bytes; ++$i) { + $val |= ord($randomByteString[$i]) << ($i * 8); + } + /** @var int $val */ + + /** + * Apply mask + */ + $val &= $mask; + $val += $valueShift; + + ++$attempts; + /** + * If $val overflows to a floating point number, + * ... or is larger than $max, + * ... or smaller than $min, + * then try again. + */ + } while (!is_int($val) || $val > $max || $val < $min); + + return (int) $val; + } +} diff --git a/Sources/tasks/ApprovePost-Notify.php b/Sources/tasks/ApprovePost-Notify.php new file mode 100644 index 0000000..4ca094d --- /dev/null +++ b/Sources/tasks/ApprovePost-Notify.php @@ -0,0 +1,126 @@ +_details['msgOptions']; + $topicOptions = $this->_details['topicOptions']; + $posterOptions = $this->_details['posterOptions']; + $type = $this->_details['type']; + + $members = array(); + $alert_rows = array(); + + // We need to know who can approve this post. + require_once($sourcedir . '/Subs-Members.php'); + $modMembers = membersAllowedTo('approve_posts', $topicOptions['board']); + + $request = $smcFunc['db_query']('', ' + SELECT id_member, email_address, lngfile + FROM {db_prefix}members + WHERE id_member IN ({array_int:members})', + array( + 'members' => $modMembers, + ) + ); + + $watched = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $members[] = $row['id_member']; + $watched[$row['id_member']] = $row; + } + $smcFunc['db_free_result']($request); + + if (empty($members)) + return true; + + require_once($sourcedir . '/Subs-Notify.php'); + $members = array_unique($members); + $prefs = getNotifyPrefs($members, 'unapproved_post', true); + foreach ($watched as $member => $data) + { + $pref = !empty($prefs[$member]['unapproved_post']) ? $prefs[$member]['unapproved_post'] : 0; + + if ($pref & self::RECEIVE_NOTIFY_EMAIL) + { + // Emails are a bit complicated. We have to do language stuff. + require_once($sourcedir . '/Subs-Post.php'); + require_once($sourcedir . '/ScheduledTasks.php'); + loadEssentialThemeData(); + + $replacements = array( + 'SUBJECT' => $msgOptions['subject'], + 'LINK' => $scripturl . '?topic=' . $topicOptions['id'] . '.new#new', + ); + + $emaildata = loadEmailTemplate('alert_unapproved_post', $replacements, empty($data['lngfile']) || empty($modSettings['userLanguage']) ? $language : $data['lngfile']); + sendmail($data['email_address'], $emaildata['subject'], $emaildata['body'], null, 'm' . $topicOptions['id'], $emaildata['is_html']); + } + + if ($pref & self::RECEIVE_NOTIFY_ALERT) + { + $alert_rows[] = array( + 'alert_time' => time(), + 'id_member' => $member, + 'id_member_started' => $posterOptions['id'], + 'member_name' => $posterOptions['name'], + 'content_type' => $type == 'topic' ? 'topic' : 'msg', + 'content_id' => $type == 'topic' ? $topicOptions['id'] : $msgOptions['id'], + 'content_action' => 'unapproved_' . $type, + 'is_read' => 0, + 'extra' => $smcFunc['json_encode'](array( + 'topic' => $topicOptions['id'], + 'board' => $topicOptions['board'], + 'content_subject' => $msgOptions['subject'], + 'content_link' => $scripturl . '?topic=' . $topicOptions['id'] . '.msg' . $msgOptions['id'] . '#msg' . $msgOptions['id'], + )), + ); + } + } + + // Insert the alerts if any + if (!empty($alert_rows)) + { + $smcFunc['db_insert']('', + '{db_prefix}user_alerts', + array('alert_time' => 'int', 'id_member' => 'int', 'id_member_started' => 'int', 'member_name' => 'string', + 'content_type' => 'string', 'content_id' => 'int', 'content_action' => 'string', 'is_read' => 'int', 'extra' => 'string'), + $alert_rows, + array() + ); + + updateMemberData(array_keys($watched), array('alerts' => '+')); + } + + return true; + } +} + +?> \ No newline at end of file diff --git a/Sources/tasks/ApproveReply-Notify.php b/Sources/tasks/ApproveReply-Notify.php new file mode 100644 index 0000000..baddaa8 --- /dev/null +++ b/Sources/tasks/ApproveReply-Notify.php @@ -0,0 +1,119 @@ +_details['msgOptions']; + $topicOptions = $this->_details['topicOptions']; + $posterOptions = $this->_details['posterOptions']; + + $members = array(); + $alert_rows = array(); + + $request = $smcFunc['db_query']('', ' + SELECT id_member, email_address, lngfile + FROM {db_prefix}topics AS t + INNER JOIN {db_prefix}members AS mem ON (mem.id_member = t.id_member_started) + WHERE id_topic = {int:topic}', + array( + 'topic' => $topicOptions['id'], + ) + ); + + $watched = array(); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $members[] = $row['id_member']; + $watched[$row['id_member']] = $row; + } + $smcFunc['db_free_result']($request); + + require_once($sourcedir . '/Subs-Notify.php'); + $prefs = getNotifyPrefs($members, 'unapproved_reply', true); + foreach ($watched as $member => $data) + { + $pref = !empty($prefs[$member]['unapproved_reply']) ? $prefs[$member]['unapproved_reply'] : 0; + + if ($pref & self::RECEIVE_NOTIFY_EMAIL) + { + // Emails are a bit complicated. We have to do language stuff. + require_once($sourcedir . '/Subs-Post.php'); + require_once($sourcedir . '/ScheduledTasks.php'); + loadEssentialThemeData(); + + $replacements = array( + 'SUBJECT' => $msgOptions['subject'], + 'LINK' => $scripturl . '?topic=' . $topicOptions['id'] . '.new#new', + 'POSTERNAME' => un_htmlspecialchars($posterOptions['name']), + ); + + $emaildata = loadEmailTemplate('alert_unapproved_reply', $replacements, empty($data['lngfile']) || empty($modSettings['userLanguage']) ? $language : $data['lngfile']); + sendmail($data['email_address'], $emaildata['subject'], $emaildata['body'], null, 'm' . $topicOptions['id'], $emaildata['is_html']); + } + + if ($pref & self::RECEIVE_NOTIFY_ALERT) + { + $alert_rows[] = array( + 'alert_time' => time(), + 'id_member' => $member, + 'id_member_started' => $posterOptions['id'], + 'member_name' => $posterOptions['name'], + 'content_type' => 'topic', + 'content_id' => $topicOptions['id'], + 'content_action' => 'unapproved_reply', + 'is_read' => 0, + 'extra' => $smcFunc['json_encode'](array( + 'topic' => $topicOptions['id'], + 'board' => $topicOptions['board'], + 'content_subject' => $msgOptions['subject'], + 'content_link' => $scripturl . '?topic=' . $topicOptions['id'] . '.new;topicseen#new', + )), + ); + } + } + + // Insert the alerts if any + if (!empty($alert_rows)) + { + $smcFunc['db_insert']('', + '{db_prefix}user_alerts', + array('alert_time' => 'int', 'id_member' => 'int', 'id_member_started' => 'int', 'member_name' => 'string', + 'content_type' => 'string', 'content_id' => 'int', 'content_action' => 'string', 'is_read' => 'int', 'extra' => 'string'), + $alert_rows, + array() + ); + + updateMemberData(array_keys($watched), array('alerts' => '+')); + } + + return true; + } +} + +?> \ No newline at end of file diff --git a/Sources/tasks/Birthday-Notify.php b/Sources/tasks/Birthday-Notify.php new file mode 100644 index 0000000..3e740fe --- /dev/null +++ b/Sources/tasks/Birthday-Notify.php @@ -0,0 +1,143 @@ + {int:year} + ' . ($smcFunc['db_title'] === POSTGRE_TITLE ? 'AND indexable_month_day(birthdate) = indexable_month_day({date:bdate})' : ''), + array( + 'year' => 1004, + 'month' => $month, + 'day' => $day, + 'bdate' => '1004-' . $month . '-' . $day, // a random leap year is here needed + ) + ); + + // Group them by languages. + $birthdays = array(); + while ($row = $smcFunc['db_fetch_assoc']($result)) + { + if (!isset($birthdays[$row['lngfile']])) + $birthdays[$row['lngfile']] = array(); + $birthdays[$row['lngfile']][$row['id_member']] = array( + 'name' => $row['real_name'], + 'email' => $row['email_address'] + ); + } + $smcFunc['db_free_result']($result); + + if (!empty($birthdays)) + { + require_once($sourcedir . '/ScheduledTasks.php'); + loadEssentialThemeData(); + // We need this for sendmail and AddMailQueue + require_once($sourcedir . '/Subs-Post.php'); + + // Send out the greetings! + foreach ($birthdays as $lang => $members) + { + // We need to do some shuffling to make this work properly. + loadLanguage('EmailTemplates', $lang); + $txt['happy_birthday_subject'] = $txtBirthdayEmails[$greeting . '_subject']; + $txt['happy_birthday_body'] = $txtBirthdayEmails[$greeting . '_body']; + require_once($sourcedir . '/Subs-Notify.php'); + + $prefs = getNotifyPrefs(array_keys($members), array('birthday'), true); + + foreach ($members as $member_id => $member) + { + $pref = !empty($prefs[$member_id]['birthday']) ? $prefs[$member_id]['birthday'] : 0; + + // Let's load replacements ahead + $replacements = array( + 'REALNAME' => $member['name'], + ); + + if ($pref & self::RECEIVE_NOTIFY_ALERT) + { + $alertdata = loadEmailTemplate('happy_birthday', $replacements, $lang, false); + // For the alerts, we need to replace \n line breaks with
    line breaks. + // For space saving sake, we'll be removing extra line breaks + $alertdata['body'] = preg_replace("~\s*[\r\n]+\s*~", '
    ', $alertdata['body']); + $alert_rows[] = array( + 'alert_time' => time(), + 'id_member' => $member_id, + 'content_type' => 'birthday', + 'content_id' => 0, + 'content_action' => 'msg', + 'is_read' => 0, + 'extra' => $smcFunc['json_encode'](array('happy_birthday' => $alertdata['body'])), + ); + } + + if ($pref & self::RECEIVE_NOTIFY_EMAIL) + { + $emaildata = loadEmailTemplate('happy_birthday', $replacements, $lang, false); + sendmail($member['email'], $emaildata['subject'], $emaildata['body'], null, 'birthday', $emaildata['is_html'], 4); + } + } + } + + // Flush the mail queue, just in case. + AddMailQueue(true); + + // Insert the alerts if any + if (!empty($alert_rows)) + { + $smcFunc['db_insert']('', + '{db_prefix}user_alerts', + array( + 'alert_time' => 'int', 'id_member' => 'int', 'content_type' => 'string', + 'content_id' => 'int', 'content_action' => 'string', 'is_read' => 'int', 'extra' => 'string', + ), + $alert_rows, + array() + ); + + updateMemberData(array_keys($members), array('alerts' => '+')); + } + } + + return true; + } +} + +?> \ No newline at end of file diff --git a/Sources/tasks/Buddy-Notify.php b/Sources/tasks/Buddy-Notify.php new file mode 100644 index 0000000..2144d4d --- /dev/null +++ b/Sources/tasks/Buddy-Notify.php @@ -0,0 +1,63 @@ +_details['receiver_id'], 'buddy_request', true); + + if ($prefs[$this->_details['receiver_id']]['buddy_request']) + { + $alert_row = array( + 'alert_time' => $this->_details['time'], + 'id_member' => $this->_details['receiver_id'], + 'id_member_started' => $this->_details['id_member'], + 'member_name' => $this->_details['member_name'], + 'content_type' => 'member', + 'content_id' => $this->_details['id_member'], + 'content_action' => 'buddy_request', + 'is_read' => 0, + 'extra' => '', + ); + + $smcFunc['db_insert']('insert', '{db_prefix}user_alerts', + array('alert_time' => 'int', 'id_member' => 'int', 'id_member_started' => 'int', 'member_name' => 'string', + 'content_type' => 'string', 'content_id' => 'int', 'content_action' => 'string', 'is_read' => 'int', 'extra' => 'string'), + $alert_row, array() + ); + + updateMemberData($this->_details['receiver_id'], array('alerts' => '+')); + } + + return true; + } +} + +?> \ No newline at end of file diff --git a/Sources/tasks/CreateAttachment-Notify.php b/Sources/tasks/CreateAttachment-Notify.php new file mode 100644 index 0000000..2a3b229 --- /dev/null +++ b/Sources/tasks/CreateAttachment-Notify.php @@ -0,0 +1,166 @@ + $this->_details['id'], + 'attachment_type' => 0, + 'is_approved' => 0, + ) + ); + // Return true if either not found or invalid so that the cron runner deletes this task. + if ($smcFunc['db_num_rows']($request) == 0) + return true; + list ($id_attach, $id_board, $id_msg, $id_topic, $id_member, $subject) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // We need to know who can approve this attachment. + require_once($sourcedir . '/Subs-Members.php'); + $modMembers = membersAllowedTo('approve_posts', $id_board); + + $request = $smcFunc['db_query']('', ' + SELECT id_member, email_address, lngfile, real_name + FROM {db_prefix}members + WHERE id_member IN ({array_int:members})', + array( + 'members' => array_merge($modMembers, array($id_member)), + ) + ); + + $members = array(); + $watched = array(); + $real_name = ''; + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + if ($row['id_member'] == $id_member) + $real_name = $row['real_name']; + else + { + $members[] = $row['id_member']; + $watched[$row['id_member']] = $row; + } + } + $smcFunc['db_free_result']($request); + + if (empty($members)) + return true; + + require_once($sourcedir . '/Subs-Notify.php'); + $members = array_unique($members); + $prefs = getNotifyPrefs($members, 'unapproved_attachment', true); + foreach ($watched as $member => $data) + { + $pref = !empty($prefs[$member]['unapproved_attachment']) ? $prefs[$member]['unapproved_attachment'] : 0; + + if ($pref & self::RECEIVE_NOTIFY_EMAIL) + { + // Emails are a bit complicated. (That's what she said) + require_once($sourcedir . '/Subs-Post.php'); + require_once($sourcedir . '/ScheduledTasks.php'); + loadEssentialThemeData(); + + $emaildata = loadEmailTemplate( + 'unapproved_attachment', + array( + 'SUBJECT' => $subject, + 'LINK' => $scripturl . '?topic=' . $id_topic . '.msg' . $id_msg . '#msg' . $id_msg, + ), + empty($data['lngfile']) || empty($modSettings['userLanguage']) ? $language : $data['lngfile'] + ); + sendmail( + $data['email_address'], + $emaildata['subject'], + $emaildata['body'], + null, + 'ma' . $id_attach, + $emaildata['is_html'] + ); + } + + if ($pref & self::RECEIVE_NOTIFY_ALERT) + { + $alert_rows[] = array( + 'alert_time' => time(), + 'id_member' => $member, + 'id_member_started' => $id_member, + 'member_name' => $real_name, + 'content_type' => 'msg', + 'content_id' => $id_msg, + 'content_action' => 'unapproved_attachment', + 'is_read' => 0, + 'extra' => $smcFunc['json_encode']( + array( + 'topic' => $id_topic, + 'board' => $id_board, + 'content_subject' => $subject, + 'content_link' => $scripturl . '?msg=' . $id_msg, + ) + ), + ); + } + } + + // Insert the alerts if any + if (!empty($alert_rows)) + { + $smcFunc['db_insert']( + 'insert', + '{db_prefix}user_alerts', + array( + 'alert_time' => 'int', + 'id_member' => 'int', + 'id_member_started' => 'int', + 'member_name' => 'string', + 'content_type' => 'string', + 'content_id' => 'int', + 'content_action' => 'string', + 'is_read' => 'int', + 'extra' => 'string', + ), + $alert_rows, + array() + ); + + updateMemberData(array_keys($watched), array('alerts' => '+')); + } + + return true; + } +} + +?> \ No newline at end of file diff --git a/Sources/tasks/CreatePost-Notify.php b/Sources/tasks/CreatePost-Notify.php new file mode 100644 index 0000000..7e804e5 --- /dev/null +++ b/Sources/tasks/CreatePost-Notify.php @@ -0,0 +1,772 @@ + array(), + 'quoted' => array(), + 'watching' => array(), + + // These ones just contain member IDs. + 'all' => array(), + 'emailed' => array(), + 'alerted' => array(), + 'done' => array(), + ); + + /** + * @var array Alerts to be inserted into the alerts table. + */ + private $alert_rows = array(); + + /** + * @var array Members' notification and alert preferences. + */ + private $prefs = array(); + + /** + * @var int Timestamp after which email notifications should be sent about + * mentions and quotes in unwatched and/or edited posts. + */ + private $mention_mail_time = 0; + + /** + * This executes the task: loads up the info, puts the email in the queue + * and inserts any alerts as needed. + * + * @return bool Always returns true + * @throws Exception + */ + public function execute() + { + global $smcFunc, $sourcedir, $scripturl, $language, $modSettings, $user_info, $txt; + + require_once($sourcedir . '/Subs-Post.php'); + require_once($sourcedir . '/Mentions.php'); + require_once($sourcedir . '/Subs-Notify.php'); + require_once($sourcedir . '/Subs.php'); + require_once($sourcedir . '/ScheduledTasks.php'); + loadEssentialThemeData(); + + $msgOptions = &$this->_details['msgOptions']; + $topicOptions = &$this->_details['topicOptions']; + $posterOptions = &$this->_details['posterOptions']; + $type = &$this->_details['type']; + + // Board id is required; if missing, log an error and return + if (!isset($topicOptions['board'])) + { + require_once($sourcedir . '/Errors.php'); + loadLanguage('Errors'); + log_error($txt['missing_board_id'], 'general', __FILE__, __LINE__); + return true; + } + + // poster_time not always supplied, but used throughout + if (empty($msgOptions['poster_time'])) + $msgOptions['poster_time'] = 0; + + $this->mention_mail_time = $msgOptions['poster_time'] + self::MENTION_DELAY * 60; + + // We need some more info about the quoted and mentioned members. + if (!empty($msgOptions['quoted_members'])) + $this->members['quoted'] = Mentions::getMentionsByContent('quote', $msgOptions['id'], array_keys($msgOptions['quoted_members'])); + if (!empty($msgOptions['mentioned_members'])) + $this->members['mentioned'] = Mentions::getMentionsByContent('msg', $msgOptions['id'], array_keys($msgOptions['mentioned_members'])); + + // Find the people interested in receiving notifications for this topic + $request = $smcFunc['db_query']('', ' + SELECT + ln.id_member, ln.id_board, ln.id_topic, ln.sent, + mem.email_address, mem.lngfile, mem.pm_ignore_list, + mem.id_group, mem.id_post_group, mem.additional_groups, + mem.time_format, mem.time_offset, mem.timezone, + b.member_groups, t.id_member_started, t.id_member_updated + FROM {db_prefix}log_notify AS ln + INNER JOIN {db_prefix}members AS mem ON (ln.id_member = mem.id_member) + LEFT JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic) + LEFT JOIN {db_prefix}boards AS b ON (b.id_board = ln.id_board OR b.id_board = t.id_board) + WHERE ln.id_member != {int:member} + AND (ln.id_topic = {int:topic} OR ln.id_board = {int:board})', + array( + 'member' => $posterOptions['id'], + 'topic' => $topicOptions['id'], + 'board' => $topicOptions['board'], + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + // Skip members who aren't allowed to see this board + $groups = array_merge(array($row['id_group'], $row['id_post_group']), (empty($row['additional_groups']) ? array() : explode(',', $row['additional_groups']))); + + $allowed_groups = explode(',', $row['member_groups']); + + if (!in_array(1, $groups) && count(array_intersect($groups, $allowed_groups)) == 0) + continue; + else + { + $row['groups'] = $groups; + unset($row['id_group'], $row['id_post_group'], $row['additional_groups']); + } + + $this->members['watching'][$row['id_member']] = $row; + } + $smcFunc['db_free_result']($request); + + // Filter out mentioned and quoted members who can't see this board. + if (!empty($this->members['mentioned']) || !empty($this->members['quoted'])) + { + // This won't be set yet if no one is watching this board or topic. + if (!isset($allowed_groups)) + { + $request = $smcFunc['db_query']('', ' + SELECT member_groups + FROM {db_prefix}boards + WHERE id_board = {int:board}', + array( + 'board' => $topicOptions['board'], + ) + ); + list($allowed_groups) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + $allowed_groups = explode(',', $allowed_groups); + } + + foreach (array('mentioned', 'quoted') as $member_type) + { + foreach ($this->members[$member_type] as $member_id => $member_data) + { + if (!in_array(1, $member_data['groups']) && count(array_intersect($member_data['groups'], $allowed_groups)) == 0) + unset($this->members[$member_type][$member_id], $msgOptions[$member_type . '_members'][$member_id]); + } + } + } + + $unnotified = array_filter($this->members['watching'], function ($member) { + return empty($member['sent']); + }); + + + // Modified post, or dealing with delayed mention and quote notifications. + if ($type == 'edit' || !empty($this->_details['respawns'])) + { + // Notifications about modified posts only go to members who were mentioned or quoted + $this->members['watching'] = $type == 'edit' ? array(): $unnotified; + + // If this post has no quotes or mentions, just delete any obsolete alerts and bail out. + if (empty($this->members['quoted']) && empty($this->members['mentioned'])) + { + $this->updateAlerts($msgOptions['id']); + return true; + } + + // Never notify about edits to ancient posts. + if (!empty($modSettings['oldTopicDays']) && time() > $msgOptions['poster_time'] + $modSettings['oldTopicDays'] * 86400) + return true; + + // If editing is only allowed for a brief time, send after editing becomes disabled. + if (!empty($modSettings['edit_disable_time']) && $modSettings['edit_disable_time'] <= self::MENTION_DELAY) + { + $this->mention_mail_time = $msgOptions['poster_time'] + $modSettings['edit_disable_time'] * 60; + } + // Otherwise, impose a delay before sending notifications about edited posts. + else + { + if (!empty($this->_details['respawns'])) + { + $request = $smcFunc['db_query']('', ' + SELECT modified_time + FROM {db_prefix}messages + WHERE id_msg = {int:msg} + LIMIT 1', + array( + 'msg' => $msgOptions['id'], + ) + ); + list($real_modified_time) = $smcFunc['db_fetch_row']($request); + $smcFunc['db_free_result']($request); + + // If it was modified again while we weren't looking, bail out. + // A future instance of this task will take care of it instead. + if ((!empty($msgOptions['modify_time']) ? $msgOptions['modify_time'] : $msgOptions['poster_time']) < $real_modified_time) + return true; + } + + $this->mention_mail_time = (!empty($msgOptions['modify_time']) ? $msgOptions['modify_time'] : $msgOptions['poster_time']) + self::MENTION_DELAY * 60; + } + } + + $this->members['all'] = array_unique(array_merge(array_keys($this->members['watching']), array_keys($this->members['quoted']), array_keys($this->members['mentioned']))); + + if (empty($this->members['all'])) + return true; + + $this->prefs = getNotifyPrefs($this->members['all'], '', true); + + // May as well disable these, since they'll be stripped out anyway. + $disable = array('attach', 'img', 'iurl', 'url', 'youtube'); + if (!empty($modSettings['disabledBBC'])) + { + $disabledBBC = $modSettings['disabledBBC']; + $disable = array_unique(array_merge($disable, explode(',', $modSettings['disabledBBC']))); + } + $modSettings['disabledBBC'] = implode(',', $disable); + + // Notify any members who were mentioned. + if (!empty($this->members['mentioned'])) + $this->handleMentionedNotifications(); + + // Notify any members who were quoted. + if (!empty($this->members['quoted'])) + $this->handleQuoteNotifications(); + + // Handle rest of the notifications for watched topics and boards + if (!empty($this->members['watching'])) + $this->handleWatchedNotifications(); + + // Put this back the way we found it. + if (!empty($disabledBBC)) + $modSettings['disabledBBC'] = $disabledBBC; + + // Track what we sent. + $members_to_log = array_intersect($this->members['emailed'], array_keys($this->members['watching'])); + if (!empty($members_to_log)) + { + $smcFunc['db_query']('', ' + UPDATE {db_prefix}log_notify + SET sent = {int:is_sent} + WHERE ' . ($type == 'topic' ? 'id_board = {int:board}' : 'id_topic = {int:topic}') . ' + AND id_member IN ({array_int:members})', + array( + 'topic' => $topicOptions['id'], + 'board' => $topicOptions['board'], + // 'members' => $this->members['emailed'], + 'members' => $members_to_log, + 'is_sent' => 1, + ) + ); + } + + // Insert it into the digest for daily/weekly notifications + if ($type != 'edit' && empty($this->_details['respawns'])) + { + $smcFunc['db_insert']('', + '{db_prefix}log_digest', + array( + 'id_topic' => 'int', 'id_msg' => 'int', 'note_type' => 'string', 'exclude' => 'int', + ), + array($topicOptions['id'], $msgOptions['id'], $type, $posterOptions['id']), + array() + ); + } + + // Insert the alerts if any + $this->updateAlerts($msgOptions['id']); + + // If there is anyone still to notify via email, create a new task later. + $unnotified = array_diff_key($unnotified, array_flip($this->members['emailed'])); + if (!empty($unnotified) || !empty($msgOptions['mentioned_members']) || !empty($msgOptions['quoted_members'])) + { + $new_details = $this->_details; + + if (empty($new_details['respawns'])) + $new_details['respawns'] = 0; + + if ($new_details['respawns']++ < 10) + { + $smcFunc['db_insert']('', + '{db_prefix}background_tasks', + array( + 'task_file' => 'string', + 'task_class' => 'string', + 'task_data' => 'string', + 'claimed_time' => 'int', + ), + array( + '$sourcedir/tasks/CreatePost-Notify.php', + 'CreatePost_Notify_Background', + $smcFunc['json_encode']($new_details), + max(0, $this->mention_mail_time - MAX_CLAIM_THRESHOLD), + ), + array('id_task') + ); + } + } + + return true; + } + + private function updateAlerts($msg_id) + { + global $smcFunc; + + // We send alerts only on the first iteration of this task. + if (!empty($this->_details['respawns'])) + return; + + // Delete alerts about any mentions and quotes that no longer exist. + if ($this->_details['type'] == 'edit') + { + $old_alerts = array(); + + $request = $smcFunc['db_query']('', ' + SELECT content_action, id_member + FROM {db_prefix}user_alerts + WHERE content_id = {int:msg_id} + AND content_type = {literal:msg} + AND (content_action = {literal:quote} OR content_action = {literal:mention})', + array( + 'msg_id' => $msg_id, + ) + ); + if ($smcFunc['db_num_rows']($request) != 0) + { + while ($row = $smcFunc['db_fetch_assoc']($request)) + $old_alerts[$row['content_action']][$row['id_member']] = $row['id_member']; + } + $smcFunc['db_free_result']($request); + + if (!empty($old_alerts)) + { + $request = $smcFunc['db_query']('', ' + SELECT content_type, id_mentioned + FROM {db_prefix}mentions + WHERE content_id = {int:msg_id} + AND (content_type = {literal:quote} OR content_type = {literal:msg})', + array( + 'msg_id' => $msg_id, + ) + ); + while ($row = $smcFunc['db_fetch_assoc']($request)) + { + $content_action = $row['content_type'] == 'quote' ? 'quote' : 'mention'; + unset($old_alerts[$content_action][$row['id_mentioned']]); + } + $smcFunc['db_free_result']($request); + + $conditions = array(); + + if (!empty($old_alerts['quote'])) + $conditions[] = '(content_action = {literal:quote} AND id_member IN ({array_int:members_not_quoted}))'; + + if (!empty($old_alerts['mention'])) + $conditions[] = '(content_action = {literal:mention} AND id_member IN ({array_int:members_not_mentioned}))'; + + if (!empty($conditions)) + { + $smcFunc['db_query']('', ' + DELETE FROM {db_prefix}user_alerts + WHERE content_id = {int:msg_id} + AND content_type = {literal:msg} + AND (' . implode(' OR ', $conditions) . ')', + array( + 'msg_id' => $msg_id, + 'members_not_quoted' => empty($old_alerts['quote']) ? array(0) : $old_alerts['quote'], + 'members_not_mentioned' => empty($old_alerts['mention']) ? array(0) : $old_alerts['mention'], + ) + ); + + foreach ($old_alerts as $member_ids) + updateMemberData($member_ids, array('alerts' => '-')); + } + } + } + + // Insert the new alerts. + if (!empty($this->alert_rows)) + { + $smcFunc['db_insert']('', + '{db_prefix}user_alerts', + array( + 'alert_time' => 'int', + 'id_member' => 'int', + 'id_member_started' => 'int', + 'member_name' => 'string', + 'content_type' => 'string', + 'content_id' => 'int', + 'content_action' => 'string', + 'is_read' => 'int', + 'extra' => 'string', + ), + $this->alert_rows, + array() + ); + + updateMemberData(array_column($this->alert_rows, 'id_member'), array('alerts' => '+')); + } + } + + /** + * Notifies members about new posts in topics they are watching + * and new topics in boards they are watching. + */ + protected function handleWatchedNotifications() + { + global $smcFunc, $scripturl, $modSettings, $user_info; + + $msgOptions = &$this->_details['msgOptions']; + $topicOptions = &$this->_details['topicOptions']; + $posterOptions = &$this->_details['posterOptions']; + $type = &$this->_details['type']; + + $user_ids = array_keys($this->members['watching']); + if (!in_array($posterOptions['id'], $user_ids)) + $user_ids[] = $posterOptions['id']; + $members_info = $this->getMinUserInfo($user_ids); + + $parsed_message = array(); + foreach ($this->members['watching'] as $member_id => $member_data) + { + if (in_array($member_id, $this->members['done'])) + continue; + + $frequency = isset($this->prefs[$member_id]['msg_notify_pref']) ? $this->prefs[$member_id]['msg_notify_pref'] : self::FREQUENCY_NOTHING; + $notify_types = !empty($this->prefs[$member_id]['msg_notify_type']) ? $this->prefs[$member_id]['msg_notify_type'] : self::NOTIFY_TYPE_REPLIES_AND_MODERATION; + + // Don't send a notification if: + // 1. The watching member ignored the member who did the action. + if (!empty($member_data['pm_ignore_list']) && in_array($member_data['id_member_updated'], explode(',', $member_data['pm_ignore_list']))) + continue; + + // 2. The watching member is not interested in moderation on this topic. + if (!in_array($type, array('reply', 'topic')) && ($notify_types == self::NOTIFY_TYPE_ONLY_REPLIES || ($notify_types == self::NOTIFY_TYPE_REPLIES_AND_OWN_TOPIC_MODERATION && $member_id != $member_data['id_member_started']))) + continue; + + // 3. This is the watching member's own post. + if (in_array($type, array('reply', 'topic')) && $member_id == $posterOptions['id']) + continue; + + // 4. The watching member doesn't want any notifications at all. + if ($notify_types == self::NOTIFY_TYPE_NOTHING) + continue; + + // 5. The watching member doesn't want notifications until later. + if (in_array($frequency, array( + self::FREQUENCY_NOTHING, + self::FREQUENCY_DAILY_DIGEST, + self::FREQUENCY_WEEKLY_DIGEST))) + continue; + + // 6. We already sent one and the watching member doesn't want more. + if ($frequency == self::FREQUENCY_FIRST_UNREAD_MSG && $member_data['sent']) + continue; + + // 7. The watching member isn't on club security's VIP list. + if (!empty($this->_details['members_only']) && !in_array($member_id, $this->_details['members_only'])) + continue; + + // Watched topic? + if (!empty($member_data['id_topic']) && $type != 'topic' && !empty($this->prefs[$member_id])) + { + $pref = !empty($this->prefs[$member_id]['topic_notify_' . $topicOptions['id']]) ? + $this->prefs[$member_id]['topic_notify_' . $topicOptions['id']] : + (!empty($this->prefs[$member_id]['topic_notify']) ? $this->prefs[$member_id]['topic_notify'] : 0); + + $message_type = 'notification_' . $type; + + if ($type == 'reply') + { + if (empty($modSettings['disallow_sendBody']) && !empty($this->prefs[$member_id]['msg_receive_body'])) + $message_type .= '_body'; + + if (!empty($frequency)) + $message_type .= '_once'; + } + + $content_type = 'topic'; + } + // A new topic in a watched board then? + elseif ($type == 'topic') + { + $pref = !empty($this->prefs[$member_id]['board_notify_' . $topicOptions['board']]) ? + $this->prefs[$member_id]['board_notify_' . $topicOptions['board']] : + (!empty($this->prefs[$member_id]['board_notify']) ? $this->prefs[$member_id]['board_notify'] : 0); + + $content_type = 'board'; + + $message_type = !empty($frequency) ? 'notify_boards_once' : 'notify_boards'; + + if (empty($modSettings['disallow_sendBody']) && !empty($this->prefs[$member_id]['msg_receive_body'])) + $message_type .= '_body'; + } + + // If neither of the above, this might be a redundant row due to the OR clause in our SQL query, skip + else + continue; + + // We need to fake some of $user_info to make BBC parsing work correctly. + if (isset($user_info)) + $real_user_info = $user_info; + + $user_info = $members_info[$member_id]; + + loadLanguage('index+Modifications', $member_data['lngfile'], false); + + // Censor and parse BBC in the receiver's localization. Don't repeat unnecessarily. + $localization = implode('|', array($member_data['lngfile'], $user_info['time_offset'], $user_info['time_format'])); + if (empty($parsed_message[$localization])) + { + $parsed_message[$localization]['subject'] = $msgOptions['subject']; + $parsed_message[$localization]['body'] = $msgOptions['body']; + + censorText($parsed_message[$localization]['subject']); + censorText($parsed_message[$localization]['body']); + + $parsed_message[$localization]['subject'] = un_htmlspecialchars($parsed_message[$localization]['subject']); + $parsed_message[$localization]['body'] = trim(un_htmlspecialchars(strip_tags(strtr(parse_bbc($parsed_message[$localization]['body'], false), array('
    ' => "\n", '' => "\n", '' => "\n", '[' => '[', ']' => ']', ''' => '\'', '' => "\n", '' => "\t", '
    ' => "\n---------------------------------------------------------------\n"))))); + } + + // Put $user_info back the way we found it. + if (isset($real_user_info)) + { + $user_info = $real_user_info; + unset($real_user_info); + } + else + $user_info = null; + + // Bitwise check: Receiving a alert? + if ($pref & self::RECEIVE_NOTIFY_ALERT) + { + $this->alert_rows[] = array( + 'alert_time' => time(), + 'id_member' => $member_id, + // Only tell sender's information for new topics and replies + 'id_member_started' => in_array($type, array('topic', 'reply')) ? $posterOptions['id'] : 0, + 'member_name' => in_array($type, array('topic', 'reply')) ? $posterOptions['name'] : '', + 'content_type' => $content_type, + 'content_id' => $topicOptions['id'], + 'content_action' => $type, + 'is_read' => 0, + 'extra' => $smcFunc['json_encode'](array( + 'topic' => $topicOptions['id'], + 'board' => $topicOptions['board'], + 'content_subject' => $parsed_message[$localization]['subject'], + 'content_link' => $scripturl . '?topic=' . $topicOptions['id'] . (in_array($type, array('reply', 'topic')) ? '.new;topicseen#new' : '.0'), + )), + ); + } + + // Bitwise check: Receiving a email notification? + if ($pref & self::RECEIVE_NOTIFY_EMAIL) + { + $itemID = $content_type == 'board' ? $topicOptions['board'] : $topicOptions['id']; + + $token = createUnsubscribeToken($member_data['id_member'], $member_data['email_address'], $content_type, $itemID); + + $replacements = array( + 'TOPICSUBJECT' => $parsed_message[$localization]['subject'], + 'POSTERNAME' => un_htmlspecialchars(isset($members_info[$posterOptions['id']]['name']) ? $members_info[$posterOptions['id']]['name'] : $posterOptions['name']), + 'TOPICLINK' => $scripturl . '?topic=' . $topicOptions['id'] . '.new#new', + 'MESSAGE' => $parsed_message[$localization]['body'], + 'UNSUBSCRIBELINK' => $scripturl . '?action=notify' . $content_type . ';' . $content_type . '=' . $itemID . ';sa=off;u=' . $member_data['id_member'] . ';token=' . $token, + ); + + $emaildata = loadEmailTemplate($message_type, $replacements, $member_data['lngfile']); + $mail_result = sendmail($member_data['email_address'], $emaildata['subject'], $emaildata['body'], null, 'm' . $topicOptions['id'], $emaildata['is_html']); + + if ($mail_result !== false) + $this->members['emailed'][] = $member_id; + } + + $this->members['done'][] = $member_id; + } + } + + /** + * Notifies members when their posts are quoted in other posts. + */ + protected function handleQuoteNotifications() + { + global $smcFunc, $modSettings, $language, $scripturl; + + $msgOptions = &$this->_details['msgOptions']; + $posterOptions = &$this->_details['posterOptions']; + + foreach ($this->members['quoted'] as $member_id => $member_data) + { + if (in_array($member_id, $this->members['done'])) + continue; + + if (!isset($this->prefs[$member_id]) || empty($this->prefs[$member_id]['msg_quote'])) + continue; + else + $pref = $this->prefs[$member_id]['msg_quote']; + + // You don't need to be notified about quoting yourself. + if ($member_id == $posterOptions['id']) + continue; + + // Bitwise check: Receiving an alert? + if ($pref & self::RECEIVE_NOTIFY_ALERT) + { + $this->alert_rows[] = array( + 'alert_time' => time(), + 'id_member' => $member_data['id'], + 'id_member_started' => $posterOptions['id'], + 'member_name' => $posterOptions['name'], + 'content_type' => 'msg', + 'content_id' => $msgOptions['id'], + 'content_action' => 'quote', + 'is_read' => 0, + 'extra' => $smcFunc['json_encode'](array( + 'content_subject' => $msgOptions['subject'], + 'content_link' => $scripturl . '?msg=' . $msgOptions['id'], + )), + ); + } + + // Bitwise check: Receiving a email notification? + if (!($pref & self::RECEIVE_NOTIFY_EMAIL)) + { + // Don't want an email, so forget this member in any respawned tasks. + unset($msgOptions['quoted_members'][$member_id]); + } + elseif (TIME_START >= $this->mention_mail_time || in_array($member_id, $this->members['watching'])) + { + $replacements = array( + 'CONTENTSUBJECT' => $msgOptions['subject'], + 'QUOTENAME' => $posterOptions['name'], + 'MEMBERNAME' => $member_data['real_name'], + 'CONTENTLINK' => $scripturl . '?msg=' . $msgOptions['id'], + ); + + $emaildata = loadEmailTemplate('msg_quote', $replacements, empty($member_data['lngfile']) || empty($modSettings['userLanguage']) ? $language : $member_data['lngfile']); + $mail_result = sendmail($member_data['email_address'], $emaildata['subject'], $emaildata['body'], null, 'msg_quote_' . $msgOptions['id'], $emaildata['is_html'], 2); + + if ($mail_result !== false) + { + // Don't send multiple notifications about the same post. + $this->members['emailed'][] = $member_id; + + // Ensure respawned tasks don't send this again. + unset($msgOptions['quoted_members'][$member_id]); + } + } + + $this->members['done'][] = $member_id; + } + } + + /** + * Notifies members when they are mentioned in other members' posts. + */ + protected function handleMentionedNotifications() + { + global $smcFunc, $scripturl, $language, $modSettings; + + $msgOptions = &$this->_details['msgOptions']; + + foreach ($this->members['mentioned'] as $member_id => $member_data) + { + if (in_array($member_id, $this->members['done'])) + continue; + + if (empty($this->prefs[$member_id]) || empty($this->prefs[$member_id]['msg_mention'])) + continue; + else + $pref = $this->prefs[$member_id]['msg_mention']; + + // Mentioning yourself is silly, and we aren't going to notify you about it. + if ($member_id == $member_data['mentioned_by']['id']) + continue; + + // Bitwise check: Receiving an alert? + if ($pref & self::RECEIVE_NOTIFY_ALERT) + { + $this->alert_rows[] = array( + 'alert_time' => time(), + 'id_member' => $member_data['id'], + 'id_member_started' => $member_data['mentioned_by']['id'], + 'member_name' => $member_data['mentioned_by']['name'], + 'content_type' => 'msg', + 'content_id' => $msgOptions['id'], + 'content_action' => 'mention', + 'is_read' => 0, + 'extra' => $smcFunc['json_encode'](array( + 'content_subject' => $msgOptions['subject'], + 'content_link' => $scripturl . '?msg=' . $msgOptions['id'], + )), + ); + } + + + // Bitwise check: Receiving a email notification? + if (!($pref & self::RECEIVE_NOTIFY_EMAIL)) + { + // Don't want an email, so forget this member in any respawned tasks. + unset($msgOptions['mentioned_members'][$member_id]); + } + elseif (TIME_START >= $this->mention_mail_time || in_array($member_id, $this->members['watching'])) + { + $replacements = array( + 'CONTENTSUBJECT' => $msgOptions['subject'], + 'MENTIONNAME' => $member_data['mentioned_by']['name'], + 'MEMBERNAME' => $member_data['real_name'], + 'CONTENTLINK' => $scripturl . '?msg=' . $msgOptions['id'], + ); + + $emaildata = loadEmailTemplate('msg_mention', $replacements, empty($member_data['lngfile']) || empty($modSettings['userLanguage']) ? $language : $member_data['lngfile']); + $mail_result = sendmail($member_data['email_address'], $emaildata['subject'], $emaildata['body'], null, 'msg_mention_' . $msgOptions['id'], $emaildata['is_html'], 2); + + if ($mail_result !== false) + { + // Don't send multiple notifications about the same post. + $this->members['emailed'][] = $member_id; + + // Ensure respawned tasks don't send this again. + unset($msgOptions['mentioned_members'][$member_id]); + } + } + + $this->members['done'][] = $member_id; + } + } +} + +?> \ No newline at end of file diff --git a/Sources/tasks/EventNew-Notify.php b/Sources/tasks/EventNew-Notify.php new file mode 100644 index 0000000..f317251 --- /dev/null +++ b/Sources/tasks/EventNew-Notify.php @@ -0,0 +1,111 @@ +_details['sender_id'])) + $members = array_diff($members, array($this->_details['sender_id'])); + + // Having successfully figured this out, now let's get the preferences of everyone. + require_once($sourcedir . '/Subs-Notify.php'); + $prefs = getNotifyPrefs($members, 'event_new', true); + + // Just before we go any further, we may not have the sender's name. Let's just quickly fix that. + // If a guest creates the event, we wouldn't be capturing a username or anything. + if (!empty($this->_details['sender_id']) && empty($this->_details['sender_name'])) + { + loadMemberData($this->_details['sender_id'], false, 'minimal'); + if (!empty($user_profile[$this->_details['sender_id']])) + $this->_details['sender_name'] = $user_profile[$this->_details['sender_id']]['real_name']; + else + $this->_details['sender_id'] = 0; + } + + // So now we find out who wants what. + $alert_bits = array( + 'alert' => self::RECEIVE_NOTIFY_ALERT, + 'email' => self::RECEIVE_NOTIFY_EMAIL, + ); + $notifies = array(); + + foreach ($prefs as $member => $pref_option) + { + foreach ($alert_bits as $type => $bitvalue) + if ($pref_option['event_new'] & $bitvalue) + $notifies[$type][] = $member; + } + + // Firstly, anyone who wants alerts. + if (!empty($notifies['alert'])) + { + // Alerts are relatively easy. + $insert_rows = array(); + foreach ($notifies['alert'] as $member) + { + $insert_rows[] = array( + 'alert_time' => $this->_details['time'], + 'id_member' => $member, + 'id_member_started' => $this->_details['sender_id'], + 'member_name' => $this->_details['sender_name'], + 'content_type' => 'event', + 'content_id' => $this->_details['event_id'], + 'content_action' => empty($this->_details['sender_id']) ? 'new_guest' : 'new', + 'is_read' => 0, + 'extra' => $smcFunc['json_encode']( + array( + "event_id" => $this->_details['event_id'], + "event_title" => $this->_details['event_title'] + ) + ), + ); + } + + $smcFunc['db_insert']('insert', + '{db_prefix}user_alerts', + array('alert_time' => 'int', 'id_member' => 'int', 'id_member_started' => 'int', + 'member_name' => 'string', 'content_type' => 'string', 'content_id' => 'int', + 'content_action' => 'string', 'is_read' => 'int', 'extra' => 'string'), + $insert_rows, + array('id_alert') + ); + + // And update the count of alerts for those people. + updateMemberData($notifies['alert'], array('alerts' => '+')); + } + + return true; + } +} + +?> \ No newline at end of file diff --git a/Sources/tasks/ExportProfileData.php b/Sources/tasks/ExportProfileData.php new file mode 100644 index 0000000..b582671 --- /dev/null +++ b/Sources/tasks/ExportProfileData.php @@ -0,0 +1,776 @@ +_details for access by the static functions + * called from integration hooks. + * + * Even though this info is unique to a specific instance of this class, we + * can get away with making this variable static because only one instance + * of this class exists at a time. + */ + private static $export_details = array(); + + /** + * @var array Temporary backup of the $modSettings array + */ + private static $real_modSettings = array(); + + /** + * @var array The XSLT stylesheet used to transform the XML into HTML and + * a (possibly empty) DOCTYPE declaration to insert into the source XML. + * + * Even though this info is unique to a specific instance of this class, we + * can get away with making this variable static because only one instance + * of this class exists at a time. + */ + private static $xslt_info = array('stylesheet' => '', 'doctype' => ''); + + /** + * @var array Info to create a follow-up background task, if necessary. + */ + private $next_task = array(); + + /** + * @var array Used to ensure we exit long running tasks cleanly. + */ + private $time_limit = 30; + + /** + * This is the main dispatcher for the class. + * It calls the correct private function based on the information stored in + * the task details. + * + * @return bool Always returns true + */ + public function execute() + { + global $sourcedir, $smcFunc; + + if (!defined('EXPORTING')) + define('EXPORTING', 1); + + // Avoid leaving files in an inconsistent state. + ignore_user_abort(true); + + $this->time_limit = (ini_get('safe_mode') === false && @set_time_limit(MAX_CLAIM_THRESHOLD) !== false) ? MAX_CLAIM_THRESHOLD : ini_get('max_execution_time'); + + // This could happen if the user manually changed the URL params of the export request. + if ($this->_details['format'] == 'HTML' && (!class_exists('DOMDocument') || !class_exists('XSLTProcessor'))) + { + require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php'); + $export_formats = get_export_formats(); + + $this->_details['format'] = 'XML_XSLT'; + $this->_details['format_settings'] = $export_formats['XML_XSLT']; + } + + // Inform static functions of the export format, etc. + self::$export_details = $this->_details; + + // For exports only, members can always see their own posts, even in boards that they can no longer access. + $member_info = $this->getMinUserInfo(array($this->_details['uid'])); + $member_info = array_merge($member_info[$this->_details['uid']], array( + 'buddies' => array(), + 'query_see_board' => '1=1', + 'query_see_message_board' => '1=1', + 'query_see_topic_board' => '1=1', + 'query_wanna_see_board' => '1=1', + 'query_wanna_see_message_board' => '1=1', + 'query_wanna_see_topic_board' => '1=1', + )); + + // Use some temporary integration hooks to manipulate BBC parsing during export. + foreach (array('pre_parsebbc', 'post_parsebbc', 'bbc_codes', 'post_parseAttachBBC', 'attach_bbc_validate') as $hook) + add_integration_function('integrate_' . $hook, 'ExportProfileData_Background::' . $hook, false); + + // Perform the export. + if ($this->_details['format'] == 'XML') + $this->exportXml($member_info); + + elseif ($this->_details['format'] == 'HTML') + $this->exportHtml($member_info); + + elseif ($this->_details['format'] == 'XML_XSLT') + $this->exportXmlXslt($member_info); + + // If necessary, create a new background task to continue the export process. + if (!empty($this->next_task)) + { + $smcFunc['db_insert']('insert', '{db_prefix}background_tasks', + array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'), + $this->next_task, + array() + ); + } + + ignore_user_abort(false); + + return true; + } + + /** + * The workhorse of this class. Compiles profile data to XML files. + * + * @param array $member_info Minimal $user_info about the relevant member. + */ + protected function exportXml($member_info) + { + global $smcFunc, $sourcedir, $context, $modSettings, $settings, $user_info, $mbname; + global $user_profile, $txt, $scripturl, $query_this_board; + + // For convenience... + $uid = $this->_details['uid']; + $lang = $this->_details['lang']; + $included = $this->_details['included']; + $start = $this->_details['start']; + $latest = $this->_details['latest']; + $datatype = $this->_details['datatype']; + + if (!isset($included[$datatype]['func']) || !isset($included[$datatype]['langfile'])) + return; + + require_once($sourcedir . DIRECTORY_SEPARATOR . 'News.php'); + require_once($sourcedir . DIRECTORY_SEPARATOR . 'ScheduledTasks.php'); + + // Setup. + $done = false; + $delay = 0; + $context['xmlnews_uid'] = $uid; + $context['xmlnews_limit'] = !empty($modSettings['export_rate']) ? $modSettings['export_rate'] : 250; + $context[$datatype . '_start'] = $start[$datatype]; + $datatypes = array_keys($included); + + // Fake a wee bit of $user_info so that loading the member data & language doesn't choke. + $user_info = $member_info; + + loadEssentialThemeData(); + $settings['actual_theme_dir'] = $settings['theme_dir']; + $context['user']['id'] = $uid; + $context['user']['language'] = $lang; + loadMemberData($uid); + loadLanguage(implode('+', array_unique(array('index', 'Modifications', 'Stats', 'Profile', $included[$datatype]['langfile']))), $lang); + + // @todo Ask lawyers whether the GDPR requires us to include posts in the recycle bin. + $query_this_board = '{query_see_message_board}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? ' AND m.id_board != ' . $modSettings['recycle_board'] : ''); + + // We need a valid export directory. + if (empty($modSettings['export_dir']) || !is_dir($modSettings['export_dir']) || !smf_chmod($modSettings['export_dir'])) + { + require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php'); + if (create_export_dir() === false) + return; + } + + $export_dir_slash = $modSettings['export_dir'] . DIRECTORY_SEPARATOR; + + $idhash = hash_hmac('sha1', $uid, get_auth_secret()); + $idhash_ext = $idhash . '.' . $this->_details['format_settings']['extension']; + + // Increment the file number until we reach one that doesn't exist. + $filenum = 1; + $realfile = $export_dir_slash . $filenum . '_' . $idhash_ext; + while (file_exists($realfile)) + $realfile = $export_dir_slash . ++$filenum . '_' . $idhash_ext; + + $tempfile = $export_dir_slash . $idhash_ext . '.tmp'; + $progressfile = $export_dir_slash . $idhash_ext . '.progress.json'; + + $feed_meta = array( + 'title' => sprintf($txt['profile_of_username'], $user_profile[$uid]['real_name']), + 'desc' => sentence_list(array_map( + function ($datatype) use ($txt) + { + return $txt[$datatype]; + }, + array_keys($included) + )), + 'author' => $mbname, + 'source' => $scripturl . '?action=profile;u=' . $uid, + 'self' => '', // Unused, but can't be null. + 'page' => &$filenum, + ); + + // Some paranoid hosts disable or hamstring the disk space functions in an attempt at security via obscurity. + $check_diskspace = !empty($modSettings['export_min_diskspace_pct']) && function_exists('disk_free_space') && function_exists('disk_total_space') && intval(@disk_total_space($modSettings['export_dir']) >= 1440); + $minspace = $check_diskspace ? ceil(disk_total_space($modSettings['export_dir']) * $modSettings['export_min_diskspace_pct'] / 100) : 0; + + // If a necessary file is missing, we need to start over. + if (!file_exists($tempfile) || !file_exists($progressfile) || filesize($progressfile) == 0) + { + foreach (array_merge(array($tempfile, $progressfile), glob($export_dir_slash . '*_' . $idhash_ext)) as $fpath) + @unlink($fpath); + + $filenum = 1; + $realfile = $export_dir_slash . $filenum . '_' . $idhash_ext; + + buildXmlFeed('smf', array(), $feed_meta, 'profile'); + file_put_contents($tempfile, implode('', $context['feed']), LOCK_EX); + + $progress = array_fill_keys($datatypes, 0); + file_put_contents($progressfile, $smcFunc['json_encode']($progress)); + } + else + $progress = $smcFunc['json_decode'](file_get_contents($progressfile), true); + + // Get the data, always in ascending order. + $xml_data = call_user_func($included[$datatype]['func'], 'smf', true); + + // No data retrived? Just move on then. + if (empty($xml_data)) + $datatype_done = true; + + // Basic profile data is quick and easy. + elseif ($datatype == 'profile') + { + buildXmlFeed('smf', $xml_data, $feed_meta, 'profile'); + file_put_contents($tempfile, implode('', $context['feed']), LOCK_EX); + + $progress[$datatype] = time(); + $datatype_done = true; + + // Cache for subsequent reuse. + $profile_basic_items = $context['feed']['items']; + cache_put_data('export_profile_basic-' . $uid, $profile_basic_items, MAX_CLAIM_THRESHOLD); + } + + // Posts and PMs... + else + { + // We need the basic profile data in every export file. + $profile_basic_items = cache_get_data('export_profile_basic-' . $uid, MAX_CLAIM_THRESHOLD); + if (empty($profile_basic_items)) + { + $profile_data = call_user_func($included['profile']['func'], 'smf', true); + buildXmlFeed('smf', $profile_data, $feed_meta, 'profile'); + $profile_basic_items = $context['feed']['items']; + cache_put_data('export_profile_basic-' . $uid, $profile_basic_items, MAX_CLAIM_THRESHOLD); + unset($context['feed']); + } + + $per_page = $this->_details['format_settings']['per_page']; + $prev_item_count = empty($this->_details['item_count']) ? 0 : $this->_details['item_count']; + + // If the temp file has grown enormous, save it so we can start a new one. + clearstatcache(); + if (file_exists($tempfile) && filesize($tempfile) >= 1024 * 1024 * 250) + { + rename($tempfile, $realfile); + $realfile = $export_dir_slash . ++$filenum . '_' . $idhash_ext; + + if (empty($context['feed']['header'])) + buildXmlFeed('smf', array(), $feed_meta, 'profile'); + + file_put_contents($tempfile, implode('', array($context['feed']['header'], $profile_basic_items, $context['feed']['footer'])), LOCK_EX); + + $prev_item_count = 0; + } + + // Split $xml_data into reasonably sized chunks. + if (empty($prev_item_count)) + { + $xml_data = array_chunk($xml_data, $per_page); + } + else + { + $first_chunk = array_splice($xml_data, 0, $per_page - $prev_item_count); + $xml_data = array_merge(array($first_chunk), array_chunk($xml_data, $per_page)); + unset($first_chunk); + } + + foreach ($xml_data as $chunk => $items) + { + unset($new_item_count, $last_id); + + // Remember the last item so we know where to start next time. + $last_item = end($items); + if (isset($last_item['content'][0]['content']) && $last_item['content'][0]['tag'] === 'id') + $last_id = $last_item['content'][0]['content']; + + // Build the XML string from the data. + buildXmlFeed('smf', $items, $feed_meta, 'profile'); + + // If disk space is insufficient, pause for a day so the admin can fix it. + if ($check_diskspace && disk_free_space($modSettings['export_dir']) - $minspace <= strlen(implode('', $context['feed']) . self::$xslt_info['stylesheet'])) + { + loadLanguage('Errors'); + log_error(sprintf($txt['export_low_diskspace'], $modSettings['export_min_diskspace_pct'])); + + $delay = 86400; + } + else + { + // We need a file to write to, of course. + if (!file_exists($tempfile)) + file_put_contents($tempfile, implode('', array($context['feed']['header'], $profile_basic_items, $context['feed']['footer'])), LOCK_EX); + + // Insert the new data before the feed footer. + $handle = fopen($tempfile, 'r+'); + if (is_resource($handle)) + { + flock($handle, LOCK_EX); + + fseek($handle, strlen($context['feed']['footer']) * -1, SEEK_END); + + $bytes_written = fwrite($handle, $context['feed']['items'] . $context['feed']['footer']); + + // If we couldn't write everything, revert the changes and consider the write to have failed. + if ($bytes_written > 0 && $bytes_written < strlen($context['feed']['items'] . $context['feed']['footer'])) + { + fseek($handle, $bytes_written * -1, SEEK_END); + $pointer_pos = ftell($handle); + ftruncate($handle, $pointer_pos); + rewind($handle); + fseek($handle, 0, SEEK_END); + fwrite($handle, $context['feed']['footer']); + + $bytes_written = false; + } + + flock($handle, LOCK_UN); + fclose($handle); + } + + // Write failed. We'll try again next time. + if (empty($bytes_written)) + { + $delay = MAX_CLAIM_THRESHOLD; + break; + } + + // All went well. + else + { + // Track progress by ID where appropriate, and by time otherwise. + $progress[$datatype] = !isset($last_id) ? time() : $last_id; + file_put_contents($progressfile, $smcFunc['json_encode']($progress)); + + // Are we done with this datatype yet? + if (!isset($last_id) || (count($items) < $per_page && $last_id >= $latest[$datatype])) + $datatype_done = true; + + // Finished the file for this chunk, so move on to the next one. + if (count($items) >= $per_page - $prev_item_count) + { + rename($tempfile, $realfile); + $realfile = $export_dir_slash . ++$filenum . '_' . $idhash_ext; + $prev_item_count = $new_item_count = 0; + } + // This was the last chunk. + else + { + // Should we append more items to this file next time? + $new_item_count = isset($last_id) ? $prev_item_count + count($items) : 0; + } + } + } + } + } + + if (!empty($datatype_done)) + { + $datatype_key = array_search($datatype, $datatypes); + $done = !isset($datatypes[$datatype_key + 1]); + + if (!$done) + $datatype = $datatypes[$datatype_key + 1]; + } + + // Remove the .tmp extension from the final tempfile so the system knows it's done. + if (!empty($done)) + { + rename($tempfile, $realfile); + } + + // Oops. Apparently some sneaky monkey cancelled the export while we weren't looking. + elseif (!file_exists($progressfile)) + { + @unlink($tempfile); + return; + } + + // We have more work to do again later. + else + { + $start[$datatype] = $progress[$datatype]; + + $new_details = array( + 'format' => $this->_details['format'], + 'uid' => $uid, + 'lang' => $lang, + 'included' => $included, + 'start' => $start, + 'latest' => $latest, + 'datatype' => $datatype, + 'format_settings' => $this->_details['format_settings'], + 'last_page' => $this->_details['last_page'], + 'dlfilename' => $this->_details['dlfilename'], + ); + if (!empty($new_item_count)) + $new_details['item_count'] = $new_item_count; + + $this->next_task = array('$sourcedir/tasks/ExportProfileData.php', 'ExportProfileData_Background', $smcFunc['json_encode']($new_details), time() - MAX_CLAIM_THRESHOLD + $delay); + + if (!file_exists($tempfile)) + { + buildXmlFeed('smf', array(), $feed_meta, 'profile'); + file_put_contents($tempfile, implode('', array($context['feed']['header'], !empty($profile_basic_items) ? $profile_basic_items : '', $context['feed']['footer'])), LOCK_EX); + } + } + + file_put_contents($progressfile, $smcFunc['json_encode']($progress)); + } + + /** + * Compiles profile data to HTML. + * + * Internally calls exportXml() and then uses an XSLT stylesheet to + * transform the XML files into HTML. + * + * @param array $member_info Minimal $user_info about the relevant member. + */ + protected function exportHtml($member_info) + { + global $modSettings, $context, $smcFunc, $sourcedir; + + $context['export_last_page'] = $this->_details['last_page']; + $context['export_dlfilename'] = $this->_details['dlfilename']; + + // Perform the export to XML. + $this->exportXml($member_info); + + // Determine which files, if any, are ready to be transformed. + $export_dir_slash = $modSettings['export_dir'] . DIRECTORY_SEPARATOR; + $idhash = hash_hmac('sha1', $this->_details['uid'], get_auth_secret()); + $idhash_ext = $idhash . '.' . $this->_details['format_settings']['extension']; + + $new_exportfiles = array(); + foreach (glob($export_dir_slash . '*_' . $idhash_ext) as $completed_file) + { + if (file_get_contents($completed_file, false, null, 0, 6) == '_details['format'], $this->_details['uid']); + + // Set up the XSLT processor. + $xslt = new DOMDocument(); + $xslt->loadXML(self::$xslt_info['stylesheet']); + $xsltproc = new XSLTProcessor(); + $xsltproc->importStylesheet($xslt); + + $libxml_options = 0; + foreach (array('LIBXML_COMPACT', 'LIBXML_PARSEHUGE', 'LIBXML_BIGLINES') as $libxml_option) + if (defined($libxml_option)) + $libxml_options = $libxml_options | constant($libxml_option); + + // Transform the files to HTML. + $i = 0; + $num_files = count($new_exportfiles); + $max_transform_time = 0; + $xmldoc = new DOMDocument(); + foreach ($new_exportfiles as $exportfile) + { + if (function_exists('apache_reset_timeout')) + @apache_reset_timeout(); + + $started = microtime(true); + $xmldoc->load($exportfile, $libxml_options); + $xsltproc->transformToURI($xmldoc, $exportfile); + $finished = microtime(true); + + $max_transform_time = max($max_transform_time, $finished - $started); + + // When deadlines loom, sometimes the best solution is procrastination. + if (++$i < $num_files && TIME_START + $this->time_limit < $finished + $max_transform_time * 2) + { + // After all, there's always next time. + if (empty($this->next_task)) + { + $progressfile = $export_dir_slash . $idhash_ext . '.progress.json'; + + $new_details = $this->_details; + $new_details['start'] = $smcFunc['json_decode'](file_get_contents($progressfile), true); + + $this->next_task = array('$sourcedir/tasks/ExportProfileData.php', 'ExportProfileData_Background', $smcFunc['json_encode']($new_details), time() - MAX_CLAIM_THRESHOLD); + } + + // So let's just relax and take a well deserved... + break; + } + } + } + + /** + * Compiles profile data to XML with embedded XSLT. + * + * Internally calls exportXml() and then embeds an XSLT stylesheet into + * the XML so that it can be processed by the client. + * + * @param array $member_info Minimal $user_info about the relevant member. + */ + protected function exportXmlXslt($member_info) + { + global $modSettings, $context, $smcFunc, $sourcedir; + + $context['export_last_page'] = $this->_details['last_page']; + $context['export_dlfilename'] = $this->_details['dlfilename']; + + // Embedded XSLT requires adding a special DTD and processing instruction in the main XML document. + add_integration_function('integrate_xml_data', 'ExportProfileData_Background::add_dtd', false); + + // Perform the export to XML. + $this->exportXml($member_info); + + // Make sure we have everything we need. + if (empty(self::$xslt_info['stylesheet'])) + { + require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php'); + self::$xslt_info = get_xslt_stylesheet($this->_details['format'], $this->_details['uid']); + } + if (empty($context['feed']['footer'])) + { + require_once($sourcedir . DIRECTORY_SEPARATOR . 'News.php'); + buildXmlFeed('smf', array(), array_fill_keys(array('title', 'desc', 'source', 'self'), ''), 'profile'); + } + + // Find any completed files that don't yet have the stylesheet embedded in them. + $export_dir_slash = $modSettings['export_dir'] . DIRECTORY_SEPARATOR; + $idhash = hash_hmac('sha1', $this->_details['uid'], get_auth_secret()); + $idhash_ext = $idhash . '.' . $this->_details['format_settings']['extension']; + + $test_length = strlen(self::$xslt_info['stylesheet'] . $context['feed']['footer']); + + $new_exportfiles = array(); + clearstatcache(); + foreach (glob($export_dir_slash . '*_' . $idhash_ext) as $completed_file) + { + if (filesize($completed_file) < $test_length || file_get_contents($completed_file, false, null, $test_length * -1) !== self::$xslt_info['stylesheet'] . $context['feed']['footer']) + $new_exportfiles[] = $completed_file; + } + if (empty($new_exportfiles)) + return; + + // Embedding the XSLT means writing to the file yet again. + foreach ($new_exportfiles as $exportfile) + { + $handle = fopen($exportfile, 'r+'); + if (is_resource($handle)) + { + flock($handle, LOCK_EX); + + fseek($handle, strlen($context['feed']['footer']) * -1, SEEK_END); + + $bytes_written = fwrite($handle, self::$xslt_info['stylesheet'] . $context['feed']['footer']); + + // If we couldn't write everything, revert the changes. + if ($bytes_written > 0 && $bytes_written < strlen(self::$xslt_info['stylesheet'] . $context['feed']['footer'])) + { + fseek($handle, $bytes_written * -1, SEEK_END); + $pointer_pos = ftell($handle); + ftruncate($handle, $pointer_pos); + rewind($handle); + fseek($handle, 0, SEEK_END); + fwrite($handle, $context['feed']['footer']); + } + + flock($handle, LOCK_UN); + fclose($handle); + } + } + } + + /** + * Adds a custom DOCTYPE definition and an XSLT processing instruction to + * the main XML file's header. + */ + public static function add_dtd(&$xml_data, &$feed_meta, &$namespaces, &$extraFeedTags, &$forceCdataKeys, &$nsKeys, $xml_format, $subaction, &$doctype) + { + global $sourcedir; + + require_once($sourcedir . DIRECTORY_SEPARATOR . 'Profile-Export.php'); + self::$xslt_info = get_xslt_stylesheet(self::$export_details['format'], self::$export_details['uid']); + + $doctype = self::$xslt_info['doctype']; + } + + /** + * Adjusts some parse_bbc() parameters for the special case of exports. + */ + public static function pre_parsebbc(&$message, &$smileys, &$cache_id, &$parse_tags) + { + global $modSettings, $context, $user_info; + + $cache_id = ''; + + if (in_array(self::$export_details['format'], array('HTML', 'XML_XSLT'))) + { + foreach (array('smileys_url', 'attachmentThumbnails') as $var) + if (isset($modSettings[$var])) + self::$real_modSettings[$var] = $modSettings[$var]; + + $modSettings['smileys_url'] = '.'; + $modSettings['attachmentThumbnails'] = false; + } + else + { + $smileys = false; + + if (!isset($modSettings['disabledBBC'])) + $modSettings['disabledBBC'] = 'attach'; + else + { + self::$real_modSettings['disabledBBC'] = $modSettings['disabledBBC']; + + if (strpos($modSettings['disabledBBC'], 'attach') === false) + $modSettings['disabledBBC'] = implode(',', array_merge(array_filter(explode(',', $modSettings['disabledBBC'])), array('attach'))); + } + } + } + + /** + * Reverses changes made by pre_parsebbc() + */ + public static function post_parsebbc(&$message, &$smileys, &$cache_id, &$parse_tags) + { + global $modSettings, $context; + + foreach (array('disabledBBC', 'smileys_url', 'attachmentThumbnails') as $var) + if (isset(self::$real_modSettings[$var])) + $modSettings[$var] = self::$real_modSettings[$var]; + } + + /** + * Adjusts certain BBCodes for the special case of exports. + */ + public static function bbc_codes(&$codes, &$no_autolink_tags) + { + foreach ($codes as &$code) + { + // To make the "Select" link work we'd need to embed a bunch more JS. Not worth it. + if ($code['tag'] === 'code') + $code['content'] = preg_replace('~' . $txt['export_download_original'] . ''; + $hidden_orig_link = ' '; + + if ($params['{display}'] == 'link') + { + $returnContext .= ' (' . $orig_link . ')'; + } + elseif (!empty($currentAttachment['is_image'])) + { + $returnContext = '' . preg_replace( + array( + 'thumbnail_toggle' => '~]*>~', + 'src' => '~src="' . preg_quote($currentAttachment['href'], '~') . ';image"~', + ), + array( + 'thumbnail_toggle' => '', + 'src' => 'src="' . $currentAttachment['href'] . '" onerror="$(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"', + ), + $returnContext + ) . $hidden_orig_link . '' ; + } + elseif (strpos($currentAttachment['mime_type'], 'video/') === 0) + { + $returnContext = preg_replace( + array( + 'src' => '~src="' . preg_quote($currentAttachment['href'], '~') . '"~', + 'opening_tag' => '~^
    '~
    $~', + ), + array( + 'src' => '$0 onerror="$(this).fadeTo(0, 0.2); $(\'.dlattach_' . $currentAttachment['id'] . '\').show(); $(\'.dlattach_' . $currentAttachment['id'] . '\').css({\'position\': \'absolute\'});"', + 'opening_tag' => '
    $hidden_orig_link . '
    ', + ), + $returnContext + ); + } + elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0) + { + $returnContext = '' . preg_replace( + array( + 'opening_tag' => '~^ '