diff options
Diffstat (limited to 'include')
52 files changed, 10234 insertions, 0 deletions
diff --git a/include/addons.php b/include/addons.php new file mode 100644 index 0000000..8a0ff48 --- /dev/null +++ b/include/addons.php @@ -0,0 +1,84 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +// Make sure no one attempts to run this script "directly" +if (!defined('PUN')) + exit; + + +/** + * Class flux_addon_manager + * + * This class is responsible for loading the addons and storing their hook listeners. + */ +class flux_addon_manager +{ + var $hooks = array(); + + var $loaded = false; + + function load() + { + $this->loaded = true; + + $d = dir(PUN_ROOT.'addons'); + if (!$d) return; + + while (($addon_file = $d->read()) !== false) + { + if (!is_dir(PUN_ROOT.'addons/'.$addon_file) && preg_match('%(\w+)\.php$%', $addon_file)) + { + $addon_name = 'addon_'.substr($addon_file, 0, -4); + + include PUN_ROOT.'addons/'.$addon_file; + $addon = new $addon_name; + + $addon->register($this); + } + } + $d->close(); + } + + function bind($hook, $callback) + { + if (!isset($this->hooks[$hook])) + $this->hooks[$hook] = array(); + + if (is_callable($callback)) + $this->hooks[$hook][] = $callback; + } + + function hook($name) + { + if (!$this->loaded) + $this->load(); + + $callbacks = isset($this->hooks[$name]) ? $this->hooks[$name] : array(); + + // Execute every registered callback for this hook + foreach ($callbacks as $callback) + { + list($addon, $method) = $callback; + $addon->$method(); + } + } +} + + +/** + * Class flux_addon + * + * This class can be extended to provide addon functionality. + * Subclasses should implement the register method which will be called so that they have a chance to register possible + * listeners for all hooks. + */ +class flux_addon +{ + function register($manager) + { } +} diff --git a/include/cache.php b/include/cache.php new file mode 100644 index 0000000..c1947ae --- /dev/null +++ b/include/cache.php @@ -0,0 +1,263 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +// Make sure no one attempts to run this script "directly" +if (!defined('PUN')) + exit; + + +// +// Generate the config cache PHP script +// +function generate_config_cache() +{ + global $db; + + // Get the forum config from the DB + $result = $db->query('SELECT * FROM '.$db->prefix.'config', true) or error('Unable to fetch forum config', __FILE__, __LINE__, $db->error()); + + $output = array(); + while ($cur_config_item = $db->fetch_row($result)) + $output[$cur_config_item[0]] = $cur_config_item[1]; + + // Output config as PHP code + $content = '<?php'."\n\n".'define(\'PUN_CONFIG_LOADED\', 1);'."\n\n".'$pun_config = '.var_export($output, true).';'."\n\n".'?>'; + fluxbb_write_cache_file('cache_config.php', $content); +} + + +// +// Generate the bans cache PHP script +// +function generate_bans_cache() +{ + global $db; + + // Get the ban list from the DB + $result = $db->query('SELECT * FROM '.$db->prefix.'bans', true) or error('Unable to fetch ban list', __FILE__, __LINE__, $db->error()); + + $output = array(); + while ($cur_ban = $db->fetch_assoc($result)) + $output[] = $cur_ban; + + // Output ban list as PHP code + $content = '<?php'."\n\n".'define(\'PUN_BANS_LOADED\', 1);'."\n\n".'$pun_bans = '.var_export($output, true).';'."\n\n".'?>'; + fluxbb_write_cache_file('cache_bans.php', $content); +} + + +// +// Generate quick jump cache PHP scripts +// +function generate_quickjump_cache($group_id = false) +{ + global $db, $lang_common; + + $groups = array(); + + // If a group_id was supplied, we generate the quick jump cache for that group only + if ($group_id !== false) + { + // Is this group even allowed to read forums? + $result = $db->query('SELECT g_read_board FROM '.$db->prefix.'groups WHERE g_id='.$group_id) or error('Unable to fetch user group read permission', __FILE__, __LINE__, $db->error()); + $read_board = $db->result($result); + + $groups[$group_id] = $read_board; + } + else + { + // A group_id was not supplied, so we generate the quick jump cache for all groups + $result = $db->query('SELECT g_id, g_read_board FROM '.$db->prefix.'groups') or error('Unable to fetch user group list', __FILE__, __LINE__, $db->error()); + + while ($row = $db->fetch_row($result)) + $groups[$row[0]] = $row[1]; + } + + // Loop through the groups in $groups and output the cache for each of them + foreach ($groups as $group_id => $read_board) + { + // Output quick jump as PHP code + $output = '<?php'."\n\n".'if (!defined(\'PUN\')) exit;'."\n".'define(\'PUN_QJ_LOADED\', 1);'."\n".'$forum_id = isset($forum_id) ? $forum_id : 0;'."\n\n".'?>'; + + if ($read_board == '1') + { + $result = $db->query('SELECT c.id AS cid, c.cat_name, f.id AS fid, f.forum_name, f.redirect_url FROM '.$db->prefix.'categories AS c INNER JOIN '.$db->prefix.'forums AS f ON c.id=f.cat_id LEFT JOIN '.$db->prefix.'forum_perms AS fp ON (fp.forum_id=f.id AND fp.group_id='.$group_id.') WHERE fp.read_forum IS NULL OR fp.read_forum=1 ORDER BY c.disp_position, c.id, f.disp_position') or error('Unable to fetch category/forum list', __FILE__, __LINE__, $db->error()); + + if ($db->num_rows($result)) + { + $output .= "\t\t\t\t".'<form id="qjump" method="get" action="viewforum.php">'."\n\t\t\t\t\t".'<div><label><span><?php echo $lang_common[\'Jump to\'] ?>'.'<br /></span>'."\n\t\t\t\t\t".'<select name="id" onchange="window.location=(\'viewforum.php?id=\'+this.options[this.selectedIndex].value)">'."\n"; + + $cur_category = 0; + while ($cur_forum = $db->fetch_assoc($result)) + { + if ($cur_forum['cid'] != $cur_category) // A new category since last iteration? + { + if ($cur_category) + $output .= "\t\t\t\t\t\t".'</optgroup>'."\n"; + + $output .= "\t\t\t\t\t\t".'<optgroup label="'.pun_htmlspecialchars($cur_forum['cat_name']).'">'."\n"; + $cur_category = $cur_forum['cid']; + } + + $redirect_tag = ($cur_forum['redirect_url'] != '') ? ' >>>' : ''; + $output .= "\t\t\t\t\t\t\t".'<option value="'.$cur_forum['fid'].'"<?php echo ($forum_id == '.$cur_forum['fid'].') ? \' selected="selected"\' : \'\' ?>>'.pun_htmlspecialchars($cur_forum['forum_name']).$redirect_tag.'</option>'."\n"; + } + + $output .= "\t\t\t\t\t\t".'</optgroup>'."\n\t\t\t\t\t".'</select></label>'."\n\t\t\t\t\t".'<input type="submit" value="<?php echo $lang_common[\'Go\'] ?>" accesskey="g" />'."\n\t\t\t\t\t".'</div>'."\n\t\t\t\t".'</form>'."\n"; + } + } + + fluxbb_write_cache_file('cache_quickjump_'.$group_id.'.php', $output); + } +} + + +// +// Generate the censoring cache PHP script +// +function generate_censoring_cache() +{ + global $db; + + $result = $db->query('SELECT search_for, replace_with FROM '.$db->prefix.'censoring') or error('Unable to fetch censoring list', __FILE__, __LINE__, $db->error()); + $num_words = $db->num_rows($result); + + $search_for = $replace_with = array(); + for ($i = 0; $i < $num_words; $i++) + { + list($search_for[$i], $replace_with[$i]) = $db->fetch_row($result); + $search_for[$i] = '%(?<=[^\p{L}\p{N}])('.str_replace('\*', '[\p{L}\p{N}]*?', preg_quote($search_for[$i], '%')).')(?=[^\p{L}\p{N}])%iu'; + } + + // Output censored words as PHP code + $content = '<?php'."\n\n".'define(\'PUN_CENSOR_LOADED\', 1);'."\n\n".'$search_for = '.var_export($search_for, true).';'."\n\n".'$replace_with = '.var_export($replace_with, true).';'."\n\n".'?>'; + fluxbb_write_cache_file('cache_censoring.php', $content); +} + + +// +// Generate the stopwords cache PHP script +// +function generate_stopwords_cache() +{ + $stopwords = array(); + + $d = dir(PUN_ROOT.'lang'); + while (($entry = $d->read()) !== false) + { + if ($entry{0} == '.') + continue; + + if (is_dir(PUN_ROOT.'lang/'.$entry) && file_exists(PUN_ROOT.'lang/'.$entry.'/stopwords.txt')) + $stopwords = array_merge($stopwords, file(PUN_ROOT.'lang/'.$entry.'/stopwords.txt')); + } + $d->close(); + + // Tidy up and filter the stopwords + $stopwords = array_map('pun_trim', $stopwords); + $stopwords = array_filter($stopwords); + + // Output stopwords as PHP code + $content = '<?php'."\n\n".'$cache_id = \''.generate_stopwords_cache_id().'\';'."\n".'if ($cache_id != generate_stopwords_cache_id()) return;'."\n\n".'define(\'PUN_STOPWORDS_LOADED\', 1);'."\n\n".'$stopwords = '.var_export($stopwords, true).';'."\n\n".'?>'; + fluxbb_write_cache_file('cache_stopwords.php', $content); +} + + +// +// Load some information about the latest registered users +// +function generate_users_info_cache() +{ + global $db; + + $stats = array(); + + $result = $db->query('SELECT COUNT(id)-1 FROM '.$db->prefix.'users WHERE group_id!='.PUN_UNVERIFIED) or error('Unable to fetch total user count', __FILE__, __LINE__, $db->error()); + $stats['total_users'] = $db->result($result); + + $result = $db->query('SELECT id, username FROM '.$db->prefix.'users WHERE group_id!='.PUN_UNVERIFIED.' ORDER BY registered DESC LIMIT 1') or error('Unable to fetch newest registered user', __FILE__, __LINE__, $db->error()); + $stats['last_user'] = $db->fetch_assoc($result); + + // Output users info as PHP code + $content = '<?php'."\n\n".'define(\'PUN_USERS_INFO_LOADED\', 1);'."\n\n".'$stats = '.var_export($stats, true).';'."\n\n".'?>'; + fluxbb_write_cache_file('cache_users_info.php', $content); +} + + +// +// Generate the admins cache PHP script +// +function generate_admins_cache() +{ + global $db; + + // Get admins from the DB + $result = $db->query('SELECT id FROM '.$db->prefix.'users WHERE group_id='.PUN_ADMIN) or error('Unable to fetch users info', __FILE__, __LINE__, $db->error()); + + $output = array(); + while ($row = $db->fetch_row($result)) + $output[] = $row[0]; + + // Output admin list as PHP code + $content = '<?php'."\n\n".'define(\'PUN_ADMINS_LOADED\', 1);'."\n\n".'$pun_admins = '.var_export($output, true).';'."\n\n".'?>'; + fluxbb_write_cache_file('cache_admins.php', $content); +} + + +// +// Safely write out a cache file. +// +function fluxbb_write_cache_file($file, $content) +{ + $fh = @fopen(FORUM_CACHE_DIR.$file, 'wb'); + if (!$fh) + error('Unable to write cache file '.pun_htmlspecialchars($file).' to cache directory. Please make sure PHP has write access to the directory \''.pun_htmlspecialchars(FORUM_CACHE_DIR).'\'', __FILE__, __LINE__); + + flock($fh, LOCK_EX); + ftruncate($fh, 0); + + fwrite($fh, $content); + + flock($fh, LOCK_UN); + fclose($fh); + + fluxbb_invalidate_cached_file(FORUM_CACHE_DIR.$file); +} + + +// +// Delete all feed caches +// +function clear_feed_cache() +{ + $d = dir(FORUM_CACHE_DIR); + while (($entry = $d->read()) !== false) + { + if (substr($entry, 0, 10) == 'cache_feed' && substr($entry, -4) == '.php') + { + @unlink(FORUM_CACHE_DIR.$entry); + fluxbb_invalidate_cached_file(FORUM_CACHE_DIR.$entry); + } + } + $d->close(); +} + + +// +// Invalidate updated php files that are cached by an opcache +// +function fluxbb_invalidate_cached_file($file) +{ + if (function_exists('opcache_invalidate')) + opcache_invalidate($file, true); + elseif (function_exists('apc_delete_file')) + @apc_delete_file($file); +} + + +define('FORUM_CACHE_FUNCTIONS_LOADED', true); diff --git a/include/common.php b/include/common.php new file mode 100644 index 0000000..ad34b5e --- /dev/null +++ b/include/common.php @@ -0,0 +1,209 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +if (!defined('PUN_ROOT')) + exit('The constant PUN_ROOT must be defined and point to a valid FluxBB installation root directory.'); + +// Define the version and database revision that this code was written for +define('FORUM_VERSION', '1.5.11'); + +define('FORUM_DB_REVISION', 21); +define('FORUM_SI_REVISION', 2); +define('FORUM_PARSER_REVISION', 2); + +// Block prefetch requests +if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch') +{ + header('HTTP/1.1 403 Prefetching Forbidden'); + + // Send no-cache headers + header('Expires: Thu, 21 Jul 1977 07:30:00 GMT'); // When yours truly first set eyes on this world! :) + header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); // For HTTP/1.0 compatibility + + exit; +} + +// Attempt to load the configuration file config.php +if (file_exists(PUN_ROOT.'config.php')) + require PUN_ROOT.'config.php'; + +// If we have the 1.3-legacy constant defined, define the proper 1.4 constant so we don't get an incorrect "need to install" message +if (defined('FORUM')) + define('PUN', FORUM); + +// If PUN isn't defined, config.php is missing or corrupt +if (!defined('PUN')) +{ + header('Location: install.php'); + exit; +} + +// Load the functions script +require PUN_ROOT.'include/functions.php'; + +// Load addon functionality +require PUN_ROOT.'include/addons.php'; + +// Load UTF-8 functions +require PUN_ROOT.'include/utf8/utf8.php'; + +// Strip out "bad" UTF-8 characters +forum_remove_bad_characters(); + +// Reverse the effect of register_globals +forum_unregister_globals(); + +// The addon manager is responsible for storing the hook listeners and communicating with the addons +$flux_addons = new flux_addon_manager(); + +// Record the start time (will be used to calculate the generation time for the page) +$pun_start = get_microtime(); + +// Seed the random number generator for systems where this does not happen automatically +mt_srand(); + +// Make sure PHP reports all errors except E_NOTICE. FluxBB supports E_ALL, but a lot of scripts it may interact with, do not +// We set this in php.ini +//error_reporting(E_ALL ^ E_NOTICE); + +// Force POSIX locale (to prevent functions such as strtolower() from messing up UTF-8 strings) +setlocale(LC_CTYPE, 'C'); + +// Turn off magic_quotes_runtime +if (get_magic_quotes_runtime()) + set_magic_quotes_runtime(0); + +// Strip slashes from GET/POST/COOKIE/REQUEST/FILES (if magic_quotes_gpc is enabled) +if (!defined('FORUM_DISABLE_STRIPSLASHES') && get_magic_quotes_gpc()) +{ + function stripslashes_array($array) + { + return is_array($array) ? array_map('stripslashes_array', $array) : stripslashes($array); + } + + $_GET = stripslashes_array($_GET); + $_POST = stripslashes_array($_POST); + $_COOKIE = stripslashes_array($_COOKIE); + $_REQUEST = stripslashes_array($_REQUEST); + if (is_array($_FILES)) + { + // Don't strip valid slashes from tmp_name path on Windows + foreach ($_FILES AS $key => $value) + $_FILES[$key]['tmp_name'] = str_replace('\\', '\\\\', $value['tmp_name']); + $_FILES = stripslashes_array($_FILES); + } +} + +// If a cookie name is not specified in config.php, we use the default (pun_cookie) +if (empty($cookie_name)) + $cookie_name = 'pun_cookie'; + +// If the cache directory is not specified, we use the default setting +if (!defined('FORUM_CACHE_DIR')) + define('FORUM_CACHE_DIR', PUN_ROOT.'cache/'); + +// Define a few commonly used constants +define('PUN_UNVERIFIED', 0); +define('PUN_ADMIN', 1); +define('PUN_MOD', 2); +define('PUN_GUEST', 3); +define('PUN_MEMBER', 4); + +// Load DB abstraction layer and connect +require PUN_ROOT.'include/dblayer/common_db.php'; + +// Start a transaction +$db->start_transaction(); + +// Load cached config +if (file_exists(FORUM_CACHE_DIR.'cache_config.php')) + include FORUM_CACHE_DIR.'cache_config.php'; + +if (!defined('PUN_CONFIG_LOADED')) +{ + if (!defined('FORUM_CACHE_FUNCTIONS_LOADED')) + require PUN_ROOT.'include/cache.php'; + + generate_config_cache(); + require FORUM_CACHE_DIR.'cache_config.php'; +} + +// Verify that we are running the proper database schema revision +if (!isset($pun_config['o_database_revision']) || $pun_config['o_database_revision'] < FORUM_DB_REVISION || + !isset($pun_config['o_searchindex_revision']) || $pun_config['o_searchindex_revision'] < FORUM_SI_REVISION || + !isset($pun_config['o_parser_revision']) || $pun_config['o_parser_revision'] < FORUM_PARSER_REVISION || + version_compare($pun_config['o_cur_version'], FORUM_VERSION, '<')) +{ + header('Location: db_update.php'); + exit; +} + +// Enable output buffering +if (!defined('PUN_DISABLE_BUFFERING')) +{ + // Should we use gzip output compression? + if ($pun_config['o_gzip'] && extension_loaded('zlib')) + ob_start('ob_gzhandler'); + else + ob_start(); +} + +// Define standard date/time formats +$forum_time_formats = array($pun_config['o_time_format'], 'H:i:s', 'H:i', 'g:i:s a', 'g:i a'); +$forum_date_formats = array($pun_config['o_date_format'], 'Y-m-d', 'Y-d-m', 'd-m-Y', 'm-d-Y', 'M j Y', 'jS M Y'); + +// Check/update/set cookie and fetch user info +$pun_user = array(); +check_cookie($pun_user); + +// Attempt to load the common language file +if (file_exists(PUN_ROOT.'lang/'.$pun_user['language'].'/common.php')) + include PUN_ROOT.'lang/'.$pun_user['language'].'/common.php'; +else + error('There is no valid language pack \''.pun_htmlspecialchars($pun_user['language']).'\' installed. Please reinstall a language of that name'); + +// Check if we are to display a maintenance message +if ($pun_config['o_maintenance'] && $pun_user['g_id'] > PUN_ADMIN && !defined('PUN_TURN_OFF_MAINT')) + maintenance_message(); + +// Load cached bans +if (file_exists(FORUM_CACHE_DIR.'cache_bans.php')) + include FORUM_CACHE_DIR.'cache_bans.php'; + +if (!defined('PUN_BANS_LOADED')) +{ + if (!defined('FORUM_CACHE_FUNCTIONS_LOADED')) + require PUN_ROOT.'include/cache.php'; + + generate_bans_cache(); + require FORUM_CACHE_DIR.'cache_bans.php'; +} + +// Check if current user is banned +check_bans(); + +// Update online list +update_users_online(); + +// Check to see if we logged in without a cookie being set +if ($pun_user['is_guest'] && isset($_GET['login'])) + message($lang_common['No cookie']); + +// The maximum size of a post, in bytes, since the field is now MEDIUMTEXT this allows ~16MB but lets cap at 1MB... +if (!defined('PUN_MAX_POSTSIZE')) + define('PUN_MAX_POSTSIZE', 1048576); + +if (!defined('PUN_SEARCH_MIN_WORD')) + define('PUN_SEARCH_MIN_WORD', 3); +if (!defined('PUN_SEARCH_MAX_WORD')) + define('PUN_SEARCH_MAX_WORD', 20); + +if (!defined('FORUM_MAX_COOKIE_SIZE')) + define('FORUM_MAX_COOKIE_SIZE', 4048); diff --git a/include/common_admin.php b/include/common_admin.php new file mode 100644 index 0000000..bb6ce50 --- /dev/null +++ b/include/common_admin.php @@ -0,0 +1,174 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +// Make sure no one attempts to run this script "directly" +if (!defined('PUN')) + exit; + +// Make sure we have a usable language pack for admin. +if (file_exists(PUN_ROOT.'lang/'.$pun_user['language'].'/admin_common.php')) + $admin_language = $pun_user['language']; +else if (file_exists(PUN_ROOT.'lang/'.$pun_config['o_default_lang'].'/admin_common.php')) + $admin_language = $pun_config['o_default_lang']; +else + $admin_language = 'English'; + +// Attempt to load the admin_common language file +require PUN_ROOT.'lang/'.$admin_language.'/admin_common.php'; + +// +// Fetch a list of available admin plugins +// +function forum_list_plugins($is_admin) +{ + $plugins = array(); + + $d = dir(PUN_ROOT.'plugins'); + if (!$d) return $plugins; + + while (($entry = $d->read()) !== false) + { + if (!is_dir(PUN_ROOT.'plugins/'.$entry) && preg_match('%^AM?P_(\w+)\.php$%i', $entry)) + { + $prefix = substr($entry, 0, strpos($entry, '_')); + + if ($prefix == 'AMP' || ($is_admin && $prefix == 'AP')) + $plugins[$entry] = substr($entry, strlen($prefix) + 1, -4); + } + } + $d->close(); + + natcasesort($plugins); + + return $plugins; +} + + +// +// Display the admin navigation menu +// +function generate_admin_menu($page = '') +{ + global $pun_config, $pun_user, $lang_admin_common; + + $is_admin = $pun_user['g_id'] == PUN_ADMIN ? true : false; + +?> +<div id="adminconsole" class="block2col"> + <div id="adminmenu" class="blockmenu"> + <h2><span><?php echo $lang_admin_common['Moderator menu'] ?></span></h2> + <div class="box"> + <div class="inbox"> + <ul> + <li<?php if ($page == 'index') echo ' class="isactive"'; ?>><a href="admin_index.php"><?php echo $lang_admin_common['Index'] ?></a></li> + <li<?php if ($page == 'users') echo ' class="isactive"'; ?>><a href="admin_users.php"><?php echo $lang_admin_common['Users'] ?></a></li> +<?php if ($is_admin || $pun_user['g_mod_ban_users'] == '1'): ?> <li<?php if ($page == 'bans') echo ' class="isactive"'; ?>><a href="admin_bans.php"><?php echo $lang_admin_common['Bans'] ?></a></li> +<?php endif; if ($is_admin || $pun_config['o_report_method'] == '0' || $pun_config['o_report_method'] == '2'): ?> <li<?php if ($page == 'reports') echo ' class="isactive"'; ?>><a href="admin_reports.php"><?php echo $lang_admin_common['Reports'] ?></a></li> +<?php endif; ?> </ul> + </div> + </div> +<?php + + if ($is_admin) + { + +?> + <h2 class="block2"><span><?php echo $lang_admin_common['Admin menu'] ?></span></h2> + <div class="box"> + <div class="inbox"> + <ul> + <li<?php if ($page == 'options') echo ' class="isactive"'; ?>><a href="admin_options.php"><?php echo $lang_admin_common['Options'] ?></a></li> + <li<?php if ($page == 'permissions') echo ' class="isactive"'; ?>><a href="admin_permissions.php"><?php echo $lang_admin_common['Permissions'] ?></a></li> + <li<?php if ($page == 'categories') echo ' class="isactive"'; ?>><a href="admin_categories.php"><?php echo $lang_admin_common['Categories'] ?></a></li> + <li<?php if ($page == 'forums') echo ' class="isactive"'; ?>><a href="admin_forums.php"><?php echo $lang_admin_common['Forums'] ?></a></li> + <li<?php if ($page == 'groups') echo ' class="isactive"'; ?>><a href="admin_groups.php"><?php echo $lang_admin_common['User groups'] ?></a></li> + <li<?php if ($page == 'censoring') echo ' class="isactive"'; ?>><a href="admin_censoring.php"><?php echo $lang_admin_common['Censoring'] ?></a></li> + <li<?php if ($page == 'maintenance') echo ' class="isactive"'; ?>><a href="admin_maintenance.php"><?php echo $lang_admin_common['Maintenance'] ?></a></li> + </ul> + </div> + </div> +<?php + + } + + // See if there are any plugins + $plugins = forum_list_plugins($is_admin); + + // Did we find any plugins? + if (!empty($plugins)) + { + +?> + <h2 class="block2"><span><?php echo $lang_admin_common['Plugins menu'] ?></span></h2> + <div class="box"> + <div class="inbox"> + <ul> +<?php + + foreach ($plugins as $plugin_name => $plugin) + echo "\t\t\t\t\t".'<li'.(($page == $plugin_name) ? ' class="isactive"' : '').'><a href="admin_loader.php?plugin='.$plugin_name.'">'.str_replace('_', ' ', $plugin).'</a></li>'."\n"; + +?> + </ul> + </div> + </div> +<?php + + } + +?> + </div> + +<?php + +} + + +// +// Delete topics from $forum_id that are "older than" $prune_date (if $prune_sticky is 1, sticky topics will also be deleted) +// +function prune($forum_id, $prune_sticky, $prune_date) +{ + global $db; + + $extra_sql = ($prune_date != -1) ? ' AND last_post<'.$prune_date : ''; + + if (!$prune_sticky) + $extra_sql .= ' AND sticky=\'0\''; + + // Fetch topics to prune + $result = $db->query('SELECT id FROM '.$db->prefix.'topics WHERE forum_id='.$forum_id.$extra_sql, true) or error('Unable to fetch topics', __FILE__, __LINE__, $db->error()); + + $topic_ids = ''; + while ($row = $db->fetch_row($result)) + $topic_ids .= (($topic_ids != '') ? ',' : '').$row[0]; + + if ($topic_ids != '') + { + // Fetch posts to prune + $result = $db->query('SELECT id FROM '.$db->prefix.'posts WHERE topic_id IN('.$topic_ids.')', true) or error('Unable to fetch posts', __FILE__, __LINE__, $db->error()); + + $post_ids = ''; + while ($row = $db->fetch_row($result)) + $post_ids .= (($post_ids != '') ? ',' : '').$row[0]; + + if ($post_ids != '') + { + // Delete topics + $db->query('DELETE FROM '.$db->prefix.'topics WHERE id IN('.$topic_ids.')') or error('Unable to prune topics', __FILE__, __LINE__, $db->error()); + // Delete subscriptions + $db->query('DELETE FROM '.$db->prefix.'topic_subscriptions WHERE topic_id IN('.$topic_ids.')') or error('Unable to prune subscriptions', __FILE__, __LINE__, $db->error()); + // Delete posts + $db->query('DELETE FROM '.$db->prefix.'posts WHERE id IN('.$post_ids.')') or error('Unable to prune posts', __FILE__, __LINE__, $db->error()); + + // We removed a bunch of posts, so now we have to update the search index + require_once PUN_ROOT.'include/search_idx.php'; + strip_search_index($post_ids); + } + } +} diff --git a/include/dblayer/common_db.php b/include/dblayer/common_db.php new file mode 100644 index 0000000..5b9e67e --- /dev/null +++ b/include/dblayer/common_db.php @@ -0,0 +1,48 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +// Make sure no one attempts to run this script "directly" +if (!defined('PUN')) + exit; + + +// Load the appropriate DB layer class +switch ($db_type) +{ + case 'mysql': + require_once PUN_ROOT.'include/dblayer/mysql.php'; + break; + + case 'mysql_innodb': + require_once PUN_ROOT.'include/dblayer/mysql_innodb.php'; + break; + + case 'mysqli': + require_once PUN_ROOT.'include/dblayer/mysqli.php'; + break; + + case 'mysqli_innodb': + require_once PUN_ROOT.'include/dblayer/mysqli_innodb.php'; + break; + + case 'pgsql': + require_once PUN_ROOT.'include/dblayer/pgsql.php'; + break; + + case 'sqlite': + require_once PUN_ROOT.'include/dblayer/sqlite.php'; + break; + + default: + error('\''.$db_type.'\' is not a valid database type. Please check settings in config.php.', __FILE__, __LINE__); + break; +} + + +// Create the database adapter object (and open/connect to/select db) +$db = new DBLayer($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect); diff --git a/include/dblayer/index.html b/include/dblayer/index.html new file mode 100644 index 0000000..89337b2 --- /dev/null +++ b/include/dblayer/index.html @@ -0,0 +1 @@ +<html><head><title>.</title></head><body>.</body></html> diff --git a/include/dblayer/mysql.php b/include/dblayer/mysql.php new file mode 100644 index 0000000..1b36648 --- /dev/null +++ b/include/dblayer/mysql.php @@ -0,0 +1,378 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +// Make sure we have built in support for MySQL +if (!function_exists('mysql_connect')) + exit('This PHP environment doesn\'t have MySQL support built in. MySQL support is required if you want to use a MySQL database to run this forum. Consult the PHP documentation for further assistance.'); + + +class DBLayer +{ + var $prefix; + var $link_id; + var $query_result; + + var $saved_queries = array(); + var $num_queries = 0; + + var $error_no = false; + var $error_msg = 'Unknown'; + + var $datatype_transformations = array( + '%^SERIAL$%' => 'INT(10) UNSIGNED AUTO_INCREMENT' + ); + + + function __construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect) + { + $this->prefix = $db_prefix; + + if ($p_connect) + $this->link_id = @mysql_pconnect($db_host, $db_username, $db_password); + else + $this->link_id = @mysql_connect($db_host, $db_username, $db_password); + + if ($this->link_id) + { + if (!@mysql_select_db($db_name, $this->link_id)) + error('Unable to select database. MySQL reported: '.mysql_error(), __FILE__, __LINE__); + } + else + error('Unable to connect to MySQL server. MySQL reported: '.mysql_error(), __FILE__, __LINE__); + + // Setup the client-server character set (UTF-8) + if (!defined('FORUM_NO_SET_NAMES')) + $this->set_names('utf8'); + + return $this->link_id; + } + + + function DBLayer($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect) + { + $this->__construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect); + } + + + function start_transaction() + { + return; + } + + + function end_transaction() + { + return; + } + + + function query($sql, $unbuffered = false) + { + if (defined('PUN_SHOW_QUERIES')) + $q_start = get_microtime(); + + if ($unbuffered) + $this->query_result = @mysql_unbuffered_query($sql, $this->link_id); + else + $this->query_result = @mysql_query($sql, $this->link_id); + + if ($this->query_result) + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array($sql, sprintf('%.5F', get_microtime() - $q_start)); + + ++$this->num_queries; + + return $this->query_result; + } + else + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array($sql, 0); + + $this->error_no = @mysql_errno($this->link_id); + $this->error_msg = @mysql_error($this->link_id); + + return false; + } + } + + + function result($query_id = 0, $row = 0, $col = 0) + { + return ($query_id) ? @mysql_result($query_id, $row, $col) : false; + } + + + function fetch_assoc($query_id = 0) + { + return ($query_id) ? @mysql_fetch_assoc($query_id) : false; + } + + + function fetch_row($query_id = 0) + { + return ($query_id) ? @mysql_fetch_row($query_id) : false; + } + + + function num_rows($query_id = 0) + { + return ($query_id) ? @mysql_num_rows($query_id) : false; + } + + + function affected_rows() + { + return ($this->link_id) ? @mysql_affected_rows($this->link_id) : false; + } + + + function insert_id() + { + return ($this->link_id) ? @mysql_insert_id($this->link_id) : false; + } + + + function get_num_queries() + { + return $this->num_queries; + } + + + function get_saved_queries() + { + return $this->saved_queries; + } + + + function free_result($query_id = false) + { + return ($query_id) ? @mysql_free_result($query_id) : false; + } + + + function escape($str) + { + if (is_array($str)) + return ''; + else if (function_exists('mysql_real_escape_string')) + return mysql_real_escape_string($str, $this->link_id); + else + return mysql_escape_string($str); + } + + + function error() + { + $result['error_sql'] = @current(@end($this->saved_queries)); + $result['error_no'] = $this->error_no; + $result['error_msg'] = $this->error_msg; + + return $result; + } + + + function close() + { + if ($this->link_id) + { + if (is_resource($this->query_result)) + @mysql_free_result($this->query_result); + + return @mysql_close($this->link_id); + } + else + return false; + } + + function get_names() + { + $result = $this->query('SHOW VARIABLES LIKE \'character_set_connection\''); + return $this->result($result, 0, 1); + } + + + function set_names($names) + { + return $this->query('SET NAMES \''.$this->escape($names).'\''); + } + + + function get_version() + { + $result = $this->query('SELECT VERSION()'); + + return array( + 'name' => 'MySQL Standard', + 'version' => preg_replace('%^([^-]+).*$%', '\\1', $this->result($result)) + ); + } + + + function table_exists($table_name, $no_prefix = false) + { + $result = $this->query('SHOW TABLES LIKE \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\''); + return $this->num_rows($result) > 0; + } + + + function field_exists($table_name, $field_name, $no_prefix = false) + { + $result = $this->query('SHOW COLUMNS FROM '.($no_prefix ? '' : $this->prefix).$table_name.' LIKE \''.$this->escape($field_name).'\''); + return $this->num_rows($result) > 0; + } + + + function index_exists($table_name, $index_name, $no_prefix = false) + { + $exists = false; + + $result = $this->query('SHOW INDEX FROM '.($no_prefix ? '' : $this->prefix).$table_name); + while ($cur_index = $this->fetch_assoc($result)) + { + if (strtolower($cur_index['Key_name']) == strtolower(($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name)) + { + $exists = true; + break; + } + } + + return $exists; + } + + + function create_table($table_name, $schema, $no_prefix = false) + { + if ($this->table_exists($table_name, $no_prefix)) + return true; + + $query = 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name." (\n"; + + // Go through every schema element and add it to the query + foreach ($schema['FIELDS'] as $field_name => $field_data) + { + $field_data['datatype'] = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_data['datatype']); + + $query .= $field_name.' '.$field_data['datatype']; + + if (isset($field_data['collation'])) + $query .= 'CHARACTER SET utf8 COLLATE utf8_'.$field_data['collation']; + + if (!$field_data['allow_null']) + $query .= ' NOT NULL'; + + if (isset($field_data['default'])) + $query .= ' DEFAULT '.$field_data['default']; + + $query .= ",\n"; + } + + // If we have a primary key, add it + if (isset($schema['PRIMARY KEY'])) + $query .= 'PRIMARY KEY ('.implode(',', $schema['PRIMARY KEY']).'),'."\n"; + + // Add unique keys + if (isset($schema['UNIQUE KEYS'])) + { + foreach ($schema['UNIQUE KEYS'] as $key_name => $key_fields) + $query .= 'UNIQUE KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$key_name.'('.implode(',', $key_fields).'),'."\n"; + } + + // Add indexes + if (isset($schema['INDEXES'])) + { + foreach ($schema['INDEXES'] as $index_name => $index_fields) + $query .= 'KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.'('.implode(',', $index_fields).'),'."\n"; + } + + // We remove the last two characters (a newline and a comma) and add on the ending + $query = substr($query, 0, strlen($query) - 2)."\n".') ENGINE = '.(isset($schema['ENGINE']) ? $schema['ENGINE'] : 'MyISAM').' CHARACTER SET utf8'; + + return $this->query($query) ? true : false; + } + + + function drop_table($table_name, $no_prefix = false) + { + if (!$this->table_exists($table_name, $no_prefix)) + return true; + + return $this->query('DROP TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false; + } + + + function rename_table($old_table, $new_table, $no_prefix = false) + { + // If the new table exists and the old one doesn't, then we're happy + if ($this->table_exists($new_table, $no_prefix) && !$this->table_exists($old_table, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$old_table.' RENAME TO '.($no_prefix ? '' : $this->prefix).$new_table) ? true : false; + } + + + function add_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) + { + if ($this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + $field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type); + + if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value)) + $default_value = '\''.$this->escape($default_value).'\''; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false; + } + + + function alter_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) + { + if (!$this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + $field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type); + + if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value)) + $default_value = '\''.$this->escape($default_value).'\''; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' MODIFY '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false; + } + + + function drop_field($table_name, $field_name, $no_prefix = false) + { + if (!$this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP '.$field_name) ? true : false; + } + + + function add_index($table_name, $index_name, $index_fields, $unique = false, $no_prefix = false) + { + if ($this->index_exists($table_name, $index_name, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.($unique ? 'UNIQUE ' : '').'INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.' ('.implode(',', $index_fields).')') ? true : false; + } + + + function drop_index($table_name, $index_name, $no_prefix = false) + { + if (!$this->index_exists($table_name, $index_name, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name) ? true : false; + } + + function truncate_table($table_name, $no_prefix = false) + { + return $this->query('TRUNCATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false; + } +} diff --git a/include/dblayer/mysql_innodb.php b/include/dblayer/mysql_innodb.php new file mode 100644 index 0000000..d284f67 --- /dev/null +++ b/include/dblayer/mysql_innodb.php @@ -0,0 +1,392 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +// Make sure we have built in support for MySQL +if (!function_exists('mysql_connect')) + exit('This PHP environment doesn\'t have MySQL support built in. MySQL support is required if you want to use a MySQL database to run this forum. Consult the PHP documentation for further assistance.'); + + +class DBLayer +{ + var $prefix; + var $link_id; + var $query_result; + var $in_transaction = 0; + + var $saved_queries = array(); + var $num_queries = 0; + + var $error_no = false; + var $error_msg = 'Unknown'; + + var $datatype_transformations = array( + '%^SERIAL$%' => 'INT(10) UNSIGNED AUTO_INCREMENT' + ); + + + function __construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect) + { + $this->prefix = $db_prefix; + + if ($p_connect) + $this->link_id = @mysql_pconnect($db_host, $db_username, $db_password); + else + $this->link_id = @mysql_connect($db_host, $db_username, $db_password); + + if ($this->link_id) + { + if (!@mysql_select_db($db_name, $this->link_id)) + error('Unable to select database. MySQL reported: '.mysql_error(), __FILE__, __LINE__); + } + else + error('Unable to connect to MySQL server. MySQL reported: '.mysql_error(), __FILE__, __LINE__); + + // Setup the client-server character set (UTF-8) + if (!defined('FORUM_NO_SET_NAMES')) + $this->set_names('utf8'); + + return $this->link_id; + } + + + function DBLayer($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect) + { + $this->__construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect); + } + + + function start_transaction() + { + ++$this->in_transaction; + + mysql_query('START TRANSACTION', $this->link_id); + return; + } + + + function end_transaction() + { + --$this->in_transaction; + + mysql_query('COMMIT', $this->link_id); + return; + } + + + function query($sql, $unbuffered = false) + { + if (defined('PUN_SHOW_QUERIES')) + $q_start = get_microtime(); + + if ($unbuffered) + $this->query_result = @mysql_unbuffered_query($sql, $this->link_id); + else + $this->query_result = @mysql_query($sql, $this->link_id); + + if ($this->query_result) + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array($sql, sprintf('%.5F', get_microtime() - $q_start)); + + ++$this->num_queries; + + return $this->query_result; + } + else + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array($sql, 0); + + $this->error_no = @mysql_errno($this->link_id); + $this->error_msg = @mysql_error($this->link_id); + + // Rollback transaction + if ($this->in_transaction) + mysql_query('ROLLBACK', $this->link_id); + + --$this->in_transaction; + + return false; + } + } + + + function result($query_id = 0, $row = 0, $col = 0) + { + return ($query_id) ? @mysql_result($query_id, $row, $col) : false; + } + + + function fetch_assoc($query_id = 0) + { + return ($query_id) ? @mysql_fetch_assoc($query_id) : false; + } + + + function fetch_row($query_id = 0) + { + return ($query_id) ? @mysql_fetch_row($query_id) : false; + } + + + function num_rows($query_id = 0) + { + return ($query_id) ? @mysql_num_rows($query_id) : false; + } + + + function affected_rows() + { + return ($this->link_id) ? @mysql_affected_rows($this->link_id) : false; + } + + + function insert_id() + { + return ($this->link_id) ? @mysql_insert_id($this->link_id) : false; + } + + + function get_num_queries() + { + return $this->num_queries; + } + + + function get_saved_queries() + { + return $this->saved_queries; + } + + + function free_result($query_id = false) + { + return ($query_id) ? @mysql_free_result($query_id) : false; + } + + + function escape($str) + { + if (is_array($str)) + return ''; + else if (function_exists('mysql_real_escape_string')) + return mysql_real_escape_string($str, $this->link_id); + else + return mysql_escape_string($str); + } + + + function error() + { + $result['error_sql'] = @current(@end($this->saved_queries)); + $result['error_no'] = $this->error_no; + $result['error_msg'] = $this->error_msg; + + return $result; + } + + + function close() + { + if ($this->link_id) + { + if (is_resource($this->query_result)) + @mysql_free_result($this->query_result); + + return @mysql_close($this->link_id); + } + else + return false; + } + + + function get_names() + { + $result = $this->query('SHOW VARIABLES LIKE \'character_set_connection\''); + return $this->result($result, 0, 1); + } + + + function set_names($names) + { + return $this->query('SET NAMES \''.$this->escape($names).'\''); + } + + + function get_version() + { + $result = $this->query('SELECT VERSION()'); + + return array( + 'name' => 'MySQL Standard (InnoDB)', + 'version' => preg_replace('%^([^-]+).*$%', '\\1', $this->result($result)) + ); + } + + + function table_exists($table_name, $no_prefix = false) + { + $result = $this->query('SHOW TABLES LIKE \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\''); + return $this->num_rows($result) > 0; + } + + + function field_exists($table_name, $field_name, $no_prefix = false) + { + $result = $this->query('SHOW COLUMNS FROM '.($no_prefix ? '' : $this->prefix).$table_name.' LIKE \''.$this->escape($field_name).'\''); + return $this->num_rows($result) > 0; + } + + + function index_exists($table_name, $index_name, $no_prefix = false) + { + $exists = false; + + $result = $this->query('SHOW INDEX FROM '.($no_prefix ? '' : $this->prefix).$table_name); + while ($cur_index = $this->fetch_assoc($result)) + { + if (strtolower($cur_index['Key_name']) == strtolower(($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name)) + { + $exists = true; + break; + } + } + + return $exists; + } + + + function create_table($table_name, $schema, $no_prefix = false) + { + if ($this->table_exists($table_name, $no_prefix)) + return true; + + $query = 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name." (\n"; + + // Go through every schema element and add it to the query + foreach ($schema['FIELDS'] as $field_name => $field_data) + { + $field_data['datatype'] = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_data['datatype']); + + $query .= $field_name.' '.$field_data['datatype']; + + if (isset($field_data['collation'])) + $query .= 'CHARACTER SET utf8 COLLATE utf8_'.$field_data['collation']; + + if (!$field_data['allow_null']) + $query .= ' NOT NULL'; + + if (isset($field_data['default'])) + $query .= ' DEFAULT '.$field_data['default']; + + $query .= ",\n"; + } + + // If we have a primary key, add it + if (isset($schema['PRIMARY KEY'])) + $query .= 'PRIMARY KEY ('.implode(',', $schema['PRIMARY KEY']).'),'."\n"; + + // Add unique keys + if (isset($schema['UNIQUE KEYS'])) + { + foreach ($schema['UNIQUE KEYS'] as $key_name => $key_fields) + $query .= 'UNIQUE KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$key_name.'('.implode(',', $key_fields).'),'."\n"; + } + + // Add indexes + if (isset($schema['INDEXES'])) + { + foreach ($schema['INDEXES'] as $index_name => $index_fields) + $query .= 'KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.'('.implode(',', $index_fields).'),'."\n"; + } + + // We remove the last two characters (a newline and a comma) and add on the ending + $query = substr($query, 0, strlen($query) - 2)."\n".') ENGINE = '.(isset($schema['ENGINE']) ? $schema['ENGINE'] : 'InnoDB').' CHARACTER SET utf8'; + + return $this->query($query) ? true : false; + } + + + function drop_table($table_name, $no_prefix = false) + { + if (!$this->table_exists($table_name, $no_prefix)) + return true; + + return $this->query('DROP TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false; + } + + + function rename_table($old_table, $new_table, $no_prefix = false) + { + // If the new table exists and the old one doesn't, then we're happy + if ($this->table_exists($new_table, $no_prefix) && !$this->table_exists($old_table, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$old_table.' RENAME TO '.($no_prefix ? '' : $this->prefix).$new_table) ? true : false; + } + + + function add_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) + { + if ($this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + $field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type); + + if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value)) + $default_value = '\''.$this->escape($default_value).'\''; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false; + } + + + function alter_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) + { + if (!$this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + $field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type); + + if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value)) + $default_value = '\''.$this->escape($default_value).'\''; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' MODIFY '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false; + } + + + function drop_field($table_name, $field_name, $no_prefix = false) + { + if (!$this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP '.$field_name) ? true : false; + } + + + function add_index($table_name, $index_name, $index_fields, $unique = false, $no_prefix = false) + { + if ($this->index_exists($table_name, $index_name, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.($unique ? 'UNIQUE ' : '').'INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.' ('.implode(',', $index_fields).')') ? true : false; + } + + + function drop_index($table_name, $index_name, $no_prefix = false) + { + if (!$this->index_exists($table_name, $index_name, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name) ? true : false; + } + + function truncate_table($table_name, $no_prefix = false) + { + return $this->query('TRUNCATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false; + } +} diff --git a/include/dblayer/mysqli.php b/include/dblayer/mysqli.php new file mode 100644 index 0000000..05ae599 --- /dev/null +++ b/include/dblayer/mysqli.php @@ -0,0 +1,385 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +// Make sure we have built in support for MySQL +if (!function_exists('mysqli_connect')) + exit('This PHP environment doesn\'t have Improved MySQL (mysqli) support built in. Improved MySQL support is required if you want to use a MySQL 4.1 (or later) database to run this forum. Consult the PHP documentation for further assistance.'); + + +class DBLayer +{ + var $prefix; + var $link_id; + var $query_result; + + var $saved_queries = array(); + var $num_queries = 0; + + var $error_no = false; + var $error_msg = 'Unknown'; + + var $datatype_transformations = array( + '%^SERIAL$%' => 'INT(10) UNSIGNED AUTO_INCREMENT' + ); + + + function __construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect) + { + $this->prefix = $db_prefix; + + // Was a custom port supplied with $db_host? + if (strpos($db_host, ':') !== false) + list($db_host, $db_port) = explode(':', $db_host); + + // Persistent connection in MySQLi are only available in PHP 5.3 and later releases + $p_connect = $p_connect && version_compare(PHP_VERSION, '5.3.0', '>=') ? 'p:' : ''; + + if (isset($db_port)) + $this->link_id = @mysqli_connect($p_connect.$db_host, $db_username, $db_password, $db_name, $db_port); + else + $this->link_id = @mysqli_connect($p_connect.$db_host, $db_username, $db_password, $db_name); + + if (!$this->link_id) + error('Unable to connect to MySQL and select database. MySQL reported: '.mysqli_connect_error(), __FILE__, __LINE__); + + // Setup the client-server character set (UTF-8) + if (!defined('FORUM_NO_SET_NAMES')) + $this->set_names('utf8'); + + return $this->link_id; + } + + + function DBLayer($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect) + { + $this->__construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect); + } + + + function start_transaction() + { + return; + } + + + function end_transaction() + { + return; + } + + + function query($sql, $unbuffered = false) + { + if (defined('PUN_SHOW_QUERIES')) + $q_start = get_microtime(); + + $this->query_result = @mysqli_query($this->link_id, $sql); + + if ($this->query_result) + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array($sql, sprintf('%.5F', get_microtime() - $q_start)); + + ++$this->num_queries; + + return $this->query_result; + } + else + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array($sql, 0); + + $this->error_no = @mysqli_errno($this->link_id); + $this->error_msg = @mysqli_error($this->link_id); + + return false; + } + } + + + function result($query_id = 0, $row = 0, $col = 0) + { + if ($query_id) + { + if ($row !== 0 && @mysqli_data_seek($query_id, $row) === false) + return false; + + $cur_row = @mysqli_fetch_row($query_id); + if ($cur_row === false) + return false; + + return $cur_row[$col]; + } + else + return false; + } + + + function fetch_assoc($query_id = 0) + { + return ($query_id) ? @mysqli_fetch_assoc($query_id) : false; + } + + + function fetch_row($query_id = 0) + { + return ($query_id) ? @mysqli_fetch_row($query_id) : false; + } + + + function num_rows($query_id = 0) + { + return ($query_id) ? @mysqli_num_rows($query_id) : false; + } + + + function affected_rows() + { + return ($this->link_id) ? @mysqli_affected_rows($this->link_id) : false; + } + + + function insert_id() + { + return ($this->link_id) ? @mysqli_insert_id($this->link_id) : false; + } + + + function get_num_queries() + { + return $this->num_queries; + } + + + function get_saved_queries() + { + return $this->saved_queries; + } + + + function free_result($query_id = false) + { + return ($query_id) ? @mysqli_free_result($query_id) : false; + } + + + function escape($str) + { + return is_array($str) ? '' : mysqli_real_escape_string($this->link_id, $str); + } + + + function error() + { + $result['error_sql'] = @current(@end($this->saved_queries)); + $result['error_no'] = $this->error_no; + $result['error_msg'] = $this->error_msg; + + return $result; + } + + + function close() + { + if ($this->link_id) + { + if ($this->query_result instanceof mysqli_result) + @mysqli_free_result($this->query_result); + + return @mysqli_close($this->link_id); + } + else + return false; + } + + + function get_names() + { + $result = $this->query('SHOW VARIABLES LIKE \'character_set_connection\''); + return $this->result($result, 0, 1); + } + + + function set_names($names) + { + return $this->query('SET NAMES \''.$this->escape($names).'\''); + } + + + function get_version() + { + $result = $this->query('SELECT VERSION()'); + + return array( + 'name' => 'MySQL Improved', + 'version' => preg_replace('%^([^-]+).*$%', '\\1', $this->result($result)) + ); + } + + + function table_exists($table_name, $no_prefix = false) + { + $result = $this->query('SHOW TABLES LIKE \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\''); + return $this->num_rows($result) > 0; + } + + + function field_exists($table_name, $field_name, $no_prefix = false) + { + $result = $this->query('SHOW COLUMNS FROM '.($no_prefix ? '' : $this->prefix).$table_name.' LIKE \''.$this->escape($field_name).'\''); + return $this->num_rows($result) > 0; + } + + + function index_exists($table_name, $index_name, $no_prefix = false) + { + $exists = false; + + $result = $this->query('SHOW INDEX FROM '.($no_prefix ? '' : $this->prefix).$table_name); + while ($cur_index = $this->fetch_assoc($result)) + { + if (strtolower($cur_index['Key_name']) == strtolower(($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name)) + { + $exists = true; + break; + } + } + + return $exists; + } + + + function create_table($table_name, $schema, $no_prefix = false) + { + if ($this->table_exists($table_name, $no_prefix)) + return true; + + $query = 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name." (\n"; + + // Go through every schema element and add it to the query + foreach ($schema['FIELDS'] as $field_name => $field_data) + { + $field_data['datatype'] = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_data['datatype']); + + $query .= $field_name.' '.$field_data['datatype']; + + if (isset($field_data['collation'])) + $query .= 'CHARACTER SET utf8 COLLATE utf8_'.$field_data['collation']; + + if (!$field_data['allow_null']) + $query .= ' NOT NULL'; + + if (isset($field_data['default'])) + $query .= ' DEFAULT '.$field_data['default']; + + $query .= ",\n"; + } + + // If we have a primary key, add it + if (isset($schema['PRIMARY KEY'])) + $query .= 'PRIMARY KEY ('.implode(',', $schema['PRIMARY KEY']).'),'."\n"; + + // Add unique keys + if (isset($schema['UNIQUE KEYS'])) + { + foreach ($schema['UNIQUE KEYS'] as $key_name => $key_fields) + $query .= 'UNIQUE KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$key_name.'('.implode(',', $key_fields).'),'."\n"; + } + + // Add indexes + if (isset($schema['INDEXES'])) + { + foreach ($schema['INDEXES'] as $index_name => $index_fields) + $query .= 'KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.'('.implode(',', $index_fields).'),'."\n"; + } + + // We remove the last two characters (a newline and a comma) and add on the ending + $query = substr($query, 0, strlen($query) - 2)."\n".') ENGINE = '.(isset($schema['ENGINE']) ? $schema['ENGINE'] : 'MyISAM').' CHARACTER SET utf8'; + + return $this->query($query) ? true : false; + } + + + function drop_table($table_name, $no_prefix = false) + { + if (!$this->table_exists($table_name, $no_prefix)) + return true; + + return $this->query('DROP TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false; + } + + + function rename_table($old_table, $new_table, $no_prefix = false) + { + // If the new table exists and the old one doesn't, then we're happy + if ($this->table_exists($new_table, $no_prefix) && !$this->table_exists($old_table, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$old_table.' RENAME TO '.($no_prefix ? '' : $this->prefix).$new_table) ? true : false; + } + + + function add_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) + { + if ($this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + $field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type); + + if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value)) + $default_value = '\''.$this->escape($default_value).'\''; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false; + } + + + function alter_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) + { + if (!$this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + $field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type); + + if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value)) + $default_value = '\''.$this->escape($default_value).'\''; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' MODIFY '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false; + } + + + function drop_field($table_name, $field_name, $no_prefix = false) + { + if (!$this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP '.$field_name) ? true : false; + } + + + function add_index($table_name, $index_name, $index_fields, $unique = false, $no_prefix = false) + { + if ($this->index_exists($table_name, $index_name, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.($unique ? 'UNIQUE ' : '').'INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.' ('.implode(',', $index_fields).')') ? true : false; + } + + + function drop_index($table_name, $index_name, $no_prefix = false) + { + if (!$this->index_exists($table_name, $index_name, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name) ? true : false; + } + + function truncate_table($table_name, $no_prefix = false) + { + return $this->query('TRUNCATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false; + } +} diff --git a/include/dblayer/mysqli_innodb.php b/include/dblayer/mysqli_innodb.php new file mode 100644 index 0000000..f132276 --- /dev/null +++ b/include/dblayer/mysqli_innodb.php @@ -0,0 +1,398 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +// Make sure we have built in support for MySQL +if (!function_exists('mysqli_connect')) + exit('This PHP environment doesn\'t have Improved MySQL (mysqli) support built in. Improved MySQL support is required if you want to use a MySQL 4.1 (or later) database to run this forum. Consult the PHP documentation for further assistance.'); + + +class DBLayer +{ + var $prefix; + var $link_id; + var $query_result; + + var $saved_queries = array(); + var $num_queries = 0; + var $in_transaction = 0; + + var $error_no = false; + var $error_msg = 'Unknown'; + + var $datatype_transformations = array( + '%^SERIAL$%' => 'INT(10) UNSIGNED AUTO_INCREMENT' + ); + + + function __construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect) + { + $this->prefix = $db_prefix; + + // Was a custom port supplied with $db_host? + if (strpos($db_host, ':') !== false) + list($db_host, $db_port) = explode(':', $db_host); + + // Persistent connection in MySQLi are only available in PHP 5.3 and later releases + $p_connect = $p_connect && version_compare(PHP_VERSION, '5.3.0', '>=') ? 'p:' : ''; + + if (isset($db_port)) + $this->link_id = @mysqli_connect($p_connect.$db_host, $db_username, $db_password, $db_name, $db_port); + else + $this->link_id = @mysqli_connect($p_connect.$db_host, $db_username, $db_password, $db_name); + + if (!$this->link_id) + error('Unable to connect to MySQL and select database. MySQL reported: '.mysqli_connect_error(), __FILE__, __LINE__); + + // Setup the client-server character set (UTF-8) + if (!defined('FORUM_NO_SET_NAMES')) + $this->set_names('utf8'); + + return $this->link_id; + } + + + function DBLayer($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect) + { + $this->__construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect); + } + + + function start_transaction() + { + ++$this->in_transaction; + + mysqli_query($this->link_id, 'START TRANSACTION'); + return; + } + + + function end_transaction() + { + --$this->in_transaction; + + mysqli_query($this->link_id, 'COMMIT'); + return; + } + + + function query($sql, $unbuffered = false) + { + if (defined('PUN_SHOW_QUERIES')) + $q_start = get_microtime(); + + $this->query_result = @mysqli_query($this->link_id, $sql); + + if ($this->query_result) + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array($sql, sprintf('%.5F', get_microtime() - $q_start)); + + ++$this->num_queries; + + return $this->query_result; + } + else + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array($sql, 0); + + $this->error_no = @mysqli_errno($this->link_id); + $this->error_msg = @mysqli_error($this->link_id); + + // Rollback transaction + if ($this->in_transaction) + mysqli_query($this->link_id, 'ROLLBACK'); + + --$this->in_transaction; + + return false; + } + } + + + function result($query_id = 0, $row = 0, $col = 0) + { + if ($query_id) + { + if ($row !== 0 && @mysqli_data_seek($query_id, $row) === false) + return false; + + $cur_row = @mysqli_fetch_row($query_id); + if ($cur_row === false) + return false; + + return $cur_row[$col]; + } + else + return false; + } + + + function fetch_assoc($query_id = 0) + { + return ($query_id) ? @mysqli_fetch_assoc($query_id) : false; + } + + + function fetch_row($query_id = 0) + { + return ($query_id) ? @mysqli_fetch_row($query_id) : false; + } + + + function num_rows($query_id = 0) + { + return ($query_id) ? @mysqli_num_rows($query_id) : false; + } + + + function affected_rows() + { + return ($this->link_id) ? @mysqli_affected_rows($this->link_id) : false; + } + + + function insert_id() + { + return ($this->link_id) ? @mysqli_insert_id($this->link_id) : false; + } + + + function get_num_queries() + { + return $this->num_queries; + } + + + function get_saved_queries() + { + return $this->saved_queries; + } + + + function free_result($query_id = false) + { + return ($query_id) ? @mysqli_free_result($query_id) : false; + } + + + function escape($str) + { + return is_array($str) ? '' : mysqli_real_escape_string($this->link_id, $str); + } + + + function error() + { + $result['error_sql'] = @current(@end($this->saved_queries)); + $result['error_no'] = $this->error_no; + $result['error_msg'] = $this->error_msg; + + return $result; + } + + + function close() + { + if ($this->link_id) + { + if ($this->query_result instanceof mysqli_result) + @mysqli_free_result($this->query_result); + + return @mysqli_close($this->link_id); + } + else + return false; + } + + + function get_names() + { + $result = $this->query('SHOW VARIABLES LIKE \'character_set_connection\''); + return $this->result($result, 0, 1); + } + + + function set_names($names) + { + return $this->query('SET NAMES \''.$this->escape($names).'\''); + } + + + function get_version() + { + $result = $this->query('SELECT VERSION()'); + + return array( + 'name' => 'MySQL Improved (InnoDB)', + 'version' => preg_replace('%^([^-]+).*$%', '\\1', $this->result($result)) + ); + } + + + function table_exists($table_name, $no_prefix = false) + { + $result = $this->query('SHOW TABLES LIKE \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\''); + return $this->num_rows($result) > 0; + } + + + function field_exists($table_name, $field_name, $no_prefix = false) + { + $result = $this->query('SHOW COLUMNS FROM '.($no_prefix ? '' : $this->prefix).$table_name.' LIKE \''.$this->escape($field_name).'\''); + return $this->num_rows($result) > 0; + } + + + function index_exists($table_name, $index_name, $no_prefix = false) + { + $exists = false; + + $result = $this->query('SHOW INDEX FROM '.($no_prefix ? '' : $this->prefix).$table_name); + while ($cur_index = $this->fetch_assoc($result)) + { + if (strtolower($cur_index['Key_name']) == strtolower(($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name)) + { + $exists = true; + break; + } + } + + return $exists; + } + + + function create_table($table_name, $schema, $no_prefix = false) + { + if ($this->table_exists($table_name, $no_prefix)) + return true; + + $query = 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name." (\n"; + + // Go through every schema element and add it to the query + foreach ($schema['FIELDS'] as $field_name => $field_data) + { + $field_data['datatype'] = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_data['datatype']); + + $query .= $field_name.' '.$field_data['datatype']; + + if (isset($field_data['collation'])) + $query .= 'CHARACTER SET utf8 COLLATE utf8_'.$field_data['collation']; + + if (!$field_data['allow_null']) + $query .= ' NOT NULL'; + + if (isset($field_data['default'])) + $query .= ' DEFAULT '.$field_data['default']; + + $query .= ",\n"; + } + + // If we have a primary key, add it + if (isset($schema['PRIMARY KEY'])) + $query .= 'PRIMARY KEY ('.implode(',', $schema['PRIMARY KEY']).'),'."\n"; + + // Add unique keys + if (isset($schema['UNIQUE KEYS'])) + { + foreach ($schema['UNIQUE KEYS'] as $key_name => $key_fields) + $query .= 'UNIQUE KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$key_name.'('.implode(',', $key_fields).'),'."\n"; + } + + // Add indexes + if (isset($schema['INDEXES'])) + { + foreach ($schema['INDEXES'] as $index_name => $index_fields) + $query .= 'KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.'('.implode(',', $index_fields).'),'."\n"; + } + + // We remove the last two characters (a newline and a comma) and add on the ending + $query = substr($query, 0, strlen($query) - 2)."\n".') ENGINE = '.(isset($schema['ENGINE']) ? $schema['ENGINE'] : 'InnoDB').' CHARACTER SET utf8'; + + return $this->query($query) ? true : false; + } + + + function drop_table($table_name, $no_prefix = false) + { + if (!$this->table_exists($table_name, $no_prefix)) + return true; + + return $this->query('DROP TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false; + } + + + function rename_table($old_table, $new_table, $no_prefix = false) + { + // If the new table exists and the old one doesn't, then we're happy + if ($this->table_exists($new_table, $no_prefix) && !$this->table_exists($old_table, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$old_table.' RENAME TO '.($no_prefix ? '' : $this->prefix).$new_table) ? true : false; + } + + + function add_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) + { + if ($this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + $field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type); + + if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value)) + $default_value = '\''.$this->escape($default_value).'\''; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false; + } + + + function alter_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) + { + if (!$this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + $field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type); + + if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value)) + $default_value = '\''.$this->escape($default_value).'\''; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' MODIFY '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false; + } + + + function drop_field($table_name, $field_name, $no_prefix = false) + { + if (!$this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP '.$field_name) ? true : false; + } + + + function add_index($table_name, $index_name, $index_fields, $unique = false, $no_prefix = false) + { + if ($this->index_exists($table_name, $index_name, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.($unique ? 'UNIQUE ' : '').'INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.' ('.implode(',', $index_fields).')') ? true : false; + } + + + function drop_index($table_name, $index_name, $no_prefix = false) + { + if (!$this->index_exists($table_name, $index_name, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name) ? true : false; + } + + function truncate_table($table_name, $no_prefix = false) + { + return $this->query('TRUNCATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false; + } +} diff --git a/include/dblayer/pgsql.php b/include/dblayer/pgsql.php new file mode 100644 index 0000000..8d13ad9 --- /dev/null +++ b/include/dblayer/pgsql.php @@ -0,0 +1,442 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +// Make sure we have built in support for PostgreSQL +if (!function_exists('pg_connect')) + exit('This PHP environment doesn\'t have PostgreSQL support built in. PostgreSQL support is required if you want to use a PostgreSQL database to run this forum. Consult the PHP documentation for further assistance.'); + + +class DBLayer +{ + var $prefix; + var $link_id; + var $query_result; + var $last_query_text = array(); + var $in_transaction = 0; + + var $saved_queries = array(); + var $num_queries = 0; + + var $error_no = false; + var $error_msg = 'Unknown'; + + var $datatype_transformations = array( + '%^(TINY|SMALL)INT( )?(\\([0-9]+\\))?( )?(UNSIGNED)?$%i' => 'SMALLINT', + '%^(MEDIUM)?INT( )?(\\([0-9]+\\))?( )?(UNSIGNED)?$%i' => 'INTEGER', + '%^BIGINT( )?(\\([0-9]+\\))?( )?(UNSIGNED)?$%i' => 'BIGINT', + '%^(TINY|MEDIUM|LONG)?TEXT$%i' => 'TEXT', + '%^DOUBLE( )?(\\([0-9,]+\\))?( )?(UNSIGNED)?$%i' => 'DOUBLE PRECISION', + '%^FLOAT( )?(\\([0-9]+\\))?( )?(UNSIGNED)?$%i' => 'REAL' + ); + + + function __construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect) + { + $this->prefix = $db_prefix; + + if ($db_host) + { + if (strpos($db_host, ':') !== false) + { + list($db_host, $dbport) = explode(':', $db_host); + $connect_str[] = 'host='.$db_host.' port='.$dbport; + } + else + $connect_str[] = 'host='.$db_host; + } + + if ($db_name) + $connect_str[] = 'dbname='.$db_name; + + if ($db_username) + $connect_str[] = 'user='.$db_username; + + if ($db_password) + $connect_str[] = 'password='.$db_password; + + if ($p_connect) + $this->link_id = @pg_pconnect(implode(' ', $connect_str)); + else + $this->link_id = @pg_connect(implode(' ', $connect_str)); + + if (!$this->link_id) + error('Unable to connect to PostgreSQL server', __FILE__, __LINE__); + + // Setup the client-server character set (UTF-8) + if (!defined('FORUM_NO_SET_NAMES')) + $this->set_names('utf8'); + + return $this->link_id; + } + + + function DBLayer($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect) + { + $this->__construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect); + } + + + function start_transaction() + { + ++$this->in_transaction; + + return (@pg_query($this->link_id, 'BEGIN')) ? true : false; + } + + + function end_transaction() + { + --$this->in_transaction; + + if (@pg_query($this->link_id, 'COMMIT')) + return true; + else + { + @pg_query($this->link_id, 'ROLLBACK'); + return false; + } + } + + + function query($sql, $unbuffered = false) // $unbuffered is ignored since there is no pgsql_unbuffered_query() + { + if (strrpos($sql, 'LIMIT') !== false) + $sql = preg_replace('%LIMIT ([0-9]+),([ 0-9]+)%', 'LIMIT \\2 OFFSET \\1', $sql); + + if (defined('PUN_SHOW_QUERIES')) + $q_start = get_microtime(); + + @pg_send_query($this->link_id, $sql); + $this->query_result = @pg_get_result($this->link_id); + + if (pg_result_status($this->query_result) != PGSQL_FATAL_ERROR) + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array($sql, sprintf('%.5F', get_microtime() - $q_start)); + + ++$this->num_queries; + + $this->last_query_text[intval($this->query_result)] = $sql; + + return $this->query_result; + } + else + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array($sql, 0); + + $this->error_no = false; + $this->error_msg = @pg_result_error($this->query_result); + + if ($this->in_transaction) + @pg_query($this->link_id, 'ROLLBACK'); + + --$this->in_transaction; + + return false; + } + } + + + function result($query_id = 0, $row = 0, $col = 0) + { + return ($query_id) ? @pg_fetch_result($query_id, $row, $col) : false; + } + + + function fetch_assoc($query_id = 0) + { + return ($query_id) ? @pg_fetch_assoc($query_id) : false; + } + + + function fetch_row($query_id = 0) + { + return ($query_id) ? @pg_fetch_row($query_id) : false; + } + + + function num_rows($query_id = 0) + { + return ($query_id) ? @pg_num_rows($query_id) : false; + } + + + function affected_rows() + { + return ($this->query_result) ? @pg_affected_rows($this->query_result) : false; + } + + + function insert_id() + { + $query_id = $this->query_result; + + if ($query_id && $this->last_query_text[intval($query_id)] != '') + { + if (preg_match('%^INSERT INTO ([a-z0-9\_\-]+)%is', $this->last_query_text[intval($query_id)], $table_name)) + { + // Hack (don't ask) + if (substr($table_name[1], -6) == 'groups') + $table_name[1] .= '_g'; + + $temp_q_id = @pg_query($this->link_id, 'SELECT currval(\''.$table_name[1].'_id_seq\')'); + return ($temp_q_id) ? intval(@pg_fetch_result($temp_q_id, 0)) : false; + } + } + + return false; + } + + + function get_num_queries() + { + return $this->num_queries; + } + + + function get_saved_queries() + { + return $this->saved_queries; + } + + + function free_result($query_id = false) + { + if (!$query_id) + $query_id = $this->query_result; + + return ($query_id) ? @pg_free_result($query_id) : false; + } + + + function escape($str) + { + return is_array($str) ? '' : pg_escape_string($str); + } + + + function error() + { + $result['error_sql'] = @current(@end($this->saved_queries)); + $result['error_no'] = $this->error_no; + $result['error_msg'] = $this->error_msg; + + return $result; + } + + + function close() + { + if ($this->link_id) + { + if ($this->in_transaction) + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array('COMMIT', 0); + + @pg_query($this->link_id, 'COMMIT'); + } + + if ($this->query_result) + @pg_free_result($this->query_result); + + return @pg_close($this->link_id); + } + else + return false; + } + + + function get_names() + { + $result = $this->query('SHOW client_encoding'); + return strtolower($this->result($result)); // MySQL returns lowercase so lets be consistent + } + + + function set_names($names) + { + return $this->query('SET NAMES \''.$this->escape($names).'\''); + } + + + function get_version() + { + $result = $this->query('SELECT VERSION()'); + + return array( + 'name' => 'PostgreSQL', + 'version' => preg_replace('%^[^0-9]+([^\s,-]+).*$%', '\\1', $this->result($result)) + ); + } + + + function table_exists($table_name, $no_prefix = false) + { + $result = $this->query('SELECT 1 FROM pg_class WHERE relname = \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\''); + return $this->num_rows($result) > 0; + } + + + function field_exists($table_name, $field_name, $no_prefix = false) + { + $result = $this->query('SELECT 1 FROM pg_class c INNER JOIN pg_attribute a ON a.attrelid = c.oid WHERE c.relname = \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\' AND a.attname = \''.$this->escape($field_name).'\''); + return $this->num_rows($result) > 0; + } + + + function index_exists($table_name, $index_name, $no_prefix = false) + { + $result = $this->query('SELECT 1 FROM pg_index i INNER JOIN pg_class c1 ON c1.oid = i.indrelid INNER JOIN pg_class c2 ON c2.oid = i.indexrelid WHERE c1.relname = \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\' AND c2.relname = \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'_'.$this->escape($index_name).'\''); + return $this->num_rows($result) > 0; + } + + + function create_table($table_name, $schema, $no_prefix = false) + { + if ($this->table_exists($table_name, $no_prefix)) + return true; + + $query = 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name." (\n"; + + // Go through every schema element and add it to the query + foreach ($schema['FIELDS'] as $field_name => $field_data) + { + $field_data['datatype'] = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_data['datatype']); + + $query .= $field_name.' '.$field_data['datatype']; + + // The SERIAL datatype is a special case where we don't need to say not null + if (!$field_data['allow_null'] && $field_data['datatype'] != 'SERIAL') + $query .= ' NOT NULL'; + + if (isset($field_data['default'])) + $query .= ' DEFAULT '.$field_data['default']; + + $query .= ",\n"; + } + + // If we have a primary key, add it + if (isset($schema['PRIMARY KEY'])) + $query .= 'PRIMARY KEY ('.implode(',', $schema['PRIMARY KEY']).'),'."\n"; + + // Add unique keys + if (isset($schema['UNIQUE KEYS'])) + { + foreach ($schema['UNIQUE KEYS'] as $key_name => $key_fields) + $query .= 'UNIQUE ('.implode(',', $key_fields).'),'."\n"; + } + + // We remove the last two characters (a newline and a comma) and add on the ending + $query = substr($query, 0, strlen($query) - 2)."\n".')'; + + $result = $this->query($query) ? true : false; + + // Add indexes + if (isset($schema['INDEXES'])) + { + foreach ($schema['INDEXES'] as $index_name => $index_fields) + $result &= $this->add_index($table_name, $index_name, $index_fields, false, $no_prefix); + } + + return $result; + } + + + function drop_table($table_name, $no_prefix = false) + { + if (!$this->table_exists($table_name, $no_prefix)) + return true; + + return $this->query('DROP TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false; + } + + + function rename_table($old_table, $new_table, $no_prefix = false) + { + // If the new table exists and the old one doesn't, then we're happy + if ($this->table_exists($new_table, $no_prefix) && !$this->table_exists($old_table, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$old_table.' RENAME TO '.($no_prefix ? '' : $this->prefix).$new_table) ? true : false; + } + + + function add_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) + { + if ($this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + $field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type); + + $result = $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.$field_name.' '.$field_type) ? true : false; + + if (!is_null($default_value)) + { + if (!is_int($default_value) && !is_float($default_value)) + $default_value = '\''.$this->escape($default_value).'\''; + + $result &= $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ALTER '.$field_name.' SET DEFAULT '.$default_value) ? true : false; + $result &= $this->query('UPDATE '.($no_prefix ? '' : $this->prefix).$table_name.' SET '.$field_name.'='.$default_value) ? true : false; + } + + if (!$allow_null) + $result &= $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ALTER '.$field_name.' SET NOT NULL') ? true : false; + + return $result; + } + + + function alter_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) + { + if (!$this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + $field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type); + + $result = $this->add_field($table_name, 'tmp_'.$field_name, $field_type, $allow_null, $default_value, $after_field, $no_prefix); + $result &= $this->query('UPDATE '.($no_prefix ? '' : $this->prefix).$table_name.' SET tmp_'.$field_name.' = '.$field_name) ? true : false; + $result &= $this->drop_field($table_name, $field_name, $no_prefix); + $result &= $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' RENAME COLUMN tmp_'.$field_name.' TO '.$field_name) ? true : false; + + return $result; + } + + + function drop_field($table_name, $field_name, $no_prefix = false) + { + if (!$this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP '.$field_name) ? true : false; + } + + + function add_index($table_name, $index_name, $index_fields, $unique = false, $no_prefix = false) + { + if ($this->index_exists($table_name, $index_name, $no_prefix)) + return true; + + return $this->query('CREATE '.($unique ? 'UNIQUE ' : '').'INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.' ON '.($no_prefix ? '' : $this->prefix).$table_name.'('.implode(',', $index_fields).')') ? true : false; + } + + + function drop_index($table_name, $index_name, $no_prefix = false) + { + if (!$this->index_exists($table_name, $index_name, $no_prefix)) + return true; + + return $this->query('DROP INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name) ? true : false; + } + + function truncate_table($table_name, $no_prefix = false) + { + return $this->query('DELETE FROM '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false; + } +} diff --git a/include/dblayer/sqlite.php b/include/dblayer/sqlite.php new file mode 100644 index 0000000..f9164fa --- /dev/null +++ b/include/dblayer/sqlite.php @@ -0,0 +1,601 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +// Make sure we have built in support for SQLite +if (!function_exists('sqlite_open')) + exit('This PHP environment doesn\'t have SQLite support built in. SQLite support is required if you want to use a SQLite database to run this forum. Consult the PHP documentation for further assistance.'); + + +class DBLayer +{ + var $prefix; + var $link_id; + var $query_result; + var $in_transaction = 0; + + var $saved_queries = array(); + var $num_queries = 0; + + var $error_no = false; + var $error_msg = 'Unknown'; + + var $datatype_transformations = array( + '%^SERIAL$%' => 'INTEGER', + '%^(TINY|SMALL|MEDIUM|BIG)?INT( )?(\\([0-9]+\\))?( )?(UNSIGNED)?$%i' => 'INTEGER', + '%^(TINY|MEDIUM|LONG)?TEXT$%i' => 'TEXT' + ); + + + function __construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect) + { + // Prepend $db_name with the path to the forum root directory + $db_name = PUN_ROOT.$db_name; + + $this->prefix = $db_prefix; + + if (!file_exists($db_name)) + { + @touch($db_name); + @chmod($db_name, 0666); + if (!file_exists($db_name)) + error('Unable to create new database \''.$db_name.'\'. Permission denied', __FILE__, __LINE__); + } + + if (!is_readable($db_name)) + error('Unable to open database \''.$db_name.'\' for reading. Permission denied', __FILE__, __LINE__); + + if (!forum_is_writable($db_name)) + error('Unable to open database \''.$db_name.'\' for writing. Permission denied', __FILE__, __LINE__); + + if ($p_connect) + $this->link_id = @sqlite_popen($db_name, 0666, $sqlite_error); + else + $this->link_id = @sqlite_open($db_name, 0666, $sqlite_error); + + if (!$this->link_id) + error('Unable to open database \''.$db_name.'\'. SQLite reported: '.$sqlite_error, __FILE__, __LINE__); + else + return $this->link_id; + } + + + function DBLayer($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect) + { + $this->__construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect); + } + + + function start_transaction() + { + ++$this->in_transaction; + + return (@sqlite_query($this->link_id, 'BEGIN')) ? true : false; + } + + + function end_transaction() + { + --$this->in_transaction; + + if (@sqlite_query($this->link_id, 'COMMIT')) + return true; + else + { + @sqlite_query($this->link_id, 'ROLLBACK'); + return false; + } + } + + + function query($sql, $unbuffered = false) + { + if (defined('PUN_SHOW_QUERIES')) + $q_start = get_microtime(); + + if ($unbuffered) + $this->query_result = @sqlite_unbuffered_query($this->link_id, $sql); + else + $this->query_result = @sqlite_query($this->link_id, $sql); + + if ($this->query_result) + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array($sql, sprintf('%.5F', get_microtime() - $q_start)); + + ++$this->num_queries; + + return $this->query_result; + } + else + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array($sql, 0); + + $this->error_no = @sqlite_last_error($this->link_id); + $this->error_msg = @sqlite_error_string($this->error_no); + + if ($this->in_transaction) + @sqlite_query($this->link_id, 'ROLLBACK'); + + --$this->in_transaction; + + return false; + } + } + + + function result($query_id = 0, $row = 0, $col = 0) + { + if ($query_id) + { + if ($row !== 0 && @sqlite_seek($query_id, $row) === false) + return false; + + $cur_row = @sqlite_current($query_id); + if ($cur_row === false) + return false; + + return $cur_row[$col]; + } + else + return false; + } + + + function fetch_assoc($query_id = 0) + { + if ($query_id) + { + $cur_row = @sqlite_fetch_array($query_id, SQLITE_ASSOC); + if ($cur_row) + { + // Horrible hack to get rid of table names and table aliases from the array keys + foreach ($cur_row as $key => $value) + { + $dot_spot = strpos($key, '.'); + if ($dot_spot !== false) + { + unset($cur_row[$key]); + $key = substr($key, $dot_spot+1); + $cur_row[$key] = $value; + } + } + } + + return $cur_row; + } + else + return false; + } + + + function fetch_row($query_id = 0) + { + return ($query_id) ? @sqlite_fetch_array($query_id, SQLITE_NUM) : false; + } + + + function num_rows($query_id = 0) + { + return ($query_id) ? @sqlite_num_rows($query_id) : false; + } + + + function affected_rows() + { + return ($this->link_id) ? @sqlite_changes($this->link_id) : false; + } + + + function insert_id() + { + return ($this->link_id) ? @sqlite_last_insert_rowid($this->link_id) : false; + } + + + function get_num_queries() + { + return $this->num_queries; + } + + + function get_saved_queries() + { + return $this->saved_queries; + } + + + function free_result($query_id = false) + { + return true; + } + + + function escape($str) + { + return is_array($str) ? '' : sqlite_escape_string($str); + } + + + function error() + { + $result['error_sql'] = @current(@end($this->saved_queries)); + $result['error_no'] = $this->error_no; + $result['error_msg'] = $this->error_msg; + + return $result; + } + + + function close() + { + if ($this->link_id) + { + if ($this->in_transaction) + { + if (defined('PUN_SHOW_QUERIES')) + $this->saved_queries[] = array('COMMIT', 0); + + @sqlite_query($this->link_id, 'COMMIT'); + } + + return @sqlite_close($this->link_id); + } + else + return false; + } + + + function get_names() + { + return ''; + } + + + function set_names($names) + { + return true; + } + + + function get_version() + { + return array( + 'name' => 'SQLite', + 'version' => sqlite_libversion() + ); + } + + + function table_exists($table_name, $no_prefix = false) + { + $result = $this->query('SELECT 1 FROM sqlite_master WHERE name = \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\' AND type=\'table\''); + return $this->num_rows($result) > 0; + } + + + function field_exists($table_name, $field_name, $no_prefix = false) + { + $result = $this->query('SELECT sql FROM sqlite_master WHERE name = \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\' AND type=\'table\''); + if (!$this->num_rows($result)) + return false; + + return preg_match('%[\r\n]'.preg_quote($field_name, '%').' %', $this->result($result)); + } + + + function index_exists($table_name, $index_name, $no_prefix = false) + { + $result = $this->query('SELECT 1 FROM sqlite_master WHERE tbl_name = \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\' AND name = \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'_'.$this->escape($index_name).'\' AND type=\'index\''); + return $this->num_rows($result) > 0; + } + + + function create_table($table_name, $schema, $no_prefix = false) + { + if ($this->table_exists($table_name, $no_prefix)) + return true; + + $query = 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name." (\n"; + + // Go through every schema element and add it to the query + foreach ($schema['FIELDS'] as $field_name => $field_data) + { + $field_data['datatype'] = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_data['datatype']); + + $query .= $field_name.' '.$field_data['datatype']; + + if (!$field_data['allow_null']) + $query .= ' NOT NULL'; + + if (isset($field_data['default'])) + $query .= ' DEFAULT '.$field_data['default']; + + $query .= ",\n"; + } + + // If we have a primary key, add it + if (isset($schema['PRIMARY KEY'])) + $query .= 'PRIMARY KEY ('.implode(',', $schema['PRIMARY KEY']).'),'."\n"; + + // Add unique keys + if (isset($schema['UNIQUE KEYS'])) + { + foreach ($schema['UNIQUE KEYS'] as $key_name => $key_fields) + $query .= 'UNIQUE ('.implode(',', $key_fields).'),'."\n"; + } + + // We remove the last two characters (a newline and a comma) and add on the ending + $query = substr($query, 0, strlen($query) - 2)."\n".')'; + + $result = $this->query($query) ? true : false; + + // Add indexes + if (isset($schema['INDEXES'])) + { + foreach ($schema['INDEXES'] as $index_name => $index_fields) + $result &= $this->add_index($table_name, $index_name, $index_fields, false, $no_prefix); + } + + return $result; + } + + + function drop_table($table_name, $no_prefix = false) + { + if (!$this->table_exists($table_name, $no_prefix)) + return true; + + return $this->query('DROP TABLE '.($no_prefix ? '' : $this->prefix).$this->escape($table_name)) ? true : false; + } + + + function rename_table($old_table, $new_table, $no_prefix = false) + { + // If the old table does not exist + if (!$this->table_exists($old_table, $no_prefix)) + return false; + // If the table names are the same + else if ($old_table == $new_table) + return true; + // If the new table already exists + else if ($this->table_exists($new_table, $no_prefix)) + return false; + + $table = $this->get_table_info($old_table, $no_prefix); + + // Create new table + $query = str_replace('CREATE TABLE '.($no_prefix ? '' : $this->prefix).$this->escape($old_table).' (', 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$this->escape($new_table).' (', $table['sql']); + $result = $this->query($query) ? true : false; + + // Recreate indexes + if (!empty($table['indices'])) + { + foreach ($table['indices'] as $cur_index) + { + $query = str_replace('CREATE INDEX '.($no_prefix ? '' : $this->prefix).$this->escape($old_table), 'CREATE INDEX '.($no_prefix ? '' : $this->prefix).$this->escape($new_table), $cur_index); + $query = str_replace('ON '.($no_prefix ? '' : $this->prefix).$this->escape($old_table), 'ON '.($no_prefix ? '' : $this->prefix).$this->escape($new_table), $query); + $result &= $this->query($query) ? true : false; + } + } + + // Copy content across + $result &= $this->query('INSERT INTO '.($no_prefix ? '' : $this->prefix).$this->escape($new_table).' SELECT * FROM '.($no_prefix ? '' : $this->prefix).$this->escape($old_table)) ? true : false; + + // Drop the old table if the new one exists + if ($this->table_exists($new_table, $no_prefix)) + $result &= $this->drop_table($old_table, $no_prefix); + + return $result; + } + + + function get_table_info($table_name, $no_prefix = false) + { + // Grab table info + $result = $this->query('SELECT sql FROM sqlite_master WHERE tbl_name = \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\' ORDER BY type DESC') or error('Unable to fetch table information', __FILE__, __LINE__, $this->error()); + $num_rows = $this->num_rows($result); + + if ($num_rows == 0) + return; + + $table = array(); + $table['indices'] = array(); + while ($cur_index = $this->fetch_assoc($result)) + { + if (empty($cur_index['sql'])) + continue; + + if (!isset($table['sql'])) + $table['sql'] = $cur_index['sql']; + else + $table['indices'][] = $cur_index['sql']; + } + + // Work out the columns in the table currently + $table_lines = explode("\n", $table['sql']); + $table['columns'] = array(); + foreach ($table_lines as $table_line) + { + $table_line = trim($table_line, " \t\n\r,"); // trim spaces, tabs, newlines, and commas + if (substr($table_line, 0, 12) == 'CREATE TABLE') + continue; + else if (substr($table_line, 0, 11) == 'PRIMARY KEY') + $table['primary_key'] = $table_line; + else if (substr($table_line, 0, 6) == 'UNIQUE') + $table['unique'] = $table_line; + else if (substr($table_line, 0, strpos($table_line, ' ')) != '') + $table['columns'][substr($table_line, 0, strpos($table_line, ' '))] = trim(substr($table_line, strpos($table_line, ' '))); + } + + return $table; + } + + + function add_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) + { + if ($this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + $table = $this->get_table_info($table_name, $no_prefix); + + // Create temp table + $now = time(); + $tmptable = str_replace('CREATE TABLE '.($no_prefix ? '' : $this->prefix).$this->escape($table_name).' (', 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'_t'.$now.' (', $table['sql']); + $result = $this->query($tmptable) ? true : false; + $result &= $this->query('INSERT INTO '.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'_t'.$now.' SELECT * FROM '.($no_prefix ? '' : $this->prefix).$this->escape($table_name)) ? true : false; + + // Create new table sql + $field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type); + $query = $field_type; + + if (!$allow_null) + $query .= ' NOT NULL'; + + if (is_string($default_value)) + $default_value = '\''.$this->escape($default_value).'\''; + + if (!is_null($default_value)) + $query .= ' DEFAULT '.$default_value; + + $old_columns = array_keys($table['columns']); + + // Determine the proper offset + if (!is_null($after_field)) + $offset = array_search($after_field, array_keys($table['columns']), true) + 1; + else + $offset = count($table['columns']); + + // Out of bounds checks + if ($offset > count($table['columns'])) + $offset = count($table['columns']); + else if ($offset < 0) + $offset = 0; + + if (!is_null($field_name) && $field_name !== '') + $table['columns'] = array_merge(array_slice($table['columns'], 0, $offset), array($field_name => $query), array_slice($table['columns'], $offset)); + + $new_table = 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$this->escape($table_name).' ('; + + foreach ($table['columns'] as $cur_column => $column_details) + $new_table .= "\n".$cur_column.' '.$column_details.','; + + if (isset($table['unique'])) + $new_table .= "\n".$table['unique'].','; + + if (isset($table['primary_key'])) + $new_table .= "\n".$table['primary_key'].','; + + $new_table = trim($new_table, ',')."\n".');'; + + // Drop old table + $result &= $this->drop_table($table_name, $no_prefix); + + // Create new table + $result &= $this->query($new_table) ? true : false; + + // Recreate indexes + if (!empty($table['indices'])) + { + foreach ($table['indices'] as $cur_index) + $result &= $this->query($cur_index) ? true : false; + } + + // Copy content back + $result &= $this->query('INSERT INTO '.($no_prefix ? '' : $this->prefix).$this->escape($table_name).' ('.implode(', ', $old_columns).') SELECT * FROM '.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'_t'.$now) ? true : false; + + // Drop temp table + $result &= $this->drop_table($table_name.'_t'.$now, $no_prefix); + + return $result; + } + + + function alter_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) + { + // Unneeded for SQLite + return true; + } + + + function drop_field($table_name, $field_name, $no_prefix = false) + { + if (!$this->field_exists($table_name, $field_name, $no_prefix)) + return true; + + $table = $this->get_table_info($table_name, $no_prefix); + + // Create temp table + $now = time(); + $tmptable = str_replace('CREATE TABLE '.($no_prefix ? '' : $this->prefix).$this->escape($table_name).' (', 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'_t'.$now.' (', $table['sql']); + $result = $this->query($tmptable) ? true : false; + $result &= $this->query('INSERT INTO '.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'_t'.$now.' SELECT * FROM '.($no_prefix ? '' : $this->prefix).$this->escape($table_name)) ? true : false; + + // Work out the columns we need to keep and the sql for the new table + unset($table['columns'][$field_name]); + $new_columns = array_keys($table['columns']); + + $new_table = 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$this->escape($table_name).' ('; + + foreach ($table['columns'] as $cur_column => $column_details) + $new_table .= "\n".$cur_column.' '.$column_details.','; + + if (isset($table['unique'])) + $new_table .= "\n".$table['unique'].','; + + if (isset($table['primary_key'])) + $new_table .= "\n".$table['primary_key'].','; + + $new_table = trim($new_table, ',')."\n".');'; + + // Drop old table + $result &= $this->drop_table($table_name, $no_prefix); + + // Create new table + $result &= $this->query($new_table) ? true : false; + + // Recreate indexes + if (!empty($table['indices'])) + { + foreach ($table['indices'] as $cur_index) + if (!preg_match('%\('.preg_quote($field_name, '%').'\)%', $cur_index)) + $result &= $this->query($cur_index) ? true : false; + } + + // Copy content back + $result &= $this->query('INSERT INTO '.($no_prefix ? '' : $this->prefix).$this->escape($table_name).' SELECT '.implode(', ', $new_columns).' FROM '.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'_t'.$now) ? true : false; + + // Drop temp table + $result &= $this->drop_table($table_name.'_t'.$now, $no_prefix); + + return $result; + } + + + function add_index($table_name, $index_name, $index_fields, $unique = false, $no_prefix = false) + { + if ($this->index_exists($table_name, $index_name, $no_prefix)) + return true; + + return $this->query('CREATE '.($unique ? 'UNIQUE ' : '').'INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.' ON '.($no_prefix ? '' : $this->prefix).$table_name.'('.implode(',', $index_fields).')') ? true : false; + } + + + function drop_index($table_name, $index_name, $no_prefix = false) + { + if (!$this->index_exists($table_name, $index_name, $no_prefix)) + return true; + + return $this->query('DROP INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name) ? true : false; + } + + function truncate_table($table_name, $no_prefix = false) + { + return $this->query('DELETE FROM '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false; + } +} diff --git a/include/email.php b/include/email.php new file mode 100644 index 0000000..b66b584 --- /dev/null +++ b/include/email.php @@ -0,0 +1,364 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +// Make sure no one attempts to run this script "directly" +if (!defined('PUN')) + exit; + +// Define line breaks in mail headers; possible values can be PHP_EOL, "\r\n", "\n" or "\r" +if (!defined('FORUM_EOL')) + define('FORUM_EOL', PHP_EOL); + +require PUN_ROOT.'include/utf8/utils/ascii.php'; + +// +// Validate an email address +// +function is_valid_email($email) +{ + if (strlen($email) > 80) + return false; + + return preg_match('%^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|("[^"]+"))@((\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\])|(([a-zA-Z\d\-]+\.)+[a-zA-Z]{2,}))$%', $email); +} + + +// +// Check if $email is banned +// +function is_banned_email($email) +{ + global $pun_bans; + + foreach ($pun_bans as $cur_ban) + { + if ($cur_ban['email'] != '' && + ($email == $cur_ban['email'] || + (strpos($cur_ban['email'], '@') === false && stristr($email, '@'.$cur_ban['email'])))) + return true; + } + + return false; +} + + +// +// Only encode with base64, if there is at least one unicode character in the string +// +function encode_mail_text($str) +{ + if (utf8_is_ascii($str)) + return $str; + + return '=?UTF-8?B?'.base64_encode($str).'?='; +} + + +// +// Make a post email safe +// +function bbcode2email($text, $wrap_length = 72) +{ + static $base_url; + + if (!isset($base_url)) + $base_url = get_base_url(); + + $text = pun_trim($text, "\t\n "); + + $shortcut_urls = array( + 'topic' => '/viewtopic.php?id=$1', + 'post' => '/viewtopic.php?pid=$1#p$1', + 'forum' => '/viewforum.php?id=$1', + 'user' => '/profile.php?id=$1', + ); + + // Split code blocks and text so BBcode in codeblocks won't be touched + list($code, $text) = extract_blocks($text, '[code]', '[/code]'); + + // Strip all bbcodes, except the quote, url, img, email, code and list items bbcodes + $text = preg_replace(array( + '%\[/?(?!(?:quote|url|topic|post|user|forum|img|email|code|list|\*))[a-z]+(?:=[^\]]+)?\]%i', + '%\n\[/?list(?:=[^\]]+)?\]%i' // A separate regex for the list tags to get rid of some whitespace + ), '', $text); + + // Match the deepest nested bbcode + // An adapted example from Mastering Regular Expressions + $match_quote_regex = '% + \[(quote|\*|url|img|email|topic|post|user|forum)(?:=([^\]]+))?\] + ( + (?>[^\[]*) + (?> + (?!\[/?\1(?:=[^\]]+)?\]) + \[ + [^\[]* + )* + ) + \[/\1\] + %ix'; + + $url_index = 1; + $url_stack = array(); + while (preg_match($match_quote_regex, $text, $matches)) + { + // Quotes + if ($matches[1] == 'quote') + { + // Put '>' or '> ' at the start of a line + $replacement = preg_replace( + array('%^(?=\>)%m', '%^(?!\>)%m'), + array('>', '> '), + $matches[2].":\n".$matches[3]); + } + + // List items + elseif ($matches[1] == '*') + { + $replacement = ' * '.$matches[3]; + } + + // URLs and emails + elseif (in_array($matches[1], array('url', 'email'))) + { + if (!empty($matches[2])) + { + $replacement = '['.$matches[3].']['.$url_index.']'; + $url_stack[$url_index] = $matches[2]; + $url_index++; + } + else + $replacement = '['.$matches[3].']'; + } + + // Images + elseif ($matches[1] == 'img') + { + if (!empty($matches[2])) + $replacement = '['.$matches[2].']['.$url_index.']'; + else + $replacement = '['.basename($matches[3]).']['.$url_index.']'; + + $url_stack[$url_index] = $matches[3]; + $url_index++; + } + + // Topic, post, forum and user URLs + elseif (in_array($matches[1], array('topic', 'post', 'forum', 'user'))) + { + $url = isset($shortcut_urls[$matches[1]]) ? $base_url.$shortcut_urls[$matches[1]] : ''; + + if (!empty($matches[2])) + { + $replacement = '['.$matches[3].']['.$url_index.']'; + $url_stack[$url_index] = str_replace('$1', $matches[2], $url); + $url_index++; + } + else + $replacement = '['.str_replace('$1', $matches[3], $url).']'; + } + + // Update the main text if there is a replacement + if (!is_null($replacement)) + { + $text = str_replace($matches[0], $replacement, $text); + $replacement = null; + } + } + + // Put code blocks and text together + if (isset($code)) + { + $parts = explode("\1", $text); + $text = ''; + foreach ($parts as $i => $part) + { + $text .= $part; + if (isset($code[$i])) + $text .= trim($code[$i], "\n\r"); + } + } + + // Put URLs at the bottom + if ($url_stack) + { + $text .= "\n\n"; + foreach ($url_stack as $i => $url) + $text .= "\n".' ['.$i.']: '.$url; + } + + // Wrap lines if $wrap_length is higher than -1 + if ($wrap_length > -1) + { + // Split all lines and wrap them individually + $parts = explode("\n", $text); + foreach ($parts as $k => $part) + { + preg_match('%^(>+ )?(.*)%', $part, $matches); + $parts[$k] = wordwrap($matches[1].$matches[2], $wrap_length - + strlen($matches[1]), "\n".$matches[1]); + } + + return implode("\n", $parts); + } + else + return $text; +} + + +// +// Wrapper for PHP's mail() +// +function pun_mail($to, $subject, $message, $reply_to_email = '', $reply_to_name = '') +{ + global $pun_config, $lang_common; + + // Use \r\n for SMTP servers, the system's line ending for local mailers + $smtp = $pun_config['o_smtp_host'] != ''; + $EOL = $smtp ? "\r\n" : FORUM_EOL; + + // Default sender/return address + $from_name = sprintf($lang_common['Mailer'], $pun_config['o_board_title']); + $from_email = $pun_config['o_webmaster_email']; + + // Do a little spring cleaning + $to = pun_trim(preg_replace('%[\n\r]+%s', '', $to)); + $subject = pun_trim(preg_replace('%[\n\r]+%s', '', $subject)); + $from_email = pun_trim(preg_replace('%[\n\r:]+%s', '', $from_email)); + $from_name = pun_trim(preg_replace('%[\n\r:]+%s', '', str_replace('"', '', $from_name))); + $reply_to_email = pun_trim(preg_replace('%[\n\r:]+%s', '', $reply_to_email)); + $reply_to_name = pun_trim(preg_replace('%[\n\r:]+%s', '', str_replace('"', '', $reply_to_name))); + + // Set up some headers to take advantage of UTF-8 + $from = '"'.encode_mail_text($from_name).'" <'.$from_email.'>'; + $subject = encode_mail_text($subject); + + $headers = 'From: '.$from.$EOL.'Date: '.gmdate('r').$EOL.'MIME-Version: 1.0'.$EOL.'Content-transfer-encoding: 8bit'.$EOL.'Content-type: text/plain; charset=utf-8'.$EOL.'X-Mailer: FluxBB Mailer'; + + // If we specified a reply-to email, we deal with it here + if (!empty($reply_to_email)) + { + $reply_to = '"'.encode_mail_text($reply_to_name).'" <'.$reply_to_email.'>'; + + $headers .= $EOL.'Reply-To: '.$reply_to; + } + + // Make sure all linebreaks are LF in message (and strip out any NULL bytes) + $message = str_replace("\0", '', pun_linebreaks($message)); + $message = str_replace("\n", $EOL, $message); + + $mailer = $smtp ? 'smtp_mail' : 'mail'; + $mailer($to, $subject, $message, $headers); +} + + +// +// This function was originally a part of the phpBB Group forum software phpBB2 (http://www.phpbb.com) +// They deserve all the credit for writing it. I made small modifications for it to suit PunBB and its coding standards +// +function server_parse($socket, $expected_response) +{ + $server_response = ''; + while (substr($server_response, 3, 1) != ' ') + { + if (!($server_response = fgets($socket, 256))) + error('Couldn\'t get mail server response codes. Please contact the forum administrator.', __FILE__, __LINE__); + } + + if (!(substr($server_response, 0, 3) == $expected_response)) + error('Unable to send email. Please contact the forum administrator with the following error message reported by the SMTP server: "'.$server_response.'"', __FILE__, __LINE__); +} + + +// +// This function was originally a part of the phpBB Group forum software phpBB2 (http://www.phpbb.com) +// They deserve all the credit for writing it. I made small modifications for it to suit PunBB and its coding standards. +// +function smtp_mail($to, $subject, $message, $headers = '') +{ + global $pun_config; + static $local_host; + + $recipients = explode(',', $to); + + // Sanitize the message + $message = str_replace("\r\n.", "\r\n..", $message); + $message = (substr($message, 0, 1) == '.' ? '.'.$message : $message); + + // Are we using port 25 or a custom port? + if (strpos($pun_config['o_smtp_host'], ':') !== false) + list($smtp_host, $smtp_port) = explode(':', $pun_config['o_smtp_host']); + else + { + $smtp_host = $pun_config['o_smtp_host']; + $smtp_port = 25; + } + + if ($pun_config['o_smtp_ssl'] == '1') + $smtp_host = 'ssl://'.$smtp_host; + + if (!($socket = fsockopen($smtp_host, $smtp_port, $errno, $errstr, 15))) + error('Could not connect to smtp host "'.$pun_config['o_smtp_host'].'" ('.$errno.') ('.$errstr.')', __FILE__, __LINE__); + + server_parse($socket, '220'); + + if (!isset($local_host)) + { + // Here we try to determine the *real* hostname (reverse DNS entry preferably) + $local_host = php_uname('n'); + + // Able to resolve name to IP + if (($local_addr = @gethostbyname($local_host)) !== $local_host) + { + // Able to resolve IP back to name + if (($local_name = @gethostbyaddr($local_addr)) !== $local_addr) + $local_host = $local_name; + } + } + + if ($pun_config['o_smtp_user'] != '' && $pun_config['o_smtp_pass'] != '') + { + fwrite($socket, 'EHLO '.$local_host."\r\n"); + server_parse($socket, '250'); + + fwrite($socket, 'AUTH LOGIN'."\r\n"); + server_parse($socket, '334'); + + fwrite($socket, base64_encode($pun_config['o_smtp_user'])."\r\n"); + server_parse($socket, '334'); + + fwrite($socket, base64_encode($pun_config['o_smtp_pass'])."\r\n"); + server_parse($socket, '235'); + } + else + { + fwrite($socket, 'HELO '.$local_host."\r\n"); + server_parse($socket, '250'); + } + + fwrite($socket, 'MAIL FROM: <'.$pun_config['o_webmaster_email'].'>'."\r\n"); + server_parse($socket, '250'); + + foreach ($recipients as $email) + { + fwrite($socket, 'RCPT TO: <'.$email.'>'."\r\n"); + server_parse($socket, '250'); + } + + fwrite($socket, 'DATA'."\r\n"); + server_parse($socket, '354'); + + fwrite($socket, 'Subject: '.$subject."\r\n".'To: <'.implode('>, <', $recipients).'>'."\r\n".$headers."\r\n\r\n".$message."\r\n"); + + fwrite($socket, '.'."\r\n"); + server_parse($socket, '250'); + + fwrite($socket, 'QUIT'."\r\n"); + fclose($socket); + + return true; +} diff --git a/include/functions.php b/include/functions.php new file mode 100644 index 0000000..ace2934 --- /dev/null +++ b/include/functions.php @@ -0,0 +1,2227 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + + + +// +// Return current timestamp (with microseconds) as a float +// +function get_microtime() +{ + list($usec, $sec) = explode(' ', microtime()); + return ((float)$usec + (float)$sec); +} + +// +// Cookie stuff! +// +function check_cookie(&$pun_user) +{ + global $db, $db_type, $pun_config, $cookie_name, $cookie_seed; + + $now = time(); + + // If the cookie is set and it matches the correct pattern, then read the values from it + if (isset($_COOKIE[$cookie_name]) && preg_match('%^(\d+)\|([0-9a-fA-F]+)\|(\d+)\|([0-9a-fA-F]+)$%', $_COOKIE[$cookie_name], $matches)) + { + $cookie = array( + 'user_id' => intval($matches[1]), + 'password_hash' => $matches[2], + 'expiration_time' => intval($matches[3]), + 'cookie_hash' => $matches[4], + ); + } + + // If it has a non-guest user, and hasn't expired + if (isset($cookie) && $cookie['user_id'] > 1 && $cookie['expiration_time'] > $now) + { + // If the cookie has been tampered with + $is_authorized = pun_hash_equals(forum_hmac($cookie['user_id'].'|'.$cookie['expiration_time'], $cookie_seed.'_cookie_hash'), $cookie['cookie_hash']); + if (!$is_authorized) + { + $expire = $now + 31536000; // The cookie expires after a year + pun_setcookie(1, pun_hash(uniqid(rand(), true)), $expire); + set_default_user(); + + return; + } + + // Check if there's a user with the user ID and password hash from the cookie + $result = $db->query('SELECT u.*, g.*, o.logged, o.idle FROM '.$db->prefix.'users AS u INNER JOIN '.$db->prefix.'groups AS g ON u.group_id=g.g_id LEFT JOIN '.$db->prefix.'online AS o ON o.user_id=u.id WHERE u.id='.intval($cookie['user_id'])) or error('Unable to fetch user information', __FILE__, __LINE__, $db->error()); + $pun_user = $db->fetch_assoc($result); + + // If user authorisation failed + $is_authorized = pun_hash_equals(forum_hmac($pun_user['password'], $cookie_seed.'_password_hash'), $cookie['password_hash']); + if (!isset($pun_user['id']) || !$is_authorized) + { + $expire = $now + 31536000; // The cookie expires after a year + pun_setcookie(1, pun_hash(uniqid(rand(), true)), $expire); + set_default_user(); + + return; + } + + // Send a new, updated cookie with a new expiration timestamp + $expire = ($cookie['expiration_time'] > $now + $pun_config['o_timeout_visit']) ? $now + 1209600 : $now + $pun_config['o_timeout_visit']; + pun_setcookie($pun_user['id'], $pun_user['password'], $expire); + + // Set a default language if the user selected language no longer exists + if (!file_exists(PUN_ROOT.'lang/'.$pun_user['language'])) + $pun_user['language'] = $pun_config['o_default_lang']; + + // Set a default style if the user selected style no longer exists + if (!file_exists(PUN_ROOT.'style/'.$pun_user['style'].'.css')) + $pun_user['style'] = $pun_config['o_default_style']; + + if (!$pun_user['disp_topics']) + $pun_user['disp_topics'] = $pun_config['o_disp_topics_default']; + if (!$pun_user['disp_posts']) + $pun_user['disp_posts'] = $pun_config['o_disp_posts_default']; + + // Define this if you want this visit to affect the online list and the users last visit data + if (!defined('PUN_QUIET_VISIT')) + { + // Update the online list + if (!$pun_user['logged']) + { + $pun_user['logged'] = $now; + + // With MySQL/MySQLi/SQLite, REPLACE INTO avoids a user having two rows in the online table + switch ($db_type) + { + case 'mysql': + case 'mysqli': + case 'mysql_innodb': + case 'mysqli_innodb': + case 'sqlite': + $db->query('REPLACE INTO '.$db->prefix.'online (user_id, ident, logged) VALUES('.$pun_user['id'].', \''.$db->escape($pun_user['username']).'\', '.$pun_user['logged'].')') or error('Unable to insert into online list', __FILE__, __LINE__, $db->error()); + break; + + default: + $db->query('INSERT INTO '.$db->prefix.'online (user_id, ident, logged) SELECT '.$pun_user['id'].', \''.$db->escape($pun_user['username']).'\', '.$pun_user['logged'].' WHERE NOT EXISTS (SELECT 1 FROM '.$db->prefix.'online WHERE user_id='.$pun_user['id'].')') or error('Unable to insert into online list', __FILE__, __LINE__, $db->error()); + break; + } + + // Reset tracked topics + set_tracked_topics(null); + } + else + { + // Special case: We've timed out, but no other user has browsed the forums since we timed out + if ($pun_user['logged'] < ($now-$pun_config['o_timeout_visit'])) + { + $db->query('UPDATE '.$db->prefix.'users SET last_visit='.$pun_user['logged'].' WHERE id='.$pun_user['id']) or error('Unable to update user visit data', __FILE__, __LINE__, $db->error()); + $pun_user['last_visit'] = $pun_user['logged']; + } + + $idle_sql = ($pun_user['idle'] == '1') ? ', idle=0' : ''; + $db->query('UPDATE '.$db->prefix.'online SET logged='.$now.$idle_sql.' WHERE user_id='.$pun_user['id']) or error('Unable to update online list', __FILE__, __LINE__, $db->error()); + + // Update tracked topics with the current expire time + if (isset($_COOKIE[$cookie_name.'_track'])) + forum_setcookie($cookie_name.'_track', $_COOKIE[$cookie_name.'_track'], $now + $pun_config['o_timeout_visit']); + } + } + else + { + if (!$pun_user['logged']) + $pun_user['logged'] = $pun_user['last_visit']; + } + + $pun_user['is_guest'] = false; + $pun_user['is_admmod'] = $pun_user['g_id'] == PUN_ADMIN || $pun_user['g_moderator'] == '1'; + } + else + set_default_user(); +} + + +// +// Converts the CDATA end sequence ]]> into ]]> +// +function escape_cdata($str) +{ + return str_replace(']]>', ']]>', $str); +} + + +// +// Authenticates the provided username and password against the user database +// $user can be either a user ID (integer) or a username (string) +// $password can be either a plaintext password or a password hash including salt ($password_is_hash must be set accordingly) +// +function authenticate_user($user, $password, $password_is_hash = false) +{ + global $db, $pun_user; + + // Check if there's a user matching $user and $password + $result = $db->query('SELECT u.*, g.*, o.logged, o.idle FROM '.$db->prefix.'users AS u INNER JOIN '.$db->prefix.'groups AS g ON g.g_id=u.group_id LEFT JOIN '.$db->prefix.'online AS o ON o.user_id=u.id WHERE '.(is_int($user) ? 'u.id='.intval($user) : 'u.username=\''.$db->escape($user).'\'')) or error('Unable to fetch user info', __FILE__, __LINE__, $db->error()); + $pun_user = $db->fetch_assoc($result); + + $is_password_authorized = pun_hash_equals($password, $pun_user['password']); + $is_hash_authorized = pun_hash_equals(pun_hash($password), $pun_user['password']); + + if (!isset($pun_user['id']) || + ($password_is_hash && !$is_password_authorized || + (!$password_is_hash && !$is_hash_authorized))) + set_default_user(); + else + $pun_user['is_guest'] = false; +} + + +// +// Try to determine the current URL +// +function get_current_url($max_length = 0) +{ + $protocol = get_current_protocol(); + $port = (isset($_SERVER['SERVER_PORT']) && (($_SERVER['SERVER_PORT'] != '80' && $protocol == 'http') || ($_SERVER['SERVER_PORT'] != '443' && $protocol == 'https')) && strpos($_SERVER['HTTP_HOST'], ':') === false) ? ':'.$_SERVER['SERVER_PORT'] : ''; + + $url = urldecode($protocol.'://'.$_SERVER['HTTP_HOST'].$port.$_SERVER['REQUEST_URI']); + + if (strlen($url) <= $max_length || $max_length == 0) + return $url; + + // We can't find a short enough url + return null; +} + + +// +// Fetch the current protocol in use - http or https +// +function get_current_protocol() +{ + $protocol = 'http'; + + // Check if the server is claiming to using HTTPS + if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') + $protocol = 'https'; + + // If we are behind a reverse proxy try to decide which protocol it is using + if (defined('FORUM_BEHIND_REVERSE_PROXY')) + { + // Check if we are behind a Microsoft based reverse proxy + if (!empty($_SERVER['HTTP_FRONT_END_HTTPS']) && strtolower($_SERVER['HTTP_FRONT_END_HTTPS']) != 'off') + $protocol = 'https'; + + // Check if we're behind a "proper" reverse proxy, and what protocol it's using + if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) + $protocol = strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']); + } + + return $protocol; +} + + +// +// Fetch the base_url, optionally support HTTPS and HTTP +// +function get_base_url($support_https = false) +{ + global $pun_config; + static $base_url; + + if (!$support_https) + return $pun_config['o_base_url']; + + if (!isset($base_url)) + { + // Make sure we are using the correct protocol + $base_url = str_replace(array('http://', 'https://'), get_current_protocol().'://', $pun_config['o_base_url']); + } + + return $base_url; +} + + +// +// Fetch admin IDs +// +function get_admin_ids() +{ + if (file_exists(FORUM_CACHE_DIR.'cache_admins.php')) + include FORUM_CACHE_DIR.'cache_admins.php'; + + if (!defined('PUN_ADMINS_LOADED')) + { + if (!defined('FORUM_CACHE_FUNCTIONS_LOADED')) + require PUN_ROOT.'include/cache.php'; + + generate_admins_cache(); + require FORUM_CACHE_DIR.'cache_admins.php'; + } + + return $pun_admins; +} + + +// +// Fill $pun_user with default values (for guests) +// +function set_default_user() +{ + global $db, $db_type, $pun_user, $pun_config; + + $remote_addr = get_remote_address(); + + // Fetch guest user + $result = $db->query('SELECT u.*, g.*, o.logged, o.last_post, o.last_search FROM '.$db->prefix.'users AS u INNER JOIN '.$db->prefix.'groups AS g ON u.group_id=g.g_id LEFT JOIN '.$db->prefix.'online AS o ON o.ident=\''.$db->escape($remote_addr).'\' WHERE u.id=1') or error('Unable to fetch guest information', __FILE__, __LINE__, $db->error()); + if (!$db->num_rows($result)) + exit('Unable to fetch guest information. Your database must contain both a guest user and a guest user group.'); + + $pun_user = $db->fetch_assoc($result); + + // Update online list + if (!$pun_user['logged']) + { + $pun_user['logged'] = time(); + + // With MySQL/MySQLi/SQLite, REPLACE INTO avoids a user having two rows in the online table + switch ($db_type) + { + case 'mysql': + case 'mysqli': + case 'mysql_innodb': + case 'mysqli_innodb': + case 'sqlite': + $db->query('REPLACE INTO '.$db->prefix.'online (user_id, ident, logged) VALUES(1, \''.$db->escape($remote_addr).'\', '.$pun_user['logged'].')') or error('Unable to insert into online list', __FILE__, __LINE__, $db->error()); + break; + + default: + $db->query('INSERT INTO '.$db->prefix.'online (user_id, ident, logged) SELECT 1, \''.$db->escape($remote_addr).'\', '.$pun_user['logged'].' WHERE NOT EXISTS (SELECT 1 FROM '.$db->prefix.'online WHERE ident=\''.$db->escape($remote_addr).'\')') or error('Unable to insert into online list', __FILE__, __LINE__, $db->error()); + break; + } + } + else + $db->query('UPDATE '.$db->prefix.'online SET logged='.time().' WHERE ident=\''.$db->escape($remote_addr).'\'') or error('Unable to update online list', __FILE__, __LINE__, $db->error()); + + $pun_user['disp_topics'] = $pun_config['o_disp_topics_default']; + $pun_user['disp_posts'] = $pun_config['o_disp_posts_default']; + $pun_user['timezone'] = $pun_config['o_default_timezone']; + $pun_user['dst'] = $pun_config['o_default_dst']; + $pun_user['language'] = $pun_config['o_default_lang']; + $pun_user['style'] = $pun_config['o_default_style']; + $pun_user['is_guest'] = true; + $pun_user['is_admmod'] = false; +} + + +// +// SHA1 HMAC with PHP 4 fallback +// +function forum_hmac($data, $key, $raw_output = false) +{ + if (function_exists('hash_hmac')) + return hash_hmac('sha1', $data, $key, $raw_output); + + // If key size more than blocksize then we hash it once + if (strlen($key) > 64) + $key = pack('H*', sha1($key)); // we have to use raw output here to match the standard + + // Ensure we're padded to exactly one block boundary + $key = str_pad($key, 64, chr(0x00)); + + $hmac_opad = str_repeat(chr(0x5C), 64); + $hmac_ipad = str_repeat(chr(0x36), 64); + + // Do inner and outer padding + for ($i = 0;$i < 64;$i++) { + $hmac_opad[$i] = $hmac_opad[$i] ^ $key[$i]; + $hmac_ipad[$i] = $hmac_ipad[$i] ^ $key[$i]; + } + + // Finally, calculate the HMAC + $hash = sha1($hmac_opad.pack('H*', sha1($hmac_ipad.$data))); + + // If we want raw output then we need to pack the final result + if ($raw_output) + $hash = pack('H*', $hash); + + return $hash; +} + + +// +// Set a cookie, FluxBB style! +// Wrapper for forum_setcookie +// +function pun_setcookie($user_id, $password_hash, $expire) +{ + global $cookie_name, $cookie_seed; + + forum_setcookie($cookie_name, $user_id.'|'.forum_hmac($password_hash, $cookie_seed.'_password_hash').'|'.$expire.'|'.forum_hmac($user_id.'|'.$expire, $cookie_seed.'_cookie_hash'), $expire); +} + + +// +// Set a cookie, FluxBB style! +// +function forum_setcookie($name, $value, $expire) +{ + global $cookie_path, $cookie_domain, $cookie_secure, $pun_config; + + if ($expire - time() - $pun_config['o_timeout_visit'] < 1) + $expire = 0; + + // Enable sending of a P3P header + header('P3P: CP="CUR ADM"'); + + if (version_compare(PHP_VERSION, '5.2.0', '>=')) + setcookie($name, $value, $expire, $cookie_path, $cookie_domain, $cookie_secure, true); + else + setcookie($name, $value, $expire, $cookie_path.'; HttpOnly', $cookie_domain, $cookie_secure); +} + + +// +// Check whether the connecting user is banned (and delete any expired bans while we're at it) +// +function check_bans() +{ + global $db, $pun_config, $lang_common, $pun_user, $pun_bans; + + // Admins and moderators aren't affected + if ($pun_user['is_admmod'] || !$pun_bans) + return; + + // Add a dot or a colon (depending on IPv4/IPv6) at the end of the IP address to prevent banned address + // 192.168.0.5 from matching e.g. 192.168.0.50 + $user_ip = get_remote_address(); + $user_ip .= (strpos($user_ip, '.') !== false) ? '.' : ':'; + + $bans_altered = false; + $is_banned = false; + + foreach ($pun_bans as $cur_ban) + { + // Has this ban expired? + if ($cur_ban['expire'] != '' && $cur_ban['expire'] <= time()) + { + $db->query('DELETE FROM '.$db->prefix.'bans WHERE id='.$cur_ban['id']) or error('Unable to delete expired ban', __FILE__, __LINE__, $db->error()); + $bans_altered = true; + continue; + } + + if ($cur_ban['username'] != '' && utf8_strtolower($pun_user['username']) == utf8_strtolower($cur_ban['username'])) + $is_banned = true; + + if ($cur_ban['ip'] != '') + { + $cur_ban_ips = explode(' ', $cur_ban['ip']); + + $num_ips = count($cur_ban_ips); + for ($i = 0; $i < $num_ips; ++$i) + { + // Add the proper ending to the ban + if (strpos($user_ip, '.') !== false) + $cur_ban_ips[$i] = $cur_ban_ips[$i].'.'; + else + $cur_ban_ips[$i] = $cur_ban_ips[$i].':'; + + if (substr($user_ip, 0, strlen($cur_ban_ips[$i])) == $cur_ban_ips[$i]) + { + $is_banned = true; + break; + } + } + } + + if ($is_banned) + { + $db->query('DELETE FROM '.$db->prefix.'online WHERE ident=\''.$db->escape($pun_user['username']).'\'') or error('Unable to delete from online list', __FILE__, __LINE__, $db->error()); + message($lang_common['Ban message'].' '.(($cur_ban['expire'] != '') ? $lang_common['Ban message 2'].' '.strtolower(format_time($cur_ban['expire'], true)).'. ' : '').(($cur_ban['message'] != '') ? $lang_common['Ban message 3'].'<br /><br /><strong>'.pun_htmlspecialchars($cur_ban['message']).'</strong><br /><br />' : '<br /><br />').$lang_common['Ban message 4'].' <a href="mailto:'.pun_htmlspecialchars($pun_config['o_admin_email']).'">'.pun_htmlspecialchars($pun_config['o_admin_email']).'</a>.', true); + } + } + + // If we removed any expired bans during our run-through, we need to regenerate the bans cache + if ($bans_altered) + { + if (!defined('FORUM_CACHE_FUNCTIONS_LOADED')) + require PUN_ROOT.'include/cache.php'; + + generate_bans_cache(); + } +} + + +// +// Check username +// +function check_username($username, $exclude_id = null) +{ + global $db, $pun_config, $errors, $lang_prof_reg, $lang_register, $lang_common, $pun_bans; + + // Include UTF-8 function + require_once PUN_ROOT.'include/utf8/strcasecmp.php'; + + // Convert multiple whitespace characters into one (to prevent people from registering with indistinguishable usernames) + $username = preg_replace('%\s+%s', ' ', $username); + + // Validate username + if (pun_strlen($username) < 2) + $errors[] = $lang_prof_reg['Username too short']; + else if (pun_strlen($username) > 25) // This usually doesn't happen since the form element only accepts 25 characters + $errors[] = $lang_prof_reg['Username too long']; + else if (!strcasecmp($username, 'Guest') || !utf8_strcasecmp($username, $lang_common['Guest'])) + $errors[] = $lang_prof_reg['Username guest']; + else if (preg_match('%[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}%', $username) || preg_match('%((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))%', $username)) + $errors[] = $lang_prof_reg['Username IP']; + else if ((strpos($username, '[') !== false || strpos($username, ']') !== false) && strpos($username, '\'') !== false && strpos($username, '"') !== false) + $errors[] = $lang_prof_reg['Username reserved chars']; + else if (preg_match('%(?:\[/?(?:b|u|s|ins|del|em|i|h|colou?r|quote|code|img|url|email|list|\*|topic|post|forum|user)\]|\[(?:img|url|quote|list)=)%i', $username)) + $errors[] = $lang_prof_reg['Username BBCode']; + + // Check username for any censored words + if ($pun_config['o_censoring'] == '1' && censor_words($username) != $username) + $errors[] = $lang_register['Username censor']; + + // Check that the username (or a too similar username) is not already registered + $query = (!is_null($exclude_id)) ? ' AND id!='.$exclude_id : ''; + + $result = $db->query('SELECT username FROM '.$db->prefix.'users WHERE (UPPER(username)=UPPER(\''.$db->escape($username).'\') OR UPPER(username)=UPPER(\''.$db->escape(ucp_preg_replace('%[^\p{L}\p{N}]%u', '', $username)).'\')) AND id>1'.$query) or error('Unable to fetch user info', __FILE__, __LINE__, $db->error()); + + if ($db->num_rows($result)) + { + $busy = $db->result($result); + $errors[] = $lang_register['Username dupe 1'].' '.pun_htmlspecialchars($busy).'. '.$lang_register['Username dupe 2']; + } + + // Check username for any banned usernames + foreach ($pun_bans as $cur_ban) + { + if ($cur_ban['username'] != '' && utf8_strtolower($username) == utf8_strtolower($cur_ban['username'])) + { + $errors[] = $lang_prof_reg['Banned username']; + break; + } + } +} + + +// +// Update "Users online" +// +function update_users_online() +{ + global $db, $pun_config; + + $now = time(); + + // Fetch all online list entries that are older than "o_timeout_online" + $result = $db->query('SELECT user_id, ident, logged, idle FROM '.$db->prefix.'online WHERE logged<'.($now-$pun_config['o_timeout_online'])) or error('Unable to fetch old entries from online list', __FILE__, __LINE__, $db->error()); + while ($cur_user = $db->fetch_assoc($result)) + { + // If the entry is a guest, delete it + if ($cur_user['user_id'] == '1') + $db->query('DELETE FROM '.$db->prefix.'online WHERE ident=\''.$db->escape($cur_user['ident']).'\'') or error('Unable to delete from online list', __FILE__, __LINE__, $db->error()); + else + { + // If the entry is older than "o_timeout_visit", update last_visit for the user in question, then delete him/her from the online list + if ($cur_user['logged'] < ($now-$pun_config['o_timeout_visit'])) + { + $db->query('UPDATE '.$db->prefix.'users SET last_visit='.$cur_user['logged'].' WHERE id='.$cur_user['user_id']) or error('Unable to update user visit data', __FILE__, __LINE__, $db->error()); + $db->query('DELETE FROM '.$db->prefix.'online WHERE user_id='.$cur_user['user_id']) or error('Unable to delete from online list', __FILE__, __LINE__, $db->error()); + } + else if ($cur_user['idle'] == '0') + $db->query('UPDATE '.$db->prefix.'online SET idle=1 WHERE user_id='.$cur_user['user_id']) or error('Unable to insert into online list', __FILE__, __LINE__, $db->error()); + } + } +} + + +// +// Display the profile navigation menu +// +function generate_profile_menu($page = '') +{ + global $lang_profile, $pun_config, $pun_user, $id; + +?> +<div id="profile" class="block2col"> + <div class="blockmenu"> + <h2><span><?php echo $lang_profile['Profile menu'] ?></span></h2> + <div class="box"> + <div class="inbox"> + <ul> + <li<?php if ($page == 'essentials') echo ' class="isactive"'; ?>><a href="profile.php?section=essentials&id=<?php echo $id ?>"><?php echo $lang_profile['Section essentials'] ?></a></li> + <li<?php if ($page == 'personal') echo ' class="isactive"'; ?>><a href="profile.php?section=personal&id=<?php echo $id ?>"><?php echo $lang_profile['Section personal'] ?></a></li> + <li<?php if ($page == 'messaging') echo ' class="isactive"'; ?>><a href="profile.php?section=messaging&id=<?php echo $id ?>"><?php echo $lang_profile['Section messaging'] ?></a></li> +<?php if ($pun_config['o_avatars'] == '1' || $pun_config['o_signatures'] == '1'): ?> <li<?php if ($page == 'personality') echo ' class="isactive"'; ?>><a href="profile.php?section=personality&id=<?php echo $id ?>"><?php echo $lang_profile['Section personality'] ?></a></li> +<?php endif; ?> <li<?php if ($page == 'display') echo ' class="isactive"'; ?>><a href="profile.php?section=display&id=<?php echo $id ?>"><?php echo $lang_profile['Section display'] ?></a></li> + <li<?php if ($page == 'privacy') echo ' class="isactive"'; ?>><a href="profile.php?section=privacy&id=<?php echo $id ?>"><?php echo $lang_profile['Section privacy'] ?></a></li> +<?php if ($pun_user['g_id'] == PUN_ADMIN || ($pun_user['g_moderator'] == '1' && $pun_user['g_mod_ban_users'] == '1')): ?> <li<?php if ($page == 'admin') echo ' class="isactive"'; ?>><a href="profile.php?section=admin&id=<?php echo $id ?>"><?php echo $lang_profile['Section admin'] ?></a></li> +<?php endif; ?> </ul> + </div> + </div> + </div> +<?php + +} + + +// +// Outputs markup to display a user's avatar +// +function generate_avatar_markup($user_id) +{ + global $pun_config; + + $filetypes = array('jpg', 'gif', 'png'); + $avatar_markup = ''; + + foreach ($filetypes as $cur_type) + { + $path = $pun_config['o_avatars_dir'].'/'.$user_id.'.'.$cur_type; + + if (file_exists(PUN_ROOT.$path) && $img_size = getimagesize(PUN_ROOT.$path)) + { + $avatar_markup = '<img src="'.pun_htmlspecialchars(get_base_url(true).'/'.$path.'?m='.filemtime(PUN_ROOT.$path)).'" '.$img_size[3].' alt="" />'; + break; + } + } + + return $avatar_markup; +} + + +// +// Generate browser's title +// +function generate_page_title($page_title, $p = null) +{ + global $lang_common; + + if (!is_array($page_title)) + $page_title = array($page_title); + + $page_title = array_reverse($page_title); + + if ($p > 1) + $page_title[0] .= ' ('.sprintf($lang_common['Page'], forum_number_format($p)).')'; + + $crumbs = implode($lang_common['Title separator'], $page_title); + + return $crumbs; +} + + +// +// Save array of tracked topics in cookie +// +function set_tracked_topics($tracked_topics) +{ + global $cookie_name, $cookie_path, $cookie_domain, $cookie_secure, $pun_config; + + $cookie_data = ''; + if (!empty($tracked_topics)) + { + // Sort the arrays (latest read first) + arsort($tracked_topics['topics'], SORT_NUMERIC); + arsort($tracked_topics['forums'], SORT_NUMERIC); + + // Homebrew serialization (to avoid having to run unserialize() on cookie data) + foreach ($tracked_topics['topics'] as $id => $timestamp) + $cookie_data .= 't'.$id.'='.$timestamp.';'; + foreach ($tracked_topics['forums'] as $id => $timestamp) + $cookie_data .= 'f'.$id.'='.$timestamp.';'; + + // Enforce a byte size limit (4096 minus some space for the cookie name - defaults to 4048) + if (strlen($cookie_data) > FORUM_MAX_COOKIE_SIZE) + { + $cookie_data = substr($cookie_data, 0, FORUM_MAX_COOKIE_SIZE); + $cookie_data = substr($cookie_data, 0, strrpos($cookie_data, ';')).';'; + } + } + + forum_setcookie($cookie_name.'_track', $cookie_data, time() + $pun_config['o_timeout_visit']); + $_COOKIE[$cookie_name.'_track'] = $cookie_data; // Set it directly in $_COOKIE as well +} + + +// +// Extract array of tracked topics from cookie +// +function get_tracked_topics() +{ + global $cookie_name; + + $cookie_data = isset($_COOKIE[$cookie_name.'_track']) ? $_COOKIE[$cookie_name.'_track'] : false; + if (!$cookie_data) + return array('topics' => array(), 'forums' => array()); + + if (strlen($cookie_data) > FORUM_MAX_COOKIE_SIZE) + return array('topics' => array(), 'forums' => array()); + + // Unserialize data from cookie + $tracked_topics = array('topics' => array(), 'forums' => array()); + $temp = explode(';', $cookie_data); + foreach ($temp as $t) + { + $type = substr($t, 0, 1) == 'f' ? 'forums' : 'topics'; + $id = intval(substr($t, 1)); + $timestamp = intval(substr($t, strpos($t, '=') + 1)); + if ($id > 0 && $timestamp > 0) + $tracked_topics[$type][$id] = $timestamp; + } + + return $tracked_topics; +} + + +// +// Shortcut method for executing all callbacks registered with the addon manager for the given hook +// +function flux_hook($name) +{ + global $flux_addons; + + $flux_addons->hook($name); +} + + +// +// Update posts, topics, last_post, last_post_id and last_poster for a forum +// +function update_forum($forum_id) +{ + global $db; + + $result = $db->query('SELECT COUNT(id), SUM(num_replies) FROM '.$db->prefix.'topics WHERE forum_id='.$forum_id) or error('Unable to fetch forum topic count', __FILE__, __LINE__, $db->error()); + list($num_topics, $num_posts) = $db->fetch_row($result); + + $num_posts = $num_posts + $num_topics; // $num_posts is only the sum of all replies (we have to add the topic posts) + + $result = $db->query('SELECT last_post, last_post_id, last_poster FROM '.$db->prefix.'topics WHERE forum_id='.$forum_id.' AND moved_to IS NULL ORDER BY last_post DESC LIMIT 1') or error('Unable to fetch last_post/last_post_id/last_poster', __FILE__, __LINE__, $db->error()); + if ($db->num_rows($result)) // There are topics in the forum + { + list($last_post, $last_post_id, $last_poster) = $db->fetch_row($result); + + $db->query('UPDATE '.$db->prefix.'forums SET num_topics='.$num_topics.', num_posts='.$num_posts.', last_post='.$last_post.', last_post_id='.$last_post_id.', last_poster=\''.$db->escape($last_poster).'\' WHERE id='.$forum_id) or error('Unable to update last_post/last_post_id/last_poster', __FILE__, __LINE__, $db->error()); + } + else // There are no topics + $db->query('UPDATE '.$db->prefix.'forums SET num_topics='.$num_topics.', num_posts='.$num_posts.', last_post=NULL, last_post_id=NULL, last_poster=NULL WHERE id='.$forum_id) or error('Unable to update last_post/last_post_id/last_poster', __FILE__, __LINE__, $db->error()); +} + + +// +// Deletes any avatars owned by the specified user ID +// +function delete_avatar($user_id) +{ + global $pun_config; + + $filetypes = array('jpg', 'gif', 'png'); + + // Delete user avatar + foreach ($filetypes as $cur_type) + { + if (file_exists(PUN_ROOT.$pun_config['o_avatars_dir'].'/'.$user_id.'.'.$cur_type)) + @unlink(PUN_ROOT.$pun_config['o_avatars_dir'].'/'.$user_id.'.'.$cur_type); + } +} + + +// +// Delete a topic and all of its posts +// +function delete_topic($topic_id) +{ + global $db; + + // Delete the topic and any redirect topics + $db->query('DELETE FROM '.$db->prefix.'topics WHERE id='.$topic_id.' OR moved_to='.$topic_id) or error('Unable to delete topic', __FILE__, __LINE__, $db->error()); + + // Create a list of the post IDs in this topic + $post_ids = ''; + $result = $db->query('SELECT id FROM '.$db->prefix.'posts WHERE topic_id='.$topic_id) or error('Unable to fetch posts', __FILE__, __LINE__, $db->error()); + while ($row = $db->fetch_row($result)) + $post_ids .= ($post_ids != '') ? ','.$row[0] : $row[0]; + + // Make sure we have a list of post IDs + if ($post_ids != '') + { + strip_search_index($post_ids); + + // Delete posts in topic + $db->query('DELETE FROM '.$db->prefix.'posts WHERE topic_id='.$topic_id) or error('Unable to delete posts', __FILE__, __LINE__, $db->error()); + } + + // Delete any subscriptions for this topic + $db->query('DELETE FROM '.$db->prefix.'topic_subscriptions WHERE topic_id='.$topic_id) or error('Unable to delete subscriptions', __FILE__, __LINE__, $db->error()); +} + + +// +// Delete a single post +// +function delete_post($post_id, $topic_id) +{ + global $db; + + $result = $db->query('SELECT id, poster, posted FROM '.$db->prefix.'posts WHERE topic_id='.$topic_id.' ORDER BY id DESC LIMIT 2') or error('Unable to fetch post info', __FILE__, __LINE__, $db->error()); + list($last_id, ,) = $db->fetch_row($result); + list($second_last_id, $second_poster, $second_posted) = $db->fetch_row($result); + + // Delete the post + $db->query('DELETE FROM '.$db->prefix.'posts WHERE id='.$post_id) or error('Unable to delete post', __FILE__, __LINE__, $db->error()); + + strip_search_index($post_id); + + // Count number of replies in the topic + $result = $db->query('SELECT COUNT(id) FROM '.$db->prefix.'posts WHERE topic_id='.$topic_id) or error('Unable to fetch post count for topic', __FILE__, __LINE__, $db->error()); + $num_replies = $db->result($result, 0) - 1; + + // If the message we deleted is the most recent in the topic (at the end of the topic) + if ($last_id == $post_id) + { + // If there is a $second_last_id there is more than 1 reply to the topic + if (!empty($second_last_id)) + $db->query('UPDATE '.$db->prefix.'topics SET last_post='.$second_posted.', last_post_id='.$second_last_id.', last_poster=\''.$db->escape($second_poster).'\', num_replies='.$num_replies.' WHERE id='.$topic_id) or error('Unable to update topic', __FILE__, __LINE__, $db->error()); + else + // We deleted the only reply, so now last_post/last_post_id/last_poster is posted/id/poster from the topic itself + $db->query('UPDATE '.$db->prefix.'topics SET last_post=posted, last_post_id=id, last_poster=poster, num_replies='.$num_replies.' WHERE id='.$topic_id) or error('Unable to update topic', __FILE__, __LINE__, $db->error()); + } + else + // Otherwise we just decrement the reply counter + $db->query('UPDATE '.$db->prefix.'topics SET num_replies='.$num_replies.' WHERE id='.$topic_id) or error('Unable to update topic', __FILE__, __LINE__, $db->error()); +} + + +// +// Delete every .php file in the forum's cache directory +// +function forum_clear_cache() +{ + $d = dir(FORUM_CACHE_DIR); + while (($entry = $d->read()) !== false) + { + if (substr($entry, -4) == '.php') + @unlink(FORUM_CACHE_DIR.$entry); + } + $d->close(); +} + + +// +// Replace censored words in $text +// +function censor_words($text) +{ + global $db; + static $search_for, $replace_with; + + // If not already built in a previous call, build an array of censor words and their replacement text + if (!isset($search_for)) + { + if (file_exists(FORUM_CACHE_DIR.'cache_censoring.php')) + include FORUM_CACHE_DIR.'cache_censoring.php'; + + if (!defined('PUN_CENSOR_LOADED')) + { + if (!defined('FORUM_CACHE_FUNCTIONS_LOADED')) + require PUN_ROOT.'include/cache.php'; + + generate_censoring_cache(); + require FORUM_CACHE_DIR.'cache_censoring.php'; + } + } + + if (!empty($search_for)) + $text = substr(ucp_preg_replace($search_for, $replace_with, ' '.$text.' '), 1, -1); + + return $text; +} + + +// +// Determines the correct title for $user +// $user must contain the elements 'username', 'title', 'posts', 'g_id' and 'g_user_title' +// +function get_title($user) +{ + global $pun_bans, $lang_common; + static $ban_list; + + // If not already built in a previous call, build an array of lowercase banned usernames + if (empty($ban_list)) + { + $ban_list = array(); + + foreach ($pun_bans as $cur_ban) + $ban_list[] = utf8_strtolower($cur_ban['username']); + } + + // If the user is banned + if (in_array(utf8_strtolower($user['username']), $ban_list)) + $user_title = $lang_common['Banned']; + // If the user has a custom title + else if ($user['title'] != '') + $user_title = pun_htmlspecialchars($user['title']); + // If the user group has a default user title + else if ($user['g_user_title'] != '') + $user_title = pun_htmlspecialchars($user['g_user_title']); + // If the user is a guest + else if ($user['g_id'] == PUN_GUEST) + $user_title = $lang_common['Guest']; + // If nothing else helps, we assign the default + else + $user_title = $lang_common['Member']; + + return $user_title; +} + + +// +// Generate a string with numbered links (for multipage scripts) +// +function paginate($num_pages, $cur_page, $link) +{ + global $lang_common; + + $pages = array(); + $link_to_all = false; + + // If $cur_page == -1, we link to all pages (used in viewforum.php) + if ($cur_page == -1) + { + $cur_page = 1; + $link_to_all = true; + } + + if ($num_pages <= 1) + $pages = array('<strong class="item1">1</strong>'); + else + { + // Add a previous page link + if ($num_pages > 1 && $cur_page > 1) + $pages[] = '<a rel="prev"'.(empty($pages) ? ' class="item1"' : '').' href="'.$link.($cur_page == 2 ? '' : '&p='.($cur_page - 1)).'">'.$lang_common['Previous'].'</a>'; + + if ($cur_page > 3) + { + $pages[] = '<a'.(empty($pages) ? ' class="item1"' : '').' href="'.$link.'">1</a>'; + + if ($cur_page > 5) + $pages[] = '<span class="spacer">'.$lang_common['Spacer'].'</span>'; + } + + // Don't ask me how the following works. It just does, OK? :-) + for ($current = ($cur_page == 5) ? $cur_page - 3 : $cur_page - 2, $stop = ($cur_page + 4 == $num_pages) ? $cur_page + 4 : $cur_page + 3; $current < $stop; ++$current) + { + if ($current < 1 || $current > $num_pages) + continue; + else if ($current != $cur_page || $link_to_all) + $pages[] = '<a'.(empty($pages) ? ' class="item1"' : '').' href="'.$link.($current == 1 ? '' : '&p='.$current).'">'.forum_number_format($current).'</a>'; + else + $pages[] = '<strong'.(empty($pages) ? ' class="item1"' : '').'>'.forum_number_format($current).'</strong>'; + } + + if ($cur_page <= ($num_pages-3)) + { + if ($cur_page != ($num_pages-3) && $cur_page != ($num_pages-4)) + $pages[] = '<span class="spacer">'.$lang_common['Spacer'].'</span>'; + + $pages[] = '<a'.(empty($pages) ? ' class="item1"' : '').' href="'.$link.'&p='.$num_pages.'">'.forum_number_format($num_pages).'</a>'; + } + + // Add a next page link + if ($num_pages > 1 && !$link_to_all && $cur_page < $num_pages) + $pages[] = '<a rel="next"'.(empty($pages) ? ' class="item1"' : '').' href="'.$link.'&p='.($cur_page +1).'">'.$lang_common['Next'].'</a>'; + } + + return implode(' ', $pages); +} + + +// +// Display a message +// +function message($message, $no_back_link = false, $http_status = null) +{ + global $db, $lang_common, $pun_config, $pun_start, $tpl_main, $pun_user; + + // Did we receive a custom header? + if(!is_null($http_status)) { + header('HTTP/1.1 ' . $http_status); + } + + if (!defined('PUN_HEADER')) + { + $page_title = array(pun_htmlspecialchars($pun_config['o_board_title']), $lang_common['Info']); + define('PUN_ACTIVE_PAGE', 'index'); + require PUN_ROOT.'header.php'; + } + +?> + +<div id="msg" class="block"> + <h2><span><?php echo $lang_common['Info'] ?></span></h2> + <div class="box"> + <div class="inbox"> + <p><?php echo $message ?></p> +<?php if (!$no_back_link): ?> <p><a href="javascript: history.go(-1)"><?php echo $lang_common['Go back'] ?></a></p> +<?php endif; ?> </div> + </div> +</div> +<?php + + require PUN_ROOT.'footer.php'; +} + + +// +// Format a time string according to $time_format and time zones +// +function format_time($timestamp, $date_only = false, $date_format = null, $time_format = null, $time_only = false, $no_text = false, $user = null) +{ + global $lang_common, $pun_user, $forum_date_formats, $forum_time_formats; + + if ($timestamp == '') + return $lang_common['Never']; + + if (is_null($user)) + $user = $pun_user; + + $diff = ($user['timezone'] + $user['dst']) * 3600; + $timestamp += $diff; + $now = time(); + + if(is_null($date_format)) + $date_format = $forum_date_formats[$user['date_format']]; + + if(is_null($time_format)) + $time_format = $forum_time_formats[$user['time_format']]; + + $date = gmdate($date_format, $timestamp); + $today = gmdate($date_format, $now+$diff); + $yesterday = gmdate($date_format, $now+$diff-86400); + + if(!$no_text) + { + if ($date == $today) + $date = $lang_common['Today']; + else if ($date == $yesterday) + $date = $lang_common['Yesterday']; + } + + if ($date_only) + return $date; + else if ($time_only) + return gmdate($time_format, $timestamp); + else + return $date.' '.gmdate($time_format, $timestamp); +} + + +// +// A wrapper for PHP's number_format function +// +function forum_number_format($number, $decimals = 0) +{ + global $lang_common; + + return is_numeric($number) ? number_format($number, $decimals, $lang_common['lang_decimal_point'], $lang_common['lang_thousands_sep']) : $number; +} + + +// +// Generate a random key of length $len +// +function random_key($len, $readable = false, $hash = false) +{ + if (!function_exists('secure_random_bytes')) + include PUN_ROOT.'include/srand.php'; + + $key = secure_random_bytes($len); + + if ($hash) + return substr(bin2hex($key), 0, $len); + else if ($readable) + { + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + $result = ''; + for ($i = 0; $i < $len; ++$i) + $result .= substr($chars, (ord($key[$i]) % strlen($chars)), 1); + + return $result; + } + + return $key; +} + + +// +// Make sure that HTTP_REFERER matches base_url/script +// +function confirm_referrer($scripts, $error_msg = false) +{ + global $lang_common; + + if (!is_array($scripts)) + $scripts = array($scripts); + + // There is no referrer + if (empty($_SERVER['HTTP_REFERER'])) + message($error_msg ? $error_msg : $lang_common['Bad referrer']); + + $referrer = parse_url(strtolower($_SERVER['HTTP_REFERER'])); + // Remove www subdomain if it exists + if (strpos($referrer['host'], 'www.') === 0) + $referrer['host'] = substr($referrer['host'], 4); + + $valid_paths = array(); + foreach ($scripts as $script) + { + $valid = parse_url(strtolower(get_base_url().'/'.$script)); + // Remove www subdomain if it exists + if (strpos($valid['host'], 'www.') === 0) + $valid['host'] = substr($valid['host'], 4); + + $valid_host = $valid['host']; + $valid_paths[] = $valid['path']; + } + + // Check the host and path match. Ignore the scheme, port, etc. + if ($referrer['host'] != $valid_host || !in_array($referrer['path'], $valid_paths, true)) + message($error_msg ? $error_msg : $lang_common['Bad referrer']); +} + + +// +// Validate the given redirect URL, use the fallback otherwise +// +function validate_redirect($redirect_url, $fallback_url) +{ + $referrer = parse_url(strtolower($redirect_url)); + + // Make sure the host component exists + if (!isset($referrer['host'])) + $referrer['host'] = ''; + + // Remove www subdomain if it exists + if (strpos($referrer['host'], 'www.') === 0) + $referrer['host'] = substr($referrer['host'], 4); + + // Make sure the path component exists + if (!isset($referrer['path'])) + $referrer['path'] = ''; + + $valid = parse_url(strtolower(get_base_url())); + + // Remove www subdomain if it exists + if (strpos($valid['host'], 'www.') === 0) + $valid['host'] = substr($valid['host'], 4); + + // Make sure the path component exists + if (!isset($valid['path'])) + $valid['path'] = ''; + + if ($referrer['host'] == $valid['host'] && preg_match('%^'.preg_quote($valid['path'], '%').'/(.*?)\.php%i', $referrer['path'])) + return $redirect_url; + else + return $fallback_url; +} + + +// +// Generate a random password of length $len +// Compatibility wrapper for random_key +// +function random_pass($len) +{ + return random_key($len, true); +} + + +// +// Compute a hash of $str +// +function pun_hash($str) +{ + return sha1($str); +} + + +// +// Compare two strings in constant time +// Inspired by WordPress +// +function pun_hash_equals($a, $b) +{ + if (function_exists('hash_equals')) + return hash_equals((string) $a, (string) $b); + + $a_length = strlen($a); + + if ($a_length !== strlen($b)) + return false; + + $result = 0; + + // Do not attempt to "optimize" this. + for ($i = 0; $i < $a_length; $i++) + $result |= ord($a[$i]) ^ ord($b[$i]); + + return $result === 0; +} + + +// +// Compute a random hash used against CSRF attacks +// +function pun_csrf_token() +{ + global $pun_user; + static $token; + + if (!isset($token)) + $token = pun_hash($pun_user['id'].$pun_user['password'].pun_hash(get_remote_address())); + + return $token; +} + +// +// Check if the CSRF hash is correct +// +function check_csrf($token) +{ + global $lang_common; + + $is_hash_authorized = pun_hash_equals($token, pun_csrf_token()); + + if (!isset($token) || !$is_hash_authorized) + message($lang_common['Bad csrf hash'], false, '404 Not Found'); +} + + +// +// Try to determine the correct remote IP-address +// +function get_remote_address() +{ + $remote_addr = $_SERVER['REMOTE_ADDR']; + + // If we are behind a reverse proxy try to find the real users IP + if (defined('FORUM_BEHIND_REVERSE_PROXY')) + { + if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) + { + // The general format of the field is: + // X-Forwarded-For: client1, proxy1, proxy2 + // where the value is a comma+space separated list of IP addresses, the left-most being the farthest downstream client, + // and each successive proxy that passed the request adding the IP address where it received the request from. + $forwarded_for = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); + $forwarded_for = trim($forwarded_for[0]); + + if (@preg_match('%^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$%', $forwarded_for) || @preg_match('%^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$%', $forwarded_for)) + $remote_addr = $forwarded_for; + } + } + + return $remote_addr; +} + + +// +// Calls htmlspecialchars with a few options already set +// +function pun_htmlspecialchars($str) +{ + return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); +} + + +// +// Calls htmlspecialchars_decode with a few options already set +// +function pun_htmlspecialchars_decode($str) +{ + if (function_exists('htmlspecialchars_decode')) + return htmlspecialchars_decode($str, ENT_QUOTES); + + static $translations; + if (!isset($translations)) + { + $translations = get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES); + $translations['''] = '\''; // get_html_translation_table doesn't include ' which is what htmlspecialchars translates ' to, but apparently that is okay?! http://bugs.php.net/bug.php?id=25927 + $translations = array_flip($translations); + } + + return strtr($str, $translations); +} + + +// +// A wrapper for utf8_strlen for compatibility +// +function pun_strlen($str) +{ + return utf8_strlen($str); +} + + +// +// Convert \r\n and \r to \n +// +function pun_linebreaks($str) +{ + return str_replace(array("\r\n", "\r"), "\n", $str); +} + + +// +// A wrapper for utf8_trim for compatibility +// +function pun_trim($str, $charlist = false) +{ + return is_string($str) ? utf8_trim($str, $charlist) : ''; +} + +// +// Checks if a string is in all uppercase +// +function is_all_uppercase($string) +{ + return utf8_strtoupper($string) == $string && utf8_strtolower($string) != $string; +} + + +// +// Inserts $element into $input at $offset +// $offset can be either a numerical offset to insert at (eg: 0 inserts at the beginning of the array) +// or a string, which is the key that the new element should be inserted before +// $key is optional: it's used when inserting a new key/value pair into an associative array +// +function array_insert(&$input, $offset, $element, $key = null) +{ + if (is_null($key)) + $key = $offset; + + // Determine the proper offset if we're using a string + if (!is_int($offset)) + $offset = array_search($offset, array_keys($input), true); + + // Out of bounds checks + if ($offset > count($input)) + $offset = count($input); + else if ($offset < 0) + $offset = 0; + + $input = array_merge(array_slice($input, 0, $offset), array($key => $element), array_slice($input, $offset)); +} + + +// +// Display a message when board is in maintenance mode +// +function maintenance_message() +{ + global $db, $pun_config, $lang_common, $pun_user; + + header('HTTP/1.1 503 Service Unavailable'); + + // Send no-cache headers + header('Expires: Thu, 21 Jul 1977 07:30:00 GMT'); // When yours truly first set eyes on this world! :) + header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); // For HTTP/1.0 compatibility + + // Send the Content-type header in case the web server is setup to send something else + header('Content-type: text/html; charset=utf-8'); + + // Deal with newlines, tabs and multiple spaces + $pattern = array("\t", ' ', ' '); + $replace = array('    ', '  ', '  '); + $message = str_replace($pattern, $replace, $pun_config['o_maintenance_message']); + + if (file_exists(PUN_ROOT.'style/'.$pun_user['style'].'/maintenance.tpl')) + { + $tpl_file = PUN_ROOT.'style/'.$pun_user['style'].'/maintenance.tpl'; + $tpl_inc_dir = PUN_ROOT.'style/'.$pun_user['style'].'/'; + } + else + { + $tpl_file = PUN_ROOT.'include/template/maintenance.tpl'; + $tpl_inc_dir = PUN_ROOT.'include/user/'; + } + + $tpl_maint = file_get_contents($tpl_file); + + // START SUBST - <pun_include "*"> + preg_match_all('%<pun_include "([^/\\\\]*?)\.(php[45]?|inc|html?|txt)">%i', $tpl_maint, $pun_includes, PREG_SET_ORDER); + + foreach ($pun_includes as $cur_include) + { + ob_start(); + + // Allow for overriding user includes, too. + if (file_exists($tpl_inc_dir.$cur_include[1].'.'.$cur_include[2])) + require $tpl_inc_dir.$cur_include[1].'.'.$cur_include[2]; + else if (file_exists(PUN_ROOT.'include/user/'.$cur_include[1].'.'.$cur_include[2])) + require PUN_ROOT.'include/user/'.$cur_include[1].'.'.$cur_include[2]; + else + error(sprintf($lang_common['Pun include error'], htmlspecialchars($cur_include[0]), basename($tpl_file))); + + $tpl_temp = ob_get_contents(); + $tpl_maint = str_replace($cur_include[0], $tpl_temp, $tpl_maint); + ob_end_clean(); + } + // END SUBST - <pun_include "*"> + + + // START SUBST - <pun_language> + $tpl_maint = str_replace('<pun_language>', $lang_common['lang_identifier'], $tpl_maint); + // END SUBST - <pun_language> + + + // START SUBST - <pun_content_direction> + $tpl_maint = str_replace('<pun_content_direction>', $lang_common['lang_direction'], $tpl_maint); + // END SUBST - <pun_content_direction> + + + // START SUBST - <pun_head> + ob_start(); + + $page_title = array(pun_htmlspecialchars($pun_config['o_board_title']), $lang_common['Maintenance']); + +?> +<title><?php echo generate_page_title($page_title) ?></title> +<link rel="stylesheet" type="text/css" href="style/<?php echo $pun_user['style'].'.css' ?>" /> +<?php + + $tpl_temp = trim(ob_get_contents()); + $tpl_maint = str_replace('<pun_head>', $tpl_temp, $tpl_maint); + ob_end_clean(); + // END SUBST - <pun_head> + + + // START SUBST - <pun_maint_main> + ob_start(); + +?> +<div class="block"> + <h2><?php echo $lang_common['Maintenance'] ?></h2> + <div class="box"> + <div class="inbox"> + <p><?php echo $message ?></p> + </div> + </div> +</div> +<?php + + $tpl_temp = trim(ob_get_contents()); + $tpl_maint = str_replace('<pun_maint_main>', $tpl_temp, $tpl_maint); + ob_end_clean(); + // END SUBST - <pun_maint_main> + + + // End the transaction + $db->end_transaction(); + + + // Close the db connection (and free up any result data) + $db->close(); + + exit($tpl_maint); +} + + +// +// Display $message and redirect user to $destination_url +// +function redirect($destination_url, $message) +{ + global $db, $pun_config, $lang_common, $pun_user; + + // Prefix with base_url (unless there's already a valid URI) + if (strpos($destination_url, 'http://') !== 0 && strpos($destination_url, 'https://') !== 0 && strpos($destination_url, '/') !== 0) + $destination_url = get_base_url(true).'/'.$destination_url; + + // Do a little spring cleaning + $destination_url = preg_replace('%([\r\n])|(\%0[ad])|(;\s*data\s*:)%i', '', $destination_url); + + // If the delay is 0 seconds, we might as well skip the redirect all together + if ($pun_config['o_redirect_delay'] == '0') + { + $db->end_transaction(); + $db->close(); + + header('Location: '.str_replace('&', '&', $destination_url)); + exit; + } + + // Send no-cache headers + header('Expires: Thu, 21 Jul 1977 07:30:00 GMT'); // When yours truly first set eyes on this world! :) + header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); // For HTTP/1.0 compatibility + + // Send the Content-type header in case the web server is setup to send something else + header('Content-type: text/html; charset=utf-8'); + + if (file_exists(PUN_ROOT.'style/'.$pun_user['style'].'/redirect.tpl')) + { + $tpl_file = PUN_ROOT.'style/'.$pun_user['style'].'/redirect.tpl'; + $tpl_inc_dir = PUN_ROOT.'style/'.$pun_user['style'].'/'; + } + else + { + $tpl_file = PUN_ROOT.'include/template/redirect.tpl'; + $tpl_inc_dir = PUN_ROOT.'include/user/'; + } + + $tpl_redir = file_get_contents($tpl_file); + + // START SUBST - <pun_include "*"> + preg_match_all('%<pun_include "([^/\\\\]*?)\.(php[45]?|inc|html?|txt)">%i', $tpl_redir, $pun_includes, PREG_SET_ORDER); + + foreach ($pun_includes as $cur_include) + { + ob_start(); + + // Allow for overriding user includes, too. + if (file_exists($tpl_inc_dir.$cur_include[1].'.'.$cur_include[2])) + require $tpl_inc_dir.$cur_include[1].'.'.$cur_include[2]; + else if (file_exists(PUN_ROOT.'include/user/'.$cur_include[1].'.'.$cur_include[2])) + require PUN_ROOT.'include/user/'.$cur_include[1].'.'.$cur_include[2]; + else + error(sprintf($lang_common['Pun include error'], htmlspecialchars($cur_include[0]), basename($tpl_file))); + + $tpl_temp = ob_get_contents(); + $tpl_redir = str_replace($cur_include[0], $tpl_temp, $tpl_redir); + ob_end_clean(); + } + // END SUBST - <pun_include "*"> + + + // START SUBST - <pun_language> + $tpl_redir = str_replace('<pun_language>', $lang_common['lang_identifier'], $tpl_redir); + // END SUBST - <pun_language> + + + // START SUBST - <pun_content_direction> + $tpl_redir = str_replace('<pun_content_direction>', $lang_common['lang_direction'], $tpl_redir); + // END SUBST - <pun_content_direction> + + + // START SUBST - <pun_head> + ob_start(); + + $page_title = array(pun_htmlspecialchars($pun_config['o_board_title']), $lang_common['Redirecting']); + +?> +<meta http-equiv="refresh" content="<?php echo $pun_config['o_redirect_delay'] ?>;URL=<?php echo $destination_url ?>" /> +<title><?php echo generate_page_title($page_title) ?></title> +<link rel="stylesheet" type="text/css" href="style/<?php echo $pun_user['style'].'.css' ?>" /> +<?php + + $tpl_temp = trim(ob_get_contents()); + $tpl_redir = str_replace('<pun_head>', $tpl_temp, $tpl_redir); + ob_end_clean(); + // END SUBST - <pun_head> + + + // START SUBST - <pun_redir_main> + ob_start(); + +?> +<div class="block"> + <h2><?php echo $lang_common['Redirecting'] ?></h2> + <div class="box"> + <div class="inbox"> + <p><?php echo $message.'<br /><br /><a href="'.$destination_url.'">'.$lang_common['Click redirect'].'</a>' ?></p> + </div> + </div> +</div> +<?php + + $tpl_temp = trim(ob_get_contents()); + $tpl_redir = str_replace('<pun_redir_main>', $tpl_temp, $tpl_redir); + ob_end_clean(); + // END SUBST - <pun_redir_main> + + + // START SUBST - <pun_footer> + ob_start(); + + // End the transaction + $db->end_transaction(); + + // Display executed queries (if enabled) + if (defined('PUN_SHOW_QUERIES')) + display_saved_queries(); + + $tpl_temp = trim(ob_get_contents()); + $tpl_redir = str_replace('<pun_footer>', $tpl_temp, $tpl_redir); + ob_end_clean(); + // END SUBST - <pun_footer> + + + // Close the db connection (and free up any result data) + $db->close(); + + exit($tpl_redir); +} + + +// +// Display a simple error message +// +function error($message, $file = null, $line = null, $db_error = false) +{ + global $pun_config, $lang_common; + + // Set some default settings if the script failed before $pun_config could be populated + if (empty($pun_config)) + { + $pun_config = array( + 'o_board_title' => 'FluxBB', + 'o_gzip' => '0' + ); + } + + // Set some default translations if the script failed before $lang_common could be populated + if (empty($lang_common)) + { + $lang_common = array( + 'Title separator' => ' / ', + 'Page' => 'Page %s' + ); + } + + // Empty all output buffers and stop buffering + while (@ob_end_clean()); + + // "Restart" output buffering if we are using ob_gzhandler (since the gzip header is already sent) + if ($pun_config['o_gzip'] && extension_loaded('zlib')) + ob_start('ob_gzhandler'); + + header('HTTP/1.1 500 Internal Server Error'); + + // Send no-cache headers + header('Expires: Thu, 21 Jul 1977 07:30:00 GMT'); // When yours truly first set eyes on this world! :) + header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); // For HTTP/1.0 compatibility + + // Send the Content-type header in case the web server is setup to send something else + header('Content-type: text/html; charset=utf-8'); + +?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +<?php $page_title = array(pun_htmlspecialchars($pun_config['o_board_title']), 'Error') ?> +<title><?php echo generate_page_title($page_title) ?></title> +<style type="text/css"> +<!-- +BODY {MARGIN: 10% 20% auto 20%; font: 10px Verdana, Arial, Helvetica, sans-serif} +#errorbox {BORDER: 1px solid #B84623} +H2 {MARGIN: 0; COLOR: #FFFFFF; BACKGROUND-COLOR: #B84623; FONT-SIZE: 1.1em; PADDING: 5px 4px} +#errorbox DIV {PADDING: 6px 5px; BACKGROUND-COLOR: #F1F1F1} +--> +</style> +</head> +<body> + +<div id="errorbox"> + <h2>An error was encountered</h2> + <div> +<?php + + if (defined('PUN_DEBUG') && !is_null($file) && !is_null($line)) + { + $file = str_replace(realpath(PUN_ROOT), '', $file); + + echo "\t\t".'<strong>File:</strong> '.$file.'<br />'."\n\t\t".'<strong>Line:</strong> '.$line.'<br /><br />'."\n\t\t".'<strong>FluxBB reported</strong>: '.$message."\n"; + + if ($db_error) + { + echo "\t\t".'<br /><br /><strong>Database reported:</strong> '.pun_htmlspecialchars($db_error['error_msg']).(($db_error['error_no']) ? ' (Errno: '.$db_error['error_no'].')' : '')."\n"; + + if ($db_error['error_sql'] != '') + echo "\t\t".'<br /><br /><strong>Failed query:</strong> '.pun_htmlspecialchars($db_error['error_sql'])."\n"; + } + } + else + echo "\t\t".'Error: <strong>'.pun_htmlspecialchars($message).'.</strong>'."\n"; + +?> + </div> +</div> + +</body> +</html> +<?php + + // If a database connection was established (before this error) we close it + if ($db_error) + $GLOBALS['db']->close(); + + exit; +} + + +// +// Unset any variables instantiated as a result of register_globals being enabled +// +function forum_unregister_globals() +{ + $register_globals = ini_get('register_globals'); + if ($register_globals === '' || $register_globals === '0' || strtolower($register_globals) === 'off') + return; + + // Prevent script.php?GLOBALS[foo]=bar + if (isset($_REQUEST['GLOBALS']) || isset($_FILES['GLOBALS'])) + exit('I\'ll have a steak sandwich and... a steak sandwich.'); + + // Variables that shouldn't be unset + $no_unset = array('GLOBALS', '_GET', '_POST', '_COOKIE', '_REQUEST', '_SERVER', '_ENV', '_FILES'); + + // Remove elements in $GLOBALS that are present in any of the superglobals + $input = array_merge($_GET, $_POST, $_COOKIE, $_SERVER, $_ENV, $_FILES, isset($_SESSION) && is_array($_SESSION) ? $_SESSION : array()); + foreach ($input as $k => $v) + { + if (!in_array($k, $no_unset) && isset($GLOBALS[$k])) + { + unset($GLOBALS[$k]); + unset($GLOBALS[$k]); // Double unset to circumvent the zend_hash_del_key_or_index hole in PHP <4.4.3 and <5.1.4 + } + } +} + + +// +// Removes any "bad" characters (characters which mess with the display of a page, are invisible, etc) from user input +// +function forum_remove_bad_characters() +{ + $_GET = remove_bad_characters($_GET); + $_POST = remove_bad_characters($_POST); + $_COOKIE = remove_bad_characters($_COOKIE); + $_REQUEST = remove_bad_characters($_REQUEST); +} + +// +// Removes any "bad" characters (characters which mess with the display of a page, are invisible, etc) from the given string +// See: http://kb.mozillazine.org/Network.IDN.blacklist_chars +// +function remove_bad_characters($array) +{ + static $bad_utf8_chars; + + if (!isset($bad_utf8_chars)) + { + $bad_utf8_chars = array( + "\xcc\xb7" => '', // COMBINING SHORT SOLIDUS OVERLAY 0337 * + "\xcc\xb8" => '', // COMBINING LONG SOLIDUS OVERLAY 0338 * + "\xe1\x85\x9F" => '', // HANGUL CHOSEONG FILLER 115F * + "\xe1\x85\xA0" => '', // HANGUL JUNGSEONG FILLER 1160 * + "\xe2\x80\x8b" => '', // ZERO WIDTH SPACE 200B * + "\xe2\x80\x8c" => '', // ZERO WIDTH NON-JOINER 200C + "\xe2\x80\x8d" => '', // ZERO WIDTH JOINER 200D + "\xe2\x80\x8e" => '', // LEFT-TO-RIGHT MARK 200E + "\xe2\x80\x8f" => '', // RIGHT-TO-LEFT MARK 200F + "\xe2\x80\xaa" => '', // LEFT-TO-RIGHT EMBEDDING 202A + "\xe2\x80\xab" => '', // RIGHT-TO-LEFT EMBEDDING 202B + "\xe2\x80\xac" => '', // POP DIRECTIONAL FORMATTING 202C + "\xe2\x80\xad" => '', // LEFT-TO-RIGHT OVERRIDE 202D + "\xe2\x80\xae" => '', // RIGHT-TO-LEFT OVERRIDE 202E + "\xe2\x80\xaf" => '', // NARROW NO-BREAK SPACE 202F * + "\xe2\x81\x9f" => '', // MEDIUM MATHEMATICAL SPACE 205F * + "\xe2\x81\xa0" => '', // WORD JOINER 2060 + "\xe3\x85\xa4" => '', // HANGUL FILLER 3164 * + "\xef\xbb\xbf" => '', // ZERO WIDTH NO-BREAK SPACE FEFF + "\xef\xbe\xa0" => '', // HALFWIDTH HANGUL FILLER FFA0 * + "\xef\xbf\xb9" => '', // INTERLINEAR ANNOTATION ANCHOR FFF9 * + "\xef\xbf\xba" => '', // INTERLINEAR ANNOTATION SEPARATOR FFFA * + "\xef\xbf\xbb" => '', // INTERLINEAR ANNOTATION TERMINATOR FFFB * + "\xef\xbf\xbc" => '', // OBJECT REPLACEMENT CHARACTER FFFC * + "\xef\xbf\xbd" => '', // REPLACEMENT CHARACTER FFFD * + "\xe2\x80\x80" => ' ', // EN QUAD 2000 * + "\xe2\x80\x81" => ' ', // EM QUAD 2001 * + "\xe2\x80\x82" => ' ', // EN SPACE 2002 * + "\xe2\x80\x83" => ' ', // EM SPACE 2003 * + "\xe2\x80\x84" => ' ', // THREE-PER-EM SPACE 2004 * + "\xe2\x80\x85" => ' ', // FOUR-PER-EM SPACE 2005 * + "\xe2\x80\x86" => ' ', // SIX-PER-EM SPACE 2006 * + "\xe2\x80\x87" => ' ', // FIGURE SPACE 2007 * + "\xe2\x80\x88" => ' ', // PUNCTUATION SPACE 2008 * + "\xe2\x80\x89" => ' ', // THIN SPACE 2009 * + "\xe2\x80\x8a" => ' ', // HAIR SPACE 200A * + "\xE3\x80\x80" => ' ', // IDEOGRAPHIC SPACE 3000 * + ); + } + + if (is_array($array)) + return array_map('remove_bad_characters', $array); + + // Strip out any invalid characters + $array = utf8_bad_strip($array); + + // Remove control characters + $array = preg_replace('%[\x00-\x08\x0b-\x0c\x0e-\x1f]%', '', $array); + + // Replace some "bad" characters + $array = str_replace(array_keys($bad_utf8_chars), array_values($bad_utf8_chars), $array); + + return $array; +} + + +// +// Converts the file size in bytes to a human readable file size +// +function file_size($size) +{ + global $lang_common; + + $units = array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'); + + for ($i = 0; $size > 1024; $i++) + $size /= 1024; + + return sprintf($lang_common['Size unit '.$units[$i]], round($size, 2)); +} + + +// +// Fetch a list of available styles +// +function forum_list_styles() +{ + $styles = array(); + + $d = dir(PUN_ROOT.'style'); + while (($entry = $d->read()) !== false) + { + if ($entry{0} == '.') + continue; + + if (substr($entry, -4) == '.css') + $styles[] = substr($entry, 0, -4); + } + $d->close(); + + natcasesort($styles); + + return $styles; +} + + +// +// Fetch a list of available language packs +// +function forum_list_langs() +{ + $languages = array(); + + $d = dir(PUN_ROOT.'lang'); + while (($entry = $d->read()) !== false) + { + if ($entry{0} == '.') + continue; + + if (is_dir(PUN_ROOT.'lang/'.$entry) && file_exists(PUN_ROOT.'lang/'.$entry.'/common.php')) + $languages[] = $entry; + } + $d->close(); + + natcasesort($languages); + + return $languages; +} + + +// +// Generate a cache ID based on the last modification time for all stopwords files +// +function generate_stopwords_cache_id() +{ + $files = glob(PUN_ROOT.'lang/*/stopwords.txt'); + if ($files === false) + return 'cache_id_error'; + + $hash = array(); + + foreach ($files as $file) + { + $hash[] = $file; + $hash[] = filemtime($file); + } + + return sha1(implode('|', $hash)); +} + + +// +// Split text into chunks ($inside contains all text inside $start and $end, and $outside contains all text outside) +// +function split_text($text, $start, $end, $retab = true) +{ + global $pun_config; + + $result = array(0 => array(), 1 => array()); // 0 = inside, 1 = outside + + // split the text into parts + $parts = preg_split('%'.preg_quote($start, '%').'(.*)'.preg_quote($end, '%').'%Us', $text, -1, PREG_SPLIT_DELIM_CAPTURE); + $num_parts = count($parts); + + // preg_split results in outside parts having even indices, inside parts having odd + for ($i = 0;$i < $num_parts;$i++) + $result[1 - ($i % 2)][] = $parts[$i]; + + if ($pun_config['o_indent_num_spaces'] != 8 && $retab) + { + $spaces = str_repeat(' ', $pun_config['o_indent_num_spaces']); + $result[1] = str_replace("\t", $spaces, $result[1]); + } + + return $result; +} + + +// +// Extract blocks from a text with a starting and ending string +// This function always matches the most outer block so nesting is possible +// +function extract_blocks($text, $start, $end, $retab = true) +{ + global $pun_config; + + $code = array(); + $start_len = strlen($start); + $end_len = strlen($end); + $regex = '%(?:'.preg_quote($start, '%').'|'.preg_quote($end, '%').')%'; + $matches = array(); + + if (preg_match_all($regex, $text, $matches)) + { + $counter = $offset = 0; + $start_pos = $end_pos = false; + + foreach ($matches[0] as $match) + { + if ($match == $start) + { + if ($counter == 0) + $start_pos = strpos($text, $start); + $counter++; + } + elseif ($match == $end) + { + $counter--; + if ($counter == 0) + $end_pos = strpos($text, $end, $offset + 1); + $offset = strpos($text, $end, $offset + 1); + } + + if ($start_pos !== false && $end_pos !== false) + { + $code[] = substr($text, $start_pos + $start_len, + $end_pos - $start_pos - $start_len); + $text = substr_replace($text, "\1", $start_pos, + $end_pos - $start_pos + $end_len); + $start_pos = $end_pos = false; + $offset = 0; + } + } + } + + if ($pun_config['o_indent_num_spaces'] != 8 && $retab) + { + $spaces = str_repeat(' ', $pun_config['o_indent_num_spaces']); + $text = str_replace("\t", $spaces, $text); + } + + return array($code, $text); +} + + +// +// function url_valid($url) { +// +// Return associative array of valid URI components, or FALSE if $url is not +// RFC-3986 compliant. If the passed URL begins with: "www." or "ftp.", then +// "http://" or "ftp://" is prepended and the corrected full-url is stored in +// the return array with a key name "url". This value should be used by the caller. +// +// Return value: FALSE if $url is not valid, otherwise array of URI components: +// e.g. +// Given: "http://www.jmrware.com:80/articles?height=10&width=75#fragone" +// Array( +// [scheme] => http +// [authority] => www.jmrware.com:80 +// [userinfo] => +// [host] => www.jmrware.com +// [IP_literal] => +// [IPV6address] => +// [ls32] => +// [IPvFuture] => +// [IPv4address] => +// [regname] => www.jmrware.com +// [port] => 80 +// [path_abempty] => /articles +// [query] => height=10&width=75 +// [fragment] => fragone +// [url] => http://www.jmrware.com:80/articles?height=10&width=75#fragone +// ) +function url_valid($url) +{ + if (strpos($url, 'www.') === 0) $url = 'http://'. $url; + if (strpos($url, 'ftp.') === 0) $url = 'ftp://'. $url; + if (!preg_match('/# Valid absolute URI having a non-empty, valid DNS host. + ^ + (?P<scheme>[A-Za-z][A-Za-z0-9+\-.]*):\/\/ + (?P<authority> + (?:(?P<userinfo>(?:[A-Za-z0-9\-._~!$&\'()*+,;=:]|%[0-9A-Fa-f]{2})*)@)? + (?P<host> + (?P<IP_literal> + \[ + (?: + (?P<IPV6address> + (?: (?:[0-9A-Fa-f]{1,4}:){6} + | ::(?:[0-9A-Fa-f]{1,4}:){5} + | (?: [0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4} + | (?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3} + | (?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2} + | (?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?:: [0-9A-Fa-f]{1,4}: + | (?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?:: + ) + (?P<ls32>[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4} + | (?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} + (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) + ) + | (?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?:: [0-9A-Fa-f]{1,4} + | (?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?:: + ) + | (?P<IPvFuture>[Vv][0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&\'()*+,;=:]+) + ) + \] + ) + | (?P<IPv4address>(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} + (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)) + | (?P<regname>(?:[A-Za-z0-9\-._~!$&\'()*+,;=]|%[0-9A-Fa-f]{2})+) + ) + (?::(?P<port>[0-9]*))? + ) + (?P<path_abempty>(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*) + (?:\?(?P<query> (?:[A-Za-z0-9\-._~!$&\'()*+,;=:@\\/?]|%[0-9A-Fa-f]{2})*))? + (?:\#(?P<fragment> (?:[A-Za-z0-9\-._~!$&\'()*+,;=:@\\/?]|%[0-9A-Fa-f]{2})*))? + $ + /mx', $url, $m)) return FALSE; + switch ($m['scheme']) + { + case 'https': + case 'http': + if ($m['userinfo']) return FALSE; // HTTP scheme does not allow userinfo. + break; + case 'ftps': + case 'ftp': + break; + default: + return FALSE; // Unrecognised URI scheme. Default to FALSE. + } + // Validate host name conforms to DNS "dot-separated-parts". + if ($m{'regname'}) // If host regname specified, check for DNS conformance. + { + if (!preg_match('/# HTTP DNS host name. + ^ # Anchor to beginning of string. + (?!.{256}) # Overall host length is less than 256 chars. + (?: # Group dot separated host part alternatives. + [0-9A-Za-z]\. # Either a single alphanum followed by dot + | # or... part has more than one char (63 chars max). + [0-9A-Za-z] # Part first char is alphanum (no dash). + [\-0-9A-Za-z]{0,61} # Internal chars are alphanum plus dash. + [0-9A-Za-z] # Part last char is alphanum (no dash). + \. # Each part followed by literal dot. + )* # One or more parts before top level domain. + (?: # Top level domains + [A-Za-z]{2,63}| # Country codes are exactly two alpha chars. + xn--[0-9A-Za-z]{4,59}) # Internationalized Domain Name (IDN) + $ # Anchor to end of string. + /ix', $m['host'])) return FALSE; + } + $m['url'] = $url; + for ($i = 0; isset($m[$i]); ++$i) unset($m[$i]); + return $m; // return TRUE == array of useful named $matches plus the valid $url. +} + +// +// Replace string matching regular expression +// +// This function takes care of possibly disabled unicode properties in PCRE builds +// +function ucp_preg_replace($pattern, $replace, $subject, $callback = false) +{ + if($callback) + $replaced = preg_replace_callback($pattern, create_function('$matches', 'return '.$replace.';'), $subject); + else + $replaced = preg_replace($pattern, $replace, $subject); + + // If preg_replace() returns false, this probably means unicode support is not built-in, so we need to modify the pattern a little + if ($replaced === false) + { + if (is_array($pattern)) + { + foreach ($pattern as $cur_key => $cur_pattern) + $pattern[$cur_key] = str_replace('\p{L}\p{N}', '\w', $cur_pattern); + + $replaced = preg_replace($pattern, $replace, $subject); + } + else + $replaced = preg_replace(str_replace('\p{L}\p{N}', '\w', $pattern), $replace, $subject); + } + + return $replaced; +} + +// +// A wrapper for ucp_preg_replace +// +function ucp_preg_replace_callback($pattern, $replace, $subject) +{ + return ucp_preg_replace($pattern, $replace, $subject, true); +} + +// +// Replace four-byte characters with a question mark +// +// As MySQL cannot properly handle four-byte characters with the default utf-8 +// charset up until version 5.5.3 (where a special charset has to be used), they +// need to be replaced, by question marks in this case. +// +function strip_bad_multibyte_chars($str) +{ + $result = ''; + $length = strlen($str); + + for ($i = 0; $i < $length; $i++) + { + // Replace four-byte characters (11110www 10zzzzzz 10yyyyyy 10xxxxxx) + $ord = ord($str[$i]); + if ($ord >= 240 && $ord <= 244) + { + $result .= '?'; + $i += 3; + } + else + { + $result .= $str[$i]; + } + } + + return $result; +} + +// +// Check whether a file/folder is writable. +// +// This function also works on Windows Server where ACLs seem to be ignored. +// +function forum_is_writable($path) +{ + if (is_dir($path)) + { + $path = rtrim($path, '/').'/'; + return forum_is_writable($path.uniqid(mt_rand()).'.tmp'); + } + + // Check temporary file for read/write capabilities + $rm = file_exists($path); + $f = @fopen($path, 'a'); + + if ($f === false) + return false; + + fclose($f); + + if (!$rm) + @unlink($path); + + return true; +} + + +// DEBUG FUNCTIONS BELOW + +// +// Display executed queries (if enabled) +// +function display_saved_queries() +{ + global $db, $lang_common; + + // Get the queries so that we can print them out + $saved_queries = $db->get_saved_queries(); + +?> + +<div id="debug" class="blocktable"> + <h2><span><?php echo $lang_common['Debug table'] ?></span></h2> + <div class="box"> + <div class="inbox"> + <table> + <thead> + <tr> + <th class="tcl" scope="col"><?php echo $lang_common['Query times'] ?></th> + <th class="tcr" scope="col"><?php echo $lang_common['Query'] ?></th> + </tr> + </thead> + <tbody> +<?php + + $query_time_total = 0.0; + foreach ($saved_queries as $cur_query) + { + $query_time_total += $cur_query[1]; + +?> + <tr> + <td class="tcl"><?php echo ($cur_query[1] != 0) ? $cur_query[1] : ' ' ?></td> + <td class="tcr"><?php echo pun_htmlspecialchars($cur_query[0]) ?></td> + </tr> +<?php + + } + +?> + <tr> + <td class="tcl" colspan="2"><?php printf($lang_common['Total query time'], $query_time_total.' s') ?></td> + </tr> + </tbody> + </table> + </div> + </div> +</div> +<?php + +} + + +// +// Dump contents of variable(s) +// +function dump() +{ + echo '<pre>'; + + $num_args = func_num_args(); + + for ($i = 0; $i < $num_args; ++$i) + { + print_r(func_get_arg($i)); + echo "\n\n"; + } + + echo '</pre>'; + exit; +} diff --git a/include/index.html b/include/index.html new file mode 100644 index 0000000..89337b2 --- /dev/null +++ b/include/index.html @@ -0,0 +1 @@ +<html><head><title>.</title></head><body>.</body></html> diff --git a/include/parser.php b/include/parser.php new file mode 100644 index 0000000..b7eb4bd --- /dev/null +++ b/include/parser.php @@ -0,0 +1,987 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +// Make sure no one attempts to run this script "directly" +if (!defined('PUN')) + exit; + +// Global variables +/* regular expression to match nested BBCode LIST tags +'% +\[list # match opening bracket and tag name of outermost LIST tag +(?:=([1a*]))?+ # optional attribute capture in group 1 +\] # closing bracket of outermost opening LIST tag +( # capture contents of LIST tag in group 2 + (?: # non capture group for either contents or whole nested LIST + [^\[]*+ # unroll the loop! consume everything up to next [ (normal *) + (?: # (See "Mastering Regular Expressions" chapter 6 for details) + (?! # negative lookahead ensures we are NOT on [LIST*] or [/LIST] + \[list # opening LIST tag + (?:=[1a*])?+ # with optional attribute + \] # closing bracket of opening LIST tag + | # or... + \[/list\] # a closing LIST tag + ) # end negative lookahead assertion (we are not on a LIST tag) + \[ # match the [ which is NOT the start of LIST tag (special) + [^\[]*+ # consume everything up to next [ (normal *) + )*+ # finish up "unrolling the loop" technique (special (normal*))* + | # or... + (?R) # recursively match a whole nested LIST element + )* # as many times as necessary until deepest nested LIST tag grabbed +) # end capturing contents of LIST tag into group 2 +\[/list\] # match outermost closing LIST tag +%iex' */ +$re_list = '%\[list(?:=([1a*]))?+\]((?:[^\[]*+(?:(?!\[list(?:=[1a*])?+\]|\[/list\])\[[^\[]*+)*+|(?R))*)\[/list\]%i'; + +// Here you can add additional smilies if you like (please note that you must escape single quote and backslash) +$smilies = array( + ':)' => 'smile.png', + '=)' => 'smile.png', + ':|' => 'neutral.png', + '=|' => 'neutral.png', + ':(' => 'sad.png', + '=(' => 'sad.png', + ':D' => 'big_smile.png', + '=D' => 'big_smile.png', + ':o' => 'yikes.png', + ':O' => 'yikes.png', + ';)' => 'wink.png', + ':/' => 'hmm.png', + ':P' => 'tongue.png', + ':p' => 'tongue.png', + ':lol:' => 'lol.png', + ':mad:' => 'mad.png', + ':rolleyes:' => 'roll.png', + ':cool:' => 'cool.png'); + +// +// Make sure all BBCodes are lower case and do a little cleanup +// +function preparse_bbcode($text, &$errors, $is_signature = false) +{ + global $pun_config, $lang_common, $lang_post, $re_list; + + // Remove empty tags + while (($new_text = strip_empty_bbcode($text)) !== false) + { + if ($new_text != $text) + { + $text = $new_text; + if ($new_text == '') + { + $errors[] = $lang_post['Empty after strip']; + return ''; + } + } + else + break; + } + + if ($is_signature) + { + global $lang_profile; + + if (preg_match('%\[/?(?:quote|code|list|h)\b[^\]]*\]%i', $text)) + $errors[] = $lang_profile['Signature quote/code/list/h']; + } + + // If the message contains a code tag we have to split it up (text within [code][/code] shouldn't be touched) + if (strpos($text, '[code]') !== false && strpos($text, '[/code]') !== false) + list($inside, $text) = extract_blocks($text, '[code]', '[/code]'); + + // Tidy up lists + $temp = preg_replace_callback($re_list, create_function('$matches', 'return preparse_list_tag($matches[2], $matches[1]);'), $text); + + // If the regex failed + if (is_null($temp)) + $errors[] = $lang_common['BBCode list size error']; + else + $text = str_replace('*'."\0".']', '*]', $temp); + + if ($pun_config['o_make_links'] == '1') + $text = do_clickable($text); + + $temp_text = false; + if (empty($errors)) + $temp_text = preparse_tags($text, $errors, $is_signature); + + if ($temp_text !== false) + $text = $temp_text; + + // If we split up the message before we have to concatenate it together again (code tags) + if (isset($inside)) + { + $outside = explode("\1", $text); + $text = ''; + + $num_tokens = count($outside); + for ($i = 0; $i < $num_tokens; ++$i) + { + $text .= $outside[$i]; + if (isset($inside[$i])) + $text .= '[code]'.$inside[$i].'[/code]'; + } + + unset($inside); + } + + // Remove empty tags + while (($new_text = strip_empty_bbcode($text)) !== false) + { + if ($new_text != $text) + { + $text = $new_text; + if ($new_text == '') + { + $errors[] = $lang_post['Empty after strip']; + break; + } + } + else + break; + } + + return pun_trim($text); +} + + +// +// Strip empty bbcode tags from some text +// +function strip_empty_bbcode($text) +{ + // If the message contains a code tag we have to split it up (empty tags within [code][/code] are fine) + if (strpos($text, '[code]') !== false && strpos($text, '[/code]') !== false) + list($inside, $text) = extract_blocks($text, '[code]', '[/code]'); + + // Remove empty tags + while (!is_null($new_text = preg_replace('%\[(b|u|s|ins|del|em|i|h|colou?r|quote|img|url|email|list|topic|post|forum|user)(?:\=[^\]]*)?\]\s*\[/\1\]%', '', $text))) + { + if ($new_text != $text) + $text = $new_text; + else + break; + } + + // If we split up the message before we have to concatenate it together again (code tags) + if (isset($inside)) + { + $parts = explode("\1", $text); + $text = ''; + foreach ($parts as $i => $part) + { + $text .= $part; + if (isset($inside[$i])) + $text .= '[code]'.$inside[$i].'[/code]'; + } + } + + // Remove empty code tags + while (!is_null($new_text = preg_replace('%\[(code)\]\s*\[/\1\]%', '', $text))) + { + if ($new_text != $text) + $text = $new_text; + else + break; + } + + return $text; +} + + +// +// Check the structure of bbcode tags and fix simple mistakes where possible +// +function preparse_tags($text, &$errors, $is_signature = false) +{ + global $lang_common, $pun_config, $pun_user; + + // Start off by making some arrays of bbcode tags and what we need to do with each one + + // List of all the tags + $tags = array('quote', 'code', 'b', 'i', 'u', 's', 'ins', 'del', 'em', 'color', 'colour', 'url', 'email', 'img', 'list', '*', 'h', 'topic', 'post', 'forum', 'user'); + // List of tags that we need to check are open (You could not put b,i,u in here then illegal nesting like [b][i][/b][/i] would be allowed) + $tags_opened = $tags; + // and tags we need to check are closed (the same as above, added it just in case) + $tags_closed = $tags; + // Tags we can nest and the depth they can be nested to + $tags_nested = array('quote' => $pun_config['o_quote_depth'], 'list' => 5, '*' => 5); + // Tags to ignore the contents of completely (just code) + $tags_ignore = array('code'); + // Tags not allowed + $tags_forbidden = array(); + // Block tags, block tags can only go within another block tag, they cannot be in a normal tag + $tags_block = array('quote', 'code', 'list', 'h', '*'); + // Inline tags, we do not allow new lines in these + $tags_inline = array('b', 'i', 'u', 's', 'ins', 'del', 'em', 'color', 'colour', 'h', 'topic', 'post', 'forum', 'user'); + // Tags we trim interior space + $tags_trim = array('img'); + // Tags we remove quotes from the argument + $tags_quotes = array('url', 'email', 'img', 'topic', 'post', 'forum', 'user'); + // Tags we limit bbcode in + $tags_limit_bbcode = array( + '*' => array('b', 'i', 'u', 's', 'ins', 'del', 'em', 'color', 'colour', 'url', 'email', 'list', 'img', 'code', 'topic', 'post', 'forum', 'user'), + 'list' => array('*'), + 'url' => array('img'), + 'email' => array('img'), + 'topic' => array('img'), + 'post' => array('img'), + 'forum' => array('img'), + 'user' => array('img'), + 'img' => array(), + 'h' => array('b', 'i', 'u', 's', 'ins', 'del', 'em', 'color', 'colour', 'url', 'email', 'topic', 'post', 'forum', 'user'), + ); + // Tags we can automatically fix bad nesting + $tags_fix = array('quote', 'b', 'i', 'u', 's', 'ins', 'del', 'em', 'color', 'colour', 'url', 'email', 'h', 'topic', 'post', 'forum', 'user'); + + // Disallow URL tags + if ($pun_user['g_post_links'] != '1') + $tags_forbidden[] = 'url'; + + $split_text = preg_split('%(\[[\*a-zA-Z0-9-/]*?(?:=.*?)?\])%', $text, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); + + $open_tags = array('fluxbb-bbcode'); + $open_args = array(''); + $opened_tag = 0; + $new_text = ''; + $current_ignore = ''; + $current_nest = ''; + $current_depth = array(); + $limit_bbcode = $tags; + $count_ignored = array(); + + foreach ($split_text as $current) + { + if ($current == '') + continue; + + // Are we dealing with a tag? + if (substr($current, 0, 1) != '[' || substr($current, -1, 1) != ']') + { + // It's not a bbcode tag so we put it on the end and continue + // If we are nested too deeply don't add to the end + if ($current_nest) + continue; + + $current = str_replace("\r\n", "\n", $current); + $current = str_replace("\r", "\n", $current); + if (in_array($open_tags[$opened_tag], $tags_inline) && strpos($current, "\n") !== false) + { + // Deal with new lines + $split_current = preg_split('%(\n\n+)%', $current, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); + $current = ''; + + if (!pun_trim($split_current[0], "\n")) // The first part is a linebreak so we need to handle any open tags first + array_unshift($split_current, ''); + + for ($i = 1; $i < count($split_current); $i += 2) + { + $temp_opened = array(); + $temp_opened_arg = array(); + $temp = $split_current[$i - 1]; + while (!empty($open_tags)) + { + $temp_tag = array_pop($open_tags); + $temp_arg = array_pop($open_args); + + if (in_array($temp_tag , $tags_inline)) + { + array_push($temp_opened, $temp_tag); + array_push($temp_opened_arg, $temp_arg); + $temp .= '[/'.$temp_tag.']'; + } + else + { + array_push($open_tags, $temp_tag); + array_push($open_args, $temp_arg); + break; + } + } + $current .= $temp.$split_current[$i]; + $temp = ''; + while (!empty($temp_opened)) + { + $temp_tag = array_pop($temp_opened); + $temp_arg = array_pop($temp_opened_arg); + if (empty($temp_arg)) + $temp .= '['.$temp_tag.']'; + else + $temp .= '['.$temp_tag.'='.$temp_arg.']'; + array_push($open_tags, $temp_tag); + array_push($open_args, $temp_arg); + } + $current .= $temp; + } + + if (array_key_exists($i - 1, $split_current)) + $current .= $split_current[$i - 1]; + } + + if (in_array($open_tags[$opened_tag], $tags_trim)) + $new_text .= pun_trim($current); + else + $new_text .= $current; + + continue; + } + + // Get the name of the tag + $current_arg = ''; + if (strpos($current, '/') === 1) + { + $current_tag = substr($current, 2, -1); + } + else if (strpos($current, '=') === false) + { + $current_tag = substr($current, 1, -1); + } + else + { + $current_tag = substr($current, 1, strpos($current, '=')-1); + $current_arg = substr($current, strpos($current, '=')+1, -1); + } + $current_tag = strtolower($current_tag); + + // Is the tag defined? + if (!in_array($current_tag, $tags)) + { + // It's not a bbcode tag so we put it on the end and continue + if (!$current_nest) + $new_text .= $current; + + continue; + } + + // We definitely have a bbcode tag + + // Make the tag string lower case + if ($equalpos = strpos($current,'=')) + { + // We have an argument for the tag which we don't want to make lowercase + if (strlen(substr($current, $equalpos)) == 2) + { + // Empty tag argument + $errors[] = sprintf($lang_common['BBCode error empty attribute'], $current_tag); + return false; + } + $current = strtolower(substr($current, 0, $equalpos)).substr($current, $equalpos); + } + else + $current = strtolower($current); + + // This is if we are currently in a tag which escapes other bbcode such as code + // We keep a count of ignored bbcodes (code tags) so we can nest them, but + // only balanced sets of tags can be nested + if ($current_ignore) + { + // Increase the current ignored tags counter + if ('['.$current_ignore.']' == $current) + $count_ignored[$current_tag]++; + + // Decrease the current ignored tags counter + if ('[/'.$current_ignore.']' == $current) + $count_ignored[$current_tag]--; + + if ('[/'.$current_ignore.']' == $current && $count_ignored[$current_tag] == 0) + { + // We've finished the ignored section + $current = '[/'.$current_tag.']'; + $current_ignore = ''; + $count_ignored = array(); + } + + $new_text .= $current; + + continue; + } + + // Is the tag forbidden? + if (in_array($current_tag, $tags_forbidden)) + { + if (isset($lang_common['BBCode error tag '.$current_tag.' not allowed'])) + $errors[] = sprintf($lang_common['BBCode error tag '.$current_tag.' not allowed']); + else + $errors[] = sprintf($lang_common['BBCode error tag not allowed'], $current_tag); + + return false; + } + + if ($current_nest) + { + // We are currently too deeply nested so lets see if we are closing the tag or not + if ($current_tag != $current_nest) + continue; + + if (substr($current, 1, 1) == '/') + $current_depth[$current_nest]--; + else + $current_depth[$current_nest]++; + + if ($current_depth[$current_nest] <= $tags_nested[$current_nest]) + $current_nest = ''; + + continue; + } + + // Check the current tag is allowed here + if (!in_array($current_tag, $limit_bbcode) && $current_tag != $open_tags[$opened_tag]) + { + $errors[] = sprintf($lang_common['BBCode error invalid nesting'], $current_tag, $open_tags[$opened_tag]); + return false; + } + + if (substr($current, 1, 1) == '/') + { + // This is if we are closing a tag + if ($opened_tag == 0 || !in_array($current_tag, $open_tags)) + { + // We tried to close a tag which is not open + if (in_array($current_tag, $tags_opened)) + { + $errors[] = sprintf($lang_common['BBCode error no opening tag'], $current_tag); + return false; + } + } + else + { + // Check nesting + while (true) + { + // Nesting is ok + if ($open_tags[$opened_tag] == $current_tag) + { + array_pop($open_tags); + array_pop($open_args); + $opened_tag--; + break; + } + + // Nesting isn't ok, try to fix it + if (in_array($open_tags[$opened_tag], $tags_closed) && in_array($current_tag, $tags_closed)) + { + if (in_array($current_tag, $open_tags)) + { + $temp_opened = array(); + $temp_opened_arg = array(); + $temp = ''; + while (!empty($open_tags)) + { + $temp_tag = array_pop($open_tags); + $temp_arg = array_pop($open_args); + + if (!in_array($temp_tag, $tags_fix)) + { + // We couldn't fix nesting + $errors[] = sprintf($lang_common['BBCode error no closing tag'], $temp_tag); + return false; + } + array_push($temp_opened, $temp_tag); + array_push($temp_opened_arg, $temp_arg); + + if ($temp_tag == $current_tag) + break; + else + $temp .= '[/'.$temp_tag.']'; + } + $current = $temp.$current; + $temp = ''; + array_pop($temp_opened); + array_pop($temp_opened_arg); + + while (!empty($temp_opened)) + { + $temp_tag = array_pop($temp_opened); + $temp_arg = array_pop($temp_opened_arg); + if (empty($temp_arg)) + $temp .= '['.$temp_tag.']'; + else + $temp .= '['.$temp_tag.'='.$temp_arg.']'; + array_push($open_tags, $temp_tag); + array_push($open_args, $temp_arg); + } + $current .= $temp; + $opened_tag--; + break; + } + else + { + // We couldn't fix nesting + $errors[] = sprintf($lang_common['BBCode error no opening tag'], $current_tag); + return false; + } + } + else if (in_array($open_tags[$opened_tag], $tags_closed)) + break; + else + { + array_pop($open_tags); + array_pop($open_args); + $opened_tag--; + } + } + } + + if (in_array($current_tag, array_keys($tags_nested))) + { + if (isset($current_depth[$current_tag])) + $current_depth[$current_tag]--; + } + + if (in_array($open_tags[$opened_tag], array_keys($tags_limit_bbcode))) + $limit_bbcode = $tags_limit_bbcode[$open_tags[$opened_tag]]; + else + $limit_bbcode = $tags; + + $new_text .= $current; + + continue; + } + else + { + // We are opening a tag + if (in_array($current_tag, array_keys($tags_limit_bbcode))) + $limit_bbcode = $tags_limit_bbcode[$current_tag]; + else + $limit_bbcode = $tags; + + if (in_array($current_tag, $tags_block) && !in_array($open_tags[$opened_tag], $tags_block) && $opened_tag != 0) + { + // We tried to open a block tag within a non-block tag + $errors[] = sprintf($lang_common['BBCode error invalid nesting'], $current_tag, $open_tags[$opened_tag]); + return false; + } + + if (in_array($current_tag, $tags_ignore)) + { + // It's an ignore tag so we don't need to worry about what's inside it + $current_ignore = $current_tag; + $count_ignored[$current_tag] = 1; + $new_text .= $current; + continue; + } + + // Deal with nested tags + if (in_array($current_tag, $open_tags) && !in_array($current_tag, array_keys($tags_nested))) + { + // We nested a tag we shouldn't + $errors[] = sprintf($lang_common['BBCode error invalid self-nesting'], $current_tag); + return false; + } + else if (in_array($current_tag, array_keys($tags_nested))) + { + // We are allowed to nest this tag + + if (isset($current_depth[$current_tag])) + $current_depth[$current_tag]++; + else + $current_depth[$current_tag] = 1; + + // See if we are nested too deep + if ($current_depth[$current_tag] > $tags_nested[$current_tag]) + { + $current_nest = $current_tag; + continue; + } + } + + // Remove quotes from arguments for certain tags + if (strpos($current, '=') !== false && in_array($current_tag, $tags_quotes)) + { + $current = preg_replace('%\['.$current_tag.'=("|\'|)(.*?)\\1\]\s*%i', '['.$current_tag.'=$2]', $current); + } + + if (in_array($current_tag, array_keys($tags_limit_bbcode))) + $limit_bbcode = $tags_limit_bbcode[$current_tag]; + + $open_tags[] = $current_tag; + $open_args[] = $current_arg; + $opened_tag++; + $new_text .= $current; + continue; + } + } + + // Check we closed all the tags we needed to + foreach ($tags_closed as $check) + { + if (in_array($check, $open_tags)) + { + // We left an important tag open + $errors[] = sprintf($lang_common['BBCode error no closing tag'], $check); + return false; + } + } + + if ($current_ignore) + { + // We left an ignore tag open + $errors[] = sprintf($lang_common['BBCode error no closing tag'], $current_ignore); + return false; + } + + return $new_text; +} + + +// +// Preparse the contents of [list] bbcode +// +function preparse_list_tag($content, $type = '*') +{ + global $lang_common, $re_list; + + if (strlen($type) != 1) + $type = '*'; + + if (strpos($content,'[list') !== false) + { + $content = preg_replace_callback($re_list, create_function('$matches', 'return preparse_list_tag($matches[2], $matches[1]);'), $content); + } + + $items = explode('[*]', str_replace('\"', '"', $content)); + + $content = ''; + foreach ($items as $item) + { + if (pun_trim($item) != '') + $content .= '[*'."\0".']'.str_replace('[/*]', '', pun_trim($item)).'[/*'."\0".']'."\n"; + } + + return '[list='.$type.']'."\n".$content.'[/list]'; +} + + +// +// Truncate URL if longer than 55 characters (add http:// or ftp:// if missing) +// +function handle_url_tag($url, $link = '', $bbcode = false) +{ + $url = pun_trim($url); + + // Deal with [url][img]http://example.com/test.png[/img][/url] + if (preg_match('%<img src=\"(.*?)\"%', $url, $matches)) + return handle_url_tag($matches[1], $url, $bbcode); + + $full_url = str_replace(array(' ', '\'', '`', '"'), array('%20', '', '', ''), $url); + if (strpos($url, 'www.') === 0) // If it starts with www, we add http:// + $full_url = 'http://'.$full_url; + else if (strpos($url, 'ftp.') === 0) // Else if it starts with ftp, we add ftp:// + $full_url = 'ftp://'.$full_url; + else if (strpos($url, '/') === 0) // Allow for relative URLs that start with a slash + $full_url = get_base_url(true).$full_url; + else if (!preg_match('#^([a-z0-9]{3,6})://#', $url)) // Else if it doesn't start with abcdef://, we add http:// + $full_url = 'http://'.$full_url; + + // Ok, not very pretty :-) + if ($bbcode) + { + if ($full_url == $link) + return '[url]'.$link.'[/url]'; + else + return '[url='.$full_url.']'.$link.'[/url]'; + } + else + { + if ($link == '' || $link == $url) + { + $url = pun_htmlspecialchars_decode($url); + $link = utf8_strlen($url) > 55 ? utf8_substr($url, 0 , 39).' … '.utf8_substr($url, -10) : $url; + $link = pun_htmlspecialchars($link); + } + else + $link = stripslashes($link); + + return '<a href="'.$full_url.'" rel="nofollow">'.$link.'</a>'; + } +} + + +// +// Turns an URL from the [img] tag into an <img> tag or a <a href...> tag +// +function handle_img_tag($url, $is_signature = false, $alt = null) +{ + global $lang_common, $pun_user; + + if (is_null($alt)) + $alt = basename($url); + + $img_tag = '<a href="'.$url.'" rel="nofollow"><'.$lang_common['Image link'].' - '.$alt.'></a>'; + + if ($is_signature && $pun_user['show_img_sig'] != '0') + $img_tag = '<img class="sigimage" src="'.$url.'" alt="'.$alt.'" />'; + else if (!$is_signature && $pun_user['show_img'] != '0') + $img_tag = '<span class="postimg"><img src="'.$url.'" alt="'.$alt.'" /></span>'; + + return $img_tag; +} + + +// +// Parse the contents of [list] bbcode +// +function handle_list_tag($content, $type = '*') +{ + global $re_list; + + if (strlen($type) != 1) + $type = '*'; + + if (strpos($content,'[list') !== false) + { + $content = preg_replace_callback($re_list, create_function('$matches', 'return handle_list_tag($matches[2], $matches[1]);'), $content); + } + + $content = preg_replace('#\s*\[\*\](.*?)\[/\*\]\s*#s', '<li><p>$1</p></li>', pun_trim($content)); + + if ($type == '*') + $content = '<ul>'.$content.'</ul>'; + else + if ($type == 'a') + $content = '<ol class="alpha">'.$content.'</ol>'; + else + $content = '<ol class="decimal">'.$content.'</ol>'; + + return '</p>'.$content.'<p>'; +} + + +// +// Convert BBCodes to their HTML equivalent +// +function do_bbcode($text, $is_signature = false) +{ + global $lang_common, $pun_user, $pun_config, $re_list; + + if (strpos($text, '[quote') !== false) + { + $text = preg_replace('%\[quote\]\s*%', '</p><div class="quotebox"><blockquote><div><p>', $text); + $text = preg_replace_callback('%\[quote=("|&\#039;|"|\'|)([^\r\n]*?)\\1\]%s', create_function('$matches', 'global $lang_common; return "</p><div class=\"quotebox\"><cite>".str_replace(array(\'[\', \'\\"\'), array(\'[\', \'"\'), $matches[2])." ".$lang_common[\'wrote\']."</cite><blockquote><div><p>";'), $text); + $text = preg_replace('%\s*\[\/quote\]%S', '</p></div></blockquote></div><p>', $text); + } + if (!$is_signature) + { + $pattern_callback[] = $re_list; + $replace_callback[] = 'handle_list_tag($matches[2], $matches[1])'; + } + + $pattern[] = '%\[b\](.*?)\[/b\]%ms'; + $pattern[] = '%\[i\](.*?)\[/i\]%ms'; + $pattern[] = '%\[u\](.*?)\[/u\]%ms'; + $pattern[] = '%\[s\](.*?)\[/s\]%ms'; + $pattern[] = '%\[del\](.*?)\[/del\]%ms'; + $pattern[] = '%\[ins\](.*?)\[/ins\]%ms'; + $pattern[] = '%\[em\](.*?)\[/em\]%ms'; + $pattern[] = '%\[colou?r=([a-zA-Z]{3,20}|\#[0-9a-fA-F]{6}|\#[0-9a-fA-F]{3})](.*?)\[/colou?r\]%ms'; + $pattern[] = '%\[h\](.*?)\[/h\]%ms'; + + $replace[] = '<strong>$1</strong>'; + $replace[] = '<em>$1</em>'; + $replace[] = '<span class="bbu">$1</span>'; + $replace[] = '<span class="bbs">$1</span>'; + $replace[] = '<del>$1</del>'; + $replace[] = '<ins>$1</ins>'; + $replace[] = '<em>$1</em>'; + $replace[] = '<span style="color: $1">$2</span>'; + $replace[] = '</p><h5>$1</h5><p>'; + + if (($is_signature && $pun_config['p_sig_img_tag'] == '1') || (!$is_signature && $pun_config['p_message_img_tag'] == '1')) + { + $pattern_callback[] = '%\[img\]((ht|f)tps?://)([^\s<"]*?)\[/img\]%'; + $pattern_callback[] = '%\[img=([^\[]*?)\]((ht|f)tps?://)([^\s<"]*?)\[/img\]%'; + if ($is_signature) + { + $replace_callback[] = 'handle_img_tag($matches[1].$matches[3], true)'; + $replace_callback[] = 'handle_img_tag($matches[2].$matches[4], true, $matches[1])'; + } + else + { + $replace_callback[] = 'handle_img_tag($matches[1].$matches[3], false)'; + $replace_callback[] = 'handle_img_tag($matches[2].$matches[4], false, $matches[1])'; + } + } + + $pattern_callback[] = '%\[url\]([^\[]*?)\[/url\]%'; + $pattern_callback[] = '%\[url=([^\[]+?)\](.*?)\[/url\]%'; + $pattern[] = '%\[email\]([^\[]*?)\[/email\]%'; + $pattern[] = '%\[email=([^\[]+?)\](.*?)\[/email\]%'; + $pattern_callback[] = '%\[topic\]([1-9]\d*)\[/topic\]%'; + $pattern_callback[] = '%\[topic=([1-9]\d*)\](.*?)\[/topic\]%'; + $pattern_callback[] = '%\[post\]([1-9]\d*)\[/post\]%'; + $pattern_callback[] = '%\[post=([1-9]\d*)\](.*?)\[/post\]%'; + $pattern_callback[] = '%\[forum\]([1-9]\d*)\[/forum\]%'; + $pattern_callback[] = '%\[forum=([1-9]\d*)\](.*?)\[/forum\]%'; + $pattern_callback[] = '%\[user\]([1-9]\d*)\[/user\]%'; + $pattern_callback[] = '%\[user=([1-9]\d*)\](.*?)\[/user\]%'; + + $replace_callback[] = 'handle_url_tag($matches[1])'; + $replace_callback[] = 'handle_url_tag($matches[1], $matches[2])'; + $replace[] = '<a href="mailto:$1">$1</a>'; + $replace[] = '<a href="mailto:$1">$2</a>'; + $replace_callback[] = 'handle_url_tag(\''.get_base_url(true).'/viewtopic.php?id=\'.$matches[1])'; + $replace_callback[] = 'handle_url_tag(\''.get_base_url(true).'/viewtopic.php?id=\'.$matches[1], $matches[2])'; + $replace_callback[] = 'handle_url_tag(\''.get_base_url(true).'/viewtopic.php?pid=\'.$matches[1].\'#p\'.$matches[1])'; + $replace_callback[] = 'handle_url_tag(\''.get_base_url(true).'/viewtopic.php?pid=\'.$matches[1].\'#p\'.$matches[1], $matches[2])'; + $replace_callback[] = 'handle_url_tag(\''.get_base_url(true).'/viewforum.php?id=\'.$matches[1])'; + $replace_callback[] = 'handle_url_tag(\''.get_base_url(true).'/viewforum.php?id=\'.$matches[1], $matches[2])'; + $replace_callback[] = 'handle_url_tag(\''.get_base_url(true).'/profile.php?id=\'.$matches[1])'; + $replace_callback[] = 'handle_url_tag(\''.get_base_url(true).'/profile.php?id=\'.$matches[1], $matches[2])'; + + // This thing takes a while! :) + $text = preg_replace($pattern, $replace, $text); + $count = count($pattern_callback); + for($i = 0 ; $i < $count ; $i++) + { + $text = preg_replace_callback($pattern_callback[$i], create_function('$matches', 'return '.$replace_callback[$i].';'), $text); + } + return $text; +} + + +// +// Make hyperlinks clickable +// +function do_clickable($text) +{ + $text = ' '.$text; + $text = ucp_preg_replace_callback('%(?<=[\s\]\)])(<)?(\[)?(\()?([\'"]?)(https?|ftp|news){1}://([\p{L}\p{N}\-]+\.([\p{L}\p{N}\-]+\.)*[\p{L}\p{N}]+(:[0-9]+)?(/(?:[^\s\[]*[^\s.,?!\[;:-])?)?)\4(?(3)(\)))(?(2)(\]))(?(1)(>))(?![^\s]*\[/(?:url|img)\])%ui', 'stripslashes($matches[1].$matches[2].$matches[3].$matches[4]).handle_url_tag($matches[5]."://".$matches[6], $matches[5]."://".$matches[6], true).stripslashes($matches[4].forum_array_key($matches, 10).forum_array_key($matches, 11).forum_array_key($matches, 12))', $text); + $text = ucp_preg_replace_callback('%(?<=[\s\]\)])(<)?(\[)?(\()?([\'"]?)(www|ftp)\.(([\p{L}\p{N}\-]+\.)+[\p{L}\p{N}]+(:[0-9]+)?(/(?:[^\s\[]*[^\s.,?!\[;:-])?)?)\4(?(3)(\)))(?(2)(\]))(?(1)(>))(?![^\s]*\[/(?:url|img)\])%ui','stripslashes($matches[1].$matches[2].$matches[3].$matches[4]).handle_url_tag($matches[5].".".$matches[6], $matches[5].".".$matches[6], true).stripslashes($matches[4].forum_array_key($matches, 10).forum_array_key($matches, 11).forum_array_key($matches, 12))', $text); + + return substr($text, 1); +} + + +// +// Return an array key, if it exists, otherwise return an empty string +// +function forum_array_key($arr, $key) +{ + return isset($arr[$key]) ? $arr[$key] : ''; +} + + +// +// Convert a series of smilies to images +// +function do_smilies($text) +{ + global $smilies; + + $text = ' '.$text.' '; + + foreach ($smilies as $smiley_text => $smiley_img) + { + if (strpos($text, $smiley_text) !== false) + $text = ucp_preg_replace('%(?<=[>\s])'.preg_quote($smiley_text, '%').'(?=[^\p{L}\p{N}])%um', '<img src="'.pun_htmlspecialchars(get_base_url(true).'/img/smilies/'.$smiley_img).'" width="15" height="15" alt="'.substr($smiley_img, 0, strrpos($smiley_img, '.')).'" />', $text); + } + + return substr($text, 1, -1); +} + + +// +// Parse message text +// +function parse_message($text, $hide_smilies) +{ + global $pun_config, $lang_common, $pun_user; + + if ($pun_config['o_censoring'] == '1') + $text = censor_words($text); + + // Convert applicable characters to HTML entities + $text = pun_htmlspecialchars($text); + + // If the message contains a code tag we have to split it up (text within [code][/code] shouldn't be touched) + if (strpos($text, '[code]') !== false && strpos($text, '[/code]') !== false) + list($inside, $text) = extract_blocks($text, '[code]', '[/code]'); + + if ($pun_config['p_message_bbcode'] == '1' && strpos($text, '[') !== false && strpos($text, ']') !== false) + $text = do_bbcode($text); + + if ($pun_config['o_smilies'] == '1' && $pun_user['show_smilies'] == '1' && $hide_smilies == '0') + $text = do_smilies($text); + + // Deal with newlines, tabs and multiple spaces + $pattern = array("\n", "\t", ' ', ' '); + $replace = array('<br />', '    ', '  ', '  '); + $text = str_replace($pattern, $replace, $text); + + // If we split up the message before we have to concatenate it together again (code tags) + if (isset($inside)) + { + $parts = explode("\1", $text); + $text = ''; + foreach ($parts as $i => $part) + { + $text .= $part; + if (isset($inside[$i])) + { + $num_lines = (substr_count($inside[$i], "\n")); + $text .= '</p><div class="codebox"><pre'.(($num_lines > 28) ? ' class="vscroll"' : '').'><code>'.pun_trim($inside[$i], "\n\r").'</code></pre></div><p>'; + } + } + } + + return clean_paragraphs($text); +} + + +// +// Clean up paragraphs and line breaks +// +function clean_paragraphs($text) +{ + // Add paragraph tag around post, but make sure there are no empty paragraphs + + $text = '<p>'.$text.'</p>'; + + // Replace any breaks next to paragraphs so our replace below catches them + $text = preg_replace('%(</?p>)(?:\s*?<br />){1,2}%i', '$1', $text); + $text = preg_replace('%(?:<br />\s*?){1,2}(</?p>)%i', '$1', $text); + + // Remove any empty paragraph tags (inserted via quotes/lists/code/etc) which should be stripped + $text = str_replace('<p></p>', '', $text); + + $text = preg_replace('%<br />\s*?<br />%i', '</p><p>', $text); + + $text = str_replace('<p><br />', '<br /><p>', $text); + $text = str_replace('<br /></p>', '</p><br />', $text); + $text = str_replace('<p></p>', '<br /><br />', $text); + + return $text; +} + + +// +// Parse signature text +// +function parse_signature($text) +{ + global $pun_config, $lang_common, $pun_user; + + if ($pun_config['o_censoring'] == '1') + $text = censor_words($text); + + // Convert applicable characters to HTML entities + $text = pun_htmlspecialchars($text); + + if ($pun_config['p_sig_bbcode'] == '1' && strpos($text, '[') !== false && strpos($text, ']') !== false) + $text = do_bbcode($text, true); + + if ($pun_config['o_smilies_sig'] == '1' && $pun_user['show_smilies'] == '1') + $text = do_smilies($text); + + + // Deal with newlines, tabs and multiple spaces + $pattern = array("\n", "\t", ' ', ' '); + $replace = array('<br />', '    ', '  ', '  '); + $text = str_replace($pattern, $replace, $text); + + return clean_paragraphs($text); +} diff --git a/include/search_idx.php b/include/search_idx.php new file mode 100644 index 0000000..49fe257 --- /dev/null +++ b/include/search_idx.php @@ -0,0 +1,316 @@ +<?php + +/** + * Copyright (C) 2008-2012 FluxBB + * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB + * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher + */ + +// The contents of this file are very much inspired by the file functions_search.php +// from the phpBB Group forum software phpBB2 (http://www.phpbb.com) + + +// Make sure no one attempts to run this script "directly" +if (!defined('PUN')) + exit; + + +// Make a regex that will match CJK or Hangul characters +define('PUN_CJK_HANGUL_REGEX', '['. + '\x{1100}-\x{11FF}'. // Hangul Jamo 1100-11FF (http://www.fileformat.info/info/unicode/block/hangul_jamo/index.htm) + '\x{3130}-\x{318F}'. // Hangul Compatibility Jamo 3130-318F (http://www.fileformat.info/info/unicode/block/hangul_compatibility_jamo/index.htm) + '\x{AC00}-\x{D7AF}'. // Hangul Syllables AC00-D7AF (http://www.fileformat.info/info/unicode/block/hangul_syllables/index.htm) + + // Hiragana + '\x{3040}-\x{309F}'. // Hiragana 3040-309F (http://www.fileformat.info/info/unicode/block/hiragana/index.htm) + + // Katakana + '\x{30A0}-\x{30FF}'. // Katakana 30A0-30FF (http://www.fileformat.info/info/unicode/block/katakana/index.htm) + '\x{31F0}-\x{31FF}'. // Katakana Phonetic Extensions 31F0-31FF (http://www.fileformat.info/info/unicode/block/katakana_phonetic_extensions/index.htm) + + // CJK Unified Ideographs (http://en.wikipedia.org/wiki/CJK_Unified_Ideographs) + '\x{2E80}-\x{2EFF}'. // CJK Radicals Supplement 2E80-2EFF (http://www.fileformat.info/info/unicode/block/cjk_radicals_supplement/index.htm) + '\x{2F00}-\x{2FDF}'. // Kangxi Radicals 2F00-2FDF (http://www.fileformat.info/info/unicode/block/kangxi_radicals/index.htm) + '\x{2FF0}-\x{2FFF}'. // Ideographic Description Characters 2FF0-2FFF (http://www.fileformat.info/info/unicode/block/ideographic_description_characters/index.htm) + '\x{3000}-\x{303F}'. // CJK Symbols and Punctuation 3000-303F (http://www.fileformat.info/info/unicode/block/cjk_symbols_and_punctuation/index.htm) + '\x{31C0}-\x{31EF}'. // CJK Strokes 31C0-31EF (http://www.fileformat.info/info/unicode/block/cjk_strokes/index.htm) + '\x{3200}-\x{32FF}'. // Enclosed CJK Letters and Months 3200-32FF (http://www.fileformat.info/info/unicode/block/enclosed_cjk_letters_and_months/index.htm) + '\x{3400}-\x{4DBF}'. // CJK Unified Ideographs Extension A 3400-4DBF (http://www.fileformat.info/info/unicode/block/cjk_unified_ideographs_extension_a/index.htm) + '\x{4E00}-\x{9FFF}'. // CJK Unified Ideographs 4E00-9FFF (http://www.fileformat.info/info/unicode/block/cjk_unified_ideographs/index.htm) + '\x{20000}-\x{2A6DF}'. // CJK Unified Ideographs Extension B 20000-2A6DF (http://www.fileformat.info/info/unicode/block/cjk_unified_ideographs_extension_b/index.htm) +']'); + + +// +// "Cleans up" a text string and returns an array of unique words +// This function depends on the current locale setting +// +function split_words($text, $idx) +{ + // Remove BBCode + $text = preg_replace('%\[/?(b|u|s|ins|del|em|i|h|colou?r|quote|code|img|url|email|list|topic|post|forum|user)(?:\=[^\]]*)?\]%', ' ', $text); + + // Remove any apostrophes or dashes which aren't part of words + $text = substr(ucp_preg_replace('%((?<=[^\p{L}\p{N}])[\'\-]|[\'\-](?=[^\p{L}\p{N}]))%u', '', ' '.$text.' '), 1, -1); + + // Remove punctuation and symbols (actually anything that isn't a letter or number), allow apostrophes and dashes (and % * if we aren't indexing) + $text = ucp_preg_replace('%(?![\'\-'.($idx ? '' : '\%\*').'])[^\p{L}\p{N}]+%u', ' ', $text); + + // Replace multiple whitespace or dashes + $text = preg_replace('%(\s){2,}%u', '\1', $text); + + // Fill an array with all the words + $words = array_unique(explode(' ', $text)); + + // Remove any words that should not be indexed + foreach ($words as $key => $value) + { + // If the word shouldn't be indexed, remove it + if (!validate_search_word($value, $idx)) + unset($words[$key]); + } + + return $words; +} + + +// +// Checks if a word is a valid searchable word +// +function validate_search_word($word, $idx) +{ + static $stopwords; + + // If the word is a keyword we don't want to index it, but we do want to be allowed to search it + if (is_keyword($word)) + return !$idx; + + if (!isset($stopwords)) + { + if (file_exists(FORUM_CACHE_DIR.'cache_stopwords.php')) + include FORUM_CACHE_DIR.'cache_stopwords.php'; + + if (!defined('PUN_STOPWORDS_LOADED')) + { + if (!defined('FORUM_CACHE_FUNCTIONS_LOADED')) + require PUN_ROOT.'include/cache.php'; + + generate_stopwords_cache(); + require FORUM_CACHE_DIR.'cache_stopwords.php'; + } + } + + // If it is a stopword it isn't valid + if (in_array($word, $stopwords)) + return false; + + // If the word is CJK we don't want to index it, but we do want to be allowed to search it + if (is_cjk($word)) + return !$idx; + + // Exclude % and * when checking whether current word is valid + $word = str_replace(array('%', '*'), '', $word); + + // Check the word is within the min/max length + $num_chars = pun_strlen($word); + return $num_chars >= PUN_SEARCH_MIN_WORD && $num_chars <= PUN_SEARCH_MAX_WORD; +} + + +// +// Check a given word is a search keyword. +// +function is_keyword($word) +{ + return $word == 'and' || $word == 'or' || $word == 'not'; +} + + +// +// Check if a given word is CJK or Hangul. +// +function is_cjk($word) +{ + return preg_match('%^'.PUN_CJK_HANGUL_REGEX.'+$%u', $word) ? true : false; +} + + +// +// Strip [img] [url] and [email] out of the message so we don't index their contents +// +function strip_bbcode($text) +{ + static $patterns; + + if (!isset($patterns)) + { + $patterns = array( + '%\[img=([^\]]*+)\]([^[]*+)\[/img\]%' => '$2 $1', // Keep the url and description + '%\[(url|email)=([^\]]*+)\]([^[]*+(?:(?!\[/\1\])\[[^[]*+)*)\[/\1\]%' => '$2 $3', // Keep the url and text + '%\[(img|url|email)\]([^[]*+(?:(?!\[/\1\])\[[^[]*+)*)\[/\1\]%' => '$2', // Keep the url + '%\[(topic|post|forum|user)\][1-9]\d*\[/\1\]%' => ' ', // Do not index topic/post/forum/user ID + ); + } + + return preg_replace(array_keys($patterns), array_values($patterns), $text); +} + + +// +// Updates the search index with the contents of $post_id (and $subject) +// +function update_search_index($mode, $post_id, $message, $subject = null) +{ + global $db_type, $db; + + $message = utf8_strtolower($message); + $subject = utf8_strtolower($subject); + + // Remove any bbcode that we shouldn't index + $message = strip_bbcode($message); + + // Split old and new post/subject to obtain array of 'words' + $words_message = split_words($message, true); + $words_subject = ($subject) ? split_words($subject, true) : array(); + + if ($mode == 'edit') + { + $result = $db->query('SELECT w.id, w.word, m.subject_match FROM '.$db->prefix.'search_words AS w INNER JOIN '.$db->prefix.'search_matches AS m ON w.id=m.word_id WHERE m.post_id='.$post_id, true) or error('Unable to fetch search index words', __FILE__, __LINE__, $db->error()); + + // Declare here to stop array_keys() and array_diff() from complaining if not set + $cur_words['post'] = array(); + $cur_words['subject'] = array(); + + while ($row = $db->fetch_row($result)) + { + $match_in = ($row[2]) ? 'subject' : 'post'; + $cur_words[$match_in][$row[1]] = $row[0]; + } + + $db->free_result($result); + + $words['add']['post'] = array_diff($words_message, array_keys($cur_words['post'])); + $words['add']['subject'] = array_diff($words_subject, array_keys($cur_words['subject'])); + $words['del']['post'] = array_diff(array_keys($cur_words['post']), $words_message); + $words['del']['subject'] = array_diff(array_keys($cur_words['subject']), $words_subject); + } + else + { + $words['add']['post'] = $words_message; + $words['add']['subject'] = $words_subject; + $words['del']['post'] = array(); + $words['del']['subject'] = array(); + } + + unset($words_message); + unset($words_subject); + + // Get unique words from the above arrays + $unique_words = array_unique(array_merge($words['add']['post'], $words['add']['subject'])); + + if (!empty($unique_words)) + { + $result = $db->query('SELECT id, word FROM '.$db->prefix.'search_words WHERE word IN(\''.implode('\',\'', array_map(array($db, 'escape'), $unique_words)).'\')', true) or error('Unable to fetch search index words', __FILE__, __LINE__, $db->error()); + + $word_ids = array(); + while ($row = $db->fetch_row($result)) + $word_ids[$row[1]] = $row[0]; + + $db->free_result($result); + + $new_words = array_diff($unique_words, array_keys($word_ids)); + unset($unique_words); + + if (!empty($new_words)) + { + switch ($db_type) + { + case 'mysql': + case 'mysqli': + case 'mysql_innodb': + case 'mysqli_innodb': + $db->query('INSERT INTO '.$db->prefix.'search_words (word) VALUES(\''.implode('\'),(\'', array_map(array($db, 'escape'), $new_words)).'\')'); + break; + + default: + foreach ($new_words as $word) + $db->query('INSERT INTO '.$db->prefix.'search_words (word) VALUES(\''.$db->escape($word).'\')'); + break; + } + } + + unset($new_words); + } + + // Delete matches (only if editing a post) + foreach ($words['del'] as $match_in => $wordlist) + { + $subject_match = ($match_in == 'subject') ? 1 : 0; + + if (!empty($wordlist)) + { + $sql = ''; + foreach ($wordlist as $word) + $sql .= (($sql != '') ? ',' : '').$cur_words[$match_in][$word]; + + $db->query('DELETE FROM '.$db->prefix.'search_matches WHERE word_id IN('.$sql.') AND post_id='.$post_id.' AND subject_match='.$subject_match) or error('Unable to delete search index word matches', __FILE__, __LINE__, $db->error()); + } + } + + // Add new matches + foreach ($words['add'] as $match_in => $wordlist) + { + $subject_match = ($match_in == 'subject') ? 1 : 0; + + if (!empty($wordlist)) + $db->query('INSERT INTO '.$db->prefix.'search_matches (post_id, word_id, subject_match) SELECT '.$post_id.', id, '.$subject_match.' FROM '.$db->prefix.'search_words WHERE word IN(\''.implode('\',\'', array_map(array($db, 'escape'), $wordlist)).'\')') or error('Unable to insert search index word matches', __FILE__, __LINE__, $db->error()); + } + + unset($words); +} + + +// +// Strip search index of indexed words in $post_ids +// +function strip_search_index($post_ids) +{ + global $db_type, $db; + + switch ($db_type) + { + case 'mysql': + case 'mysqli': + case 'mysql_innodb': + case 'mysqli_innodb': + { + $result = $db->query('SELECT word_id FROM '.$db->prefix.'search_matches WHERE post_id IN('.$post_ids.') GROUP BY word_id') or error('Unable to fetch search index word match', __FILE__, __LINE__, $db->error()); + + if ($db->num_rows($result)) + { + $word_ids = ''; + while ($row = $db->fetch_row($result)) + $word_ids .= ($word_ids != '') ? ','.$row[0] : $row[0]; + + $result = $db->query('SELECT word_id FROM '.$db->prefix.'search_matches WHERE word_id IN('.$word_ids.') GROUP BY word_id HAVING COUNT(word_id)=1') or error('Unable to fetch search index word match', __FILE__, __LINE__, $db->error()); + + if ($db->num_rows($result)) + { + $word_ids = ''; + while ($row = $db->fetch_row($result)) + $word_ids .= ($word_ids != '') ? ','.$row[0] : $row[0]; + + $db->query('DELETE FROM '.$db->prefix.'search_words WHERE id IN('.$word_ids.')') or error('Unable to delete search index word', __FILE__, __LINE__, $db->error()); + } + } + + break; + } + + default: + $db->query('DELETE FROM '.$db->prefix.'search_words WHERE id IN(SELECT word_id FROM '.$db->prefix.'search_matches WHERE word_id IN(SELECT word_id FROM '.$db->prefix.'search_matches WHERE post_id IN('.$post_ids.') GROUP BY word_id) GROUP BY word_id HAVING COUNT(word_id)=1)') or error('Unable to delete from search index', __FILE__, __LINE__, $db->error()); + break; + } + + $db->query('DELETE FROM '.$db->prefix.'search_matches WHERE post_id IN('.$post_ids.')') or error('Unable to delete search index word match', __FILE__, __LINE__, $db->error()); +} diff --git a/include/srand.php b/include/srand.php new file mode 100644 index 0000000..cb7985f --- /dev/null +++ b/include/srand.php @@ -0,0 +1,151 @@ +<?php + +/* + * Author: + * George Argyros <argyros.george@gmail.com> + * + * Copyright (c) 2012, George Argyros + * 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 the <organization> 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 GEORGE ARGYROS 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. + * + * + * + * The function is providing, at least at the systems tested :), + * $len bytes of entropy under any PHP installation or operating system. + * The execution time should be at most 10-20 ms in any system. + */ +function secure_random_bytes($len = 10) +{ + + /* + * Our primary choice for a cryptographic strong randomness function is + * openssl_random_pseudo_bytes. + */ + $SSLstr = '4'; // http://xkcd.com/221/ + if (function_exists('openssl_random_pseudo_bytes') && + (substr(PHP_VERSION, 0, 3) == '5.4' && version_compare(PHP_VERSION, '5.4.44') >= 0) || + (substr(PHP_VERSION, 0, 3) == '5.5' && version_compare(PHP_VERSION, '5.5.28') >= 0) || + (version_compare(PHP_VERSION, '5.6.12') >= 0)) + { + $SSLstr = openssl_random_pseudo_bytes($len, $strong); + if ($strong) { + return $SSLstr; + } + } + + /* + * If mcrypt extension is available then we use it to gather entropy from + * the operating system's PRNG. This is better than reading /dev/urandom + * directly since it avoids reading larger blocks of data than needed. + * Older versions of mcrypt_create_iv may be broken or take too much time + * to finish so we only use this function with PHP 5.3.7 and above. + * @see https://bugs.php.net/bug.php?id=55169 + */ + if (function_exists('mcrypt_create_iv') && + (version_compare(PHP_VERSION, '5.3.7') >= 0 || + substr(PHP_OS, 0, 3) !== 'WIN')) { + $str = mcrypt_create_iv($len, MCRYPT_DEV_URANDOM); + if ($str !== false) { + return $str; + } + } + + + /* + * No build-in crypto randomness function found. We collect any entropy + * available in the PHP core PRNGs along with some filesystem info and memory + * stats. To make this data cryptographically strong we add data either from + * /dev/urandom or if its unavailable, we gather entropy by measuring the + * time needed to compute a number of SHA-1 hashes. + */ + $str = ''; + $bits_per_round = 2; // bits of entropy collected in each clock drift round + $msec_per_round = 400; // expected running time of each round in microseconds + $hash_len = 20; // SHA-1 Hash length + $total = $len; // total bytes of entropy to collect + + $handle = @fopen('/dev/urandom', 'rb'); + if ($handle && function_exists('stream_set_read_buffer')) { + @stream_set_read_buffer($handle, 0); + } + + do + { + $bytes = ($total > $hash_len)? $hash_len : $total; + $total -= $bytes; + + //collect any entropy available from the PHP system and filesystem + $entropy = rand() . uniqid(mt_rand(), true) . $SSLstr; + $entropy .= implode('', @fstat(@fopen( __FILE__, 'r'))); + $entropy .= memory_get_usage() . getmypid(); + $entropy .= serialize($_ENV) . serialize($_SERVER); + if (function_exists('posix_times')) { + $entropy .= serialize(posix_times()); + } + if (function_exists('zend_thread_id')) { + $entropy .= zend_thread_id(); + } + if ($handle) { + $entropy .= @fread($handle, $bytes); + } else { + // Measure the time that the operations will take on average + for ($i = 0; $i < 3; $i++) + { + $c1 = get_microtime(); + $var = sha1(mt_rand()); + for ($j = 0; $j < 50; $j++) { + $var = sha1($var); + } + $c2 = get_microtime(); + $entropy .= $c1 . $c2; + } + + // Based on the above measurement determine the total rounds + // in order to bound the total running time. + $rounds = (int) ($msec_per_round * 50 / (int) (($c2 - $c1) * 1000000)); + + // Take the additional measurements. On average we can expect + // at least $bits_per_round bits of entropy from each measurement. + $iter = $bytes * (int) (ceil(8 / $bits_per_round)); + for ($i = 0; $i < $iter; $i++) + { + $c1 = get_microtime(); + $var = sha1(mt_rand()); + for ($j = 0; $j < $rounds; $j++) { + $var = sha1($var); + } + $c2 = get_microtime(); + $entropy .= $c1 . $c2; + } + + } + // We assume sha1 is a deterministic extractor for the $entropy variable. + $str .= sha1($entropy, true); + } while ($len > strlen($str)); + + if ($handle) { + @fclose($handle); + } + return substr($str, 0, $len); +} diff --git a/include/template/admin.tpl b/include/template/admin.tpl new file mode 100644 index 0000000..b87e0af --- /dev/null +++ b/include/template/admin.tpl @@ -0,0 +1,38 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<pun_language>" lang="<pun_language>" dir="<pun_content_direction>"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +<pun_head> +</head> + +<body> + +<div id="punadmin" class="pun"> +<div class="top-box"></div> +<div class="punwrap"> + +<div id="brdheader" class="block"> + <div class="box"> + <div id="brdtitle" class="inbox"> + <pun_title> + <pun_desc> + </div> + <pun_navlinks> + <pun_status> + </div> +</div> + +<pun_announcement> + +<div id="brdmain"> +<pun_main> +</div> + +<pun_footer> + +</div> +<div class="end-box"></div> +</div> + +</body> +</html> diff --git a/include/template/help.tpl b/include/template/help.tpl new file mode 100644 index 0000000..6d923bf --- /dev/null +++ b/include/template/help.tpl @@ -0,0 +1,23 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<pun_language>" lang="<pun_language>" dir="<pun_content_direction>"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +<pun_head> +</head> + +<body> + +<div id="punhelp" class="pun"> +<div class="top-box"></div> +<div class="punwrap"> + +<div id="brdmain"> +<pun_main> +</div> + +</div> +<div class="end-box"></div> +</div> + +</body> +</html> diff --git a/include/template/index.html b/include/template/index.html new file mode 100644 index 0000000..89337b2 --- /dev/null +++ b/include/template/index.html @@ -0,0 +1 @@ +<html><head><title>.</title></head><body>.</body></html> diff --git a/include/template/main.tpl b/include/template/main.tpl new file mode 100644 index 0000000..733a063 --- /dev/null +++ b/include/template/main.tpl @@ -0,0 +1,38 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<pun_language>" lang="<pun_language>" dir="<pun_content_direction>"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +<pun_head> +</head> + +<body> + +<div id="pun<pun_page>" class="pun"> +<div class="top-box"></div> +<div class="punwrap"> + +<div id="brdheader" class="block"> + <div class="box"> + <div id="brdtitle" class="inbox"> + <pun_title> + <pun_desc> + </div> + <pun_navlinks> + <pun_status> + </div> +</div> + +<pun_announcement> + +<div id="brdmain"> +<pun_main> +</div> + +<pun_footer> + +</div> +<div class="end-box"></div> +</div> + +</body> +</html> diff --git a/include/template/maintenance.tpl b/include/template/maintenance.tpl new file mode 100644 index 0000000..fe55db4 --- /dev/null +++ b/include/template/maintenance.tpl @@ -0,0 +1,23 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<pun_language>" lang="<pun_language>" dir="<pun_content_direction>"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +<pun_head> +</head> + +<body> + +<div id="punmaint" class="pun"> +<div class="top-box"></div> +<div class="punwrap"> + +<div id="brdmain"> +<pun_maint_main> +</div> + +</div> +<div class="end-box"></div> +</div> + +</body> +</html> diff --git a/include/template/redirect.tpl b/include/template/redirect.tpl new file mode 100644 index 0000000..ce5fadd --- /dev/null +++ b/include/template/redirect.tpl @@ -0,0 +1,25 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<pun_language>" lang="<pun_language>" dir="<pun_content_direction>"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +<pun_head> +</head> + +<body> + +<div id="punredirect" class="pun"> +<div class="top-box"></div> +<div class="punwrap"> + +<div id="brdmain"> +<pun_redir_main> +</div> + +<pun_footer> + +</div> +<div class="end-box"></div> +</div> + +</body> +</html> diff --git a/include/user/index.html b/include/user/index.html new file mode 100644 index 0000000..89337b2 --- /dev/null +++ b/include/user/index.html @@ -0,0 +1 @@ +<html><head><title>.</title></head><body>.</body></html> diff --git a/include/utf8/index.html b/include/utf8/index.html new file mode 100644 index 0000000..89337b2 --- /dev/null +++ b/include/utf8/index.html @@ -0,0 +1 @@ +<html><head><title>.</title></head><body>.</body></html> diff --git a/include/utf8/mbstring/core.php b/include/utf8/mbstring/core.php new file mode 100644 index 0000000..bea1c32 --- /dev/null +++ b/include/utf8/mbstring/core.php @@ -0,0 +1,144 @@ +<?php + +/** +* @version $Id: core.php,v 1.5 2006/02/28 22:12:25 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +// Define UTF8_CORE as required +if (!defined('UTF8_CORE')) + define('UTF8_CORE', true); + +/** +* Wrapper round mb_strlen +* Assumes you have mb_internal_encoding to UTF-8 already +* Note: this function does not count bad bytes in the string - these +* are simply ignored +* @param string UTF-8 string +* @return int number of UTF-8 characters in string +* @package utf8 +* @subpackage strings +*/ +function utf8_strlen($str) +{ + return mb_strlen($str); +} + +/** +* Assumes mbstring internal encoding is set to UTF-8 +* Wrapper around mb_strpos +* Find position of first occurrence of a string +* @param string haystack +* @param string needle (you should validate this with utf8_is_valid) +* @param integer offset in characters (from left) +* @return mixed integer position or FALSE on failure +* @package utf8 +* @subpackage strings +*/ +function utf8_strpos($str, $search, $offset = false) +{ + // Strip unvalid characters + $str = utf8_bad_strip($str); + + if ($offset === false) + return mb_strpos($str, $search); + else + return mb_strpos($str, $search, $offset); +} + +/** +* Assumes mbstring internal encoding is set to UTF-8 +* Wrapper around mb_strrpos +* Find position of last occurrence of a char in a string +* @param string haystack +* @param string needle (you should validate this with utf8_is_valid) +* @param integer (optional) offset (from left) +* @return mixed integer position or FALSE on failure +* @package utf8 +* @subpackage strings +*/ +function utf8_strrpos($str, $search, $offset = false) +{ + // Strip unvalid characters + $str = utf8_bad_strip($str); + + if (!$offset) + { + // Emulate behaviour of strrpos rather than raising warning + if (empty($str)) + return false; + + return mb_strrpos($str, $search); + } + else + { + if (!is_int($offset)) + { + trigger_error('utf8_strrpos expects parameter 3 to be long', E_USER_WARNING); + return false; + } + + $str = mb_substr($str, $offset); + + if (($pos = mb_strrpos($str, $search)) !== false) + return $pos + $offset; + + return false; + } +} + +/** +* Assumes mbstring internal encoding is set to UTF-8 +* Wrapper around mb_substr +* Return part of a string given character offset (and optionally length) +* @param string +* @param integer number of UTF-8 characters offset (from left) +* @param integer (optional) length in UTF-8 characters from offset +* @return mixed string or FALSE if failure +* @package utf8 +* @subpackage strings +*/ +function utf8_substr($str, $offset, $length = false) +{ + if ($length === false) + return mb_substr($str, $offset); + else + return mb_substr($str, $offset, $length); +} + +/** +* Assumes mbstring internal encoding is set to UTF-8 +* Wrapper around mb_strtolower +* Make a string lowercase +* Note: The concept of a characters "case" only exists is some alphabets +* such as Latin, Greek, Cyrillic, Armenian and archaic Georgian - it does +* not exist in the Chinese alphabet, for example. See Unicode Standard +* Annex #21: Case Mappings +* @param string +* @return mixed either string in lowercase or FALSE is UTF-8 invalid +* @package utf8 +* @subpackage strings +*/ +function utf8_strtolower($str) +{ + return mb_strtolower($str); +} + +/** +* Assumes mbstring internal encoding is set to UTF-8 +* Wrapper around mb_strtoupper +* Make a string uppercase +* Note: The concept of a characters "case" only exists is some alphabets +* such as Latin, Greek, Cyrillic, Armenian and archaic Georgian - it does +* not exist in the Chinese alphabet, for example. See Unicode Standard +* Annex #21: Case Mappings +* @param string +* @return mixed either string in lowercase or FALSE is UTF-8 invalid +* @package utf8 +* @subpackage strings +*/ +function utf8_strtoupper($str) +{ + return mb_strtoupper($str); +} diff --git a/include/utf8/mbstring/index.html b/include/utf8/mbstring/index.html new file mode 100644 index 0000000..89337b2 --- /dev/null +++ b/include/utf8/mbstring/index.html @@ -0,0 +1 @@ +<html><head><title>.</title></head><body>.</body></html> diff --git a/include/utf8/native/core.php b/include/utf8/native/core.php new file mode 100644 index 0000000..58636f5 --- /dev/null +++ b/include/utf8/native/core.php @@ -0,0 +1,422 @@ +<?php + +/** +* @version $Id: core.php,v 1.9 2007/08/12 01:11:33 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +// Define UTF8_CORE as required +if (!defined('UTF8_CORE')) + define('UTF8_CORE', true); + +/** +* Unicode aware replacement for strlen(). Returns the number +* of characters in the string (not the number of bytes), replacing +* multibyte characters with a single byte equivalent +* utf8_decode() converts characters that are not in ISO-8859-1 +* to '?', which, for the purpose of counting, is alright - It's +* much faster than iconv_strlen +* Note: this function does not count bad UTF-8 bytes in the string +* - these are simply ignored +* @author <chernyshevsky at hotmail dot com> +* @link http://www.php.net/manual/en/function.strlen.php +* @link http://www.php.net/manual/en/function.utf8-decode.php +* @param string UTF-8 string +* @return int number of UTF-8 characters in string +* @package utf8 +* @subpackage strings +*/ +function utf8_strlen($str) +{ + return strlen(utf8_decode($str)); +} + +/** +* UTF-8 aware alternative to strpos +* Find position of first occurrence of a string +* Note: This will get alot slower if offset is used +* Note: requires utf8_strlen amd utf8_substr to be loaded +* @param string haystack +* @param string needle (you should validate this with utf8_is_valid) +* @param integer offset in characters (from left) +* @return mixed integer position or FALSE on failure +* @see http://www.php.net/strpos +* @see utf8_strlen +* @see utf8_substr +* @package utf8 +* @subpackage strings +*/ +function utf8_strpos($str, $needle, $offset = false) +{ + if ($offset === false) + { + $ar = explode($needle, $str, 2); + + if (count($ar) > 1) + return utf8_strlen($ar[0]); + + return false; + } + else + { + if (!is_int($offset)) + { + trigger_error('utf8_strpos: Offset must be an integer', E_USER_ERROR); + return false; + } + + $str = utf8_substr($str, $offset); + + if (($pos = utf8_strpos($str, $needle)) !== false) + return $pos + $offset; + + return false; + } +} + +/** +* UTF-8 aware alternative to strrpos +* Find position of last occurrence of a char in a string +* Note: This will get alot slower if offset is used +* Note: requires utf8_substr and utf8_strlen to be loaded +* @param string haystack +* @param string needle (you should validate this with utf8_is_valid) +* @param integer (optional) offset (from left) +* @return mixed integer position or FALSE on failure +* @see http://www.php.net/strrpos +* @see utf8_substr +* @see utf8_strlen +* @package utf8 +* @subpackage strings +*/ +function utf8_strrpos($str, $needle, $offset = false) +{ + if ($offset === false) + { + $ar = explode($needle, $str); + + if (count($ar) > 1) + { + // Pop off the end of the string where the last match was made + array_pop($ar); + $str = join($needle, $ar); + + return utf8_strlen($str); + } + + return false; + } + else + { + if (!is_int($offset)) + { + trigger_error('utf8_strrpos expects parameter 3 to be long', E_USER_WARNING); + return false; + } + + $str = utf8_substr($str, $offset); + + if (($pos = utf8_strrpos($str, $needle)) !== false) + return $pos + $offset; + + return false; + } +} + +/** +* UTF-8 aware alternative to substr +* Return part of a string given character offset (and optionally length) +* +* Note arguments: comparied to substr - if offset or length are +* not integers, this version will not complain but rather massages them +* into an integer. +* +* Note on returned values: substr documentation states false can be +* returned in some cases (e.g. offset > string length) +* mb_substr never returns false, it will return an empty string instead. +* This adopts the mb_substr approach +* +* Note on implementation: PCRE only supports repetitions of less than +* 65536, in order to accept up to MAXINT values for offset and length, +* we'll repeat a group of 65535 characters when needed. +* +* Note on implementation: calculating the number of characters in the +* string is a relatively expensive operation, so we only carry it out when +* necessary. It isn't necessary for +ve offsets and no specified length +* +* @author Chris Smith<chris@jalakai.co.uk> +* @param string +* @param integer number of UTF-8 characters offset (from left) +* @param integer (optional) length in UTF-8 characters from offset +* @return mixed string or FALSE if failure +* @package utf8 +* @subpackage strings +*/ +function utf8_substr($str, $offset, $length = false) +{ + // Generates E_NOTICE for PHP4 objects, but not PHP5 objects + $str = (string) $str; + $offset = (int) $offset; + + if ($length) + $length = (int) $length; + + // Handle trivial cases + if ($length === 0) + return ''; + if ($offset < 0 && $length < 0 && $length < $offset) + return ''; + + // Normalise negative offsets (we could use a tail + // anchored pattern, but they are horribly slow!) + if ($offset < 0) + { + // See notes + $strlen = utf8_strlen($str); + $offset = $strlen + $offset; + + if ($offset < 0) + $offset = 0; + } + + $Op = ''; + $Lp = ''; + + // Establish a pattern for offset, a + // non-captured group equal in length to offset + if ($offset > 0) + { + $Ox = (int) ($offset / 65535); + $Oy = $offset % 65535; + + if ($Ox) + $Op = '(?:.{65535}){'.$Ox.'}'; + + $Op = '^(?:'.$Op.'.{'.$Oy.'})'; + } + else + $Op = '^'; + + + // Establish a pattern for length + if (!$length) + { + // The rest of the string + $Lp = '(.*)$'; + } + else + { + // See notes + if (!isset($strlen)) + $strlen = strlen(utf8_decode($str)); + + // Another trivial case + if ($offset > $strlen) + return ''; + + if ($length > 0) + { + // Reduce any length that would go passed the end of the string + $length = min($strlen-$offset, $length); + + $Lx = (int)( $length / 65535 ); + $Ly = $length % 65535; + + // Negative length requires a captured group of length characters + if ($Lx) $Lp = '(?:.{65535}){'.$Lx.'}'; + $Lp = '('.$Lp.'.{'.$Ly.'})'; + } + else if ($length < 0) + { + + if ($length < ($offset - $strlen)) + return ''; + + $Lx = (int)((-$length)/65535); + $Ly = (-$length)%65535; + + // Negative length requires ... capture everything except a group of + // -length characters anchored at the tail-end of the string + if ($Lx) + $Lp = '(?:.{65535}){'.$Lx.'}'; + + $Lp = '(.*)(?:'.$Lp.'.{'.$Ly.'})$'; + } + } + + if (!preg_match('#'.$Op.$Lp.'#us', $str, $match)) + return ''; + + return $match[1]; +} + +/** +* UTF-8 aware alternative to strtolower +* Make a string lowercase +* Note: The concept of a characters "case" only exists is some alphabets +* such as Latin, Greek, Cyrillic, Armenian and archaic Georgian - it does +* not exist in the Chinese alphabet, for example. See Unicode Standard +* Annex #21: Case Mappings +* Note: requires utf8_to_unicode and utf8_from_unicode +* @author Andreas Gohr <andi@splitbrain.org> +* @param string +* @return mixed either string in lowercase or FALSE is UTF-8 invalid +* @see http://www.php.net/strtolower +* @see utf8_to_unicode +* @see utf8_from_unicode +* @see http://www.unicode.org/reports/tr21/tr21-5.html +* @see http://dev.splitbrain.org/view/darcs/dokuwiki/inc/utf8.php +* @package utf8 +* @subpackage strings +*/ +function utf8_strtolower($string) +{ + static $UTF8_UPPER_TO_LOWER = false; + + if (!$UTF8_UPPER_TO_LOWER) + { + $UTF8_UPPER_TO_LOWER = array( + 0x0041=>0x0061, 0x03A6=>0x03C6, 0x0162=>0x0163, 0x00C5=>0x00E5, 0x0042=>0x0062, + 0x0139=>0x013A, 0x00C1=>0x00E1, 0x0141=>0x0142, 0x038E=>0x03CD, 0x0100=>0x0101, + 0x0490=>0x0491, 0x0394=>0x03B4, 0x015A=>0x015B, 0x0044=>0x0064, 0x0393=>0x03B3, + 0x00D4=>0x00F4, 0x042A=>0x044A, 0x0419=>0x0439, 0x0112=>0x0113, 0x041C=>0x043C, + 0x015E=>0x015F, 0x0143=>0x0144, 0x00CE=>0x00EE, 0x040E=>0x045E, 0x042F=>0x044F, + 0x039A=>0x03BA, 0x0154=>0x0155, 0x0049=>0x0069, 0x0053=>0x0073, 0x1E1E=>0x1E1F, + 0x0134=>0x0135, 0x0427=>0x0447, 0x03A0=>0x03C0, 0x0418=>0x0438, 0x00D3=>0x00F3, + 0x0420=>0x0440, 0x0404=>0x0454, 0x0415=>0x0435, 0x0429=>0x0449, 0x014A=>0x014B, + 0x0411=>0x0431, 0x0409=>0x0459, 0x1E02=>0x1E03, 0x00D6=>0x00F6, 0x00D9=>0x00F9, + 0x004E=>0x006E, 0x0401=>0x0451, 0x03A4=>0x03C4, 0x0423=>0x0443, 0x015C=>0x015D, + 0x0403=>0x0453, 0x03A8=>0x03C8, 0x0158=>0x0159, 0x0047=>0x0067, 0x00C4=>0x00E4, + 0x0386=>0x03AC, 0x0389=>0x03AE, 0x0166=>0x0167, 0x039E=>0x03BE, 0x0164=>0x0165, + 0x0116=>0x0117, 0x0108=>0x0109, 0x0056=>0x0076, 0x00DE=>0x00FE, 0x0156=>0x0157, + 0x00DA=>0x00FA, 0x1E60=>0x1E61, 0x1E82=>0x1E83, 0x00C2=>0x00E2, 0x0118=>0x0119, + 0x0145=>0x0146, 0x0050=>0x0070, 0x0150=>0x0151, 0x042E=>0x044E, 0x0128=>0x0129, + 0x03A7=>0x03C7, 0x013D=>0x013E, 0x0422=>0x0442, 0x005A=>0x007A, 0x0428=>0x0448, + 0x03A1=>0x03C1, 0x1E80=>0x1E81, 0x016C=>0x016D, 0x00D5=>0x00F5, 0x0055=>0x0075, + 0x0176=>0x0177, 0x00DC=>0x00FC, 0x1E56=>0x1E57, 0x03A3=>0x03C3, 0x041A=>0x043A, + 0x004D=>0x006D, 0x016A=>0x016B, 0x0170=>0x0171, 0x0424=>0x0444, 0x00CC=>0x00EC, + 0x0168=>0x0169, 0x039F=>0x03BF, 0x004B=>0x006B, 0x00D2=>0x00F2, 0x00C0=>0x00E0, + 0x0414=>0x0434, 0x03A9=>0x03C9, 0x1E6A=>0x1E6B, 0x00C3=>0x00E3, 0x042D=>0x044D, + 0x0416=>0x0436, 0x01A0=>0x01A1, 0x010C=>0x010D, 0x011C=>0x011D, 0x00D0=>0x00F0, + 0x013B=>0x013C, 0x040F=>0x045F, 0x040A=>0x045A, 0x00C8=>0x00E8, 0x03A5=>0x03C5, + 0x0046=>0x0066, 0x00DD=>0x00FD, 0x0043=>0x0063, 0x021A=>0x021B, 0x00CA=>0x00EA, + 0x0399=>0x03B9, 0x0179=>0x017A, 0x00CF=>0x00EF, 0x01AF=>0x01B0, 0x0045=>0x0065, + 0x039B=>0x03BB, 0x0398=>0x03B8, 0x039C=>0x03BC, 0x040C=>0x045C, 0x041F=>0x043F, + 0x042C=>0x044C, 0x00DE=>0x00FE, 0x00D0=>0x00F0, 0x1EF2=>0x1EF3, 0x0048=>0x0068, + 0x00CB=>0x00EB, 0x0110=>0x0111, 0x0413=>0x0433, 0x012E=>0x012F, 0x00C6=>0x00E6, + 0x0058=>0x0078, 0x0160=>0x0161, 0x016E=>0x016F, 0x0391=>0x03B1, 0x0407=>0x0457, + 0x0172=>0x0173, 0x0178=>0x00FF, 0x004F=>0x006F, 0x041B=>0x043B, 0x0395=>0x03B5, + 0x0425=>0x0445, 0x0120=>0x0121, 0x017D=>0x017E, 0x017B=>0x017C, 0x0396=>0x03B6, + 0x0392=>0x03B2, 0x0388=>0x03AD, 0x1E84=>0x1E85, 0x0174=>0x0175, 0x0051=>0x0071, + 0x0417=>0x0437, 0x1E0A=>0x1E0B, 0x0147=>0x0148, 0x0104=>0x0105, 0x0408=>0x0458, + 0x014C=>0x014D, 0x00CD=>0x00ED, 0x0059=>0x0079, 0x010A=>0x010B, 0x038F=>0x03CE, + 0x0052=>0x0072, 0x0410=>0x0430, 0x0405=>0x0455, 0x0402=>0x0452, 0x0126=>0x0127, + 0x0136=>0x0137, 0x012A=>0x012B, 0x038A=>0x03AF, 0x042B=>0x044B, 0x004C=>0x006C, + 0x0397=>0x03B7, 0x0124=>0x0125, 0x0218=>0x0219, 0x00DB=>0x00FB, 0x011E=>0x011F, + 0x041E=>0x043E, 0x1E40=>0x1E41, 0x039D=>0x03BD, 0x0106=>0x0107, 0x03AB=>0x03CB, + 0x0426=>0x0446, 0x00DE=>0x00FE, 0x00C7=>0x00E7, 0x03AA=>0x03CA, 0x0421=>0x0441, + 0x0412=>0x0432, 0x010E=>0x010F, 0x00D8=>0x00F8, 0x0057=>0x0077, 0x011A=>0x011B, + 0x0054=>0x0074, 0x004A=>0x006A, 0x040B=>0x045B, 0x0406=>0x0456, 0x0102=>0x0103, + 0x039B=>0x03BB, 0x00D1=>0x00F1, 0x041D=>0x043D, 0x038C=>0x03CC, 0x00C9=>0x00E9, + 0x00D0=>0x00F0, 0x0407=>0x0457, 0x0122=>0x0123); + } + + $uni = utf8_to_unicode($string); + + if (!$uni) + return false; + + $cnt = count($uni); + + for ($i=0; $i < $cnt; $i++) + if (isset($UTF8_UPPER_TO_LOWER[$uni[$i]])) + $uni[$i] = $UTF8_UPPER_TO_LOWER[$uni[$i]]; + + return utf8_from_unicode($uni); +} + +/** +* UTF-8 aware alternative to strtoupper +* Make a string uppercase +* Note: The concept of a characters "case" only exists is some alphabets +* such as Latin, Greek, Cyrillic, Armenian and archaic Georgian - it does +* not exist in the Chinese alphabet, for example. See Unicode Standard +* Annex #21: Case Mappings +* Note: requires utf8_to_unicode and utf8_from_unicode +* @author Andreas Gohr <andi@splitbrain.org> +* @param string +* @return mixed either string in lowercase or FALSE is UTF-8 invalid +* @see http://www.php.net/strtoupper +* @see utf8_to_unicode +* @see utf8_from_unicode +* @see http://www.unicode.org/reports/tr21/tr21-5.html +* @see http://dev.splitbrain.org/view/darcs/dokuwiki/inc/utf8.php +* @package utf8 +* @subpackage strings +*/ +function utf8_strtoupper($string) +{ + static $UTF8_LOWER_TO_UPPER = false; + + if (!$UTF8_LOWER_TO_UPPER) + { + $UTF8_LOWER_TO_UPPER = array( + 0x0061=>0x0041, 0x03C6=>0x03A6, 0x0163=>0x0162, 0x00E5=>0x00C5, 0x0062=>0x0042, + 0x013A=>0x0139, 0x00E1=>0x00C1, 0x0142=>0x0141, 0x03CD=>0x038E, 0x0101=>0x0100, + 0x0491=>0x0490, 0x03B4=>0x0394, 0x015B=>0x015A, 0x0064=>0x0044, 0x03B3=>0x0393, + 0x00F4=>0x00D4, 0x044A=>0x042A, 0x0439=>0x0419, 0x0113=>0x0112, 0x043C=>0x041C, + 0x015F=>0x015E, 0x0144=>0x0143, 0x00EE=>0x00CE, 0x045E=>0x040E, 0x044F=>0x042F, + 0x03BA=>0x039A, 0x0155=>0x0154, 0x0069=>0x0049, 0x0073=>0x0053, 0x1E1F=>0x1E1E, + 0x0135=>0x0134, 0x0447=>0x0427, 0x03C0=>0x03A0, 0x0438=>0x0418, 0x00F3=>0x00D3, + 0x0440=>0x0420, 0x0454=>0x0404, 0x0435=>0x0415, 0x0449=>0x0429, 0x014B=>0x014A, + 0x0431=>0x0411, 0x0459=>0x0409, 0x1E03=>0x1E02, 0x00F6=>0x00D6, 0x00F9=>0x00D9, + 0x006E=>0x004E, 0x0451=>0x0401, 0x03C4=>0x03A4, 0x0443=>0x0423, 0x015D=>0x015C, + 0x0453=>0x0403, 0x03C8=>0x03A8, 0x0159=>0x0158, 0x0067=>0x0047, 0x00E4=>0x00C4, + 0x03AC=>0x0386, 0x03AE=>0x0389, 0x0167=>0x0166, 0x03BE=>0x039E, 0x0165=>0x0164, + 0x0117=>0x0116, 0x0109=>0x0108, 0x0076=>0x0056, 0x00FE=>0x00DE, 0x0157=>0x0156, + 0x00FA=>0x00DA, 0x1E61=>0x1E60, 0x1E83=>0x1E82, 0x00E2=>0x00C2, 0x0119=>0x0118, + 0x0146=>0x0145, 0x0070=>0x0050, 0x0151=>0x0150, 0x044E=>0x042E, 0x0129=>0x0128, + 0x03C7=>0x03A7, 0x013E=>0x013D, 0x0442=>0x0422, 0x007A=>0x005A, 0x0448=>0x0428, + 0x03C1=>0x03A1, 0x1E81=>0x1E80, 0x016D=>0x016C, 0x00F5=>0x00D5, 0x0075=>0x0055, + 0x0177=>0x0176, 0x00FC=>0x00DC, 0x1E57=>0x1E56, 0x03C3=>0x03A3, 0x043A=>0x041A, + 0x006D=>0x004D, 0x016B=>0x016A, 0x0171=>0x0170, 0x0444=>0x0424, 0x00EC=>0x00CC, + 0x0169=>0x0168, 0x03BF=>0x039F, 0x006B=>0x004B, 0x00F2=>0x00D2, 0x00E0=>0x00C0, + 0x0434=>0x0414, 0x03C9=>0x03A9, 0x1E6B=>0x1E6A, 0x00E3=>0x00C3, 0x044D=>0x042D, + 0x0436=>0x0416, 0x01A1=>0x01A0, 0x010D=>0x010C, 0x011D=>0x011C, 0x00F0=>0x00D0, + 0x013C=>0x013B, 0x045F=>0x040F, 0x045A=>0x040A, 0x00E8=>0x00C8, 0x03C5=>0x03A5, + 0x0066=>0x0046, 0x00FD=>0x00DD, 0x0063=>0x0043, 0x021B=>0x021A, 0x00EA=>0x00CA, + 0x03B9=>0x0399, 0x017A=>0x0179, 0x00EF=>0x00CF, 0x01B0=>0x01AF, 0x0065=>0x0045, + 0x03BB=>0x039B, 0x03B8=>0x0398, 0x03BC=>0x039C, 0x045C=>0x040C, 0x043F=>0x041F, + 0x044C=>0x042C, 0x00FE=>0x00DE, 0x00F0=>0x00D0, 0x1EF3=>0x1EF2, 0x0068=>0x0048, + 0x00EB=>0x00CB, 0x0111=>0x0110, 0x0433=>0x0413, 0x012F=>0x012E, 0x00E6=>0x00C6, + 0x0078=>0x0058, 0x0161=>0x0160, 0x016F=>0x016E, 0x03B1=>0x0391, 0x0457=>0x0407, + 0x0173=>0x0172, 0x00FF=>0x0178, 0x006F=>0x004F, 0x043B=>0x041B, 0x03B5=>0x0395, + 0x0445=>0x0425, 0x0121=>0x0120, 0x017E=>0x017D, 0x017C=>0x017B, 0x03B6=>0x0396, + 0x03B2=>0x0392, 0x03AD=>0x0388, 0x1E85=>0x1E84, 0x0175=>0x0174, 0x0071=>0x0051, + 0x0437=>0x0417, 0x1E0B=>0x1E0A, 0x0148=>0x0147, 0x0105=>0x0104, 0x0458=>0x0408, + 0x014D=>0x014C, 0x00ED=>0x00CD, 0x0079=>0x0059, 0x010B=>0x010A, 0x03CE=>0x038F, + 0x0072=>0x0052, 0x0430=>0x0410, 0x0455=>0x0405, 0x0452=>0x0402, 0x0127=>0x0126, + 0x0137=>0x0136, 0x012B=>0x012A, 0x03AF=>0x038A, 0x044B=>0x042B, 0x006C=>0x004C, + 0x03B7=>0x0397, 0x0125=>0x0124, 0x0219=>0x0218, 0x00FB=>0x00DB, 0x011F=>0x011E, + 0x043E=>0x041E, 0x1E41=>0x1E40, 0x03BD=>0x039D, 0x0107=>0x0106, 0x03CB=>0x03AB, + 0x0446=>0x0426, 0x00FE=>0x00DE, 0x00E7=>0x00C7, 0x03CA=>0x03AA, 0x0441=>0x0421, + 0x0432=>0x0412, 0x010F=>0x010E, 0x00F8=>0x00D8, 0x0077=>0x0057, 0x011B=>0x011A, + 0x0074=>0x0054, 0x006A=>0x004A, 0x045B=>0x040B, 0x0456=>0x0406, 0x0103=>0x0102, + 0x03BB=>0x039B, 0x00F1=>0x00D1, 0x043D=>0x041D, 0x03CC=>0x038C, 0x00E9=>0x00C9, + 0x00F0=>0x00D0, 0x0457=>0x0407, 0x0123=>0x0122); + } + + $uni = utf8_to_unicode($string); + + if (!$uni) + return false; + + $cnt = count($uni); + + for ($i=0; $i < $cnt; $i++) + if(isset($UTF8_LOWER_TO_UPPER[$uni[$i]])) + $uni[$i] = $UTF8_LOWER_TO_UPPER[$uni[$i]]; + + return utf8_from_unicode($uni); +} diff --git a/include/utf8/native/index.html b/include/utf8/native/index.html new file mode 100644 index 0000000..89337b2 --- /dev/null +++ b/include/utf8/native/index.html @@ -0,0 +1 @@ +<html><head><title>.</title></head><body>.</body></html> diff --git a/include/utf8/ord.php b/include/utf8/ord.php new file mode 100644 index 0000000..a333f96 --- /dev/null +++ b/include/utf8/ord.php @@ -0,0 +1,78 @@ +<?php + +/** +* @version $Id: ord.php,v 1.4 2006/09/11 15:22:54 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +/** +* UTF-8 aware alternative to ord +* Returns the unicode ordinal for a character +* @param string UTF-8 encoded character +* @return int unicode ordinal for the character +* @see http://www.php.net/ord +* @see http://www.php.net/manual/en/function.ord.php#46267 +*/ +function utf8_ord($chr) +{ + $ord0 = ord($chr); + + if ($ord0 >= 0 && $ord0 <= 127) + return $ord0; + + if (!isset($chr{1})) + { + trigger_error('Short sequence - at least 2 bytes expected, only 1 seen'); + return false; + } + + $ord1 = ord($chr{1}); + if ($ord0 >= 192 && $ord0 <= 223) + return ($ord0 - 192) * 64 + ($ord1 - 128); + + if (!isset($chr{2})) + { + trigger_error('Short sequence - at least 3 bytes expected, only 2 seen'); + return false; + } + + $ord2 = ord($chr{2}); + if ($ord0 >= 224 && $ord0 <= 239) + return ($ord0-224)*4096 + ($ord1-128)*64 + ($ord2-128); + + if (!isset($chr{3})) + { + trigger_error('Short sequence - at least 4 bytes expected, only 3 seen'); + return false; + } + + $ord3 = ord($chr{3}); + if ($ord0>=240 && $ord0<=247) + return ($ord0-240)*262144 + ($ord1-128)*4096 + ($ord2-128)*64 + ($ord3-128); + + if (!isset($chr{4})) + { + trigger_error('Short sequence - at least 5 bytes expected, only 4 seen'); + return false; + } + + $ord4 = ord($chr{4}); + if ($ord0>=248 && $ord0<=251) + return ($ord0-248)*16777216 + ($ord1-128)*262144 + ($ord2-128)*4096 + ($ord3-128)*64 + ($ord4-128); + + if (!isset($chr{5})) + { + trigger_error('Short sequence - at least 6 bytes expected, only 5 seen'); + return false; + } + + if ($ord0>=252 && $ord0<=253) + return ($ord0-252) * 1073741824 + ($ord1-128)*16777216 + ($ord2-128)*262144 + ($ord3-128)*4096 + ($ord4-128)*64 + (ord($c{5})-128); + + if ($ord0 >= 254 && $ord0 <= 255) + { + trigger_error('Invalid UTF-8 with surrogate ordinal '.$ord0); + return false; + } +} diff --git a/include/utf8/str_ireplace.php b/include/utf8/str_ireplace.php new file mode 100644 index 0000000..7257b0a --- /dev/null +++ b/include/utf8/str_ireplace.php @@ -0,0 +1,72 @@ +<?php + +/** +* @version $Id: str_ireplace.php,v 1.2 2007/08/12 01:20:46 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +/** +* UTF-8 aware alternative to str_ireplace +* Case-insensitive version of str_replace +* Note: requires utf8_strtolower +* Note: it's not fast and gets slower if $search / $replace is array +* Notes: it's based on the assumption that the lower and uppercase +* versions of a UTF-8 character will have the same length in bytes +* which is currently true given the hash table to strtolower +* @param string +* @return string +* @see http://www.php.net/str_ireplace +* @see utf8_strtolower +* @package utf8 +* @subpackage strings +*/ +function utf8_ireplace($search, $replace, $str, $count=null) +{ + if (!is_array($search)) + { + $slen = strlen($search); + + if ($slen == 0) + return $str; + + $lendif = strlen($replace) - strlen($search); + $search = utf8_strtolower($search); + + $search = preg_quote($search); + $lstr = utf8_strtolower($str); + $i = 0; + $matched = 0; + + while (preg_match('/(.*)'.$search.'/Us', $lstr, $matches)) + { + if ($i === $count) + break; + + $mlen = strlen($matches[0]); + $lstr = substr($lstr, $mlen); + $str = substr_replace($str, $replace, $matched+strlen($matches[1]), $slen); + $matched += $mlen + $lendif; + $i++; + } + + return $str; + } + else + { + foreach (array_keys($search) as $k) + { + if (is_array($replace)) + { + if (array_key_exists($k, $replace)) + $str = utf8_ireplace($search[$k], $replace[$k], $str, $count); + else + $str = utf8_ireplace($search[$k], '', $str, $count); + } + else + $str = utf8_ireplace($search[$k], $replace, $str, $count); + } + + return $str; + } +} diff --git a/include/utf8/str_pad.php b/include/utf8/str_pad.php new file mode 100644 index 0000000..93a559a --- /dev/null +++ b/include/utf8/str_pad.php @@ -0,0 +1,59 @@ +<?php + +/** +* @version $Id: str_pad.php,v 1.1 2006/09/03 09:25:13 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +/** +* Replacement for str_pad. $padStr may contain multi-byte characters. +* +* @author Oliver Saunders <oliver (a) osinternetservices.com> +* @param string $input +* @param int $length +* @param string $padStr +* @param int $type ( same constants as str_pad ) +* @return string +* @see http://www.php.net/str_pad +* @see utf8_substr +* @package utf8 +* @subpackage strings +*/ +function utf8_str_pad($input, $length, $padStr=' ', $type=STR_PAD_RIGHT) +{ + $inputLen = utf8_strlen($input); + if ($length <= $inputLen) + return $input; + + $padStrLen = utf8_strlen($padStr); + $padLen = $length - $inputLen; + + if ($type == STR_PAD_RIGHT) + { + $repeatTimes = ceil($padLen / $padStrLen); + return utf8_substr($input.str_repeat($padStr, $repeatTimes), 0, $length); + } + + if ($type == STR_PAD_LEFT) + { + $repeatTimes = ceil($padLen / $padStrLen); + return utf8_substr(str_repeat($padStr, $repeatTimes), 0, floor($padLen)).$input; + } + + if ($type == STR_PAD_BOTH) + { + $padLen /= 2; + $padAmountLeft = floor($padLen); + $padAmountRight = ceil($padLen); + $repeatTimesLeft = ceil($padAmountLeft / $padStrLen); + $repeatTimesRight = ceil($padAmountRight / $padStrLen); + + $paddingLeft = utf8_substr(str_repeat($padStr, $repeatTimesLeft), 0, $padAmountLeft); + $paddingRight = utf8_substr(str_repeat($padStr, $repeatTimesRight), 0, $padAmountLeft); + + return $paddingLeft.$input.$paddingRight; + } + + trigger_error('utf8_str_pad: Unknown padding type ('.$type.')', E_USER_ERROR); +} diff --git a/include/utf8/str_split.php b/include/utf8/str_split.php new file mode 100644 index 0000000..15bc215 --- /dev/null +++ b/include/utf8/str_split.php @@ -0,0 +1,33 @@ +<?php + +/** +* @version $Id: str_split.php,v 1.1 2006/02/25 13:50:17 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +/** +* UTF-8 aware alternative to str_split +* Convert a string to an array +* Note: requires utf8_strlen to be loaded +* @param string UTF-8 encoded +* @param int number to characters to split string by +* @return string characters in string reverses +* @see http://www.php.net/str_split +* @see utf8_strlen +* @package utf8 +* @subpackage strings +*/ +function utf8_str_split($str, $split_len=1) +{ + if (!preg_match('/^[0-9]+$/',$split_len) || $split_len < 1) + return false; + + $len = utf8_strlen($str); + if ($len <= $split_len) + return array($str); + + preg_match_all('/.{'.$split_len.'}|[^\x00]{1,'.$split_len.'}$/us', $str, $ar); + + return $ar[0]; +} diff --git a/include/utf8/strcasecmp.php b/include/utf8/strcasecmp.php new file mode 100644 index 0000000..423f443 --- /dev/null +++ b/include/utf8/strcasecmp.php @@ -0,0 +1,27 @@ +<?php + +/** +* @version $Id: strcasecmp.php,v 1.1 2006/02/25 13:50:17 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +/** +* UTF-8 aware alternative to strcasecmp +* A case insensivite string comparison +* Note: requires utf8_strtolower +* @param string +* @param string +* @return int +* @see http://www.php.net/strcasecmp +* @see utf8_strtolower +* @package utf8 +* @subpackage strings +*/ +function utf8_strcasecmp($strX, $strY) +{ + $strX = utf8_strtolower($strX); + $strY = utf8_strtolower($strY); + + return strcmp($strX, $strY); +} diff --git a/include/utf8/strcspn.php b/include/utf8/strcspn.php new file mode 100644 index 0000000..b05e327 --- /dev/null +++ b/include/utf8/strcspn.php @@ -0,0 +1,36 @@ +<?php + +/** +* @version $Id: strcspn.php,v 1.1 2006/02/25 13:50:17 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +/** +* UTF-8 aware alternative to strcspn +* Find length of initial segment not matching mask +* Note: requires utf8_strlen and utf8_substr (if start, length are used) +* @param string +* @return int +* @see http://www.php.net/strcspn +* @see utf8_strlen +* @package utf8 +* @subpackage strings +*/ +function utf8_strcspn($str, $mask, $start=null, $length=null) +{ + if (empty($mask) || strlen($mask) == 0) + return null; + + $mask = preg_replace('!([\\\\\\-\\]\\[/^])!','\\\${1}', $mask); + + if (!is_null($start) || !is_null($length)) + $str = utf8_substr($str, $start, $length); + + preg_match('/^[^'.$mask.']+/u', $str, $matches); + + if (isset($matches[0])) + return utf8_strlen($matches[0]); + + return 0; +} diff --git a/include/utf8/stristr.php b/include/utf8/stristr.php new file mode 100644 index 0000000..fb9e6a5 --- /dev/null +++ b/include/utf8/stristr.php @@ -0,0 +1,34 @@ +<?php + +/** +* @version $Id: stristr.php,v 1.1 2006/02/25 13:50:17 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +/** +* UTF-8 aware alternative to stristr +* Find first occurrence of a string using case insensitive comparison +* Note: requires utf8_strtolower +* @param string +* @param string +* @return int +* @see http://www.php.net/strcasecmp +* @see utf8_strtolower +* @package utf8 +* @subpackage strings +*/ +function utf8_stristr($str, $search) +{ + if (strlen($search) == 0) + return $str; + + $lstr = utf8_strtolower($str); + $lsearch = utf8_strtolower($search); + preg_match('/^(.*)'.preg_quote($lsearch).'/Us', $lstr, $matches); + + if (count($matches) == 2) + return substr($str, strlen($matches[1])); + + return false; +} diff --git a/include/utf8/strrev.php b/include/utf8/strrev.php new file mode 100644 index 0000000..ae9c32b --- /dev/null +++ b/include/utf8/strrev.php @@ -0,0 +1,22 @@ +<?php + +/** +* @version $Id: strrev.php,v 1.1 2006/02/25 13:50:17 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +/** +* UTF-8 aware alternative to strrev +* Reverse a string +* @param string UTF-8 encoded +* @return string characters in string reverses +* @see http://www.php.net/strrev +* @package utf8 +* @subpackage strings +*/ +function utf8_strrev($str) +{ + preg_match_all('/./us', $str, $ar); + return implode(array_reverse($ar[0])); +} diff --git a/include/utf8/strspn.php b/include/utf8/strspn.php new file mode 100644 index 0000000..49d300a --- /dev/null +++ b/include/utf8/strspn.php @@ -0,0 +1,32 @@ +<?php + +/** +* @version $Id: strspn.php,v 1.1 2006/02/25 13:50:17 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +/** +* UTF-8 aware alternative to strspn +* Find length of initial segment matching mask +* Note: requires utf8_strlen and utf8_substr (if start, length are used) +* @param string +* @return int +* @see http://www.php.net/strspn +* @package utf8 +* @subpackage strings +*/ +function utf8_strspn($str, $mask, $start=null, $length=null) +{ + $mask = preg_replace('!([\\\\\\-\\]\\[/^])!', '\\\${1}', $mask); + + if (!is_null($start)|| !is_null($length)) + $str = utf8_substr($str, $start, $length); + + preg_match('/^['.$mask.']+/u', $str, $matches); + + if (isset($matches[0])) + return utf8_strlen($matches[0]); + + return 0; +} diff --git a/include/utf8/substr_replace.php b/include/utf8/substr_replace.php new file mode 100644 index 0000000..20a43b5 --- /dev/null +++ b/include/utf8/substr_replace.php @@ -0,0 +1,27 @@ +<?php + +/** +* @version $Id: substr_replace.php,v 1.1 2006/02/25 13:50:17 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +/** +* UTF-8 aware substr_replace. +* Note: requires utf8_substr to be loaded +* @see http://www.php.net/substr_replace +* @see utf8_strlen +* @see utf8_substr +*/ +function utf8_substr_replace($str, $repl, $start , $length=null) +{ + preg_match_all('/./us', $str, $ar); + preg_match_all('/./us', $repl, $rar); + + if(is_null($length)) + $length = utf8_strlen($str); + + array_splice($ar[0], $start, $length, $rar[0]); + + return implode($ar[0]); +} diff --git a/include/utf8/trim.php b/include/utf8/trim.php new file mode 100644 index 0000000..3d22840 --- /dev/null +++ b/include/utf8/trim.php @@ -0,0 +1,74 @@ +<?php + +/** +* @version $Id: trim.php,v 1.1 2006/02/25 13:50:17 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +/** +* UTF-8 aware replacement for ltrim() +* Note: you only need to use this if you are supplying the charlist +* optional arg and it contains UTF-8 characters. Otherwise ltrim will +* work normally on a UTF-8 string +* @author Andreas Gohr <andi@splitbrain.org> +* @see http://www.php.net/ltrim +* @see http://dev.splitbrain.org/view/darcs/dokuwiki/inc/utf8.php +* @return string +* @package utf8 +* @subpackage strings +*/ +function utf8_ltrim( $str, $charlist=false) +{ + if($charlist === false) + return ltrim($str); + + // Quote charlist for use in a characterclass + $charlist = preg_replace('!([\\\\\\-\\]\\[/^])!', '\\\${1}', $charlist); + + return preg_replace('/^['.$charlist.']+/u', '', $str); +} + +/** +* UTF-8 aware replacement for rtrim() +* Note: you only need to use this if you are supplying the charlist +* optional arg and it contains UTF-8 characters. Otherwise rtrim will +* work normally on a UTF-8 string +* @author Andreas Gohr <andi@splitbrain.org> +* @see http://www.php.net/rtrim +* @see http://dev.splitbrain.org/view/darcs/dokuwiki/inc/utf8.php +* @return string +* @package utf8 +* @subpackage strings +*/ +function utf8_rtrim($str, $charlist=false) +{ + if($charlist === false) + return rtrim($str); + + // Quote charlist for use in a characterclass + $charlist = preg_replace('!([\\\\\\-\\]\\[/^])!', '\\\${1}', $charlist); + + return preg_replace('/['.$charlist.']+$/u', '', $str); +} + +//--------------------------------------------------------------- +/** +* UTF-8 aware replacement for trim() +* Note: you only need to use this if you are supplying the charlist +* optional arg and it contains UTF-8 characters. Otherwise trim will +* work normally on a UTF-8 string +* @author Andreas Gohr <andi@splitbrain.org> +* @see http://www.php.net/trim +* @see http://dev.splitbrain.org/view/darcs/dokuwiki/inc/utf8.php +* @return string +* @package utf8 +* @subpackage strings +*/ +function utf8_trim( $str, $charlist=false) +{ + if($charlist === false) + return trim($str); + + return utf8_ltrim(utf8_rtrim($str, $charlist), $charlist); +} diff --git a/include/utf8/ucfirst.php b/include/utf8/ucfirst.php new file mode 100644 index 0000000..efee55d --- /dev/null +++ b/include/utf8/ucfirst.php @@ -0,0 +1,35 @@ +<?php + +/** +* @version $Id: ucfirst.php,v 1.1 2006/02/25 13:50:17 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +/** +* UTF-8 aware alternative to ucfirst +* Make a string's first character uppercase +* Note: requires utf8_strtoupper +* @param string +* @return string with first character as upper case (if applicable) +* @see http://www.php.net/ucfirst +* @see utf8_strtoupper +* @package utf8 +* @subpackage strings +*/ +function utf8_ucfirst($str) +{ + switch (utf8_strlen($str)) + { + case 0: + return ''; + break; + case 1: + return utf8_strtoupper($str); + break; + default: + preg_match('/^(.{1})(.*)$/us', $str, $matches); + return utf8_strtoupper($matches[1]).$matches[2]; + break; + } +} diff --git a/include/utf8/ucwords.php b/include/utf8/ucwords.php new file mode 100644 index 0000000..e985cee --- /dev/null +++ b/include/utf8/ucwords.php @@ -0,0 +1,46 @@ +<?php + +/** +* @version $Id: ucwords.php,v 1.1 2006/02/25 13:50:17 harryf Exp $ +* @package utf8 +* @subpackage strings +*/ + +/** +* UTF-8 aware alternative to ucwords +* Uppercase the first character of each word in a string +* Note: requires utf8_substr_replace and utf8_strtoupper +* @param string +* @return string with first char of each word uppercase +* @see http://www.php.net/ucwords +* @package utf8 +* @subpackage strings +*/ +function utf8_ucwords($str) +{ + // Note: [\x0c\x09\x0b\x0a\x0d\x20] matches; + // Form feeds, horizontal tabs, vertical tabs, linefeeds and carriage returns + // This corresponds to the definition of a "word" defined at http://www.php.net/ucwords + $pattern = '/(^|([\x0c\x09\x0b\x0a\x0d\x20]+))([^\x0c\x09\x0b\x0a\x0d\x20]{1})[^\x0c\x09\x0b\x0a\x0d\x20]*/u'; + + return preg_replace_callback($pattern, 'utf8_ucwords_callback', $str); +} + +/** +* Callback function for preg_replace_callback call in utf8_ucwords +* You don't need to call this yourself +* @param array of matches corresponding to a single word +* @return string with first char of the word in uppercase +* @see utf8_ucwords +* @see utf8_strtoupper +* @package utf8 +* @subpackage strings +*/ +function utf8_ucwords_callback($matches) +{ + $leadingws = $matches[2]; + $ucfirst = utf8_strtoupper($matches[3]); + $ucword = utf8_substr_replace(ltrim($matches[0]), $ucfirst, 0, 1); + + return $leadingws.$ucword; +} diff --git a/include/utf8/utf8.php b/include/utf8/utf8.php new file mode 100644 index 0000000..661b2d7 --- /dev/null +++ b/include/utf8/utf8.php @@ -0,0 +1,72 @@ +<?php + +/** +* This is the dynamic loader for the library. It checks whether you have +* the mbstring extension available and includes relevant files +* on that basis, falling back to the native (as in written in PHP) version +* if mbstring is unavailabe. +* +* It's probably easiest to use this, if you don't want to understand +* the dependencies involved, in conjunction with PHP versions etc. At +* the same time, you might get better performance by managing loading +* yourself. The smartest way to do this, bearing in mind performance, +* is probably to "load on demand" - i.e. just before you use these +* functions in your code, load the version you need. +* +* It makes sure the the following functions are available; +* utf8_strlen, utf8_strpos, utf8_strrpos, utf8_substr, +* utf8_strtolower, utf8_strtoupper +* Other functions in the ./native directory depend on these +* six functions being available +* @package utf8 +*/ + +// Check whether PCRE has been compiled with UTF-8 support +$UTF8_ar = array(); +if (preg_match('/^.{1}$/u', "ñ", $UTF8_ar) != 1) + trigger_error('PCRE is not compiled with UTF-8 support', E_USER_ERROR); + +unset($UTF8_ar); + +// Put the current directory in this constant +if (!defined('UTF8')) + define('UTF8', dirname(__FILE__)); + +if (extension_loaded('mbstring') && !defined('UTF8_USE_MBSTRING') && !defined('UTF8_USE_NATIVE')) + define('UTF8_USE_MBSTRING', true); +else if (!defined('UTF8_USE_NATIVE')) + define('UTF8_USE_NATIVE', true); + +// utf8_strpos() and utf8_strrpos() need utf8_bad_strip() to strip invalid +// characters. Mbstring doesn't do this while the Native implementation does. +require UTF8.'/utils/bad.php'; + +if (defined('UTF8_USE_MBSTRING')) +{ + /** + * If string overloading is active, it will break many of the + * native implementations. mbstring.func_overload must be set + * to 0, 1 or 4 in php.ini (string overloading disabled). + * Also need to check we have the correct internal mbstring + * encoding + */ + if (ini_get('mbstring.func_overload') & MB_OVERLOAD_STRING) + trigger_error('String functions are overloaded by mbstring', E_USER_ERROR); + + mb_language('uni'); + mb_internal_encoding('UTF-8'); + + if (!defined('UTF8_CORE')) + require UTF8.'/mbstring/core.php'; +} +elseif (defined('UTF8_USE_NATIVE')) +{ + if (!defined('UTF8_CORE')) + { + require UTF8.'/utils/unicode.php'; + require UTF8.'/native/core.php'; + } +} + +// Load the native implementation of utf8_trim +require UTF8.'/trim.php'; diff --git a/include/utf8/utils/ascii.php b/include/utf8/utils/ascii.php new file mode 100644 index 0000000..af75b92 --- /dev/null +++ b/include/utf8/utils/ascii.php @@ -0,0 +1,221 @@ +<?php + +/** +* Tools to help with ASCII in UTF-8 +* @version $Id: ascii.php,v 1.5 2006/10/16 20:38:12 harryf Exp $ +* @package utf8 +* @subpackage ascii +*/ + +/** +* Tests whether a string contains only 7bit ASCII bytes. +* You might use this to conditionally check whether a string +* needs handling as UTF-8 or not, potentially offering performance +* benefits by using the native PHP equivalent if it's just ASCII e.g.; +* +* <code> +* if ( utf8_is_ascii($someString) ) { +* // It's just ASCII - use the native PHP version +* $someString = strtolower($someString); +* } else { +* $someString = utf8_strtolower($someString); +* } +* </code> +* +* @param string +* @return boolean TRUE if it's all ASCII +* @package utf8 +* @subpackage ascii +* @see utf8_is_ascii_ctrl +*/ +function utf8_is_ascii($str) +{ + // Search for any bytes which are outside the ASCII range... + return (preg_match('/(?:[^\x00-\x7F])/', $str) !== 1); +} + +/** +* Tests whether a string contains only 7bit ASCII bytes with device +* control codes omitted. The device control codes can be found on the +* second table here: http://www.w3schools.com/tags/ref_ascii.asp +* +* @param string +* @return boolean TRUE if it's all ASCII without device control codes +* @package utf8 +* @subpackage ascii +* @see utf8_is_ascii +*/ +function utf8_is_ascii_ctrl($str) +{ + // Search for any bytes which are outside the ASCII range, or are device control codes + if (strlen($str) > 0) + return (preg_match('/[^\x09\x0A\x0D\x20-\x7E]/', $str) !== 1); + + return false; +} + +/** +* Strip out all non-7bit ASCII bytes +* If you need to transmit a string to system which you know can only +* support 7bit ASCII, you could use this function. +* @param string +* @return string with non ASCII bytes removed +* @package utf8 +* @subpackage ascii +* @see utf8_strip_non_ascii_ctrl +*/ +function utf8_strip_non_ascii($str) +{ + ob_start(); + + while (preg_match('/^([\x00-\x7F]+)|([^\x00-\x7F]+)/S', $str, $matches)) + { + if (!isset($matches[2])) + echo $matches[0]; + + $str = substr($str, strlen($matches[0])); + } + + $result = ob_get_contents(); + ob_end_clean(); + + return $result; +} + +/** +* Strip out device control codes in the ASCII range +* which are not permitted in XML. Note that this leaves +* multi-byte characters untouched - it only removes device +* control codes +* @see http://hsivonen.iki.fi/producing-xml/#controlchar +* @param string +* @return string control codes removed +*/ +function utf8_strip_ascii_ctrl($str) +{ + ob_start(); + + while (preg_match('/^([^\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+)|([\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+)/S', $str, $matches)) + { + if (!isset($matches[2])) + echo $matches[0]; + + $str = substr($str, strlen($matches[0])); + } + + $result = ob_get_contents(); + ob_end_clean(); + + return $result; +} + +/** +* Strip out all non 7bit ASCII bytes and ASCII device control codes. +* For a list of ASCII device control codes see the 2nd table here: +* http://www.w3schools.com/tags/ref_ascii.asp +* +* @param string +* @return boolean TRUE if it's all ASCII +* @package utf8 +* @subpackage ascii +*/ +function utf8_strip_non_ascii_ctrl($str) +{ + ob_start(); + + while (preg_match( '/^([\x09\x0A\x0D\x20-\x7E]+)|([^\x09\x0A\x0D\x20-\x7E]+)/S', $str, $matches)) + { + if (!isset($matches[2])) + echo $matches[0]; + + $str = substr($str, strlen($matches[0])); + } + + $result = ob_get_contents(); + ob_end_clean(); + + return $result; +} + +/** +* Replace accented UTF-8 characters by unaccented ASCII-7 "equivalents". +* The purpose of this function is to replace characters commonly found in Latin +* alphabets with something more or less equivalent from the ASCII range. This can +* be useful for converting a UTF-8 to something ready for a filename, for example. +* Following the use of this function, you would probably also pass the string +* through utf8_strip_non_ascii to clean out any other non-ASCII chars +* Use the optional parameter to just deaccent lower ($case = -1) or upper ($case = 1) +* letters. Default is to deaccent both cases ($case = 0) +* +* For a more complete implementation of transliteration, see the utf8_to_ascii package +* available from the phputf8 project downloads: +* http://prdownloads.sourceforge.net/phputf8 +* +* @param string UTF-8 string +* @param int (optional) -1 lowercase only, +1 uppercase only, 1 both cases +* @param string UTF-8 with accented characters replaced by ASCII chars +* @return string accented chars replaced with ascii equivalents +* @author Andreas Gohr <andi@splitbrain.org> +* @package utf8 +* @subpackage ascii +*/ +function utf8_accents_to_ascii($str, $case=0) +{ + static $UTF8_LOWER_ACCENTS = null; + static $UTF8_UPPER_ACCENTS = null; + + if($case <= 0) + { + + if (is_null($UTF8_LOWER_ACCENTS)) + { + $UTF8_LOWER_ACCENTS = array( + 'à' => 'a', 'ô' => 'o', 'ď' => 'd', 'ḟ' => 'f', 'ë' => 'e', 'š' => 's', 'ơ' => 'o', + 'ß' => 'ss', 'ă' => 'a', 'ř' => 'r', 'ț' => 't', 'ň' => 'n', 'ā' => 'a', 'ķ' => 'k', + 'ŝ' => 's', 'ỳ' => 'y', 'ņ' => 'n', 'ĺ' => 'l', 'ħ' => 'h', 'ṗ' => 'p', 'ó' => 'o', + 'ú' => 'u', 'ě' => 'e', 'é' => 'e', 'ç' => 'c', 'ẁ' => 'w', 'ċ' => 'c', 'õ' => 'o', + 'ṡ' => 's', 'ø' => 'o', 'ģ' => 'g', 'ŧ' => 't', 'ș' => 's', 'ė' => 'e', 'ĉ' => 'c', + 'ś' => 's', 'î' => 'i', 'ű' => 'u', 'ć' => 'c', 'ę' => 'e', 'ŵ' => 'w', 'ṫ' => 't', + 'ū' => 'u', 'č' => 'c', 'ö' => 'oe', 'è' => 'e', 'ŷ' => 'y', 'ą' => 'a', 'ł' => 'l', + 'ų' => 'u', 'ů' => 'u', 'ş' => 's', 'ğ' => 'g', 'ļ' => 'l', 'ƒ' => 'f', 'ž' => 'z', + 'ẃ' => 'w', 'ḃ' => 'b', 'å' => 'a', 'ì' => 'i', 'ï' => 'i', 'ḋ' => 'd', 'ť' => 't', + 'ŗ' => 'r', 'ä' => 'ae', 'í' => 'i', 'ŕ' => 'r', 'ê' => 'e', 'ü' => 'ue', 'ò' => 'o', + 'ē' => 'e', 'ñ' => 'n', 'ń' => 'n', 'ĥ' => 'h', 'ĝ' => 'g', 'đ' => 'd', 'ĵ' => 'j', + 'ÿ' => 'y', 'ũ' => 'u', 'ŭ' => 'u', 'ư' => 'u', 'ţ' => 't', 'ý' => 'y', 'ő' => 'o', + 'â' => 'a', 'ľ' => 'l', 'ẅ' => 'w', 'ż' => 'z', 'ī' => 'i', 'ã' => 'a', 'ġ' => 'g', + 'ṁ' => 'm', 'ō' => 'o', 'ĩ' => 'i', 'ù' => 'u', 'į' => 'i', 'ź' => 'z', 'á' => 'a', + 'û' => 'u', 'þ' => 'th', 'ð' => 'dh', 'æ' => 'ae', 'µ' => 'u', 'ĕ' => 'e', + ); + } + + $str = str_replace(array_keys($UTF8_LOWER_ACCENTS), array_values($UTF8_LOWER_ACCENTS), $str); + } + + if($case >= 0) + { + if (is_null($UTF8_UPPER_ACCENTS)) + { + $UTF8_UPPER_ACCENTS = array( + 'À' => 'A', 'Ô' => 'O', 'Ď' => 'D', 'Ḟ' => 'F', 'Ë' => 'E', 'Š' => 'S', 'Ơ' => 'O', + 'Ă' => 'A', 'Ř' => 'R', 'Ț' => 'T', 'Ň' => 'N', 'Ā' => 'A', 'Ķ' => 'K', + 'Ŝ' => 'S', 'Ỳ' => 'Y', 'Ņ' => 'N', 'Ĺ' => 'L', 'Ħ' => 'H', 'Ṗ' => 'P', 'Ó' => 'O', + 'Ú' => 'U', 'Ě' => 'E', 'É' => 'E', 'Ç' => 'C', 'Ẁ' => 'W', 'Ċ' => 'C', 'Õ' => 'O', + 'Ṡ' => 'S', 'Ø' => 'O', 'Ģ' => 'G', 'Ŧ' => 'T', 'Ș' => 'S', 'Ė' => 'E', 'Ĉ' => 'C', + 'Ś' => 'S', 'Î' => 'I', 'Ű' => 'U', 'Ć' => 'C', 'Ę' => 'E', 'Ŵ' => 'W', 'Ṫ' => 'T', + 'Ū' => 'U', 'Č' => 'C', 'Ö' => 'Oe', 'È' => 'E', 'Ŷ' => 'Y', 'Ą' => 'A', 'Ł' => 'L', + 'Ų' => 'U', 'Ů' => 'U', 'Ş' => 'S', 'Ğ' => 'G', 'Ļ' => 'L', 'Ƒ' => 'F', 'Ž' => 'Z', + 'Ẃ' => 'W', 'Ḃ' => 'B', 'Å' => 'A', 'Ì' => 'I', 'Ï' => 'I', 'Ḋ' => 'D', 'Ť' => 'T', + 'Ŗ' => 'R', 'Ä' => 'Ae', 'Í' => 'I', 'Ŕ' => 'R', 'Ê' => 'E', 'Ü' => 'Ue', 'Ò' => 'O', + 'Ē' => 'E', 'Ñ' => 'N', 'Ń' => 'N', 'Ĥ' => 'H', 'Ĝ' => 'G', 'Đ' => 'D', 'Ĵ' => 'J', + 'Ÿ' => 'Y', 'Ũ' => 'U', 'Ŭ' => 'U', 'Ư' => 'U', 'Ţ' => 'T', 'Ý' => 'Y', 'Ő' => 'O', + 'Â' => 'A', 'Ľ' => 'L', 'Ẅ' => 'W', 'Ż' => 'Z', 'Ī' => 'I', 'Ã' => 'A', 'Ġ' => 'G', + 'Ṁ' => 'M', 'Ō' => 'O', 'Ĩ' => 'I', 'Ù' => 'U', 'Į' => 'I', 'Ź' => 'Z', 'Á' => 'A', + 'Û' => 'U', 'Þ' => 'Th', 'Ð' => 'Dh', 'Æ' => 'Ae', 'Ĕ' => 'E', + ); + } + + $str = str_replace(array_keys($UTF8_UPPER_ACCENTS), array_values($UTF8_UPPER_ACCENTS), $str); + } + + return $str; +} diff --git a/include/utf8/utils/bad.php b/include/utf8/utils/bad.php new file mode 100644 index 0000000..2704294 --- /dev/null +++ b/include/utf8/utils/bad.php @@ -0,0 +1,430 @@ +<?php + +/** +* @version $Id: bad.php,v 1.2 2006/02/26 13:20:44 harryf Exp $ +* Tools for locating / replacing bad bytes in UTF-8 strings +* The Original Code is Mozilla Communicator client code. +* The Initial Developer of the Original Code is +* Netscape Communications Corporation. +* Portions created by the Initial Developer are Copyright (C) 1998 +* the Initial Developer. All Rights Reserved. +* Ported to PHP by Henri Sivonen (http://hsivonen.iki.fi) +* Slight modifications to fit with phputf8 library by Harry Fuecks (hfuecks gmail com) +* @see http://lxr.mozilla.org/seamonkey/source/intl/uconv/src/nsUTF8ToUnicode.cpp +* @see http://lxr.mozilla.org/seamonkey/source/intl/uconv/src/nsUnicodeToUTF8.cpp +* @see http://hsivonen.iki.fi/php-utf8/ +* @package utf8 +* @subpackage bad +* @see utf8_is_valid +*/ + +/** +* Locates the first bad byte in a UTF-8 string returning it's +* byte index in the string +* PCRE Pattern to locate bad bytes in a UTF-8 string +* Comes from W3 FAQ: Multilingual Forms +* Note: modified to include full ASCII range including control chars +* @see http://www.w3.org/International/questions/qa-forms-utf-8 +* @param string +* @return mixed integer byte index or FALSE if no bad found +* @package utf8 +* @subpackage bad +*/ +function utf8_bad_find($str) +{ + $UTF8_BAD = + '([\x00-\x7F]'. # ASCII (including control chars) + '|[\xC2-\xDF][\x80-\xBF]'. # Non-overlong 2-byte + '|\xE0[\xA0-\xBF][\x80-\xBF]'. # Excluding overlongs + '|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}'. # Straight 3-byte + '|\xED[\x80-\x9F][\x80-\xBF]'. # Excluding surrogates + '|\xF0[\x90-\xBF][\x80-\xBF]{2}'. # Planes 1-3 + '|[\xF1-\xF3][\x80-\xBF]{3}'. # Planes 4-15 + '|\xF4[\x80-\x8F][\x80-\xBF]{2}'. # Plane 16 + '|(.{1}))'; # Invalid byte + $pos = 0; + $badList = array(); + + while (preg_match('/'.$UTF8_BAD.'/S', $str, $matches)) + { + $bytes = strlen($matches[0]); + + if (isset($matches[2])) + return $pos; + + $pos += $bytes; + $str = substr($str,$bytes); + } + + return false; +} + +/** +* Locates all bad bytes in a UTF-8 string and returns a list of their +* byte index in the string +* PCRE Pattern to locate bad bytes in a UTF-8 string +* Comes from W3 FAQ: Multilingual Forms +* Note: modified to include full ASCII range including control chars +* @see http://www.w3.org/International/questions/qa-forms-utf-8 +* @param string +* @return mixed array of integers or FALSE if no bad found +* @package utf8 +* @subpackage bad +*/ +function utf8_bad_findall($str) +{ + $UTF8_BAD = + '([\x00-\x7F]'. # ASCII (including control chars) + '|[\xC2-\xDF][\x80-\xBF]'. # Non-overlong 2-byte + '|\xE0[\xA0-\xBF][\x80-\xBF]'. # Excluding overlongs + '|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}'. # Straight 3-byte + '|\xED[\x80-\x9F][\x80-\xBF]'. # Excluding surrogates + '|\xF0[\x90-\xBF][\x80-\xBF]{2}'. # Planes 1-3 + '|[\xF1-\xF3][\x80-\xBF]{3}'. # Planes 4-15 + '|\xF4[\x80-\x8F][\x80-\xBF]{2}'. # Plane 16 + '|(.{1}))'; # Invalid byte + $pos = 0; + $badList = array(); + + while (preg_match('/'.$UTF8_BAD.'/S', $str, $matches)) + { + $bytes = strlen($matches[0]); + + if (isset($matches[2])) + $badList[] = $pos; + + $pos += $bytes; + $str = substr($str,$bytes); + } + + if (count($badList) > 0) + return $badList; + + return false; +} + +/** +* Strips out any bad bytes from a UTF-8 string and returns the rest +* PCRE Pattern to locate bad bytes in a UTF-8 string +* Comes from W3 FAQ: Multilingual Forms +* Note: modified to include full ASCII range including control chars +* @see http://www.w3.org/International/questions/qa-forms-utf-8 +* @param string +* @return string +* @package utf8 +* @subpackage bad +*/ +function utf8_bad_strip($original) +{ + return utf8_bad_replace($original, ''); +} + +/** +* Replace bad bytes with an alternative character - ASCII character +* recommended is replacement char +* PCRE Pattern to locate bad bytes in a UTF-8 string +* Comes from W3 FAQ: Multilingual Forms +* Note: modified to include full ASCII range including control chars +* @see http://www.w3.org/International/questions/qa-forms-utf-8 +* @param string to search +* @param string to replace bad bytes with (defaults to '?') - use ASCII +* @return string +* @package utf8 +* @subpackage bad +*/ +function utf8_bad_replace($original, $replace = '?') { + $result = ''; + + $strlen = strlen($original); + for ($i = 0; $i < $strlen;) { + $char = $original[$i++]; + $byte = ord($char); + + if ($byte < 0x80) $bytes = 0; // 1-bytes (00000000 - 01111111) + else if ($byte < 0xC0) { // 1-bytes (10000000 - 10111111) + $result .= $replace; + continue; + } + else if ($byte < 0xE0) $bytes = 1; // 2-bytes (11000000 - 11011111) + else if ($byte < 0xF0) $bytes = 2; // 3-bytes (11100000 - 11101111) + else if ($byte < 0xF8) $bytes = 3; // 4-bytes (11110000 - 11110111) + else if ($byte < 0xFC) $bytes = 4; // 5-bytes (11111000 - 11111011) + else if ($byte < 0xFE) $bytes = 5; // 6-bytes (11111100 - 11111101) + else { // Otherwise it's something invalid + $result .= $replace; + continue; + } + + // Check our input actually has enough data + if ($i + $bytes > $strlen) { + $result .= $replace; + continue; + } + + // If we've got this far then we have a multiple-byte character + for ($j = 0; $j < $bytes; $j++) { + $byte = $original[$i + $j]; + + $char .= $byte; + $byte = ord($byte); + + // Every following byte must be 10000000 - 10111111 + if ($byte < 0x80 || $byte > 0xBF) { + $result .= $replace; + continue 2; + } + } + + $i += $bytes; + $result .= $char; + } + + return $result; +} + +/** +* Return code from utf8_bad_identify() when a five octet sequence is detected. +* Note: 5 octets sequences are valid UTF-8 but are not supported by Unicode so +* do not represent a useful character +* @see utf8_bad_identify +* @package utf8 +* @subpackage bad +*/ +define('UTF8_BAD_5OCTET', 1); + +/** +* Return code from utf8_bad_identify() when a six octet sequence is detected. +* Note: 6 octets sequences are valid UTF-8 but are not supported by Unicode so +* do not represent a useful character +* @see utf8_bad_identify +* @package utf8 +* @subpackage bad +*/ +define('UTF8_BAD_6OCTET', 2); + +/** +* Return code from utf8_bad_identify(). +* Invalid octet for use as start of multi-byte UTF-8 sequence +* @see utf8_bad_identify +* @package utf8 +* @subpackage bad +*/ +define('UTF8_BAD_SEQID', 3); + +/** +* Return code from utf8_bad_identify(). +* From Unicode 3.1, non-shortest form is illegal +* @see utf8_bad_identify +* @package utf8 +* @subpackage bad +*/ +define('UTF8_BAD_NONSHORT', 4); + +/** +* Return code from utf8_bad_identify(). +* From Unicode 3.2, surrogate characters are illegal +* @see utf8_bad_identify +* @package utf8 +* @subpackage bad +*/ +define('UTF8_BAD_SURROGATE', 5); + +/** +* Return code from utf8_bad_identify(). +* Codepoints outside the Unicode range are illegal +* @see utf8_bad_identify +* @package utf8 +* @subpackage bad +*/ +define('UTF8_BAD_UNIOUTRANGE', 6); + +/** +* Return code from utf8_bad_identify(). +* Incomplete multi-octet sequence +* Note: this is kind of a "catch-all" +* @see utf8_bad_identify +* @package utf8 +* @subpackage bad +*/ +define('UTF8_BAD_SEQINCOMPLETE', 7); + +/** +* Reports on the type of bad byte found in a UTF-8 string. Returns a +* status code on the first bad byte found +* @author <hsivonen@iki.fi> +* @param string UTF-8 encoded string +* @return mixed integer constant describing problem or FALSE if valid UTF-8 +* @see utf8_bad_explain +* @see http://hsivonen.iki.fi/php-utf8/ +* @package utf8 +* @subpackage bad +*/ +function utf8_bad_identify($str, &$i) +{ + $mState = 0; // Cached expected number of octets after the current octet + // until the beginning of the next UTF8 character sequence + $mUcs4 = 0; // Cached Unicode character + $mBytes = 1; // Cached expected number of octets in the current sequence + + $len = strlen($str); + + for($i=0; $i < $len; $i++) + { + $in = ord($str{$i}); + + if ( $mState == 0) + { + // When mState is zero we expect either a US-ASCII character or a multi-octet sequence. + if (0 == (0x80 & ($in))) + { + // US-ASCII, pass straight through. + $mBytes = 1; + } + else if (0xC0 == (0xE0 & ($in))) + { + // First octet of 2 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x1F) << 6; + $mState = 1; + $mBytes = 2; + } + else if (0xE0 == (0xF0 & ($in))) + { + // First octet of 3 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x0F) << 12; + $mState = 2; + $mBytes = 3; + } + else if (0xF0 == (0xF8 & ($in))) + { + // First octet of 4 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x07) << 18; + $mState = 3; + $mBytes = 4; + } + else if (0xF8 == (0xFC & ($in))) + { + /* First octet of 5 octet sequence. + * + * This is illegal because the encoded codepoint must be either + * (a) not the shortest form or + * (b) outside the Unicode range of 0-0x10FFFF. + */ + return UTF8_BAD_5OCTET; + } + else if (0xFC == (0xFE & ($in))) + { + // First octet of 6 octet sequence, see comments for 5 octet sequence. + return UTF8_BAD_6OCTET; + } + else + { + // Current octet is neither in the US-ASCII range nor a legal first + // octet of a multi-octet sequence. + return UTF8_BAD_SEQID; + } + } + else + { + // When mState is non-zero, we expect a continuation of the multi-octet sequence + if (0x80 == (0xC0 & ($in))) + { + // Legal continuation. + $shift = ($mState - 1) * 6; + $tmp = $in; + $tmp = ($tmp & 0x0000003F) << $shift; + $mUcs4 |= $tmp; + + /** + * End of the multi-octet sequence. mUcs4 now contains the final + * Unicode codepoint to be output + */ + if (0 == --$mState) + { + // From Unicode 3.1, non-shortest form is illegal + if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || + ((3 == $mBytes) && ($mUcs4 < 0x0800)) || + ((4 == $mBytes) && ($mUcs4 < 0x10000)) ) + return UTF8_BAD_NONSHORT; + else if (($mUcs4 & 0xFFFFF800) == 0xD800) // From Unicode 3.2, surrogate characters are illegal + return UTF8_BAD_SURROGATE; + else if ($mUcs4 > 0x10FFFF) // Codepoints outside the Unicode range are illegal + return UTF8_BAD_UNIOUTRANGE; + + // Initialize UTF8 cache + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + } + + } + else + { + // ((0xC0 & (*in) != 0x80) && (mState != 0)) + // Incomplete multi-octet sequence. + $i--; + return UTF8_BAD_SEQINCOMPLETE; + } + } + } + + // Incomplete multi-octet sequence + if ($mState != 0) + { + $i--; + return UTF8_BAD_SEQINCOMPLETE; + } + + // No bad octets found + $i = null; + return false; +} + +/** +* Takes a return code from utf8_bad_identify() are returns a message +* (in English) explaining what the problem is. +* @param int return code from utf8_bad_identify +* @return mixed string message or FALSE if return code unknown +* @see utf8_bad_identify +* @package utf8 +* @subpackage bad +*/ +function utf8_bad_explain($code) +{ + switch ($code) + { + case UTF8_BAD_5OCTET: + return 'Five octet sequences are valid UTF-8 but are not supported by Unicode'; + break; + + case UTF8_BAD_6OCTET: + return 'Six octet sequences are valid UTF-8 but are not supported by Unicode'; + break; + + case UTF8_BAD_SEQID: + return 'Invalid octet for use as start of multi-byte UTF-8 sequence'; + break; + + case UTF8_BAD_NONSHORT: + return 'From Unicode 3.1, non-shortest form is illegal'; + break; + + case UTF8_BAD_SURROGATE: + return 'From Unicode 3.2, surrogate characters are illegal'; + break; + + case UTF8_BAD_UNIOUTRANGE: + return 'Codepoints outside the Unicode range are illegal'; + break; + + case UTF8_BAD_SEQINCOMPLETE: + return 'Incomplete multi-octet sequence'; + break; + } + + trigger_error('Unknown error code: '.$code, E_USER_WARNING); + + return false; +} diff --git a/include/utf8/utils/index.html b/include/utf8/utils/index.html new file mode 100644 index 0000000..89337b2 --- /dev/null +++ b/include/utf8/utils/index.html @@ -0,0 +1 @@ +<html><head><title>.</title></head><body>.</body></html> diff --git a/include/utf8/utils/patterns.php b/include/utf8/utils/patterns.php new file mode 100644 index 0000000..5a85a4f --- /dev/null +++ b/include/utf8/utils/patterns.php @@ -0,0 +1,67 @@ +<?php + +/** +* PCRE Regular expressions for UTF-8. Note this file is not actually used by +* the rest of the library but these regular expressions can be useful to have +* available. +* @version $Id: patterns.php,v 1.1 2006/02/25 14:20:02 harryf Exp $ +* @see http://www.w3.org/International/questions/qa-forms-utf-8 +* @package utf8 +* @subpackage patterns +*/ + +/** +* PCRE Pattern to check a UTF-8 string is valid +* Comes from W3 FAQ: Multilingual Forms +* Note: modified to include full ASCII range including control chars +* @see http://www.w3.org/International/questions/qa-forms-utf-8 +* @package utf8 +* @subpackage patterns +*/ +$UTF8_VALID = '^('. + '[\x00-\x7F]'. # ASCII (including control chars) + '|[\xC2-\xDF][\x80-\xBF]'. # Non-overlong 2-byte + '|\xE0[\xA0-\xBF][\x80-\xBF]'. # Excluding overlongs + '|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}'. # Straight 3-byte + '|\xED[\x80-\x9F][\x80-\xBF]'. # Excluding surrogates + '|\xF0[\x90-\xBF][\x80-\xBF]{2}'. # Planes 1-3 + '|[\xF1-\xF3][\x80-\xBF]{3}'. # Planes 4-15 + '|\xF4[\x80-\x8F][\x80-\xBF]{2}'. # Plane 16 + ')*$'; + +/** +* PCRE Pattern to match single UTF-8 characters +* Comes from W3 FAQ: Multilingual Forms +* Note: modified to include full ASCII range including control chars +* @see http://www.w3.org/International/questions/qa-forms-utf-8 +* @package utf8 +* @subpackage patterns +*/ +$UTF8_MATCH = + '([\x00-\x7F])'. # ASCII (including control chars) + '|([\xC2-\xDF][\x80-\xBF])'. # Non-overlong 2-byte + '|(\xE0[\xA0-\xBF][\x80-\xBF])'. # Excluding overlongs + '|([\xE1-\xEC\xEE\xEF][\x80-\xBF]{2})'. # Straight 3-byte + '|(\xED[\x80-\x9F][\x80-\xBF])'. # Excluding surrogates + '|(\xF0[\x90-\xBF][\x80-\xBF]{2})'. # Planes 1-3 + '|([\xF1-\xF3][\x80-\xBF]{3})'. # Planes 4-15 + '|(\xF4[\x80-\x8F][\x80-\xBF]{2})'; # Plane 16 + +/** +* PCRE Pattern to locate bad bytes in a UTF-8 string +* Comes from W3 FAQ: Multilingual Forms +* Note: modified to include full ASCII range including control chars +* @see http://www.w3.org/International/questions/qa-forms-utf-8 +* @package utf8 +* @subpackage patterns +*/ +$UTF8_BAD = + '([\x00-\x7F]'. # ASCII (including control chars) + '|[\xC2-\xDF][\x80-\xBF]'. # Non-overlong 2-byte + '|\xE0[\xA0-\xBF][\x80-\xBF]'. # Excluding overlongs + '|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}'. # Straight 3-byte + '|\xED[\x80-\x9F][\x80-\xBF]'. # Excluding surrogates + '|\xF0[\x90-\xBF][\x80-\xBF]{2}'. # Planes 1-3 + '|[\xF1-\xF3][\x80-\xBF]{3}'. # Planes 4-15 + '|\xF4[\x80-\x8F][\x80-\xBF]{2}'. # Plane 16 + '|(.{1}))'; # Invalid byte diff --git a/include/utf8/utils/position.php b/include/utf8/utils/position.php new file mode 100644 index 0000000..7c62d10 --- /dev/null +++ b/include/utf8/utils/position.php @@ -0,0 +1,171 @@ +<?php + +/** +* Locate a byte index given a UTF-8 character index +* @version $Id: position.php,v 1.1 2006/10/01 00:01:31 harryf Exp $ +* @package utf8 +* @subpackage position +*/ + +/** +* Given a string and a character index in the string, in +* terms of the UTF-8 character position, returns the byte +* index of that character. Can be useful when you want to +* PHP's native string functions but we warned, locating +* the byte can be expensive +* Takes variable number of parameters - first must be +* the search string then 1 to n UTF-8 character positions +* to obtain byte indexes for - it is more efficient to search +* the string for multiple characters at once, than make +* repeated calls to this function +* +* @author Chris Smith<chris@jalakai.co.uk> +* @param string string to locate index in +* @param int (n times) +* @return mixed - int if only one input int, array if more +* @return boolean TRUE if it's all ASCII +* @package utf8 +* @subpackage position +*/ +function utf8_byte_position() +{ + $args = func_get_args(); + $str =& array_shift($args); + + if (!is_string($str)) + return false; + + $result = array(); + $prev = array(0, 0); // Trivial byte index, character offset pair + $i = utf8_locate_next_chr($str, 300); // Use a short piece of str to estimate bytes per character. $i (& $j) -> byte indexes into $str + $c = strlen(utf8_decode(substr($str, 0, $i))); // $c -> character offset into $str + + // Deal with arguments from lowest to highest + sort($args); + + foreach ($args as $offset) + { + // Sanity checks FIXME + + // 0 is an easy check + if ($offset == 0) + { + $result[] = 0; continue; + } + + // Ensure no endless looping + $safety_valve = 50; + + do + { + if (($c - $prev[1]) == 0) + { + // Hack: gone past end of string + $error = 0; + $i = strlen($str); + break; + } + + $j = $i + (int)(($offset-$c) * ($i - $prev[0]) / ($c - $prev[1])); + $j = utf8_locate_next_chr($str, $j); // Correct to utf8 character boundary + $prev = array($i,$c); // Save the index, offset for use next iteration + + if ($j > $i) + $c += strlen(utf8_decode(substr($str, $i, $j-$i))); // Determine new character offset + else + $c -= strlen(utf8_decode(substr($str, $j, $i-$j))); // Ditto + + $error = abs($c-$offset); + $i = $j; // Ready for next time around + } + while (($error > 7) && --$safety_valve); // From 7 it is faster to iterate over the string + + if ($error && $error <= 7) + { + if ($c < $offset) + { + // Move up + while ($error--) + $i = utf8_locate_next_chr($str, ++$i); + } + else + { + // Move down + while ($error--) + $i = utf8_locate_current_chr($str, --$i); + } + + // Ready for next arg + $c = $offset; + } + + $result[] = $i; + } + + if (count($result) == 1) + return $result[0]; + + return $result; +} + +/** +* Given a string and any byte index, returns the byte index +* of the start of the current UTF-8 character, relative to supplied +* position. If the current character begins at the same place as the +* supplied byte index, that byte index will be returned. Otherwise +* this function will step backwards, looking for the index where +* curent UTF-8 character begins +* @author Chris Smith<chris@jalakai.co.uk> +* @param string +* @param int byte index in the string +* @return int byte index of start of next UTF-8 character +* @package utf8 +* @subpackage position +*/ +function utf8_locate_current_chr( &$str, $idx ) +{ + if ($idx <= 0) + return 0; + + $limit = strlen($str); + if ($idx >= $limit) + return $limit; + + // Binary value for any byte after the first in a multi-byte UTF-8 character + // will be like 10xxxxxx so & 0xC0 can be used to detect this kind + // of byte - assuming well formed UTF-8 + while ($idx && ((ord($str[$idx]) & 0xC0) == 0x80)) + $idx--; + + return $idx; +} + +/** +* Given a string and any byte index, returns the byte index +* of the start of the next UTF-8 character, relative to supplied +* position. If the next character begins at the same place as the +* supplied byte index, that byte index will be returned. +* @author Chris Smith<chris@jalakai.co.uk> +* @param string +* @param int byte index in the string +* @return int byte index of start of next UTF-8 character +* @package utf8 +* @subpackage position +*/ +function utf8_locate_next_chr(&$str, $idx) +{ + if ($idx <= 0) + return 0; + + $limit = strlen($str); + if ($idx >= $limit) + return $limit; + + // Binary value for any byte after the first in a multi-byte UTF-8 character + // will be like 10xxxxxx so & 0xC0 can be used to detect this kind + // of byte - assuming well formed UTF-8 + while (($idx < $limit) && ((ord($str[$idx]) & 0xC0) == 0x80)) + $idx++; + + return $idx; +} diff --git a/include/utf8/utils/specials.php b/include/utf8/utils/specials.php new file mode 100644 index 0000000..69219dc --- /dev/null +++ b/include/utf8/utils/specials.php @@ -0,0 +1,131 @@ +<?php + +/** +* Utilities for processing "special" characters in UTF-8. "Special" largely means anything which would +* be regarded as a non-word character, like ASCII control characters and punctuation. This has a "Roman" +* bias - it would be unaware of modern Chinese "punctuation" characters for example. +* Note: requires utils/unicode.php to be loaded +* @version $Id: specials.php,v 1.2 2006/10/16 21:13:59 harryf Exp $ +* @package utf8 +* @subpackage utils +* @see utf8_is_valid +*/ + +/** +* Used internally. Builds a PCRE pattern from the $UTF8_SPECIAL_CHARS +* array defined in this file +* The $UTF8_SPECIAL_CHARS should contain all special characters (non-letter/non-digit) +* defined in the various local charsets - it's not a complete list of +* non-alphanum characters in UTF-8. It's not perfect but should match most +* cases of special chars. +* This function adds the control chars 0x00 to 0x19 to the array of +* special chars (they are not included in $UTF8_SPECIAL_CHARS) +* @package utf8 +* @subpackage utils +* @return string +* @see utf8_from_unicode +* @see utf8_is_word_chars +* @see utf8_strip_specials +*/ +function utf8_specials_pattern() +{ + static $pattern = null; + + if (!$pattern) + { + $UTF8_SPECIAL_CHARS = array( + 0x001a, 0x001b, 0x001c, 0x001d, 0x001e, 0x001f, 0x0020, 0x0021, 0x0022, 0x0023, + 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, + 0x002f, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f, 0x0040, 0x005b, + 0x005c, 0x005d, 0x005e, 0x0060, 0x007b, 0x007c, 0x007d, 0x007e, + 0x007f, 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, + 0x0089, 0x008a, 0x008b, 0x008c, 0x008d, 0x008e, 0x008f, 0x0090, 0x0091, 0x0092, + 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009a, 0x009b, 0x009c, + 0x009d, 0x009e, 0x009f, 0x00a0, 0x00a1, 0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, + 0x00a7, 0x00a8, 0x00a9, 0x00aa, 0x00ab, 0x00ac, 0x00ad, 0x00ae, 0x00af, 0x00b0, + 0x00b1, 0x00b2, 0x00b3, 0x00b4, 0x00b5, 0x00b6, 0x00b7, 0x00b8, 0x00b9, 0x00ba, + 0x00bb, 0x00bc, 0x00bd, 0x00be, 0x00bf, 0x00d7, 0x00f7, 0x02c7, 0x02d8, 0x02d9, + 0x02da, 0x02db, 0x02dc, 0x02dd, 0x0300, 0x0301, 0x0303, 0x0309, 0x0323, 0x0384, + 0x0385, 0x0387, 0x03b2, 0x03c6, 0x03d1, 0x03d2, 0x03d5, 0x03d6, 0x05b0, 0x05b1, + 0x05b2, 0x05b3, 0x05b4, 0x05b5, 0x05b6, 0x05b7, 0x05b8, 0x05b9, 0x05bb, 0x05bc, + 0x05bd, 0x05be, 0x05bf, 0x05c0, 0x05c1, 0x05c2, 0x05c3, 0x05f3, 0x05f4, 0x060c, + 0x061b, 0x061f, 0x0640, 0x064b, 0x064c, 0x064d, 0x064e, 0x064f, 0x0650, 0x0651, + 0x0652, 0x066a, 0x0e3f, 0x200c, 0x200d, 0x200e, 0x200f, 0x2013, 0x2014, 0x2015, + 0x2017, 0x2018, 0x2019, 0x201a, 0x201c, 0x201d, 0x201e, 0x2020, 0x2021, 0x2022, + 0x2026, 0x2030, 0x2032, 0x2033, 0x2039, 0x203a, 0x2044, 0x20a7, 0x20aa, 0x20ab, + 0x20ac, 0x2116, 0x2118, 0x2122, 0x2126, 0x2135, 0x2190, 0x2191, 0x2192, 0x2193, + 0x2194, 0x2195, 0x21b5, 0x21d0, 0x21d1, 0x21d2, 0x21d3, 0x21d4, 0x2200, 0x2202, + 0x2203, 0x2205, 0x2206, 0x2207, 0x2208, 0x2209, 0x220b, 0x220f, 0x2211, 0x2212, + 0x2215, 0x2217, 0x2219, 0x221a, 0x221d, 0x221e, 0x2220, 0x2227, 0x2228, 0x2229, + 0x222a, 0x222b, 0x2234, 0x223c, 0x2245, 0x2248, 0x2260, 0x2261, 0x2264, 0x2265, + 0x2282, 0x2283, 0x2284, 0x2286, 0x2287, 0x2295, 0x2297, 0x22a5, 0x22c5, 0x2310, + 0x2320, 0x2321, 0x2329, 0x232a, 0x2469, 0x2500, 0x2502, 0x250c, 0x2510, 0x2514, + 0x2518, 0x251c, 0x2524, 0x252c, 0x2534, 0x253c, 0x2550, 0x2551, 0x2552, 0x2553, + 0x2554, 0x2555, 0x2556, 0x2557, 0x2558, 0x2559, 0x255a, 0x255b, 0x255c, 0x255d, + 0x255e, 0x255f, 0x2560, 0x2561, 0x2562, 0x2563, 0x2564, 0x2565, 0x2566, 0x2567, + 0x2568, 0x2569, 0x256a, 0x256b, 0x256c, 0x2580, 0x2584, 0x2588, 0x258c, 0x2590, + 0x2591, 0x2592, 0x2593, 0x25a0, 0x25b2, 0x25bc, 0x25c6, 0x25ca, 0x25cf, 0x25d7, + 0x2605, 0x260e, 0x261b, 0x261e, 0x2660, 0x2663, 0x2665, 0x2666, 0x2701, 0x2702, + 0x2703, 0x2704, 0x2706, 0x2707, 0x2708, 0x2709, 0x270c, 0x270d, 0x270e, 0x270f, + 0x2710, 0x2711, 0x2712, 0x2713, 0x2714, 0x2715, 0x2716, 0x2717, 0x2718, 0x2719, + 0x271a, 0x271b, 0x271c, 0x271d, 0x271e, 0x271f, 0x2720, 0x2721, 0x2722, 0x2723, + 0x2724, 0x2725, 0x2726, 0x2727, 0x2729, 0x272a, 0x272b, 0x272c, 0x272d, 0x272e, + 0x272f, 0x2730, 0x2731, 0x2732, 0x2733, 0x2734, 0x2735, 0x2736, 0x2737, 0x2738, + 0x2739, 0x273a, 0x273b, 0x273c, 0x273d, 0x273e, 0x273f, 0x2740, 0x2741, 0x2742, + 0x2743, 0x2744, 0x2745, 0x2746, 0x2747, 0x2748, 0x2749, 0x274a, 0x274b, 0x274d, + 0x274f, 0x2750, 0x2751, 0x2752, 0x2756, 0x2758, 0x2759, 0x275a, 0x275b, 0x275c, + 0x275d, 0x275e, 0x2761, 0x2762, 0x2763, 0x2764, 0x2765, 0x2766, 0x2767, 0x277f, + 0x2789, 0x2793, 0x2794, 0x2798, 0x2799, 0x279a, 0x279b, 0x279c, 0x279d, 0x279e, + 0x279f, 0x27a0, 0x27a1, 0x27a2, 0x27a3, 0x27a4, 0x27a5, 0x27a6, 0x27a7, 0x27a8, + 0x27a9, 0x27aa, 0x27ab, 0x27ac, 0x27ad, 0x27ae, 0x27af, 0x27b1, 0x27b2, 0x27b3, + 0x27b4, 0x27b5, 0x27b6, 0x27b7, 0x27b8, 0x27b9, 0x27ba, 0x27bb, 0x27bc, 0x27bd, + 0x27be, 0xf6d9, 0xf6da, 0xf6db, 0xf8d7, 0xf8d8, 0xf8d9, 0xf8da, 0xf8db, 0xf8dc, + 0xf8dd, 0xf8de, 0xf8df, 0xf8e0, 0xf8e1, 0xf8e2, 0xf8e3, 0xf8e4, 0xf8e5, 0xf8e6, + 0xf8e7, 0xf8e8, 0xf8e9, 0xf8ea, 0xf8eb, 0xf8ec, 0xf8ed, 0xf8ee, 0xf8ef, 0xf8f0, + 0xf8f1, 0xf8f2, 0xf8f3, 0xf8f4, 0xf8f5, 0xf8f6, 0xf8f7, 0xf8f8, 0xf8f9, 0xf8fa, + 0xf8fb, 0xf8fc, 0xf8fd, 0xf8fe, 0xfe7c, 0xfe7d); + + $pattern = preg_quote(utf8_from_unicode($UTF8_SPECIAL_CHARS), '/'); + $pattern = '/[\x00-\x19'.$pattern.']/u'; + } + + return $pattern; +} + +/** +* Checks a string for whether it contains only word characters. This +* is logically equivalent to the \w PCRE meta character. Note that +* this is not a 100% guarantee that the string only contains alpha / +* numeric characters but just that common non-alphanumeric are not +* in the string, including ASCII device control characters. +* @package utf8 +* @subpackage utils +* @param string to check +* @return boolean TRUE if the string only contains word characters +* @see utf8_specials_pattern +*/ +function utf8_is_word_chars($str) +{ + return !(bool) preg_match(utf8_specials_pattern(), $str); +} + +/** +* Removes special characters (nonalphanumeric) from a UTF-8 string +* +* This can be useful as a helper for sanitizing a string for use as +* something like a file name or a unique identifier. Be warned though +* it does not handle all possible non-alphanumeric characters and is +* not intended is some kind of security / injection filter. +* +* @package utf8 +* @subpackage utils +* @author Andreas Gohr <andi@splitbrain.org> +* @param string $string The UTF8 string to strip of special chars +* @param string (optional) $repl Replace special with this string +* @return string with common non-alphanumeric characters removed +* @see utf8_specials_pattern +*/ +function utf8_strip_specials($string, $repl='') +{ + return preg_replace(utf8_specials_pattern(), $repl, $string); +} diff --git a/include/utf8/utils/unicode.php b/include/utf8/utils/unicode.php new file mode 100644 index 0000000..f0e86cb --- /dev/null +++ b/include/utf8/utils/unicode.php @@ -0,0 +1,241 @@ +<?php + +/** +* @version $Id: unicode.php,v 1.2 2006/02/26 13:20:44 harryf Exp $ +* Tools for conversion between UTF-8 and unicode +* The Original Code is Mozilla Communicator client code. +* The Initial Developer of the Original Code is +* Netscape Communications Corporation. +* Portions created by the Initial Developer are Copyright (C) 1998 +* the Initial Developer. All Rights Reserved. +* Ported to PHP by Henri Sivonen (http://hsivonen.iki.fi) +* Slight modifications to fit with phputf8 library by Harry Fuecks (hfuecks gmail com) +* @see http://lxr.mozilla.org/seamonkey/source/intl/uconv/src/nsUTF8ToUnicode.cpp +* @see http://lxr.mozilla.org/seamonkey/source/intl/uconv/src/nsUnicodeToUTF8.cpp +* @see http://hsivonen.iki.fi/php-utf8/ +* @package utf8 +* @subpackage unicode +*/ + +/** +* Takes an UTF-8 string and returns an array of ints representing the +* Unicode characters. Astral planes are supported ie. the ints in the +* output can be > 0xFFFF. Occurrances of the BOM are ignored. Surrogates +* are not allowed. +* Returns false if the input string isn't a valid UTF-8 octet sequence +* and raises a PHP error at level E_USER_WARNING +* Note: this function has been modified slightly in this library to +* trigger errors on encountering bad bytes +* @author <hsivonen@iki.fi> +* @param string UTF-8 encoded string +* @return mixed array of unicode code points or FALSE if UTF-8 invalid +* @see utf8_from_unicode +* @see http://hsivonen.iki.fi/php-utf8/ +* @package utf8 +* @subpackage unicode +*/ +function utf8_to_unicode($str) +{ + $mState = 0; // Cached expected number of octets after the current octet + // until the beginning of the next UTF8 character sequence + $mUcs4 = 0; // Cached Unicode character + $mBytes = 1; // Cached expected number of octets in the current sequence + + $out = array(); + $len = strlen($str); + + for($i = 0; $i < $len; $i++) + { + $in = ord($str[$i]); + + if ($mState == 0) + { + // When mState is zero we expect either a US-ASCII character or a multi-octet sequence. + if (0 == (0x80 & ($in))) + { + // US-ASCII, pass straight through. + $out[] = $in; + $mBytes = 1; + } + else if (0xC0 == (0xE0 & ($in))) + { + // First octet of 2 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x1F) << 6; + $mState = 1; + $mBytes = 2; + } + else if (0xE0 == (0xF0 & ($in))) + { + // First octet of 3 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x0F) << 12; + $mState = 2; + $mBytes = 3; + } + else if (0xF0 == (0xF8 & ($in))) + { + // First octet of 4 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x07) << 18; + $mState = 3; + $mBytes = 4; + } + else if (0xF8 == (0xFC & ($in))) + { + /* First octet of 5 octet sequence. + * + * This is illegal because the encoded codepoint must be either + * (a) not the shortest form or + * (b) outside the Unicode range of 0-0x10FFFF. + * Rather than trying to resynchronize, we will carry on until the end + * of the sequence and let the later error handling code catch it. + */ + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x03) << 24; + $mState = 4; + $mBytes = 5; + } + else if (0xFC == (0xFE & ($in))) + { + // First octet of 6 octet sequence, see comments for 5 octet sequence. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 1) << 30; + $mState = 5; + $mBytes = 6; + } + else + { + // Current octet is neither in the US-ASCII range nor a legal first octet of a multi-octet sequence + trigger_error('utf8_to_unicode: Illegal sequence identifier in UTF-8 at byte '.$i, E_USER_WARNING); + return false; + } + } + else + { + // When mState is non-zero, we expect a continuation of the multi-octet sequence + if (0x80 == (0xC0 & ($in))) + { + // Legal continuation. + $shift = ($mState - 1) * 6; + $tmp = $in; + $tmp = ($tmp & 0x0000003F) << $shift; + $mUcs4 |= $tmp; + + /** + * End of the multi-octet sequence. mUcs4 now contains the final + * Unicode codepoint to be output + */ + if (0 == --$mState) + { + /* + * Check for illegal sequences and codepoints. + */ + // From Unicode 3.1, non-shortest form is illegal + if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || ((3 == $mBytes) && ($mUcs4 < 0x0800)) || + ((4 == $mBytes) && ($mUcs4 < 0x10000)) || (4 < $mBytes) || + // From Unicode 3.2, surrogate characters are illegal + (($mUcs4 & 0xFFFFF800) == 0xD800) || + // Codepoints outside the Unicode range are illegal + ($mUcs4 > 0x10FFFF)) + { + trigger_error('utf8_to_unicode: Illegal sequence or codepoint in UTF-8 at byte '.$i, E_USER_WARNING); + return false; + } + + // BOM is legal but we don't want to output it + if (0xFEFF != $mUcs4) + $out[] = $mUcs4; + + // Initialize UTF8 cache + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + } + + } + else + { + /* ((0xC0 & (*in) != 0x80) && (mState != 0)) + Incomplete multi-octet sequence. */ + trigger_error('utf8_to_unicode: Incomplete multi-octet sequence in UTF-8 at byte '.$i, E_USER_WARNING); + return false; + } + } + } + + return $out; +} + +/** +* Takes an array of ints representing the Unicode characters and returns +* a UTF-8 string. Astral planes are supported ie. the ints in the +* input can be > 0xFFFF. Occurrances of the BOM are ignored. Surrogates +* are not allowed. +* Returns false if the input array contains ints that represent +* surrogates or are outside the Unicode range +* and raises a PHP error at level E_USER_WARNING +* Note: this function has been modified slightly in this library to use +* output buffering to concatenate the UTF-8 string (faster) as well as +* reference the array by it's keys +* @param array of unicode code points representing a string +* @return mixed UTF-8 string or FALSE if array contains invalid code points +* @author <hsivonen@iki.fi> +* @see utf8_to_unicode +* @see http://hsivonen.iki.fi/php-utf8/ +* @package utf8 +* @subpackage unicode +*/ +function utf8_from_unicode($arr) +{ + ob_start(); + + foreach (array_keys($arr) as $k) + { + if ( ($arr[$k] >= 0) && ($arr[$k] <= 0x007f) ) // ASCII range (including control chars) + { + echo chr($arr[$k]); + } + else if ($arr[$k] <= 0x07ff) //2 byte sequence + { + echo chr(0xc0 | ($arr[$k] >> 6)); + echo chr(0x80 | ($arr[$k] & 0x003f)); + } + else if($arr[$k] == 0xFEFF) // Byte order mark (skip) + { + // Nop -- zap the BOM + } + else if ($arr[$k] >= 0xD800 && $arr[$k] <= 0xDFFF) // Test for illegal surrogates + { + // Found a surrogate + trigger_error('utf8_from_unicode: Illegal surrogate at index: '.$k.', value: '.$arr[$k], E_USER_WARNING); + + return false; + } + else if ($arr[$k] <= 0xffff) // 3 byte sequence + { + echo chr(0xe0 | ($arr[$k] >> 12)); + echo chr(0x80 | (($arr[$k] >> 6) & 0x003f)); + echo chr(0x80 | ($arr[$k] & 0x003f)); + } + else if ($arr[$k] <= 0x10ffff) // 4 byte sequence + { + echo chr(0xf0 | ($arr[$k] >> 18)); + echo chr(0x80 | (($arr[$k] >> 12) & 0x3f)); + echo chr(0x80 | (($arr[$k] >> 6) & 0x3f)); + echo chr(0x80 | ($arr[$k] & 0x3f)); + } + else + { + trigger_error('utf8_from_unicode: Codepoint out of Unicode range at index: '.$k.', value: '.$arr[$k], E_USER_WARNING); + + // Out of range + return false; + } + } + + $result = ob_get_contents(); + ob_end_clean(); + + return $result; +} diff --git a/include/utf8/utils/validation.php b/include/utf8/utils/validation.php new file mode 100644 index 0000000..90dce8e --- /dev/null +++ b/include/utf8/utils/validation.php @@ -0,0 +1,186 @@ +<?php + +/** +* @version $Id: validation.php,v 1.2 2006/02/26 13:20:44 harryf Exp $ +* Tools for validing a UTF-8 string is well formed. +* The Original Code is Mozilla Communicator client code. +* The Initial Developer of the Original Code is +* Netscape Communications Corporation. +* Portions created by the Initial Developer are Copyright (C) 1998 +* the Initial Developer. All Rights Reserved. +* Ported to PHP by Henri Sivonen (http://hsivonen.iki.fi) +* Slight modifications to fit with phputf8 library by Harry Fuecks (hfuecks gmail com) +* @see http://lxr.mozilla.org/seamonkey/source/intl/uconv/src/nsUTF8ToUnicode.cpp +* @see http://lxr.mozilla.org/seamonkey/source/intl/uconv/src/nsUnicodeToUTF8.cpp +* @see http://hsivonen.iki.fi/php-utf8/ +* @package utf8 +* @subpackage validation +*/ + +/** +* Tests a string as to whether it's valid UTF-8 and supported by the +* Unicode standard +* Note: this function has been modified to simple return true or false +* @author <hsivonen@iki.fi> +* @param string UTF-8 encoded string +* @return boolean true if valid +* @see http://hsivonen.iki.fi/php-utf8/ +* @see utf8_compliant +* @package utf8 +* @subpackage validation +*/ +function utf8_is_valid($str) +{ + $mState = 0; // Cached expected number of octets after the current octet + // until the beginning of the next UTF8 character sequence + $mUcs4 = 0; // Cached Unicode character + $mBytes = 1; // Cached expected number of octets in the current sequence + + $len = strlen($str); + + for($i = 0; $i < $len; $i++) + { + $in = ord($str{$i}); + + if ( $mState == 0) + { + // When mState is zero we expect either a US-ASCII character or a multi-octet sequence. + if (0 == (0x80 & ($in))) + { + $mBytes = 1; // US-ASCII, pass straight through + } + else if (0xC0 == (0xE0 & ($in))) + { + // First octet of 2 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x1F) << 6; + $mState = 1; + $mBytes = 2; + } + else if (0xE0 == (0xF0 & ($in))) + { + // First octet of 3 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x0F) << 12; + $mState = 2; + $mBytes = 3; + } + else if (0xF0 == (0xF8 & ($in))) + { + // First octet of 4 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x07) << 18; + $mState = 3; + $mBytes = 4; + } + else if (0xF8 == (0xFC & ($in))) + { + /* First octet of 5 octet sequence. + * + * This is illegal because the encoded codepoint must be either + * (a) not the shortest form or + * (b) outside the Unicode range of 0-0x10FFFF. + * Rather than trying to resynchronize, we will carry on until the end + * of the sequence and let the later error handling code catch it. + */ + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x03) << 24; + $mState = 4; + $mBytes = 5; + } + else if (0xFC == (0xFE & ($in))) + { + // First octet of 6 octet sequence, see comments for 5 octet sequence. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 1) << 30; + $mState = 5; + $mBytes = 6; + } + else + { + // Current octet is neither in the US-ASCII range nor a legal first octet of a multi-octet sequence. + return false; + } + } + else + { + // When mState is non-zero, we expect a continuation of the multi-octet sequence + if (0x80 == (0xC0 & ($in))) + { + // Legal continuation. + $shift = ($mState - 1) * 6; + $tmp = $in; + $tmp = ($tmp & 0x0000003F) << $shift; + $mUcs4 |= $tmp; + + /** + * End of the multi-octet sequence. mUcs4 now contains the final + * Unicode codepoint to be output + */ + if (0 == --$mState) + { + /* + * Check for illegal sequences and codepoints. + */ + // From Unicode 3.1, non-shortest form is illegal + if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || ((3 == $mBytes) && ($mUcs4 < 0x0800)) || + ((4 == $mBytes) && ($mUcs4 < 0x10000)) || (4 < $mBytes) || + // From Unicode 3.2, surrogate characters are illegal + (($mUcs4 & 0xFFFFF800) == 0xD800) || + // Codepoints outside the Unicode range are illegal + ($mUcs4 > 0x10FFFF)) + { + return FALSE; + } + + // Initialize UTF8 cache + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + } + } + else + { + /** + *((0xC0 & (*in) != 0x80) && (mState != 0)) + * Incomplete multi-octet sequence. + */ + + return false; + } + } + } + + return true; +} + +/** +* Tests whether a string complies as UTF-8. This will be much +* faster than utf8_is_valid, but will pass five and six octet +* UTF-8 sequences, which are not supported by Unicode and +* so cannot be displayed correctly in a browser. In other words +* it is not as strict as utf8_is_valid but it's faster. If you use +* is to validate user input, you place yourself at the risk that +* attackers will be able to inject 5 and 6 byte sequences (which +* may or may not be a significant risk, depending on what you are +* are doing) +* Note: Does not pass five and six octet UTF-8 sequences anymore in +* in the unit tests. +* @see utf8_is_valid +* @see http://www.php.net/manual/en/reference.pcre.pattern.modifiers.php#54805 +* @param string UTF-8 string to check +* @return boolean TRUE if string is valid UTF-8 +* @package utf8 +* @subpackage validation +*/ +function utf8_compliant($str) +{ + if (strlen($str) == 0) + return true; + + // If even just the first character can be matched, when the /u + // modifier is used, then it's valid UTF-8. If the UTF-8 is somehow + // invalid, nothing at all will match, even if the string contains + // some valid sequences + return (preg_match('/^.{1}/us', $str, $ar) == 1); +} |