From 3b06ee0d381dc1be5f40ca98ad4278046d869d21 Mon Sep 17 00:00:00 2001 From: Andreas Baumann Date: Sun, 17 Nov 2019 20:57:39 +0100 Subject: checked in initial customized verison for Archlinux32 --- include/addons.php | 84 ++ include/cache.php | 263 +++++ include/common.php | 209 ++++ include/common_admin.php | 174 +++ include/dblayer/common_db.php | 48 + include/dblayer/index.html | 1 + include/dblayer/mysql.php | 378 +++++++ include/dblayer/mysql_innodb.php | 392 +++++++ include/dblayer/mysqli.php | 385 +++++++ include/dblayer/mysqli_innodb.php | 398 +++++++ include/dblayer/pgsql.php | 442 ++++++++ include/dblayer/sqlite.php | 601 ++++++++++ include/email.php | 364 ++++++ include/functions.php | 2227 +++++++++++++++++++++++++++++++++++++ include/index.html | 1 + include/parser.php | 987 ++++++++++++++++ include/search_idx.php | 316 ++++++ include/srand.php | 151 +++ include/template/admin.tpl | 38 + include/template/help.tpl | 23 + include/template/index.html | 1 + include/template/main.tpl | 38 + include/template/maintenance.tpl | 23 + include/template/redirect.tpl | 25 + include/user/index.html | 1 + include/utf8/index.html | 1 + include/utf8/mbstring/core.php | 144 +++ include/utf8/mbstring/index.html | 1 + include/utf8/native/core.php | 422 +++++++ include/utf8/native/index.html | 1 + include/utf8/ord.php | 78 ++ include/utf8/str_ireplace.php | 72 ++ include/utf8/str_pad.php | 59 + include/utf8/str_split.php | 33 + include/utf8/strcasecmp.php | 27 + include/utf8/strcspn.php | 36 + include/utf8/stristr.php | 34 + include/utf8/strrev.php | 22 + include/utf8/strspn.php | 32 + include/utf8/substr_replace.php | 27 + include/utf8/trim.php | 74 ++ include/utf8/ucfirst.php | 35 + include/utf8/ucwords.php | 46 + include/utf8/utf8.php | 72 ++ include/utf8/utils/ascii.php | 221 ++++ include/utf8/utils/bad.php | 430 +++++++ include/utf8/utils/index.html | 1 + include/utf8/utils/patterns.php | 67 ++ include/utf8/utils/position.php | 171 +++ include/utf8/utils/specials.php | 131 +++ include/utf8/utils/unicode.php | 241 ++++ include/utf8/utils/validation.php | 186 ++++ 52 files changed, 10234 insertions(+) create mode 100644 include/addons.php create mode 100644 include/cache.php create mode 100644 include/common.php create mode 100644 include/common_admin.php create mode 100644 include/dblayer/common_db.php create mode 100644 include/dblayer/index.html create mode 100644 include/dblayer/mysql.php create mode 100644 include/dblayer/mysql_innodb.php create mode 100644 include/dblayer/mysqli.php create mode 100644 include/dblayer/mysqli_innodb.php create mode 100644 include/dblayer/pgsql.php create mode 100644 include/dblayer/sqlite.php create mode 100644 include/email.php create mode 100644 include/functions.php create mode 100644 include/index.html create mode 100644 include/parser.php create mode 100644 include/search_idx.php create mode 100644 include/srand.php create mode 100644 include/template/admin.tpl create mode 100644 include/template/help.tpl create mode 100644 include/template/index.html create mode 100644 include/template/main.tpl create mode 100644 include/template/maintenance.tpl create mode 100644 include/template/redirect.tpl create mode 100644 include/user/index.html create mode 100644 include/utf8/index.html create mode 100644 include/utf8/mbstring/core.php create mode 100644 include/utf8/mbstring/index.html create mode 100644 include/utf8/native/core.php create mode 100644 include/utf8/native/index.html create mode 100644 include/utf8/ord.php create mode 100644 include/utf8/str_ireplace.php create mode 100644 include/utf8/str_pad.php create mode 100644 include/utf8/str_split.php create mode 100644 include/utf8/strcasecmp.php create mode 100644 include/utf8/strcspn.php create mode 100644 include/utf8/stristr.php create mode 100644 include/utf8/strrev.php create mode 100644 include/utf8/strspn.php create mode 100644 include/utf8/substr_replace.php create mode 100644 include/utf8/trim.php create mode 100644 include/utf8/ucfirst.php create mode 100644 include/utf8/ucwords.php create mode 100644 include/utf8/utf8.php create mode 100644 include/utf8/utils/ascii.php create mode 100644 include/utf8/utils/bad.php create mode 100644 include/utf8/utils/index.html create mode 100644 include/utf8/utils/patterns.php create mode 100644 include/utf8/utils/position.php create mode 100644 include/utf8/utils/specials.php create mode 100644 include/utf8/utils/unicode.php create mode 100644 include/utf8/utils/validation.php (limited to 'include') 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 @@ +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 @@ +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 = ''; + 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 = ''; + 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 = ''; + + 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".'
'."\n\t\t\t\t\t".'
'."\n\t\t\t\t\t".''."\n\t\t\t\t\t".'
'."\n\t\t\t\t".'
'."\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 = ''; + 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 = ''; + 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 = ''; + 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 = ''; + 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 @@ + $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 @@ +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; + +?> +
+
+

+
+
+
    + > + > + > + > +
+
+
+ +

+
+
+
    + > + > + > + > + > + > + > +
+
+
+ +

+
+
+ +
+
+ +
+ +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 @@ +.. 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 @@ + '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 @@ + '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 @@ + '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 @@ + '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 @@ + '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 @@ + '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 @@ + 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 @@ + intval($matches[1]), + 'password_hash' => $matches[2], + 'expiration_time' => intval($matches[3]), + 'cookie_hash' => $matches[4], + ); + } + + // If it has a non-guest user, and hasn't expired + if (isset($cookie) && $cookie['user_id'] > 1 && $cookie['expiration_time'] > $now) + { + // If the cookie has been tampered with + $is_authorized = pun_hash_equals(forum_hmac($cookie['user_id'].'|'.$cookie['expiration_time'], $cookie_seed.'_cookie_hash'), $cookie['cookie_hash']); + if (!$is_authorized) + { + $expire = $now + 31536000; // The cookie expires after a year + pun_setcookie(1, pun_hash(uniqid(rand(), true)), $expire); + set_default_user(); + + return; + } + + // Check if there's a user with the user ID and password hash from the cookie + $result = $db->query('SELECT u.*, g.*, o.logged, o.idle FROM '.$db->prefix.'users AS u INNER JOIN '.$db->prefix.'groups AS g ON u.group_id=g.g_id LEFT JOIN '.$db->prefix.'online AS o ON o.user_id=u.id WHERE u.id='.intval($cookie['user_id'])) or error('Unable to fetch user information', __FILE__, __LINE__, $db->error()); + $pun_user = $db->fetch_assoc($result); + + // If user authorisation failed + $is_authorized = pun_hash_equals(forum_hmac($pun_user['password'], $cookie_seed.'_password_hash'), $cookie['password_hash']); + if (!isset($pun_user['id']) || !$is_authorized) + { + $expire = $now + 31536000; // The cookie expires after a year + pun_setcookie(1, pun_hash(uniqid(rand(), true)), $expire); + set_default_user(); + + return; + } + + // Send a new, updated cookie with a new expiration timestamp + $expire = ($cookie['expiration_time'] > $now + $pun_config['o_timeout_visit']) ? $now + 1209600 : $now + $pun_config['o_timeout_visit']; + pun_setcookie($pun_user['id'], $pun_user['password'], $expire); + + // Set a default language if the user selected language no longer exists + if (!file_exists(PUN_ROOT.'lang/'.$pun_user['language'])) + $pun_user['language'] = $pun_config['o_default_lang']; + + // Set a default style if the user selected style no longer exists + if (!file_exists(PUN_ROOT.'style/'.$pun_user['style'].'.css')) + $pun_user['style'] = $pun_config['o_default_style']; + + if (!$pun_user['disp_topics']) + $pun_user['disp_topics'] = $pun_config['o_disp_topics_default']; + if (!$pun_user['disp_posts']) + $pun_user['disp_posts'] = $pun_config['o_disp_posts_default']; + + // Define this if you want this visit to affect the online list and the users last visit data + if (!defined('PUN_QUIET_VISIT')) + { + // Update the online list + if (!$pun_user['logged']) + { + $pun_user['logged'] = $now; + + // With MySQL/MySQLi/SQLite, REPLACE INTO avoids a user having two rows in the online table + switch ($db_type) + { + case 'mysql': + case 'mysqli': + case 'mysql_innodb': + case 'mysqli_innodb': + case 'sqlite': + $db->query('REPLACE INTO '.$db->prefix.'online (user_id, ident, logged) VALUES('.$pun_user['id'].', \''.$db->escape($pun_user['username']).'\', '.$pun_user['logged'].')') or error('Unable to insert into online list', __FILE__, __LINE__, $db->error()); + break; + + default: + $db->query('INSERT INTO '.$db->prefix.'online (user_id, ident, logged) SELECT '.$pun_user['id'].', \''.$db->escape($pun_user['username']).'\', '.$pun_user['logged'].' WHERE NOT EXISTS (SELECT 1 FROM '.$db->prefix.'online WHERE user_id='.$pun_user['id'].')') or error('Unable to insert into online list', __FILE__, __LINE__, $db->error()); + break; + } + + // Reset tracked topics + set_tracked_topics(null); + } + else + { + // Special case: We've timed out, but no other user has browsed the forums since we timed out + if ($pun_user['logged'] < ($now-$pun_config['o_timeout_visit'])) + { + $db->query('UPDATE '.$db->prefix.'users SET last_visit='.$pun_user['logged'].' WHERE id='.$pun_user['id']) or error('Unable to update user visit data', __FILE__, __LINE__, $db->error()); + $pun_user['last_visit'] = $pun_user['logged']; + } + + $idle_sql = ($pun_user['idle'] == '1') ? ', idle=0' : ''; + $db->query('UPDATE '.$db->prefix.'online SET logged='.$now.$idle_sql.' WHERE user_id='.$pun_user['id']) or error('Unable to update online list', __FILE__, __LINE__, $db->error()); + + // Update tracked topics with the current expire time + if (isset($_COOKIE[$cookie_name.'_track'])) + forum_setcookie($cookie_name.'_track', $_COOKIE[$cookie_name.'_track'], $now + $pun_config['o_timeout_visit']); + } + } + else + { + if (!$pun_user['logged']) + $pun_user['logged'] = $pun_user['last_visit']; + } + + $pun_user['is_guest'] = false; + $pun_user['is_admmod'] = $pun_user['g_id'] == PUN_ADMIN || $pun_user['g_moderator'] == '1'; + } + else + set_default_user(); +} + + +// +// Converts the CDATA end sequence ]]> into ]]> +// +function escape_cdata($str) +{ + return str_replace(']]>', ']]>', $str); +} + + +// +// Authenticates the provided username and password against the user database +// $user can be either a user ID (integer) or a username (string) +// $password can be either a plaintext password or a password hash including salt ($password_is_hash must be set accordingly) +// +function authenticate_user($user, $password, $password_is_hash = false) +{ + global $db, $pun_user; + + // Check if there's a user matching $user and $password + $result = $db->query('SELECT u.*, g.*, o.logged, o.idle FROM '.$db->prefix.'users AS u INNER JOIN '.$db->prefix.'groups AS g ON g.g_id=u.group_id LEFT JOIN '.$db->prefix.'online AS o ON o.user_id=u.id WHERE '.(is_int($user) ? 'u.id='.intval($user) : 'u.username=\''.$db->escape($user).'\'')) or error('Unable to fetch user info', __FILE__, __LINE__, $db->error()); + $pun_user = $db->fetch_assoc($result); + + $is_password_authorized = pun_hash_equals($password, $pun_user['password']); + $is_hash_authorized = pun_hash_equals(pun_hash($password), $pun_user['password']); + + if (!isset($pun_user['id']) || + ($password_is_hash && !$is_password_authorized || + (!$password_is_hash && !$is_hash_authorized))) + set_default_user(); + else + $pun_user['is_guest'] = false; +} + + +// +// Try to determine the current URL +// +function get_current_url($max_length = 0) +{ + $protocol = get_current_protocol(); + $port = (isset($_SERVER['SERVER_PORT']) && (($_SERVER['SERVER_PORT'] != '80' && $protocol == 'http') || ($_SERVER['SERVER_PORT'] != '443' && $protocol == 'https')) && strpos($_SERVER['HTTP_HOST'], ':') === false) ? ':'.$_SERVER['SERVER_PORT'] : ''; + + $url = urldecode($protocol.'://'.$_SERVER['HTTP_HOST'].$port.$_SERVER['REQUEST_URI']); + + if (strlen($url) <= $max_length || $max_length == 0) + return $url; + + // We can't find a short enough url + return null; +} + + +// +// Fetch the current protocol in use - http or https +// +function get_current_protocol() +{ + $protocol = 'http'; + + // Check if the server is claiming to using HTTPS + if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') + $protocol = 'https'; + + // If we are behind a reverse proxy try to decide which protocol it is using + if (defined('FORUM_BEHIND_REVERSE_PROXY')) + { + // Check if we are behind a Microsoft based reverse proxy + if (!empty($_SERVER['HTTP_FRONT_END_HTTPS']) && strtolower($_SERVER['HTTP_FRONT_END_HTTPS']) != 'off') + $protocol = 'https'; + + // Check if we're behind a "proper" reverse proxy, and what protocol it's using + if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) + $protocol = strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']); + } + + return $protocol; +} + + +// +// Fetch the base_url, optionally support HTTPS and HTTP +// +function get_base_url($support_https = false) +{ + global $pun_config; + static $base_url; + + if (!$support_https) + return $pun_config['o_base_url']; + + if (!isset($base_url)) + { + // Make sure we are using the correct protocol + $base_url = str_replace(array('http://', 'https://'), get_current_protocol().'://', $pun_config['o_base_url']); + } + + return $base_url; +} + + +// +// Fetch admin IDs +// +function get_admin_ids() +{ + if (file_exists(FORUM_CACHE_DIR.'cache_admins.php')) + include FORUM_CACHE_DIR.'cache_admins.php'; + + if (!defined('PUN_ADMINS_LOADED')) + { + if (!defined('FORUM_CACHE_FUNCTIONS_LOADED')) + require PUN_ROOT.'include/cache.php'; + + generate_admins_cache(); + require FORUM_CACHE_DIR.'cache_admins.php'; + } + + return $pun_admins; +} + + +// +// Fill $pun_user with default values (for guests) +// +function set_default_user() +{ + global $db, $db_type, $pun_user, $pun_config; + + $remote_addr = get_remote_address(); + + // Fetch guest user + $result = $db->query('SELECT u.*, g.*, o.logged, o.last_post, o.last_search FROM '.$db->prefix.'users AS u INNER JOIN '.$db->prefix.'groups AS g ON u.group_id=g.g_id LEFT JOIN '.$db->prefix.'online AS o ON o.ident=\''.$db->escape($remote_addr).'\' WHERE u.id=1') or error('Unable to fetch guest information', __FILE__, __LINE__, $db->error()); + if (!$db->num_rows($result)) + exit('Unable to fetch guest information. Your database must contain both a guest user and a guest user group.'); + + $pun_user = $db->fetch_assoc($result); + + // Update online list + if (!$pun_user['logged']) + { + $pun_user['logged'] = time(); + + // With MySQL/MySQLi/SQLite, REPLACE INTO avoids a user having two rows in the online table + switch ($db_type) + { + case 'mysql': + case 'mysqli': + case 'mysql_innodb': + case 'mysqli_innodb': + case 'sqlite': + $db->query('REPLACE INTO '.$db->prefix.'online (user_id, ident, logged) VALUES(1, \''.$db->escape($remote_addr).'\', '.$pun_user['logged'].')') or error('Unable to insert into online list', __FILE__, __LINE__, $db->error()); + break; + + default: + $db->query('INSERT INTO '.$db->prefix.'online (user_id, ident, logged) SELECT 1, \''.$db->escape($remote_addr).'\', '.$pun_user['logged'].' WHERE NOT EXISTS (SELECT 1 FROM '.$db->prefix.'online WHERE ident=\''.$db->escape($remote_addr).'\')') or error('Unable to insert into online list', __FILE__, __LINE__, $db->error()); + break; + } + } + else + $db->query('UPDATE '.$db->prefix.'online SET logged='.time().' WHERE ident=\''.$db->escape($remote_addr).'\'') or error('Unable to update online list', __FILE__, __LINE__, $db->error()); + + $pun_user['disp_topics'] = $pun_config['o_disp_topics_default']; + $pun_user['disp_posts'] = $pun_config['o_disp_posts_default']; + $pun_user['timezone'] = $pun_config['o_default_timezone']; + $pun_user['dst'] = $pun_config['o_default_dst']; + $pun_user['language'] = $pun_config['o_default_lang']; + $pun_user['style'] = $pun_config['o_default_style']; + $pun_user['is_guest'] = true; + $pun_user['is_admmod'] = false; +} + + +// +// SHA1 HMAC with PHP 4 fallback +// +function forum_hmac($data, $key, $raw_output = false) +{ + if (function_exists('hash_hmac')) + return hash_hmac('sha1', $data, $key, $raw_output); + + // If key size more than blocksize then we hash it once + if (strlen($key) > 64) + $key = pack('H*', sha1($key)); // we have to use raw output here to match the standard + + // Ensure we're padded to exactly one block boundary + $key = str_pad($key, 64, chr(0x00)); + + $hmac_opad = str_repeat(chr(0x5C), 64); + $hmac_ipad = str_repeat(chr(0x36), 64); + + // Do inner and outer padding + for ($i = 0;$i < 64;$i++) { + $hmac_opad[$i] = $hmac_opad[$i] ^ $key[$i]; + $hmac_ipad[$i] = $hmac_ipad[$i] ^ $key[$i]; + } + + // Finally, calculate the HMAC + $hash = sha1($hmac_opad.pack('H*', sha1($hmac_ipad.$data))); + + // If we want raw output then we need to pack the final result + if ($raw_output) + $hash = pack('H*', $hash); + + return $hash; +} + + +// +// Set a cookie, FluxBB style! +// Wrapper for forum_setcookie +// +function pun_setcookie($user_id, $password_hash, $expire) +{ + global $cookie_name, $cookie_seed; + + forum_setcookie($cookie_name, $user_id.'|'.forum_hmac($password_hash, $cookie_seed.'_password_hash').'|'.$expire.'|'.forum_hmac($user_id.'|'.$expire, $cookie_seed.'_cookie_hash'), $expire); +} + + +// +// Set a cookie, FluxBB style! +// +function forum_setcookie($name, $value, $expire) +{ + global $cookie_path, $cookie_domain, $cookie_secure, $pun_config; + + if ($expire - time() - $pun_config['o_timeout_visit'] < 1) + $expire = 0; + + // Enable sending of a P3P header + header('P3P: CP="CUR ADM"'); + + if (version_compare(PHP_VERSION, '5.2.0', '>=')) + setcookie($name, $value, $expire, $cookie_path, $cookie_domain, $cookie_secure, true); + else + setcookie($name, $value, $expire, $cookie_path.'; HttpOnly', $cookie_domain, $cookie_secure); +} + + +// +// Check whether the connecting user is banned (and delete any expired bans while we're at it) +// +function check_bans() +{ + global $db, $pun_config, $lang_common, $pun_user, $pun_bans; + + // Admins and moderators aren't affected + if ($pun_user['is_admmod'] || !$pun_bans) + return; + + // Add a dot or a colon (depending on IPv4/IPv6) at the end of the IP address to prevent banned address + // 192.168.0.5 from matching e.g. 192.168.0.50 + $user_ip = get_remote_address(); + $user_ip .= (strpos($user_ip, '.') !== false) ? '.' : ':'; + + $bans_altered = false; + $is_banned = false; + + foreach ($pun_bans as $cur_ban) + { + // Has this ban expired? + if ($cur_ban['expire'] != '' && $cur_ban['expire'] <= time()) + { + $db->query('DELETE FROM '.$db->prefix.'bans WHERE id='.$cur_ban['id']) or error('Unable to delete expired ban', __FILE__, __LINE__, $db->error()); + $bans_altered = true; + continue; + } + + if ($cur_ban['username'] != '' && utf8_strtolower($pun_user['username']) == utf8_strtolower($cur_ban['username'])) + $is_banned = true; + + if ($cur_ban['ip'] != '') + { + $cur_ban_ips = explode(' ', $cur_ban['ip']); + + $num_ips = count($cur_ban_ips); + for ($i = 0; $i < $num_ips; ++$i) + { + // Add the proper ending to the ban + if (strpos($user_ip, '.') !== false) + $cur_ban_ips[$i] = $cur_ban_ips[$i].'.'; + else + $cur_ban_ips[$i] = $cur_ban_ips[$i].':'; + + if (substr($user_ip, 0, strlen($cur_ban_ips[$i])) == $cur_ban_ips[$i]) + { + $is_banned = true; + break; + } + } + } + + if ($is_banned) + { + $db->query('DELETE FROM '.$db->prefix.'online WHERE ident=\''.$db->escape($pun_user['username']).'\'') or error('Unable to delete from online list', __FILE__, __LINE__, $db->error()); + message($lang_common['Ban message'].' '.(($cur_ban['expire'] != '') ? $lang_common['Ban message 2'].' '.strtolower(format_time($cur_ban['expire'], true)).'. ' : '').(($cur_ban['message'] != '') ? $lang_common['Ban message 3'].'

'.pun_htmlspecialchars($cur_ban['message']).'

' : '

').$lang_common['Ban message 4'].' '.pun_htmlspecialchars($pun_config['o_admin_email']).'.', 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; + +?> +
+
+

+
+
+
    + > + > + > + > + > + > + > +
+
+
+
+'; + 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('1'); + else + { + // Add a previous page link + if ($num_pages > 1 && $cur_page > 1) + $pages[] = ''; + + if ($cur_page > 3) + { + $pages[] = '1'; + + if ($cur_page > 5) + $pages[] = ''.$lang_common['Spacer'].''; + } + + // 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[] = ''.forum_number_format($current).''; + else + $pages[] = ''.forum_number_format($current).''; + } + + if ($cur_page <= ($num_pages-3)) + { + if ($cur_page != ($num_pages-3) && $cur_page != ($num_pages-4)) + $pages[] = ''.$lang_common['Spacer'].''; + + $pages[] = ''.forum_number_format($num_pages).''; + } + + // Add a next page link + if ($num_pages > 1 && !$link_to_all && $cur_page < $num_pages) + $pages[] = ''; + } + + 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'; + } + +?> + +
+

+
+
+

+

+
+
+
+ count($input)) + $offset = count($input); + else if ($offset < 0) + $offset = 0; + + $input = array_merge(array_slice($input, 0, $offset), array($key => $element), array_slice($input, $offset)); +} + + +// +// Display a message when board is in maintenance mode +// +function maintenance_message() +{ + global $db, $pun_config, $lang_common, $pun_user; + + header('HTTP/1.1 503 Service Unavailable'); + + // Send no-cache headers + header('Expires: Thu, 21 Jul 1977 07:30:00 GMT'); // When yours truly first set eyes on this world! :) + header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); // For HTTP/1.0 compatibility + + // Send the Content-type header in case the web server is setup to send something else + header('Content-type: text/html; charset=utf-8'); + + // Deal with newlines, tabs and multiple spaces + $pattern = array("\t", ' ', ' '); + $replace = array('    ', '  ', '  '); + $message = str_replace($pattern, $replace, $pun_config['o_maintenance_message']); + + if (file_exists(PUN_ROOT.'style/'.$pun_user['style'].'/maintenance.tpl')) + { + $tpl_file = PUN_ROOT.'style/'.$pun_user['style'].'/maintenance.tpl'; + $tpl_inc_dir = PUN_ROOT.'style/'.$pun_user['style'].'/'; + } + else + { + $tpl_file = PUN_ROOT.'include/template/maintenance.tpl'; + $tpl_inc_dir = PUN_ROOT.'include/user/'; + } + + $tpl_maint = file_get_contents($tpl_file); + + // START SUBST - + preg_match_all('%%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 - + + + // START SUBST - + $tpl_maint = str_replace('', $lang_common['lang_identifier'], $tpl_maint); + // END SUBST - + + + // START SUBST - + $tpl_maint = str_replace('', $lang_common['lang_direction'], $tpl_maint); + // END SUBST - + + + // START SUBST - + ob_start(); + + $page_title = array(pun_htmlspecialchars($pun_config['o_board_title']), $lang_common['Maintenance']); + +?> +<?php echo generate_page_title($page_title) ?> + +', $tpl_temp, $tpl_maint); + ob_end_clean(); + // END SUBST - + + + // START SUBST - + ob_start(); + +?> +
+

+
+
+

+
+
+
+', $tpl_temp, $tpl_maint); + ob_end_clean(); + // END SUBST - + + + // End the transaction + $db->end_transaction(); + + + // Close the db connection (and free up any result data) + $db->close(); + + exit($tpl_maint); +} + + +// +// Display $message and redirect user to $destination_url +// +function redirect($destination_url, $message) +{ + global $db, $pun_config, $lang_common, $pun_user; + + // Prefix with base_url (unless there's already a valid URI) + if (strpos($destination_url, 'http://') !== 0 && strpos($destination_url, 'https://') !== 0 && strpos($destination_url, '/') !== 0) + $destination_url = get_base_url(true).'/'.$destination_url; + + // Do a little spring cleaning + $destination_url = preg_replace('%([\r\n])|(\%0[ad])|(;\s*data\s*:)%i', '', $destination_url); + + // If the delay is 0 seconds, we might as well skip the redirect all together + if ($pun_config['o_redirect_delay'] == '0') + { + $db->end_transaction(); + $db->close(); + + header('Location: '.str_replace('&', '&', $destination_url)); + exit; + } + + // Send no-cache headers + header('Expires: Thu, 21 Jul 1977 07:30:00 GMT'); // When yours truly first set eyes on this world! :) + header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); // For HTTP/1.0 compatibility + + // Send the Content-type header in case the web server is setup to send something else + header('Content-type: text/html; charset=utf-8'); + + if (file_exists(PUN_ROOT.'style/'.$pun_user['style'].'/redirect.tpl')) + { + $tpl_file = PUN_ROOT.'style/'.$pun_user['style'].'/redirect.tpl'; + $tpl_inc_dir = PUN_ROOT.'style/'.$pun_user['style'].'/'; + } + else + { + $tpl_file = PUN_ROOT.'include/template/redirect.tpl'; + $tpl_inc_dir = PUN_ROOT.'include/user/'; + } + + $tpl_redir = file_get_contents($tpl_file); + + // START SUBST - + preg_match_all('%%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 - + + + // START SUBST - + $tpl_redir = str_replace('', $lang_common['lang_identifier'], $tpl_redir); + // END SUBST - + + + // START SUBST - + $tpl_redir = str_replace('', $lang_common['lang_direction'], $tpl_redir); + // END SUBST - + + + // START SUBST - + ob_start(); + + $page_title = array(pun_htmlspecialchars($pun_config['o_board_title']), $lang_common['Redirecting']); + +?> + +<?php echo generate_page_title($page_title) ?> + +', $tpl_temp, $tpl_redir); + ob_end_clean(); + // END SUBST - + + + // START SUBST - + ob_start(); + +?> + +', $tpl_temp, $tpl_redir); + ob_end_clean(); + // END SUBST - + + + // START SUBST - + 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('', $tpl_temp, $tpl_redir); + ob_end_clean(); + // END SUBST - + + + // 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'); + +?> + + + + + +<?php echo generate_page_title($page_title) ?> + + + + +
+

An error was encountered

+
+File: '.$file.'
'."\n\t\t".'Line: '.$line.'

'."\n\t\t".'FluxBB reported: '.$message."\n"; + + if ($db_error) + { + echo "\t\t".'

Database reported: '.pun_htmlspecialchars($db_error['error_msg']).(($db_error['error_no']) ? ' (Errno: '.$db_error['error_no'].')' : '')."\n"; + + if ($db_error['error_sql'] != '') + echo "\t\t".'

Failed query: '.pun_htmlspecialchars($db_error['error_sql'])."\n"; + } + } + else + echo "\t\t".'Error: '.pun_htmlspecialchars($message).'.'."\n"; + +?> +
+
+ + + +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[A-Za-z][A-Za-z0-9+\-.]*):\/\/ + (?P + (?:(?P(?:[A-Za-z0-9\-._~!$&\'()*+,;=:]|%[0-9A-Fa-f]{2})*)@)? + (?P + (?P + \[ + (?: + (?P + (?: (?:[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[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[Vv][0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&\'()*+,;=:]+) + ) + \] + ) + | (?P(?:(?: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(?:[A-Za-z0-9\-._~!$&\'()*+,;=]|%[0-9A-Fa-f]{2})+) + ) + (?::(?P[0-9]*))? + ) + (?P(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*) + (?:\?(?P (?:[A-Za-z0-9\-._~!$&\'()*+,;=:@\\/?]|%[0-9A-Fa-f]{2})*))? + (?:\#(?P (?:[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(); + +?> + +
+

+
+
+ + + + + + + + + + + + + + + + + + +
+
+
+
+'; + + $num_args = func_num_args(); + + for ($i = 0; $i < $num_args; ++$i) + { + print_r(func_get_arg($i)); + echo "\n\n"; + } + + echo ''; + 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 @@ +.. 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 @@ + '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('% 55 ? utf8_substr($url, 0 , 39).' … '.utf8_substr($url, -10) : $url; + $link = pun_htmlspecialchars($link); + } + else + $link = stripslashes($link); + + return ''.$link.''; + } +} + + +// +// Turns an URL from the [img] tag into an tag or a 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 = '<'.$lang_common['Image link'].' - '.$alt.'>'; + + if ($is_signature && $pun_user['show_img_sig'] != '0') + $img_tag = ''.$alt.''; + else if (!$is_signature && $pun_user['show_img'] != '0') + $img_tag = ''.$alt.''; + + 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', '
  • $1

  • ', pun_trim($content)); + + if ($type == '*') + $content = '
      '.$content.'
    '; + else + if ($type == 'a') + $content = '
      '.$content.'
    '; + else + $content = '
      '.$content.'
    '; + + return '

    '.$content.'

    '; +} + + +// +// 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*%', '

    ', $text); + $text = preg_replace_callback('%\[quote=("|&\#039;|"|\'|)([^\r\n]*?)\\1\]%s', create_function('$matches', 'global $lang_common; return "

    ".str_replace(array(\'[\', \'\\"\'), array(\'[\', \'"\'), $matches[2])." ".$lang_common[\'wrote\']."

    ";'), $text); + $text = preg_replace('%\s*\[\/quote\]%S', '

    ', $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[] = '$1'; + $replace[] = '$1'; + $replace[] = '$1'; + $replace[] = '$1'; + $replace[] = '$1'; + $replace[] = '$1'; + $replace[] = '$1'; + $replace[] = '$2'; + $replace[] = '

    $1

    '; + + 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[] = '$1'; + $replace[] = '$2'; + $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', ''.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('
    ', '    ', '  ', '  '); + $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 .= '

    28) ? ' class="vscroll"' : '').'>'.pun_trim($inside[$i], "\n\r").'

    '; + } + } + } + + 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 = '

    '.$text.'

    '; + + // Replace any breaks next to paragraphs so our replace below catches them + $text = preg_replace('%()(?:\s*?
    ){1,2}%i', '$1', $text); + $text = preg_replace('%(?:
    \s*?){1,2}()%i', '$1', $text); + + // Remove any empty paragraph tags (inserted via quotes/lists/code/etc) which should be stripped + $text = str_replace('

    ', '', $text); + + $text = preg_replace('%
    \s*?
    %i', '

    ', $text); + + $text = str_replace('


    ', '

    ', $text); + $text = str_replace('

    ', '


    ', $text); + $text = str_replace('

    ', '

    ', $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('
    ', '    ', '  ', '  '); + $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 @@ + $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 @@ + + * + * 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 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 @@ + + + + + + + + + +
    +
    +
    + +
    +
    +
    + + +
    + + +
    +
    + + + +
    + +
    + + + +
    +
    +
    + + + 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 @@ + + + + + + + + + +
    +
    +
    + +
    + +
    + +
    +
    +
    + + + 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 @@ +.. 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 @@ + + + + + + + + + +
    +
    +
    + +
    +
    +
    + + +
    + + +
    +
    + + + +
    + +
    + + + +
    +
    +
    + + + 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 @@ + + + + + + + + + +
    +
    +
    + +
    + +
    + +
    +
    +
    + + + 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 @@ + + + + + + + + + +
    +
    +
    + +
    + +
    + + + +
    +
    +
    + + + 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 @@ +.. 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 @@ +.. 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 @@ +.. 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 @@ + +* @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 +* @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 +* @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 +* @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 @@ +.. 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 @@ += 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 @@ + +* @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 @@ + +* @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 +* @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 +* @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 @@ + +* if ( utf8_is_ascii($someString) ) { +* // It's just ASCII - use the native PHP version +* $someString = strtolower($someString); +* } else { +* $someString = utf8_strtolower($someString); +* } +* +* +* @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 +* @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 @@ + 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 +* @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 @@ +.. 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 @@ + +* @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 +* @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 +* @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 @@ + +* @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 @@ + 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 +* @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 +* @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 @@ + +* @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); +} -- cgit v1.2.3-54-g00ecf