sqlite-integration/pdoengine.class.php

1379 lines
40 KiB
PHP

<?php
/**
* This file defines PDOEngine class.
*
* @package SQLite Integration
* @author Kojima Toshiyasu
*
*/
/**
* This class extends PDO class and does the real work.
*
* It accepts a request from wpdb class, initialize PDO instance,
* execute SQL statement, and returns the results to WordPress.
*/
class PDOEngine extends PDO {
/**
* Class variable to check if there is an error.
*
* @var boolean
*/
public $is_error = false;
/**
* Class variable which is used for CALC_FOUND_ROW query.
*
* @var unsigned integer
*/
public $found_rows_result = null;
/**
* Class variable used for query with ORDER BY FIELD()
*
* @var array of the object
*/
public $pre_ordered_results = null;
/**
* Class variable to store the rewritten queries.
*
* @var array
* @access private
*/
private $rewritten_query;
/**
* Class variable to have what kind of query to execute.
*
* @var string
* @access private
*/
private $query_type;
/**
* Class variable to store the result of the query.
*
* @var reference to the PHP object
* @access private
*/
private $results = null;
/**
* Class variable to store the results of the query.
*
* This is for the backward compatibility.
*
* @var reference to the PHP object
* @access private
*/
private $_results = null;
/**
* Class variable to reference to the PDO instance.
*
* @var PDO object
* @access private
*/
private $pdo;
/**
* Class variable to store the query string prepared to execute.
*
* @var string|array
*/
private $prepared_query;
/**
* Class variable to store the values in the query string.
*
* @var array
* @access private
*/
private $extracted_variables = array();
/**
* Class variable to store the error messages.
*
* @var array
* @access private
*/
private $error_messages = array();
/**
* Class variable to store the file name and function to cause error.
*
* @var array
* @access private
*/
private $errors;
/**
* Class variable to store the query strings.
*
* @var array
*/
public $queries = array();
/**
* Class variable to store the affected row id.
*
* @var unsigned integer
* @access private
*/
private $last_insert_id;
/**
* Class variable to store the number of rows affected.
*
* @var unsigned integer
*/
private $affected_rows;
/**
* Class variable to store the queried column info.
*
* @var array
*/
private $column_data;
/**
* Variable to emulate MySQL affected row.
*
* @var integer
*/
private $num_rows;
/**
* Return value from query().
*
* Each query has its own return value.
*
* @var mixed
*/
private $return_value;
/**
* Variable to determine which insert query to use.
*
* Whether VALUES clause in the INSERT query can take multiple values or not
* depends on the version of SQLite library. We check the version and set
* this varable to true or false.
*
* @var boolean
*/
private $can_insert_multiple_rows = false;
/**
*
* @var integer
*/
private $param_num;
/**
* Varible to check if there is an active transaction.
* @var boolean
* @access protected
*/
protected $has_active_transaction = false;
/**
* Constructor
*
* Create PDO object, set user defined functions and initialize other settings.
* Don't use parent::__construct() because this class does not only returns
* PDO instance but many others jobs.
*
* Constructor definition is changed since version 1.7.1.
*
* @param none
*/
function __construct() {
register_shutdown_function(array($this, '__destruct'));
$dsn = 'sqlite' . FQDB;
if (isset($GLOBALS['@pdo'])) {
$this->pdo = $GLOBALS['@pdo'];
} else {
$locked = false;
$status = 0;
do {
try {
$this->pdo = new PDO($dsn, null, null, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
require_once UDF_FILE;
new PDOSQLiteUDFS($this->pdo);
$GLOBALS['@pdo'] = $this->pdo;
} catch (PDOException $ex) {
$status = $ex->getCode();
if ($status == 5 || $status == 6) {
$locked = true;
} else {
$err_message = $ex->getMessage();
}
}
} while ($locked);
if ($status > 0) {
$message = 'Database initialization error!<br />' .
'Code: ' . $status . '<br />Error Message: ' . $err_message;
$this->set_error(__LINE__, __FILE__, $message);
return false;
}
}
$this->init();
}
/**
* Destructor
*
* If SQLITE_MEM_DEBUG constant is defined, append information about
* memory usage into database/mem_debug.txt.
*
* This definition is changed since version 1.7.
*
* @return boolean
*/
function __destruct() {
if (defined('SQLITE_MEM_DEBUG') && SQLITE_MEM_DEBUG) {
$max = ini_get('memory_limit');
if (is_null($max)) {
$message = sprintf("[%s] Memory_limit is not set in php.ini file.", date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']));
file_put_contents(FQDBDIR . 'mem_debug.txt', $message, FILE_APPEND);
return true;
}
if (stripos($max, 'M') !== false) {
$max = (int) $max * 1024 * 1024;
}
$peak = memory_get_peak_usage(true);
$used = round((int) $peak / (int) $max * 100, 2);
if ($used > 90) {
$message = sprintf("[%s] Memory peak usage warning: %s %% used. (max: %sM, now: %sM)\n", date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']), $used, $max, $peak);
file_put_contents(FQDBDIR . 'mem_debug.txt', $message, FILE_APPEND);
}
}
//$this->pdo = null;
return true;
}
/**
* Method to initialize database, executed in the contructor.
*
* It checks if WordPress is in the installing process and does the required
* jobs. SQLite library version specific settings are also in this function.
*
* Some developers use WP_INSTALLING constant for other purposes, if so, this
* function will do no harms.
*
* @return boolean
*/
private function init() {
if (defined('WP_INSTALLING') && WP_INSTALLING) {
$this->prepare_directory();
$statement = $this->pdo->query("SELECT COUNT(*) FROM sqlite_master WHERE type='table'");
$number_of_tables = $statement->fetchColumn(0);
$statement = null;
if ($number_of_tables == 0) $this->make_sqlite_tables();
}
if (version_compare($this->get_sqlite_version(), '3.7.11', '>=')) {
$this->can_insert_multiple_rows = true;
}
$statement = $this->pdo->query('PRAGMA foreign_keys');
if ($statement->fetchColumn(0) == '0') $this->pdo->query('PRAGMA foreign_keys = ON');
}
/**
* This method makes database direcotry and .htaccess file.
*
* It is executed only once when the installation begins.
*/
private function prepare_directory() {
global $wpdb;
$u = umask(0000);
if (!is_dir(FQDBDIR)) {
if (!@mkdir(FQDBDIR, 0707, true)) {
umask($u);
$message = 'Unable to create the required directory! Please check your server settings.';
wp_die($message, 'Error!');
}
}
if (!is_writable(FQDBDIR)) {
umask($u);
$message = 'Unable to create a file in the directory! Please check your server settings.';
wp_die($message, 'Error!');
}
if (!is_file(FQDBDIR . '.htaccess')) {
$fh = fopen(FQDBDIR . '.htaccess', "w");
if (!$fh) {
umask($u);
$message = 'Unable to create a file in the directory! Please check your server settings.';
echo $message;
return false;
}
fwrite($fh, 'DENY FROM ALL');
fclose($fh);
}
if (!is_file(FQDBDIR . 'index.php')) {
$fh = fopen(FQDBDIR . 'index.php', "w");
if (!$fh) {
umask($u);
$message = 'Unable to create a file in the directory! Please check your server settings.';
echo $message;
return false;
}
fwrite($fh, '<?php // Silence is gold. ?>');
fclose($fh);
}
umask($u);
return true;
}
/**
* Method to call install() function which overrides WordPress install().
*
* This function is executed only once during the installation process.
*/
private function make_sqlite_tables() {
require_once PDODIR . 'install.php';
}
/**
* Method to execute query().
*
* Divide the query types into seven different ones. That is to say:
*
* 1. SELECT SQL_CALC_FOUND_ROWS
* 2. INSERT
* 3. CREATE TABLE(INDEX)
* 4. ALTER TABLE
* 5. SHOW VARIABLES
* 6. DROP INDEX
* 7. THE OTHERS
*
* #1 is just a tricky play. See the private function handle_sql_count() in query.class.php.
* From #2 through #5 call different functions respectively.
* #6 call the ALTER TABLE query.
* #7 is a normal process: sequentially call prepare_query() and execute_query().
*
* #1 process has been changed since version 1.5.1.
*
* @param $query full SQL statement string
* @return mixed according to the query type
* @see PDO::query()
*/
public function query($query) {
$this->flush();
$this->queries[] = "Raw query:\n$query";
$res = $this->determine_query_type($query);
if (!$res && defined(PDO_DEBUG) && PDO_DEBUG) {
$bailoutString = sprintf(__("<h1>Unknown query type</h1><p>Sorry, we cannot determine the type of query that is requested.</p><p>The query is %s</p>", 'sqlite-integration'), $query);
$this->set_error(__LINE__, __FUNCTION__, $bailoutString);
}
switch (strtolower($this->query_type)) {
case 'set':
$this->return_value = false;
break;
case 'foundrows':
$_column = array('FOUND_ROWS()' => '');
$column = array();
if (!is_null($this->found_rows_result)) {
$this->num_rows = $this->found_rows_result;
$_column['FOUND_ROWS()'] = $this->num_rows;
//foreach ($this->found_rows_result[0] as $key => $value) {
//$_column['FOUND_ROWS()'] = $value;
//}
$column[] = new ObjectArray($_column);
$this->results = $column;
$this->found_rows_result = null;
}
break;
case 'insert':
if ($this->can_insert_multiple_rows) {
$this->execute_insert_query_new($query);
} else {
$this->execute_insert_query($query);
}
break;
case 'create':
$result = $this->execute_create_query($query);
$this->return_value = $result;
break;
case 'alter':
$result = $this->execute_alter_query($query);
$this->return_value = $result;
break;
case 'show_variables':
$result = $this->show_variables_workaround($query);
break;
case 'showstatus':
$result = $this->show_status_workaround($query);
break;
case 'drop_index':
$pattern = '/^\\s*(DROP\\s*INDEX\\s*.*?)\\s*ON\\s*(.*)/im';
if (preg_match($pattern, $query, $match)) {
$drop_query = 'ALTER TABLE ' . trim($match[2]) . ' ' . trim($match[1]);
$this->query_type = 'alter';
$result = $this->execute_alter_query($drop_query);
$this->return_value = $result;
} else {
$this->return_value = false;
}
break;
default:
$engine = $this->prepare_engine($this->query_type);
$this->rewritten_query = $engine->rewrite_query($query, $this->query_type);
if (!is_null($this->pre_ordered_results)) {
$this->results = $this->pre_ordered_results;
$this->num_rows = $this->return_value = count($this->results);
$this->pre_ordered_results = null;
break;
}
$this->queries[] = "Rewritten:\n$this->rewritten_query";
$this->extract_variables();
$statement = $this->prepare_query();
$this->execute_query($statement);
if (!$this->is_error) {
$this->process_results($engine);
} else {// Error
;
}
break;
}
if (defined('PDO_DEBUG') && PDO_DEBUG === true) {
file_put_contents(FQDBDIR . 'debug.txt', $this->get_debug_info(), FILE_APPEND);
}
return $this->return_value;
}
/**
* Method to return inserted row id.
*
* @return unsigned integer
*/
public function get_insert_id() {
return $this->last_insert_id;
}
/**
* Method to return the number of rows affected.
*
* @return unsigned integer
*/
public function get_affected_rows() {
return $this->affected_rows;
}
/**
* Method to return the queried column names.
*
* These data are meaningless for SQLite. So they are dummy emulating
* MySQL columns data.
*
* @return array of the object
*/
public function get_columns() {
if (!empty($this->results)) {
$primary_key = array(
'meta_id', 'comment_ID', 'link_ID', 'option_id',
'blog_id', 'option_name', 'ID', 'term_id', 'object_id',
'term_taxonomy_id', 'umeta_id', 'id');
$unique_key = array('term_id', 'taxonomy', 'slug');
$data = array(
'name' => '', // column name
'table' => '', // table name
'max_length' => 0, // max length of the column
'not_null' => 1, // 1 if not null
'primary_key' => 0, // 1 if column has primary key
'unique_key' => 0, // 1 if column has unique key
'multiple_key' => 0, // 1 if column doesn't have unique key
'numeric' => 0, // 1 if column has numeric value
'blob' => 0, // 1 if column is blob
'type' => '', // type of the column
'unsigned' => 0, // 1 if column is unsigned integer
'zerofill' => 0 // 1 if column is zero-filled
);
if (preg_match("/\s*FROM\s*(.*)?\s*/i", $this->rewritten_query, $match)) {
$table_name = trim($match[1]);
} else {
$table_name = '';
}
foreach ($this->results[0] as $key => $value) {
$data['name'] = $key;
$data['table'] = $table_name;
if (in_array($key, $primary_key)) {
$data['primary_key'] = 1;
} elseif (in_array($key, $unique_key)) {
$data['unique_key'] = 1;
} else {
$data['multiple_key'] = 1;
}
$this->column_data[] = new ObjectArray($data);
$data['name'] = '';
$data['table'] = '';
$data['primary_key'] = 0;
$data['unique_key'] = 0;
$data['multiple_key'] = 0;
}
return $this->column_data;
} else {
return null;
}
}
/**
* Method to return the queried result data.
*
* @return mixed
*/
public function get_query_results() {
return $this->results;
}
/**
* Method to return the number of rows from the queried result.
*
* @return unsigned integer
*/
public function get_num_rows() {
return $this->num_rows;
}
/**
* Method to return the queried results according to the query types.
*
* @return mixed
*/
public function get_return_value() {
return $this->return_value;
}
/**
* Method to return error messages.
*
* @return string
*/
public function get_error_message(){
if (count($this->error_messages) === 0){
$this->is_error = false;
$this->error_messages = array();
return '';
}
$output = '<div style="clear:both">&nbsp;</div>';
if ($this->is_error === false){
//return $output;
return '';
}
$output .= "<div class=\"queries\" style=\"clear:both; margin_bottom:2px; border: red dotted thin;\">Queries made or created this session were<br/>\r\n\t<ol>\r\n";
foreach ($this->queries as $q){
$output .= "\t\t<li>".$q."</li>\r\n";
}
$output .= "\t</ol>\r\n</div>";
foreach ($this->error_messages as $num=>$m){
$output .= "<div style=\"clear:both; margin_bottom:2px; border: red dotted thin;\" class=\"error_message\" style=\"border-bottom:dotted blue thin;\">Error occurred at line {$this->errors[$num]['line']} in Function {$this->errors[$num]['function']}. <br/> Error message was: $m </div>";
}
ob_start();
debug_print_backtrace();
$output .= '<pre>' . ob_get_contents() . '</pre>';
ob_end_clean();
return $output;
}
/**
* Method to return information about query string for debugging.
*
* @return string
*/
private function get_debug_info(){
$output = '';
foreach ($this->queries as $q){
$output .= $q ."\n";
}
return $output;
}
/**
* Method to clear previous data.
*/
private function flush(){
$this->rewritten_query = '';
$this->query_type = '';
$this->results = null;
$this->_results = null;
$this->last_insert_id = null;
$this->affected_rows = null;
$this->column_data = array();
$this->num_rows = null;
$this->return_value = null;
$this->extracted_variables = array();
$this->error_messages = array();
$this->is_error = false;
$this->queries = array();
$this->param_num = 0;
}
/**
* Method to include the apropreate class files.
*
* It is not a good habit to change the include files programatically.
* Needs to be fixed some other way.
*
* @param string $query_type
* @return object reference to apropreate driver
*/
private function prepare_engine($query_type = null) {
if (stripos($query_type, 'create') !== false) {
require_once PDODIR . 'query_create.class.php';
$engine = new CreateQuery();
} elseif (stripos($query_type, 'alter') !== false) {
require_once PDODIR . 'query_alter.class.php';
$engine = new AlterQuery();
} else {
require_once PDODIR . 'query.class.php';
$engine = new PDOSQLiteDriver();
}
return $engine;
}
/**
* Method to create a PDO statement object from the query string.
*
* @return PDOStatement
*/
private function prepare_query(){
$this->queries[] = "Prepare:\n" . $this->prepared_query;
$reason = 0;
$message = '';
$statement = null;
do {
try {
$statement = $this->pdo->prepare($this->prepared_query);
} catch (PDOException $err) {
$reason = $err->getCode();
$message = $err->getMessage();
}
} while (5 == $reason || 6 == $reason);
if ($reason > 0){
$err_message = sprintf("Problem preparing the PDO SQL Statement. Error was: %s", $message);
$this->set_error(__LINE__, __FUNCTION__, $err_message);
}
return $statement;
}
/**
* Method to execute PDO statement object.
*
* This function executes query and sets the variables to give back to WordPress.
* The variables are class fields. So if success, no return value. If failure, it
* returns void and stops.
*
* @param object $statement of PDO statement
* @return boolean
*/
private function execute_query($statement) {
$reason = 0;
$message = '';
if (!is_object($statement))
return false;
if (count($this->extracted_variables) > 0) {
$this->queries[] = "Executing:\n" . var_export($this->extracted_variables, true);
do {
if ($this->query_type == 'update' || $this->query_type == 'replace') {
try {
$this->beginTransaction();
$statement->execute($this->extracted_variables);
$this->commit();
} catch (PDOException $err) {
$reason = $err->getCode();
$message = $err->getMessage();
$this->rollBack();
}
} else {
try {
$statement->execute($this->extracted_variables);
} catch (PDOException $err) {
$reason = $err->getCode();
$message = $err->getMessage();
}
}
} while (5 == $reason || 6 == $reason);
} else {
$this->queries[] = 'Executing: (no parameters)';
do{
if ($this->query_type == 'update' || $this->query_type == 'replace') {
try {
$this->beginTransaction();
$statement->execute();
$this->commit();
} catch (PDOException $err) {
$reason = $err->getCode();
$message = $err->getMessage();
$this->rollBack();
}
} else {
try {
$statement->execute();
} catch (PDOException $err) {
$reason = $err->getCode();
$message = $err->getMessage();
}
}
} while (5 == $reason || 6 == $reason);
}
if ($reason > 0) {
$err_message = sprintf("Error while executing query! Error message was: %s", $message);
$this->set_error(__LINE__, __FUNCTION__, $err_message);
return false;
} else {
$this->_results = $statement->fetchAll(PDO::FETCH_OBJ);
}
//generate the results that $wpdb will want to see
switch ($this->query_type) {
case 'insert':
case 'update':
case 'replace':
$this->last_insert_id = $this->pdo->lastInsertId();
$this->affected_rows = $statement->rowCount();
$this->return_value = $this->affected_rows;
break;
case 'select':
case 'show':
case 'showcolumns':
case 'showindex':
case 'describe':
case 'desc':
case 'check':
case 'analyze':
//case "foundrows":
$this->num_rows = count($this->_results);
$this->return_value = $this->num_rows;
break;
case 'delete':
$this->affected_rows = $statement->rowCount();
$this->return_value = $this->affected_rows;
break;
case 'alter':
case 'drop':
case 'create':
case 'optimize':
case 'truncate':
if ($this->is_error) {
$this->return_value = false;
} else {
$this->return_value = true;
}
break;
}
}
/**
* Method to extract field data to an array and prepare the query statement.
*
* If original SQL statement is CREATE query, this function do nothing and return
* true. This returned value is not used.
*
* @return boolean
*/
private function extract_variables() {
if ($this->query_type == 'create') {
$this->prepared_query = $this->rewritten_query;
return true;
}
//long queries can really kill this
$pattern = '/(?<!\\\\)([\'"])(.*?)(?<!\\\\)\\1/imsx';
$_limit = $limit = ini_get('pcre.backtrack_limit');
// if user's setting is more than default * 10, make PHP do the job.
if ($limit > 10000000) {
$query = preg_replace_callback($pattern, array($this, 'replace_variables_with_placeholders'), $this->rewritten_query);
} else {
do {
if ($limit > 10000000) {
$message = 'The query is too big to parse properly';
$this->set_error(__LINE__, __FUNCTION__, $message);
break; //no point in continuing execution, would get into a loop
} else {
ini_set('pcre.backtrack_limit', $limit);
$query = preg_replace_callback($pattern, array($this,'replace_variables_with_placeholders'), $this->rewritten_query);
}
$limit = $limit * 10;
} while (is_null($query));
//reset the pcre.backtrack_limist
ini_set('pcre.backtrack_limit', $_limit);
}
$this->queries[] = "With Placeholders:\n" . $query;
$this->prepared_query = $query;
}
/**
* Call back function to replace field data with PDO parameter.
*
* @param string $matches
* @return string
*/
private function replace_variables_with_placeholders($matches) {
//remove the wordpress escaping mechanism
$param = stripslashes($matches[0]);
//remove trailing spaces
$param = trim($param);
//remove the quotes at the end and the beginning
if (in_array($param{strlen($param)-1}, array("'",'"'))) {
$param = substr($param,0,-1) ;//end
}
if (in_array($param{0}, array("'",'"'))) {
$param = substr($param, 1); //start
}
//$this->extracted_variables[] = $param;
$key = ':param_'.$this->param_num++;
$this->extracted_variables[] = $param;
//return the placeholder
//return ' ? ';
return ' '.$key.' ';
}
/**
* Method to determine which query type the argument is.
*
* It takes the query string ,determines the type and returns the type string.
* If the query is the type that SQLite Integration can't executes, returns false.
*
* @param string $query
* @return boolean|string
*/
private function determine_query_type($query) {
$result = preg_match('/^\\s*(SET|EXPLAIN|PRAGMA|SELECT\\s*FOUND_ROWS|SELECT|INSERT|UPDATE|REPLACE|DELETE|ALTER|CREATE|DROP\\s*INDEX|DROP|SHOW\\s*\\w+\\s*\\w+\\s*|DESCRIBE|DESC|TRUNCATE|OPTIMIZE|CHECK|ANALYZE)/i', $query, $match);
if (!$result) {
return false;
}
$this->query_type = strtolower($match[1]);
if (stripos($this->query_type, 'found') !== false) {
$this->query_type = 'foundrows';
}
if (stripos($this->query_type, 'show') !== false) {
if (stripos($this->query_type, 'show table status') !== false) {
$this->query_type = 'showstatus';
} elseif (stripos($this->query_type, 'show tables') !== false || stripos($this->query_type, 'show full tables') !== false) {
$this->query_type = 'show';
} elseif (stripos($this->query_type, 'show columns') !== false || stripos($this->query_type, 'show fields') !== false || stripos($this->query_type, 'show full columns') !== false) {
$this->query_type = 'showcolumns';
} elseif (stripos($this->query_type, 'show index') !== false || stripos($this->query_type, 'show indexes') !== false || stripos($this->query_type, 'show keys') !== false) {
$this->query_type = 'showindex';
} elseif (stripos($this->query_type, 'show variables') !== false || stripos($this->query_type, 'show global variables') !== false || stripos($this->query_type, 'show session variables') !== false) {
$this->query_type = 'show_variables';
} else {
return false;
}
}
if (stripos($this->query_type, 'drop index') !== false) {
$this->query_type = 'drop_index';
}
return true;
}
/**
* Method to execute INSERT query for SQLite version 3.7.11 or later.
*
* SQLite version 3.7.11 began to support multiple rows insert with values
* clause. This is for that version or later.
*
* @param string $query
*/
private function execute_insert_query_new($query) {
$engine = $this->prepare_engine($this->query_type);
$this->rewritten_query = $engine->rewrite_query($query, $this->query_type);
$this->queries[] = "Rewritten:\n" . $this->rewritten_query;
$this->extract_variables();
$statement = $this->prepare_query();
$this->execute_query($statement);
}
/**
* Method to execute INSERT query for SQLite version 3.7.10 or lesser.
*
* It executes the INSERT query for SQLite version 3.7.10 or lesser. It is
* necessary to rewrite multiple row values.
*
* @param string $query
*/
private function execute_insert_query($query) {
global $wpdb;
$multi_insert = false;
$statement = null;
$engine = $this->prepare_engine($this->query_type);
if (preg_match('/(INSERT.*?VALUES\\s*)(\(.*\))/imsx', $query, $matched)) {
$query_prefix = $matched[1];
$values_data = $matched[2];
if (stripos($values_data, 'ON DUPLICATE KEY') !== false) {
$exploded_parts = $values_data;
} elseif (stripos($query_prefix, "INSERT INTO $wpdb->comments") !== false) {
$exploded_parts = $values_data;
} else {
$exploded_parts = $this->parse_multiple_inserts($values_data);
}
$count = count($exploded_parts);
if ($count > 1) {
$multi_insert = true;
}
}
if ($multi_insert) {
$first = true;
foreach ($exploded_parts as $value) {
if (substr($value, -1, 1) === ')') {
$suffix = '';
} else {
$suffix = ')';
}
$query_string = $query_prefix . ' ' . $value . $suffix;
$this->rewritten_query = $engine->rewrite_query($query_string, $this->query_type);
$this->queries[] = "Rewritten:\n" . $this->rewritten_query;
$this->extracted_variables = array();
$this->extract_variables();
if ($first) {
$statement = $this->prepare_query();
$this->execute_query($statement);
$first = false;
} else {
$this->execute_query($statement);
}
}
} else {
$this->rewritten_query = $engine->rewrite_query($query, $this->query_type);
$this->queries[] = "Rewritten:\n" . $this->rewritten_query;
$this->extract_variables();
$statement = $this->prepare_query();
$this->execute_query($statement);
}
}
/**
* Method to help rewriting multiple row values insert query.
*
* It splits the values clause into an array to execute separately.
*
* @param string $values
* @return array
*/
private function parse_multiple_inserts($values) {
$tokens = preg_split("/(''|(?<!\\\\)'|(?<!\()\),(?=\s*\())/s", $values, -1, PREG_SPLIT_DELIM_CAPTURE);
$exploded_parts = array();
$part = '';
$literal = false;
foreach ($tokens as $token) {
switch ($token) {
case "),":
if (!$literal) {
$exploded_parts[] = $part;
$part = '';
} else {
$part .= $token;
}
break;
case "'":
if ($literal) {
$literal = false;
} else {
$literal = true;
}
$part .= $token;
break;
default:
$part .= $token;
break;
}
}
if (!empty($part)) {
$exploded_parts[] = $part;
}
return $exploded_parts;
}
/**
* Method to execute CREATE query.
*
* @param string
* @return boolean
*/
private function execute_create_query($query) {
$engine = $this->prepare_engine($this->query_type);
$rewritten_query = $engine->rewrite_query($query);
$reason = 0;
$message = '';
//$queries = explode(";", $this->rewritten_query);
try {
$this->beginTransaction();
foreach ($rewritten_query as $single_query) {
$this->queries[] = "Executing:\n" . $single_query;
$single_query = trim($single_query);
if (empty($single_query)) continue;
$this->pdo->exec($single_query);
}
$this->commit();
} catch (PDOException $err) {
$reason = $err->getCode();
$message = $err->getMessage();
if (5 == $reason || 6 == $reason) {
$this->commit();
} else {
$this->rollBack();
}
}
if ($reason > 0) {
$err_message = sprintf("Problem in creating table or index. Error was: %s", $message);
$this->set_error(__LINE__, __FUNCTION__, $err_message);
return false;
}
return true;
}
/**
* Method to execute ALTER TABLE query.
*
* @param string
* @return boolean
*/
private function execute_alter_query($query) {
$engine = $this->prepare_engine($this->query_type);
$reason = 0;
$message = '';
$re_query = '';
$rewritten_query = $engine->rewrite_query($query, $this->query_type);
if (is_array($rewritten_query) && array_key_exists('recursion', $rewritten_query)) {
$re_query = $rewritten_query['recursion'];
unset($rewritten_query['recursion']);
}
try {
$this->beginTransaction();
if (is_array($rewritten_query)) {
foreach ($rewritten_query as $single_query) {
$this->queries[] = "Executing:\n" . $single_query;
$single_query = trim($single_query);
if (empty($single_query)) continue;
$this->pdo->exec($single_query);
}
} else {
$this->queries[] = "Executing:\n" . $rewritten_query;
$rewritten_query = trim($rewritten_query);
$this->pdo->exec($rewritten_query);
}
$this->commit();
} catch (PDOException $err) {
$reason = $err->getCode();
$message = $err->getMessage();
if (5 == $reason || 6 == $reason) {
$this->commit();
usleep(10000);
} else {
$this->rollBack();
}
}
if ($re_query != '') {
$this->query($re_query);
}
if ($reason > 0) {
$err_message = sprintf("Problem in executing alter query. Error was: %s", $message);
$this->set_error(__LINE__, __FUNCTION__, $err_message);
return false;
}
return true;
}
/**
* Method to execute SHOW VARIABLES query
*
* This query is meaningless for SQLite. This function returns null data with some
* exceptions and only avoids the error message.
*
* @param string
* @return ObjectArray
*/
private function show_variables_workaround($query) {
$dummy_data = array('Variable_name' => '', 'Value' => null);
$pattern = '/SHOW\\s*VARIABLES\\s*LIKE\\s*(.*)?$/im';
if (preg_match($pattern, $query, $match)) {
$value = str_replace("'", '', $match[1]);
$dummy_data['Variable_name'] = trim($value);
// this is set for Wordfence Security Plugin
if ($value == 'max_allowed_packet') {
$dummy_data['Value'] = 1047552;
} else {
$dummy_data['Value'] = '';
}
}
$_results[] = new ObjectArray($dummy_data);
$this->results = $_results;
$this->num_rows = count($this->results);
$this->return_value = $this->num_rows;
return true;
}
/**
* Method to execute SHOW TABLE STATUS query.
*
* This query is meaningless for SQLite. This function return dummy data.
*
* @param string
* @return ObjectArray
*/
private function show_status_workaround($query) {
$pattern = '/^SHOW\\s*TABLE\\s*STATUS\\s*LIKE\\s*(.*?)$/im';
if (preg_match($pattern, $query, $match)) {
$table_name = str_replace("'", '', $match[1]);
} else {
$table_name = '';
}
$dummy_data = array(
'Name' => $table_name, 'Engine' => '', 'Version' => '',
'Row_format' => '', 'Rows' => 0, 'Avg_row_length' => 0,
'Data_length' => 0, 'Max_data_length' => 0, 'Index_length' => 0,
'Data_free' => 0, 'Auto_increment' => 0, 'Create_time' => '',
'Update_time' => '', 'Check_time' => '', 'Collation' => '',
'Checksum' => '', 'Create_options' => '', 'Comment' => ''
);
$_results[] = new ObjectArray($dummy_data);
$this->results = $_results;
$this->num_rows = count($this->results);
$this->return_value = $this->num_rows;
return true;
}
/**
* Method to format the queried data to that of MySQL.
*
* @param string $engine
*/
private function process_results($engine) {
if (in_array($this->query_type, array('describe', 'desc', 'showcolumns'))) {
$this->convert_to_columns_object();
} elseif ('showindex' === $this->query_type){
$this->convert_to_index_object();
} elseif (in_array($this->query_type, array('check', 'analyze'))) {
$this->convert_result_check_or_analyze();
} else {
$this->results = $this->_results;
}
}
/**
* Method to format the error messages and put out to the file.
*
* When $wpdb::suppress_errors is set to true or $wpdb::show_errors is set to false,
* the error messages are ignored.
*
* @param string $line where the error occurred.
* @param string $function to indicate the function name where the error occurred.
* @param string $message
* @return boolean
*/
private function set_error ($line, $function, $message){
global $wpdb;
$this->errors[] = array("line"=>$line, "function"=>$function);
$this->error_messages[] = $message;
$this->is_error = true;
if ($wpdb->suppress_errors) return false;
if (!$wpdb->show_errors) return false;
file_put_contents (FQDBDIR .'debug.txt', "Line $line, Function: $function, Message: $message \n", FILE_APPEND);
}
/**
* Method to change the queried data to PHP object format.
*
* It takes the associative array of query results and creates a numeric
* array of anonymous objects
*
* @access private
*/
private function convert_to_object(){
$_results = array();
if (count ($this->results) === 0){
echo $this->get_error_message();
} else {
foreach($this->results as $row){
$_results[] = new ObjectArray($row);
}
}
$this->results = $_results;
}
/**
* Method to convert the SHOW COLUMNS query data to an object.
*
* It rewrites pragma results to mysql compatible array
* when query_type is describe, we use sqlite pragma function.
*
* @access private
*/
private function convert_to_columns_object() {
$_results = array();
$_columns = array( //Field names MySQL SHOW COLUMNS returns
'Field' => "",
'Type' => "",
'Null' => "",
'Key' => "",
'Default' => "",
'Extra' => ""
);
if (count($this->_results) === 0) {
echo $this->get_error_message();
} else {
foreach ($this->_results as $row) {
$_columns['Field'] = $row->name;
$_columns['Type'] = $row->type;
$_columns['Null'] = $row->notnull ? "NO" : "YES";
$_columns['Key'] = $row->pk ? "PRI" : "";
$_columns['Default'] = $row->dflt_value;
$_results[] = new ObjectArray($_columns);
}
}
$this->results = $_results;
}
/**
* Method to convert SHOW INDEX query data to PHP object.
*
* It rewrites the result of SHOW INDEX to the Object compatible with MySQL
* added the WHERE clause manipulation (ver 1.3.1)
*
* @access private
*/
private function convert_to_index_object() {
$_results = array();
$_columns = array(
'Table' => "",
'Non_unique' => "",// unique -> 0, not unique -> 1
'Key_name' => "",// the name of the index
'Seq_in_index' => "",// column sequence number in the index. begins at 1
'Column_name' => "",
'Collation' => "",//A(scend) or NULL
'Cardinality' => "",
'Sub_part' => "",// set to NULL
'Packed' => "",// How to pack key or else NULL
'Null' => "",// If column contains null, YES. If not, NO.
'Index_type' => "",// BTREE, FULLTEXT, HASH, RTREE
'Comment' => ""
);
if (count($this->_results) == 0) {
echo $this->get_error_message();
} else {
foreach ($this->_results as $row) {
if ($row->type == 'table' && !stripos($row->sql, 'primary'))
continue;
if ($row->type == 'index' && stripos($row->name, 'sqlite_autoindex') !== false)
continue;
switch ($row->type) {
case 'table':
$pattern1 = '/^\\s*PRIMARY.*\((.*)\)/im';
$pattern2 = '/^\\s*(\\w+)?\\s*.*PRIMARY.*(?!\()/im';
if (preg_match($pattern1, $row->sql, $match)) {
$col_name = trim($match[1]);
$_columns['Key_name'] = 'PRIMARY';
$_columns['Non_unique'] = 0;
$_columns['Column_name'] = $col_name;
} elseif (preg_match($pattern2, $row->sql, $match)) {
$col_name = trim($match[1]);
$_columns['Key_name'] = 'PRIMARY';
$_columns['Non_unique'] = 0;
$_columns['Column_name'] = $col_name;
}
break;
case 'index':
if (stripos($row->sql, 'unique') !== false) {
$_columns['Non_unique'] = 0;
} else {
$_columns['Non_unique'] = 1;
}
if (preg_match('/^.*\((.*)\)/i', $row->sql, $match)) {
$col_name = str_replace("'", '', $match[1]);
$_columns['Column_name'] = trim($col_name);
}
$_columns['Key_name'] = $row->name;
break;
default:
break;
}
$_columns['Table'] = $row->tbl_name;
$_columns['Collation'] = NULL;
$_columns['Cardinality'] = 0;
$_columns['Sub_part'] = NULL;
$_columns['Packed'] = NULL;
$_columns['Null'] = 'NO';
$_columns['Index_type'] = 'BTREE';
$_columns['Comment'] = '';
$_results[] = new ObjectArray($_columns);
}
if (stripos($this->queries[0], 'WHERE') !== false) {
preg_match('/WHERE\\s*(.*)$/im', $this->queries[0], $match);
list($key, $value) = explode('=', $match[1]);
$key = trim($key);
$value = preg_replace("/[\';]/", '', $value);
$value = trim($value);
foreach ($_results as $result) {
if (stripos($value, $result->$key) !== false) {
unset($_results);
$_results[] = $result;
break;
}
}
}
}
$this->results = $_results;
}
/**
* Method to the CHECK query data to an object.
*
* @access private
*/
private function convert_result_check_or_analyze() {
$results = array();
if ($this->query_type == 'check') {
$_columns = array(
'Table' => '',
'Op' => 'check',
'Msg_type' => 'status',
'Msg_text' => 'OK'
);
} else {
$_columns = array(
'Table' => '',
'Op' => 'analyze',
'Msg_type' => 'status',
'Msg_text' => 'Table is already up to date'
);
}
$_results[] = new ObjectArray($_columns);
$this->results = $_results;
}
/**
* Method to check SQLite library version.
*
* This is used for checking if SQLite can execute multiple rows insert.
*
* @return version number string or 0
* @access private
*/
private function get_sqlite_version() {
try {
$statement = $this->pdo->prepare('SELECT sqlite_version()');
$statement->execute();
$result = $statement->fetch(PDO::FETCH_NUM);
return $result[0];
} catch (PDOException $err) {
return '0';
}
}
/**
* Method to call PDO::beginTransaction().
*
* @see PDO::beginTransaction()
* @return boolean
*/
public function beginTransaction() {
if ($this->has_active_transaction) {
return false;
} else {
$this->has_active_transaction = $this->pdo->beginTransaction();
return $this->has_active_transaction;
}
}
/**
* Method to call PDO::commit().
*
* @see PDO::commit()
*/
public function commit() {
$this->pdo->commit();
$this->has_active_transaction = false;
}
/**
* Method to call PDO::rollBack().
*
* @see PDO::rollBack()
*/
public function rollBack() {
$this->pdo->rollBack();
$this->has_active_transaction = false;
}
}
/**
* Class to change queried data to PHP object.
*
* @author kjm
*/
class ObjectArray {
function __construct($data = null,&$node= null) {
foreach ($data as $key => $value) {
if ( is_array($value) ) {
if (!$node) {
$node =& $this;
}
$node->$key = new stdClass();
self::__construct($value,$node->$key);
} else {
if (!$node) {
$node =& $this;
}
$node->$key = $value;
}
}
}
}
?>