Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/include
diff options
context:
space:
mode:
Diffstat (limited to 'include')
-rw-r--r--include/addons.php84
-rw-r--r--include/cache.php263
-rw-r--r--include/common.php209
-rw-r--r--include/common_admin.php174
-rw-r--r--include/dblayer/common_db.php48
-rw-r--r--include/dblayer/index.html1
-rw-r--r--include/dblayer/mysql.php378
-rw-r--r--include/dblayer/mysql_innodb.php392
-rw-r--r--include/dblayer/mysqli.php385
-rw-r--r--include/dblayer/mysqli_innodb.php398
-rw-r--r--include/dblayer/pgsql.php442
-rw-r--r--include/dblayer/sqlite.php601
-rw-r--r--include/email.php364
-rw-r--r--include/functions.php2227
-rw-r--r--include/index.html1
-rw-r--r--include/parser.php987
-rw-r--r--include/search_idx.php316
-rw-r--r--include/srand.php151
-rw-r--r--include/template/admin.tpl38
-rw-r--r--include/template/help.tpl23
-rw-r--r--include/template/index.html1
-rw-r--r--include/template/main.tpl38
-rw-r--r--include/template/maintenance.tpl23
-rw-r--r--include/template/redirect.tpl25
-rw-r--r--include/user/index.html1
-rw-r--r--include/utf8/index.html1
-rw-r--r--include/utf8/mbstring/core.php144
-rw-r--r--include/utf8/mbstring/index.html1
-rw-r--r--include/utf8/native/core.php422
-rw-r--r--include/utf8/native/index.html1
-rw-r--r--include/utf8/ord.php78
-rw-r--r--include/utf8/str_ireplace.php72
-rw-r--r--include/utf8/str_pad.php59
-rw-r--r--include/utf8/str_split.php33
-rw-r--r--include/utf8/strcasecmp.php27
-rw-r--r--include/utf8/strcspn.php36
-rw-r--r--include/utf8/stristr.php34
-rw-r--r--include/utf8/strrev.php22
-rw-r--r--include/utf8/strspn.php32
-rw-r--r--include/utf8/substr_replace.php27
-rw-r--r--include/utf8/trim.php74
-rw-r--r--include/utf8/ucfirst.php35
-rw-r--r--include/utf8/ucwords.php46
-rw-r--r--include/utf8/utf8.php72
-rw-r--r--include/utf8/utils/ascii.php221
-rw-r--r--include/utf8/utils/bad.php430
-rw-r--r--include/utf8/utils/index.html1
-rw-r--r--include/utf8/utils/patterns.php67
-rw-r--r--include/utf8/utils/position.php171
-rw-r--r--include/utf8/utils/specials.php131
-rw-r--r--include/utf8/utils/unicode.php241
-rw-r--r--include/utf8/utils/validation.php186
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'] != '') ? ' &gt;&gt;&gt;' : '';
+ $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 ]]&gt;
+//
+function escape_cdata($str)
+{
+ return str_replace(']]>', ']]&gt;', $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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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 ? '' : '&amp;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 ? '' : '&amp;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.'&amp;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.'&amp;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['&#039;'] = '\''; // get_html_translation_table doesn't include &#039; 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('&#160; &#160; ', '&#160; ', ' &#160;');
+ $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('&amp;', '&', $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] : '&#160;' ?></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">&lt;'.$lang_common['Image link'].' - '.$alt.'&gt;</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=(&quot;|&\#039;|"|\'|)([^\r\n]*?)\\1\]%s', create_function('$matches', 'global $lang_common; return "</p><div class=\"quotebox\"><cite>".str_replace(array(\'[\', \'\\"\'), array(\'&#91;\', \'"\'), $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 />', '&#160; &#160; ', '&#160; ', ' &#160;');
+ $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 />', '&#160; &#160; ', '&#160; ', ' &#160;');
+ $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);
+}