query_type = $query_type; $this->_query = $query; $this->parse_query(); switch ($this->query_type) { case 'truncate': $this->handle_truncate_query(); break; case 'alter': $this->handle_alter_query(); break; case 'create': $this->handle_create_query(); break; case 'describe': case 'desc': $this->handle_describe_query(); break; case 'show': $this->handle_show_query(); break; case 'showcolumns': $this->handle_show_columns_query(); break; case 'showindex': $this->handle_show_index(); break; case 'select': //$this->strip_backticks(); $this->handle_sql_count(); $this->rewrite_date_sub(); $this->delete_index_hints(); $this->rewrite_regexp(); //$this->rewrite_boolean(); $this->fix_date_quoting(); $this->rewrite_between(); break; case 'insert': //$this->safe_strip_backticks(); $this->execute_duplicate_key_update(); $this->rewrite_insert_ignore(); $this->rewrite_regexp(); $this->fix_date_quoting(); break; case 'update': //$this->safe_strip_backticks(); $this->rewrite_update_ignore(); // $this->_rewrite_date_sub(); $this->rewrite_limit_usage(); $this->rewrite_order_by_usage(); $this->rewrite_regexp(); $this->rewrite_between(); break; case 'delete': //$this->strip_backticks(); $this->rewrite_limit_usage(); $this->rewrite_order_by_usage(); $this->rewrite_date_sub(); $this->rewrite_regexp(); $this->delete_workaround(); break; case 'replace': //$this->safe_strip_backticks(); $this->rewrite_date_sub(); $this->rewrite_regexp(); break; case 'optimize': $this->rewrite_optimize(); break; case 'pragma': break; default: if (defined(WP_DEBUG) && WP_DEBUG) { break; } else { $this->return_true(); break; } } return $this->_query; } /** * Method to parse query string and determine which operation is needed. * * Remove backticks and change true/false values into 1/0. And determines * if rewriting CALC_FOUND_ROWS or ON DUPLICATE KEY UPDATE etc is needed. * * @access private */ private function parse_query() { $tokens = preg_split("/(\\\'|''|')/s", $this->_query, -1, PREG_SPLIT_DELIM_CAPTURE); $literal = false; $query_string = ''; foreach ($tokens as $token) { if ($token == "'") { if ($literal) { $literal = false; } else { $literal = true; } } else { if ($literal === false) { if (strpos($token, '`') !== false) { $token = str_replace('`', '', $token); } if (preg_match('/\\bTRUE\\b/i', $token)) { $token = str_ireplace('TRUE', '1', $token); } if (preg_match('/\\bFALSE\\b/i', $token)) { $token = str_ireplace('FALSE', '0', $token); } if (stripos($token, 'SQL_CALC_FOUND_ROWS') !== false) { $this->rewrite_calc_found = true; } if (stripos($token, 'ON DUPLICATE KEY UPDATE') !== false) { $this->rewrite_duplicate_key = true; } if (stripos($token, 'USE INDEX') !== false) { $this->rewrite_index_hint = true; } if (stripos($token, 'IGNORE INDEX') !== false) { $this->rewrite_index_hint = true; } if (stripos($token, 'FORCE INDEX') !== false) { $this->rewrite_index_hint = true; } if (stripos($token, 'BETWEEN') !== false) { $this->rewrite_between = true; } } } $query_string .= $token; } $this->_query = $query_string; } /** * method to handle SHOW TABLES query. * * @access private */ private function handle_show_query(){ $this->_query = str_ireplace(' FULL', '', $this->_query); $table_name = ''; $pattern = '/^\\s*SHOW\\s*TABLES\\s*.*?(LIKE\\s*(.*))$/im'; if (preg_match($pattern, $this->_query, $matches)) { $table_name = str_replace(array("'", ';'), '', $matches[2]); } if (!empty($table_name)) { $suffix = ' AND name LIKE '. "'" . $table_name . "'"; } else { $suffix = ''; } $this->_query = "SELECT name FROM sqlite_master WHERE type='table'" . $suffix . ' ORDER BY name DESC'; } /** * Method to strip all column qualifiers (backticks) from a query. * * All the back quotes in the query string are removed automatically. * So it must not be used when INSERT, UPDATE or REPLACE query is executed. * * Obsolite since 1.5.1 * * @access private */ private function strip_backticks(){ $this->_query = str_replace('`', '', $this->_query); } /** * Method to strip all column qualifiers (backticks) from a query except post data. * * All the back quotes execpt user data to be inserted are revomved automatically. * This method must be used when INSERT, UPDATE or REPLACE query is executed. * * Obsolite since 1.5.1 * * @access private */ private function safe_strip_backticks() { $query_string = ''; $tokens = preg_split("/(''|')/s", $this->_query, -1, PREG_SPLIT_DELIM_CAPTURE); $literal = false; $query_string = ''; foreach ($tokens as $token) { if ($token == "'") { if ($literal) { $literal = false; } else { $literal = true; } } else { if ($literal === false && strpos($token, '`') !== false) { $token = str_replace('`', '', $token); } } $query_string .= $token; } $this->_query = $query_string; } /** * Method to emulate the SQL_CALC_FOUND_ROWS placeholder for MySQL. * * This is a kind of tricky play. * 1. remove SQL_CALC_FOUND_ROWS option, and give it to the pdo engine * 2. make another $wpdb instance, and execute the rewritten query * 3. give the returned value (integer: number of the rows) to the original instance variable without LIMIT * * We no longer use SELECT COUNT query, because it returns the inexact values when used with WP_Meta_Query(). * * This kind of statement is required for WordPress to calculate the paging information. * see also WP_Query class in wp-includes/query.php * * @access private */ private function handle_sql_count(){ if (!$this->rewrite_calc_found) return; global $wpdb; // first strip the code. this is the end of rewriting process $this->_query = str_ireplace('SQL_CALC_FOUND_ROWS', '', $this->_query); // we make the data for next SELECE FOUND_ROWS() statement $unlimited_query = preg_replace('/\\bLIMIT\\s*.*/imsx', '', $this->_query); //$unlimited_query = preg_replace('/\\bGROUP\\s*BY\\s*.*/imsx', '', $unlimited_query); // we no longer use SELECT COUNT query //$unlimited_query = $this->_transform_to_count($unlimited_query); $_wpdb = new PDODB(); $result = $_wpdb->query($unlimited_query); $wpdb->dbh->found_rows_result = $result; $_wpdb = null; } /** * Call back method to change SELECT query to SELECT COUNT(). * * obsolite since version 1.5.1 * * @param string $query the query to be transformed * @return string the transformed query * @access private */ private function _transform_to_count($query){ $pattern = '/^\\s*SELECT\\s*(DISTINCT|)?.*?FROM\b/isx'; $_query = preg_replace($pattern, 'SELECT \\1 COUNT(*) FROM ', $query); return $_query; } /** * Method to rewrite INSERT IGNORE to INSERT OR IGNORE. * * @access private */ private function rewrite_insert_ignore(){ $this->_query = str_ireplace('INSERT IGNORE', 'INSERT OR IGNORE ', $this->_query); } /** * Method to rewrite UPDATE IGNORE to UPDATE OR IGNORE. * * @access private */ private function rewrite_update_ignore(){ $this->_query = str_ireplace('UPDATE IGNORE', 'UPDATE OR IGNORE ', $this->_query); } /** * Method to rewrite DATE_ADD() function. * * DATE_ADD has a parameter PHP function can't parse, so we quote the list and * pass it to the user defined function. * * @access private */ private function rewrite_date_add(){ //(date,interval expression unit) $pattern = '/\\s*date_add\\s*\(([^,]*),([^\)]*)\)/imsx'; if (preg_match($pattern, $this->_query, $matches)) { $expression = "'".trim($matches[2])."'"; $this->_query = preg_replace($pattern, " date_add($matches[1], $expression) ", $this->_query); } } /** * Method to rewrite DATE_SUB() function. * * DATE_SUB has a parameter PHP function can't parse, so we quote the list and * pass it to the user defined function. * * @access private */ private function rewrite_date_sub(){ //(date,interval expression unit) $pattern = '/\\s*date_sub\\s*\(([^,]*),([^\)]*)\)/imsx'; if (preg_match($pattern, $this->_query, $matches)) { $expression = "'".trim($matches[2])."'"; $this->_query = preg_replace($pattern, " date_sub($matches[1], $expression) ", $this->_query); } } /** * Method to handle CREATE query. * * If the query is CREATE query, it will be passed to the query_create.class.php. * So this method can't be used. It's here for safety. * * @access private */ private function handle_create_query(){ require_once PDODIR . 'query_create.class.php'; $engine = new CreateQuery(); $this->_query = $engine->rewrite_query($this->_query); $engine = null; } /** * Method to handle ALTER query. * * If the query is ALTER query, it will be passed ot the query_alter.class.php. * So this method can't be used. It is here for safety. * * @access private */ private function handle_alter_query(){ require_once PDODIR . 'query_alter.class.php'; $engine = new AlterQuery(); $this->_query = $engine->rewrite_query($this->_query, 'alter'); $engine = null; } /** * Method to handle DESCRIBE or DESC query. * * DESCRIBE is required for WordPress installation process. DESC is * an alias for DESCRIBE, but it is not used in core WordPress. * * @access private */ private function handle_describe_query(){ // $this->_query = "select 1=1"; $pattern = '/^\\s*(DESCRIBE|DESC)\\s*(.*)/i'; if (preg_match($pattern, $this->_query, $match)) { $tablename = preg_replace('/[\';]/', '', $match[2]); $this->_query = "PRAGMA table_info($tablename)"; } } /** * Method to remove LIMIT clause from DELETE or UPDATE query. * * The author of the original 'PDO for WordPress' says update method of wpdb * insists on adding LIMIT. But the newest version of WordPress doesn't do that. * Nevertheless some plugins use DELETE with LIMIT, UPDATE with LIMIT. * We need to exclude sub query's LIMIT. And if SQLite is compiled with * ENABLE_UPDATE_DELETE_LIMIT option, we don't remove it. * * @access private */ private function rewrite_limit_usage(){ $_wpdb = new PDODB(); $options = $_wpdb->get_results('PRAGMA compile_options'); foreach ($options as $opt) { if (stripos($opt->compile_option, 'ENABLE_UPDATE_DELETE_LIMIT') !== false) return; } if (stripos($this->_query, '(select') === false) { $this->_query = preg_replace('/\\s*LIMIT\\s*[0-9]$/i', '', $this->_query); } } /** * Method to remove ORDER BY clause from DELETE or UPDATE query. * * SQLite compiled without SQLITE_ENABLE_UPDATE_DELETE_LIMIT option can't * execute UPDATE with ORDER BY, DELETE with GROUP BY. * We need to exclude sub query's GROUP BY. * * @access private */ private function rewrite_order_by_usage() { $_wpdb = new PDODB(); $options = $_wpdb->get_results('PRAGMA compile_options'); foreach ($options as $opt) { if (stripos($opt->compile_option, 'ENABLE_UPDATE_DELETE_LIMIT') !== false) return; } if (stripos($this->_query, '(select') === false) { $this->_query = preg_replace('/\\s*ORDER\\s*BY\\s*.*$/i', '', $this->_query); } } /** * Method to handle TRUNCATE query. * * @access private */ private function handle_truncate_query(){ $pattern = '/TRUNCATE TABLE (.*)/im'; $this->_query = preg_replace($pattern, 'DELETE FROM $1', $this->_query); } /** * Method to handle OPTIMIZE query. * * Original query has the table names, but they are simply ignored. * Table names are meaningless in SQLite. * * @access private */ private function rewrite_optimize(){ $this->_query ="VACUUM"; } /** * Method to rewrite day. * * Jusitn Adie says: some wp UI interfaces (notably the post interface) * badly composes the day part of the date leading to problems in sqlite * sort ordering etc. * * I don't understand that... * * @return void * @access private */ private function rewrite_badly_formed_dates(){ $pattern = '/([12]\d{3,}-\d{2}-)(\d )/ims'; $this->_query = preg_replace($pattern, '${1}0$2', $this->_query); } /** * Method to remove INDEX HINT. * * @return void * @access private */ private function delete_index_hints(){ $pattern = '/\\s*(use|ignore|force)\\s+index\\s*\(.*?\)/i'; $this->_query = preg_replace($pattern, '', $this->_query); } /** * Method to fix the date string and quoting. * * This is required for the calendar widget. * * WHERE month(fieldname)=08 is converted to month(fieldname)='8' * WHERE month(fieldname)='08' is converted to month(fieldname)='8' * * I use preg_replace_callback instead of 'e' option because of security reason. * cf. PHP manual (regular expression) * * @return void * @access private */ private function fix_date_quoting() { $pattern = '/(month|year|second|day|minute|hour|dayofmonth)\\s*\((.*?)\)\\s*=\\s*["\']?(\d{1,4})[\'"]?\\s*/im'; $this->_query = preg_replace_callback($pattern, array($this, '_fix_date_quoting'), $this->_query); } /** * Call back method to rewrite date string. * * @param string $match * @return string * @access private */ private function _fix_date_quoting($match) { $fixed_val = "{$match[1]}({$match[2]})='" . intval($match[3]) . "' "; return $fixed_val; } /** * Method to rewrite REGEXP() function. * * This method changes function name to regexpp() and pass it to the user defined * function. * * @access private */ private function rewrite_regexp(){ $pattern = '/\s([^\s]*)\s*regexp\s*(\'.*?\')/im'; $this->_query = preg_replace($pattern, ' regexpp(\1, \2)', $this->_query); } /** * Method to rewrite boolean to number. * * SQLite doesn't support true/false type, so we need to convert them to 1/0. * * Obsolite since 1.5.1 * * @access private */ private function rewrite_boolean() { $query = str_ireplace('TRUE', "1", $this->_query); $query = str_ireplace('FALSE', "0", $query); $this->_query = $query; } /** * Method to handl SHOW COLUMN query. * * @access private */ private function handle_show_columns_query() { $this->_query = str_ireplace(' FULL', '', $this->_query); $pattern_like = '/^\\s*SHOW\\s*(COLUMNS|FIELDS)\\s*FROM\\s*(.*)?\\s*LIKE\\s*(.*)?/i'; $pattern = '/^\\s*SHOW\\s*(COLUMNS|FIELDS)\\s*FROM\\s*(.*)?/i'; if (preg_match($pattern_like, $this->_query, $matches)) { $table_name = str_replace("'", "", trim($matches[2])); $column_name = str_replace("'", "", trim($matches[3])); $query_string = "SELECT sql FROM sqlite_master WHERE tbl_name='$table_name' AND sql LIKE '%$column_name%'"; $this->_query = $query_string; } elseif (preg_match($pattern, $this->_query, $matches)) { $table_name = $matches[2]; $query_string = preg_replace($pattern, "PRAGMA table_info($table_name)", $this->_query); $this->_query = $query_string; } } /** * Method to handle SHOW INDEX query. * * Moved the WHERE clause manipulation to pdoengin.class.php (ver 1.3.1) * * @access private */ private function handle_show_index() { $pattern = '/^\\s*SHOW\\s*(?:INDEX|INDEXES|KEYS)\\s*FROM\\s*(\\w+)?/im'; if (preg_match($pattern, $this->_query, $match)) { $table_name = preg_replace("/[\';]/", '', $match[1]); $table_name = trim($table_name); $this->_query = "SELECT * FROM sqlite_master WHERE tbl_name='$table_name'"; } } /** * Method to handle ON DUPLICATE KEY UPDATE statement. * * First we use SELECT query and check if INSERT is allowed or not. * Rewriting procedure looks like a detour, but I've got no other ways. * * Added the literal check since the version 1.5.1. * * @return void * @access private */ private function execute_duplicate_key_update() { if (!$this->rewrite_duplicate_key) return; $unique_keys_for_cond = array(); $unique_keys_for_check = array(); $pattern = '/^\\s*INSERT\\s*INTO\\s*(\\w+)?\\s*(.*)\\s*ON\\s*DUPLICATE\\s*KEY\\s*UPDATE\\s*(.*)$/ims'; if (preg_match($pattern, $this->_query, $match_0)) { $table_name = trim($match_0[1]); $insert_data = trim($match_0[2]); $update_data = trim($match_0[3]); // prepare two unique key data for the table // 1. array('col1', 'col2, col3', etc) 2. array('col1', 'col2', 'col3', etc) $_wpdb = new PDODB(); $indexes = $_wpdb->get_results("SHOW INDEX FROM {$table_name}"); if (!empty($indexes)) { foreach ($indexes as $index) { if ($index->Non_unique == 0) { $unique_keys_for_cond[] = $index->Column_name; if (strpos($index->Column_name, ',') !== false) { $unique_keys_for_check = array_merge($unique_keys_for_check, explode(',', $index->Column_name)); } else { $unique_keys_for_check[] = $index->Column_name; } } } $unique_keys_for_check = array_map('trim', $unique_keys_for_check); } else { // Without unique key or primary key, UPDATE statement will affect all the rows! $query = 'INSERT INTO '.$table_name.' '.$insert_data; $this->_query = $query; $_wpdb = null; return; } // data check if (preg_match('/^\((.*)\)\\s*VALUES\\s*\((.*)\)$/ims', $insert_data, $match_1)) { $col_array = explode(',', $match_1[1]); $ins_data_array = explode(',', $match_1[2]); foreach ($col_array as $col) { $val = trim(array_shift($ins_data_array)); $ins_data_assoc[trim($col)] = $val; } // $ins_data_assoc = array_combine($col_array, $ins_array); $condition = ''; foreach ($unique_keys_for_cond as $unique_key) { if (strpos($unique_key, ',') !== false) { $unique_key_array = explode(',', $unique_key); $counter = count($unique_key_array); for ($i = 0; $i < $counter; ++$i) { $col = trim($unique_key_array[$i]); if (isset($ins_data_assoc[$col]) && $i == $counter - 1) { $condition .= $col . '=' . $ins_data_assoc[$col] . ' OR '; } elseif (isset($ins_data_assoc[$col])) { $condition .= $col . '=' . $ins_data_assoc[$col] . ' AND '; } else { continue; } } // $condition = rtrim($condition, ' AND '); } else { $col = trim($unique_key); if (isset($ins_data_assoc[$col])) { $condition .= $col . '=' . $ins_data_assoc[$col] . ' OR '; } else { continue; } } } $condition = rtrim($condition, ' OR '); $test_query = "SELECT * FROM {$table_name} WHERE {$condition}"; $results = $_wpdb->query($test_query); $_wpdb = null; if ($results == 0) { $this->_query = 'INSERT INTO '.$table_name.' '.$insert_data; return; } else { // change (col, col...) values (data, data...) to array(col=>data, col=>data...) if (preg_match('/^\((.*)\)\\s*VALUES\\s*\((.*)\)$/im', $insert_data, $match_2)) { $col_array = explode(',', $match_2[1]); $ins_array = explode(',', $match_2[2]); $count = count($col_array); for ($i = 0; $i < $count; $i++) { $col = trim($col_array[$i]); $val = trim($ins_array[$i]); $ins_array_assoc[$col] = $val; } } // change col = data, col = data to array(col=>data, col=>data) // some plugins have semi-colon at the end of the query $update_data = rtrim($update_data, ';'); $tmp_array = explode(',', $update_data); foreach ($tmp_array as $pair) { list($col, $value) = explode('=', $pair); $col = trim($col); $value = trim($value); $update_array_assoc[$col] = $value; } // change array(col=>values(col)) to array(col=>data) foreach ($update_array_assoc as $key => &$value) { if (preg_match('/^VALUES\\s*\((.*)\)$/im', $value, $match_3)) { $col = trim($match_3[1]); $value = $ins_array_assoc[$col]; } } foreach ($ins_array_assoc as $key => $val) { if (in_array($key, $unique_keys_for_check)) { $where_array[] = $key . '=' . $val; } } $update_strings = ''; foreach ($update_array_assoc as $key => $val) { if (in_array($key, $unique_keys_for_check)) { $where_array[] = $key . '=' . $val; } else { $update_strings .= $key . '=' . $val . ','; } } $update_strings = rtrim($update_strings, ','); $unique_where = array_unique($where_array, SORT_REGULAR); $where_string = ' WHERE ' . implode(' AND ', $unique_where); // $where_string = ' WHERE ' . rtrim($where_string, ','); $update_query = 'UPDATE ' . $table_name . ' SET ' . $update_strings . $where_string; $this->_query = $update_query; } } } // else { // $pattern = '/ ON DUPLICATE KEY UPDATE.*$/im'; // $replace_query = preg_replace($pattern, '', $this->_query); // $replace_query = str_ireplace('INSERT ', 'INSERT OR REPLACE ', $replace_query); // $this->_query = $replace_query; // } } /** * Method to rewrite BETWEEN A AND B clause. * * This clause is the same form as natural language, so we have to check if it is * in the data or SQL statement. * * @access private */ private function rewrite_between() { if (!$this->rewrite_between) return; $pattern = '/\\s*(CAST\(.+?\)|[^\\s\(]*)?\\s*BETWEEN\\s*([^\\s]*)?\\s*AND\\s*([^\\s\)]*)?\\s*/ims'; while (preg_match($pattern, $this->_query, $match)) { $column_name = trim($match[1]); $min_value = trim($match[2]); $max_value = trim($match[3]); $max_value = rtrim($max_value); $replacement = " $column_name >= $min_value AND $column_name <= $max_value"; $this->_query = str_ireplace($match[0], $replacement, $this->_query); } } /** * Method to avoid DELETE with JOIN statement. * * wp-admin/includes/upgrade.php contains 'DELETE ... JOIN' statement. * This query can't be replaced with regular expression or udf, so we * replace all the statement with another. But this query was used in * the very old version of WordPress when it was upgraded. So we won't * have no chance that this method should be used. * * @access private */ private function delete_workaround() { global $wpdb; // $pattern = "DELETE o1 FROM $wpdb->options AS o1 JOIN $wpdb->options AS o2 USING (option_name) WHERE o2.option_id > o1.option_id"; $pattern = "DELETE o1 FROM $wpdb->options AS o1 JOIN $wpdb->options AS o2"; $rewritten = "DELETE FROM $wpdb->options WHERE option_id IN (SELECT MIN(option_id) FROM $wpdb->options GROUP BY option_name HAVING COUNT(*) > 1)"; if (stripos($this->_query, $pattern) !== false) { $this->_query = $rewritten; } } /** * Method to suppress errors. * * When the query string is the one that this class can't manipulate, * the query string is replaced with the one that always returns true * and does nothing. * * @access private */ private function return_true() { $this->_query = 'SELECT 1=1'; } } ?>