fix_mysql8_query('ALTER TABLE ' . $table_name . ' ADD ' . $type . ' ' . $index_name . ' (' . $_fields . ')')); } /** * Change the primary key of a table. * * @param ID_TEXT $table_name The name of the table to create the index on * @param array $new_key A list of fields to put in the new key * @param array $db The DB connection to make on */ public function db_change_primary_key($table_name, $new_key, $db) { $this->db_query('ALTER TABLE ' . $table_name . ' DROP PRIMARY KEY, ADD PRIMARY KEY (' . implode(',', $new_key) . ')', $db); } /** * Get the number of rows in a table, with approximation support for performance (if necessary on the particular database backend). * * @param string $table The table name * @param array $where WHERE clauses if it will help get a more reliable number when we're not approximating in map form * @param string $where_clause WHERE clauses if it will help get a more reliable number when we're not approximating in SQL form * @param object $db The DB connection to check against * @return ?integer The count (null: do it normally) */ public function get_table_count_approx($table, $where, $where_clause, $db) { if (get_value('slow_counts') === '1') { $sql = 'SELECT TABLE_ROWS FROM information_schema.tables WHERE table_schema=DATABASE() AND TABLE_NAME=\'' . $db->get_table_prefix() . $table . '\''; return $db->query_value_if_there($sql, false, true); } return null; } /** * Assemble part of a WHERE clause for doing full-text search * * @param string $content Our match string (assumes "?" has been stripped already) * @param boolean $boolean Whether to do a boolean full text search * @return string Part of a WHERE clause for doing full-text search */ public function db_full_text_assemble($content, $boolean) { static $stopwords = null; if (is_null($stopwords)) { require_code('database_search'); $stopwords = get_stopwords_list(); } $words = preg_split('#\s+#', strtolower($content)); $all_stopwords = true; foreach ($words as $word) { if (trim($word) != '') { $word = trim($word, $boolean ? '"+-' : '"'); if (!isset($stopwords[$word])) { $all_stopwords = false; } } } if ($all_stopwords) { // This is an imperfect solution for searching for a stop-word // It will not cover the case where the stop-word is within the wider text. But we can't handle that case efficiently anyway $_content = trim($content, $boolean ? '"+-' : '"'); if (cms_mb_strlen($_content) > 8) { return '? LIKE \'' . db_encode_like($_content) . '%\''; } return db_string_equal_to('?', $_content); } if (!$boolean) { // These just cause muddling during full-text natural search $content = str_replace('"', '', $content); $content = str_replace('?', '', $content); db_escape_string($content); // Hack to so SQL injection detector doesn't get confused if ((strtoupper($content) == $content) && (!is_numeric($content))) { return 'MATCH (?) AGAINST (_latin1\'' . $this->db_escape_string($content) . '\' COLLATE latin1_general_cs)'; } return 'MATCH (?) AGAINST (\'' . $this->db_escape_string($content) . '\')'; } // These risk parse errors during full-text natural search and aren't supported for Composr searching $content = str_replace(array('>', '<', '(', ')', '~', '?', '@'), array('', '', '', '', '', '', ''), $content); // Risks parse error and not supported $content = preg_replace('#([\-+*])[\-+*]+#', '$1', $content); // Parse error if repeated on some servers $content = cms_preg_replace_safe('#[\-+]($|\s)#', '$1', $content); // Parse error if on end on some servers $content = cms_preg_replace_safe('#(^|\s)\*#', '$1', $content); // Parse error if on start on some servers db_escape_string($content); // Hack to so SQL injection detector doesn't get confused return 'MATCH (?) AGAINST (\'' . $this->db_escape_string($content) . '\' IN BOOLEAN MODE)'; } /** * Get the ID of the first row in an auto-increment table (used whenever we need to reference the first). * * @return integer First ID used */ public function db_get_first_id() { return 1; } /** * Get a map of Composr field types, to actual database types. * * @return array The map */ public function db_get_type_remap() { $type_remap = array( 'AUTO' => 'integer unsigned auto_increment', 'AUTO_LINK' => 'integer', // not unsigned because it's useful to have -ve for temporary usage while importing (NB: *_TRANS is signed, so trans fields are not perfectly AUTO_LINK compatible and can have double the positive range -- in the real world it will not matter though) 'INTEGER' => 'integer', 'UINTEGER' => 'integer unsigned', 'SHORT_INTEGER' => 'tinyint', 'REAL' => 'real', 'BINARY' => 'tinyint(1)', 'MEMBER' => 'integer', // not unsigned because it's useful to have -ve for temporary usage while importing 'GROUP' => 'integer', // not unsigned because it's useful to have -ve for temporary usage while importing 'TIME' => 'integer unsigned', 'LONG_TRANS' => 'integer unsigned', 'SHORT_TRANS' => 'integer unsigned', 'LONG_TRANS__COMCODE' => 'integer', 'SHORT_TRANS__COMCODE' => 'integer', 'SHORT_TEXT' => 'varchar(255)', 'LONG_TEXT' => 'longtext', 'ID_TEXT' => 'varchar(80)', 'MINIID_TEXT' => 'varchar(40)', 'IP' => 'varchar(40)', // 15 for ip4, but we now support ip6 'LANGUAGE_NAME' => 'varchar(5)', 'URLPATH' => 'varchar(255) BINARY', ); return $type_remap; } /** * Get SQL for creating a new table. * * @param ID_TEXT $table_name The table name * @param array $fields A map of field names to Composr field types (with *#? encodings) * @param array $db The DB connection to make on * @param ID_TEXT $raw_table_name The table name with no table prefix * @param boolean $save_bytes Whether to use lower-byte table storage, with tradeoffs of not being able to support all unicode characters; use this if key length is an issue * @return array List of SQL queries to run */ public function db_create_table($table_name, $fields, $db, $raw_table_name, $save_bytes = false) { $type_remap = $this->db_get_type_remap(); $_fields = ''; $keys = ''; foreach ($fields as $name => $type) { if ($type[0] == '*') { // Is a key $type = substr($type, 1); if ($keys !== '') { $keys .= ', '; } $keys .= $name; } if ($type[0] == '?') { // Is perhaps null $type = substr($type, 1); $perhaps_null = 'NULL'; } else { $perhaps_null = 'NOT NULL'; } $type = isset($type_remap[$type]) ? $type_remap[$type] : $type; $_fields .= ' ' . $name . ' ' . $type; /*if (substr($name, -13) == '__text_parsed') { BLOB/TEXT column 'description__text_parsed' can't have a default value $_fields .= ' DEFAULT \'\''; } else*/ if (substr($name, -13) == '__source_user') { $_fields .= ' DEFAULT ' . strval(db_get_first_id()); } $_fields .= ' ' . $perhaps_null . ',' . "\n"; } $innodb = ((function_exists('get_value')) && (get_value('innodb') == '1')); $table_type = ($innodb ? 'INNODB' : 'MyISAM'); $type_key = 'engine'; /*if ($raw_table_name == 'sessions') { $table_type = 'HEAP'; Some MySQL servers are very regularly reset }*/ $query = 'CREATE TABLE ' . $table_name . ' (' . "\n" . $_fields . ' PRIMARY KEY (' . $keys . ")\n)"; global $SITE_INFO; if (empty($SITE_INFO['database_charset'])) { $SITE_INFO['database_charset'] = (get_charset() == 'utf-8') ? 'utf8mb4' : 'latin1'; } $charset = $SITE_INFO['database_charset']; if ($charset == 'utf8mb4' && $save_bytes) { $charset = 'utf8'; } $query .= ' CHARACTER SET=' . preg_replace('#\_.*$#', '', $charset); $query .= ' ' . $type_key . '=' . $table_type; return array($this->fix_mysql8_query($query)); } /** * Encode an SQL statement fragment for a conditional to see if two strings are equal. * * @param ID_TEXT $attribute The attribute * @param string $compare The comparison * @return string The SQL */ public function db_string_equal_to($attribute, $compare) { return $attribute . "='" . db_escape_string($compare) . "'"; } /** * Encode an SQL statement fragment for a conditional to see if two strings are not equal. * * @param ID_TEXT $attribute The attribute * @param string $compare The comparison * @return string The SQL */ public function db_string_not_equal_to($attribute, $compare) { return $attribute . "<>'" . db_escape_string($compare) . "'"; } /** * Find whether expression ordering support is present * * @param array $db A DB connection * @return boolean Whether it is */ public function db_has_expression_ordering($db) { return true; } /** * This function is internal to the database system, allowing SQL statements to be build up appropriately. Some databases require IS NULL to be used to check for blank strings. * * @return boolean Whether a blank string IS NULL */ public function db_empty_is_null() { return false; } /** * Find whether table truncation support is present * * @return boolean Whether it is */ public function db_supports_truncate_table() { return true; } /** * Find whether drop table "if exists" is present * * @return boolean Whether it is */ public function db_supports_drop_table_if_exists() { return true; } /** * Delete a table. * * @param ID_TEXT $table The table name * @param array $db The DB connection to delete on * @return array List of SQL queries to run */ public function db_drop_table_if_exists($table, $db) { return array('DROP TABLE IF EXISTS ' . $table); } /** * Determine whether the database is a flat file database, and thus not have a meaningful connect username and password. * * @return boolean Whether the database is a flat file database */ public function db_is_flat_file_simple() { return false; } /** * Encode a LIKE string comparision fragement for the database system. The pattern is a mixture of characters and ? and % wildcard symbols. * * @param string $pattern The pattern * @return string The encoded pattern */ public function db_encode_like($pattern) { return str_replace('\\\\_'/*MySQL escaped underscores*/, '\\_', $this->db_escape_string($pattern)); } /** * Close the database connections. We don't really need to close them (will close at exit), just disassociate so we can refresh them. */ public function db_close_connections() { $this->cache_db = array(); $this->last_select_db = null; } /** * Create an SQL cast. * * @param string $field The field identifier * @param string $type The type wanted * @set CHAR INT FLOAT * @return string The database type */ public function db_cast($field, $type) { switch ($type) { case 'CHAR': $_type = $type . '(20)'; break; case 'INT': $_type = 'SIGNED INTEGER'; break; case 'FLOAT': $_type = 'DECIMAL'; break; default: fatal_exit(do_lang_tempcode('INTERNAL_ERROR')); } return 'CAST(' . $field . ' AS ' . $_type . ')'; } /** * Fix a query so it runs on MySQL 8, by adding extra token escaping as required. * * @param string $query Query * @return string Fixed query */ protected function fix_mysql8_query($query) { if (preg_match('#(descriptions|groups|path)#i', $query) == 0) { return $query; } $new_query = ''; $tokens_to_escape = array_flip(array('description', 'groups', 'path')); $tokens = $this->tokenise_query($query); foreach ($tokens as $i => $token) { if (isset($tokens_to_escape[strtolower($token)])) { $new_query .= '`' . $token . '`'; } else { $new_query .= $token; } } return $new_query; } /** * Tokenise a MySQL query (assumes a basic syntax Composr is using). * * @param string $query Query * @return array The tokens */ protected function tokenise_query($query) { static $symbolic_tokens = null; if ($symbolic_tokens === null) { $symbolic_tokens = array_flip(array("\t", ' ', "\n", '+', '-', '*', '/', '<>', '>', '<', '>=', '<=', '=', '(', ')', ',')); } $i = 0; $query .= ' '; // Cheat so that we do not have to handle the end state differently $len = strlen($query); $tokens = array(); $current_token = ''; $doing_symbol_delimiter = true; while ($i < $len) { $next = $query[$i]; if ($next == "'") { if ($current_token != '') { $tokens[] = $current_token; } $current_token = ''; $i++; $backslash_mode = false; while ($i < $len) { $next = $query[$i]; if ($backslash_mode) { $current_token .= $next; $backslash_mode = false; } else { if ($next == '\\') { $current_token .= $next; $backslash_mode = true; } elseif ($next == "'") { $tokens[] = "'" . $current_token . "'"; break; } else { $current_token .= $next; } } $i++; } $current_token = ''; } elseif (($next == '/') && ($i + 1 < $len) && ($query[$i + 1] == '*')) { if ($current_token != '') { $tokens[] = $current_token; } $current_token = ''; $i += 2; while ($i < $len) { $next = $query[$i]; if (($next == '*') && ($i + 1 < $len) && ($query[$i + 1] == '/')) { $tokens[] = '/*' . $current_token . '*/'; $i++; break; } else { $current_token .= $next; } $i++; } $current_token = ''; } else { $symbol_delimiter_coming = ((isset($symbolic_tokens[$next])) && ((isset($symbolic_tokens[$next])) || (($i + 1 < $len) && (isset($symbolic_tokens[$next . $query[$i + 1]]))))); // (NB: symbol delimiters are a maximum of two in length) if ($symbol_delimiter_coming || $doing_symbol_delimiter) { if ($current_token != '') { $tokens[] = $current_token; } $current_token = $next; $doing_symbol_delimiter = (isset($symbolic_tokens[$next])); } else { $current_token .= $next; if ($doing_symbol_delimiter) { $doing_symbol_delimiter = isset($symbolic_tokens[$next]); } } } $i++; } return $tokens; } }