You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1380 lines
40 KiB

  1. <?php
  2. /**
  3. * This file defines PDOEngine class.
  4. *
  5. * @package SQLite Integration
  6. * @author Kojima Toshiyasu
  7. *
  8. */
  9. /**
  10. * This class extends PDO class and does the real work.
  11. *
  12. * It accepts a request from wpdb class, initialize PDO instance,
  13. * execute SQL statement, and returns the results to WordPress.
  14. */
  15. class PDOEngine extends PDO {
  16. /**
  17. * Class variable to check if there is an error.
  18. *
  19. * @var boolean
  20. */
  21. public $is_error = false;
  22. /**
  23. * Class variable which is used for CALC_FOUND_ROW query.
  24. *
  25. * @var unsigned integer
  26. */
  27. public $found_rows_result = null;
  28. /**
  29. * Class variable used for query with ORDER BY FIELD()
  30. *
  31. * @var array of the object
  32. */
  33. public $pre_ordered_results = null;
  34. /**
  35. * Class variable to store the rewritten queries.
  36. *
  37. * @var array
  38. * @access private
  39. */
  40. private $rewritten_query;
  41. /**
  42. * Class variable to have what kind of query to execute.
  43. *
  44. * @var string
  45. * @access private
  46. */
  47. private $query_type;
  48. /**
  49. * Class variable to store the result of the query.
  50. *
  51. * @var reference to the PHP object
  52. * @access private
  53. */
  54. private $results = null;
  55. /**
  56. * Class variable to store the results of the query.
  57. *
  58. * This is for the backward compatibility.
  59. *
  60. * @var reference to the PHP object
  61. * @access private
  62. */
  63. private $_results = null;
  64. /**
  65. * Class variable to reference to the PDO instance.
  66. *
  67. * @var PDO object
  68. * @access private
  69. */
  70. private $pdo;
  71. /**
  72. * Class variable to store the query string prepared to execute.
  73. *
  74. * @var string|array
  75. */
  76. private $prepared_query;
  77. /**
  78. * Class variable to store the values in the query string.
  79. *
  80. * @var array
  81. * @access private
  82. */
  83. private $extracted_variables = array();
  84. /**
  85. * Class variable to store the error messages.
  86. *
  87. * @var array
  88. * @access private
  89. */
  90. private $error_messages = array();
  91. /**
  92. * Class variable to store the file name and function to cause error.
  93. *
  94. * @var array
  95. * @access private
  96. */
  97. private $errors;
  98. /**
  99. * Class variable to store the query strings.
  100. *
  101. * @var array
  102. */
  103. public $queries = array();
  104. /**
  105. * Class variable to store the affected row id.
  106. *
  107. * @var unsigned integer
  108. * @access private
  109. */
  110. private $last_insert_id;
  111. /**
  112. * Class variable to store the number of rows affected.
  113. *
  114. * @var unsigned integer
  115. */
  116. private $affected_rows;
  117. /**
  118. * Class variable to store the queried column info.
  119. *
  120. * @var array
  121. */
  122. private $column_data;
  123. /**
  124. * Variable to emulate MySQL affected row.
  125. *
  126. * @var integer
  127. */
  128. private $num_rows;
  129. /**
  130. * Return value from query().
  131. *
  132. * Each query has its own return value.
  133. *
  134. * @var mixed
  135. */
  136. private $return_value;
  137. /**
  138. * Variable to determine which insert query to use.
  139. *
  140. * Whether VALUES clause in the INSERT query can take multiple values or not
  141. * depends on the version of SQLite library. We check the version and set
  142. * this varable to true or false.
  143. *
  144. * @var boolean
  145. */
  146. private $can_insert_multiple_rows = false;
  147. /**
  148. *
  149. * @var integer
  150. */
  151. private $param_num;
  152. /**
  153. * Varible to check if there is an active transaction.
  154. * @var boolean
  155. * @access protected
  156. */
  157. protected $has_active_transaction = false;
  158. /**
  159. * Constructor
  160. *
  161. * Create PDO object, set user defined functions and initialize other settings.
  162. * Don't use parent::__construct() because this class does not only returns
  163. * PDO instance but many others jobs.
  164. *
  165. * Constructor definition is changed since version 1.7.1.
  166. *
  167. * @param none
  168. */
  169. function __construct() {
  170. register_shutdown_function(array($this, '__destruct'));
  171. if (!is_file(FQDB)) {
  172. $this->prepare_directory();
  173. }
  174. $dsn = 'sqlite:' . FQDB;
  175. if (isset($GLOBALS['@pdo'])) {
  176. $this->pdo = $GLOBALS['@pdo'];
  177. } else {
  178. $locked = false;
  179. $status = 0;
  180. do {
  181. try {
  182. $this->pdo = new PDO($dsn, null, null, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
  183. require_once UDF_FILE;
  184. new PDOSQLiteUDFS($this->pdo);
  185. $GLOBALS['@pdo'] = $this->pdo;
  186. } catch (PDOException $ex) {
  187. $status = $ex->getCode();
  188. if ($status == 5 || $status == 6) {
  189. $locked = true;
  190. } else {
  191. $err_message = $ex->getMessage();
  192. }
  193. }
  194. } while ($locked);
  195. if ($status > 0) {
  196. $message = 'Database initialization error!<br />' .
  197. 'Code: ' . $status . '<br />Error Message: ' . $err_message;
  198. $this->set_error(__LINE__, __FILE__, $message);
  199. return false;
  200. }
  201. }
  202. $this->init();
  203. }
  204. /**
  205. * Destructor
  206. *
  207. * If SQLITE_MEM_DEBUG constant is defined, append information about
  208. * memory usage into database/mem_debug.txt.
  209. *
  210. * This definition is changed since version 1.7.
  211. *
  212. * @return boolean
  213. */
  214. function __destruct() {
  215. if (defined('SQLITE_MEM_DEBUG') && SQLITE_MEM_DEBUG) {
  216. $max = ini_get('memory_limit');
  217. if (is_null($max)) {
  218. $message = sprintf("[%s] Memory_limit is not set in php.ini file.", date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']));
  219. file_put_contents(FQDBDIR . 'mem_debug.txt', $message, FILE_APPEND);
  220. return true;
  221. }
  222. if (stripos($max, 'M') !== false) {
  223. $max = (int) $max * 1024 * 1024;
  224. }
  225. $peak = memory_get_peak_usage(true);
  226. $used = round((int) $peak / (int) $max * 100, 2);
  227. if ($used > 90) {
  228. $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);
  229. file_put_contents(FQDBDIR . 'mem_debug.txt', $message, FILE_APPEND);
  230. }
  231. }
  232. //$this->pdo = null;
  233. return true;
  234. }
  235. /**
  236. * Method to initialize database, executed in the contructor.
  237. *
  238. * It checks if WordPress is in the installing process and does the required
  239. * jobs. SQLite library version specific settings are also in this function.
  240. *
  241. * Some developers use WP_INSTALLING constant for other purposes, if so, this
  242. * function will do no harms.
  243. *
  244. * @return boolean
  245. */
  246. private function init() {
  247. if (defined('WP_INSTALLING') && WP_INSTALLING) {
  248. $statement = $this->pdo->query("SELECT COUNT(*) FROM sqlite_master WHERE type='table'");
  249. $number_of_tables = $statement->fetchColumn(0);
  250. $statement = null;
  251. if ($number_of_tables == 0) $this->make_sqlite_tables();
  252. }
  253. if (version_compare($this->get_sqlite_version(), '3.7.11', '>=')) {
  254. $this->can_insert_multiple_rows = true;
  255. }
  256. $statement = $this->pdo->query('PRAGMA foreign_keys');
  257. if ($statement->fetchColumn(0) == '0') $this->pdo->query('PRAGMA foreign_keys = ON');
  258. }
  259. /**
  260. * This method makes database direcotry and .htaccess file.
  261. *
  262. * It is executed only once when the installation begins.
  263. */
  264. private function prepare_directory() {
  265. global $wpdb;
  266. $u = umask(0000);
  267. if (!is_dir(FQDBDIR)) {
  268. if (!@mkdir(FQDBDIR, 0704, true)) {
  269. umask($u);
  270. $message = 'Unable to create the required directory! Please check your server settings.';
  271. wp_die($message, 'Error!');
  272. }
  273. }
  274. if (!is_writable(FQDBDIR)) {
  275. umask($u);
  276. $message = 'Unable to create a file in the directory! Please check your server settings.';
  277. wp_die($message, 'Error!');
  278. }
  279. if (!is_file(FQDBDIR . '.htaccess')) {
  280. $fh = fopen(FQDBDIR . '.htaccess', "w");
  281. if (!$fh) {
  282. umask($u);
  283. $message = 'Unable to create a file in the directory! Please check your server settings.';
  284. echo $message;
  285. return false;
  286. }
  287. fwrite($fh, 'DENY FROM ALL');
  288. fclose($fh);
  289. }
  290. if (!is_file(FQDBDIR . 'index.php')) {
  291. $fh = fopen(FQDBDIR . 'index.php', "w");
  292. if (!$fh) {
  293. umask($u);
  294. $message = 'Unable to create a file in the directory! Please check your server settings.';
  295. echo $message;
  296. return false;
  297. }
  298. fwrite($fh, '<?php // Silence is gold. ?>');
  299. fclose($fh);
  300. }
  301. umask($u);
  302. return true;
  303. }
  304. /**
  305. * Method to call install() function which overrides WordPress install().
  306. *
  307. * This function is executed only once during the installation process.
  308. */
  309. private function make_sqlite_tables() {
  310. require_once PDODIR . 'install.php';
  311. }
  312. /**
  313. * Method to execute query().
  314. *
  315. * Divide the query types into seven different ones. That is to say:
  316. *
  317. * 1. SELECT SQL_CALC_FOUND_ROWS
  318. * 2. INSERT
  319. * 3. CREATE TABLE(INDEX)
  320. * 4. ALTER TABLE
  321. * 5. SHOW VARIABLES
  322. * 6. DROP INDEX
  323. * 7. THE OTHERS
  324. *
  325. * #1 is just a tricky play. See the private function handle_sql_count() in query.class.php.
  326. * From #2 through #5 call different functions respectively.
  327. * #6 call the ALTER TABLE query.
  328. * #7 is a normal process: sequentially call prepare_query() and execute_query().
  329. *
  330. * #1 process has been changed since version 1.5.1.
  331. *
  332. * @param $query full SQL statement string
  333. * @return mixed according to the query type
  334. * @see PDO::query()
  335. */
  336. public function query($query) {
  337. $this->flush();
  338. $this->queries[] = "Raw query:\n$query";
  339. $res = $this->determine_query_type($query);
  340. if (!$res && defined(PDO_DEBUG) && PDO_DEBUG) {
  341. $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);
  342. $this->set_error(__LINE__, __FUNCTION__, $bailoutString);
  343. }
  344. switch (strtolower($this->query_type)) {
  345. case 'set':
  346. $this->return_value = false;
  347. break;
  348. case 'foundrows':
  349. $_column = array('FOUND_ROWS()' => '');
  350. $column = array();
  351. if (!is_null($this->found_rows_result)) {
  352. $this->num_rows = $this->found_rows_result;
  353. $_column['FOUND_ROWS()'] = $this->num_rows;
  354. //foreach ($this->found_rows_result[0] as $key => $value) {
  355. //$_column['FOUND_ROWS()'] = $value;
  356. //}
  357. $column[] = new ObjectArray($_column);
  358. $this->results = $column;
  359. $this->found_rows_result = null;
  360. }
  361. break;
  362. case 'insert':
  363. if ($this->can_insert_multiple_rows) {
  364. $this->execute_insert_query_new($query);
  365. } else {
  366. $this->execute_insert_query($query);
  367. }
  368. break;
  369. case 'create':
  370. $result = $this->execute_create_query($query);
  371. $this->return_value = $result;
  372. break;
  373. case 'alter':
  374. $result = $this->execute_alter_query($query);
  375. $this->return_value = $result;
  376. break;
  377. case 'show_variables':
  378. $result = $this->show_variables_workaround($query);
  379. break;
  380. case 'showstatus':
  381. $result = $this->show_status_workaround($query);
  382. break;
  383. case 'drop_index':
  384. $pattern = '/^\\s*(DROP\\s*INDEX\\s*.*?)\\s*ON\\s*(.*)/im';
  385. if (preg_match($pattern, $query, $match)) {
  386. $drop_query = 'ALTER TABLE ' . trim($match[2]) . ' ' . trim($match[1]);
  387. $this->query_type = 'alter';
  388. $result = $this->execute_alter_query($drop_query);
  389. $this->return_value = $result;
  390. } else {
  391. $this->return_value = false;
  392. }
  393. break;
  394. default:
  395. $engine = $this->prepare_engine($this->query_type);
  396. $this->rewritten_query = $engine->rewrite_query($query, $this->query_type);
  397. if (!is_null($this->pre_ordered_results)) {
  398. $this->results = $this->pre_ordered_results;
  399. $this->num_rows = $this->return_value = count($this->results);
  400. $this->pre_ordered_results = null;
  401. break;
  402. }
  403. $this->queries[] = "Rewritten:\n$this->rewritten_query";
  404. $this->extract_variables();
  405. $statement = $this->prepare_query();
  406. $this->execute_query($statement);
  407. if (!$this->is_error) {
  408. $this->process_results($engine);
  409. } else {// Error
  410. ;
  411. }
  412. break;
  413. }
  414. if (defined('PDO_DEBUG') && PDO_DEBUG === true) {
  415. file_put_contents(FQDBDIR . 'debug.txt', $this->get_debug_info(), FILE_APPEND);
  416. }
  417. return $this->return_value;
  418. }
  419. /**
  420. * Method to return inserted row id.
  421. *
  422. * @return unsigned integer
  423. */
  424. public function get_insert_id() {
  425. return $this->last_insert_id;
  426. }
  427. /**
  428. * Method to return the number of rows affected.
  429. *
  430. * @return unsigned integer
  431. */
  432. public function get_affected_rows() {
  433. return $this->affected_rows;
  434. }
  435. /**
  436. * Method to return the queried column names.
  437. *
  438. * These data are meaningless for SQLite. So they are dummy emulating
  439. * MySQL columns data.
  440. *
  441. * @return array of the object
  442. */
  443. public function get_columns() {
  444. if (!empty($this->results)) {
  445. $primary_key = array(
  446. 'meta_id', 'comment_ID', 'link_ID', 'option_id',
  447. 'blog_id', 'option_name', 'ID', 'term_id', 'object_id',
  448. 'term_taxonomy_id', 'umeta_id', 'id');
  449. $unique_key = array('term_id', 'taxonomy', 'slug');
  450. $data = array(
  451. 'name' => '', // column name
  452. 'table' => '', // table name
  453. 'max_length' => 0, // max length of the column
  454. 'not_null' => 1, // 1 if not null
  455. 'primary_key' => 0, // 1 if column has primary key
  456. 'unique_key' => 0, // 1 if column has unique key
  457. 'multiple_key' => 0, // 1 if column doesn't have unique key
  458. 'numeric' => 0, // 1 if column has numeric value
  459. 'blob' => 0, // 1 if column is blob
  460. 'type' => '', // type of the column
  461. 'unsigned' => 0, // 1 if column is unsigned integer
  462. 'zerofill' => 0 // 1 if column is zero-filled
  463. );
  464. if (preg_match("/\s*FROM\s*(.*)?\s*/i", $this->rewritten_query, $match)) {
  465. $table_name = trim($match[1]);
  466. } else {
  467. $table_name = '';
  468. }
  469. foreach ($this->results[0] as $key => $value) {
  470. $data['name'] = $key;
  471. $data['table'] = $table_name;
  472. if (in_array($key, $primary_key)) {
  473. $data['primary_key'] = 1;
  474. } elseif (in_array($key, $unique_key)) {
  475. $data['unique_key'] = 1;
  476. } else {
  477. $data['multiple_key'] = 1;
  478. }
  479. $this->column_data[] = new ObjectArray($data);
  480. $data['name'] = '';
  481. $data['table'] = '';
  482. $data['primary_key'] = 0;
  483. $data['unique_key'] = 0;
  484. $data['multiple_key'] = 0;
  485. }
  486. return $this->column_data;
  487. } else {
  488. return null;
  489. }
  490. }
  491. /**
  492. * Method to return the queried result data.
  493. *
  494. * @return mixed
  495. */
  496. public function get_query_results() {
  497. return $this->results;
  498. }
  499. /**
  500. * Method to return the number of rows from the queried result.
  501. *
  502. * @return unsigned integer
  503. */
  504. public function get_num_rows() {
  505. return $this->num_rows;
  506. }
  507. /**
  508. * Method to return the queried results according to the query types.
  509. *
  510. * @return mixed
  511. */
  512. public function get_return_value() {
  513. return $this->return_value;
  514. }
  515. /**
  516. * Method to return error messages.
  517. *
  518. * @return string
  519. */
  520. public function get_error_message(){
  521. if (count($this->error_messages) === 0){
  522. $this->is_error = false;
  523. $this->error_messages = array();
  524. return '';
  525. }
  526. $output = '<div style="clear:both">&nbsp;</div>';
  527. if ($this->is_error === false){
  528. //return $output;
  529. return '';
  530. }
  531. $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";
  532. foreach ($this->queries as $q){
  533. $output .= "\t\t<li>".$q."</li>\r\n";
  534. }
  535. $output .= "\t</ol>\r\n</div>";
  536. foreach ($this->error_messages as $num=>$m){
  537. $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>";
  538. }
  539. ob_start();
  540. debug_print_backtrace();
  541. $output .= '<pre>' . ob_get_contents() . '</pre>';
  542. ob_end_clean();
  543. return $output;
  544. }
  545. /**
  546. * Method to return information about query string for debugging.
  547. *
  548. * @return string
  549. */
  550. private function get_debug_info(){
  551. $output = '';
  552. foreach ($this->queries as $q){
  553. $output .= $q ."\n";
  554. }
  555. return $output;
  556. }
  557. /**
  558. * Method to clear previous data.
  559. */
  560. private function flush(){
  561. $this->rewritten_query = '';
  562. $this->query_type = '';
  563. $this->results = null;
  564. $this->_results = null;
  565. $this->last_insert_id = null;
  566. $this->affected_rows = null;
  567. $this->column_data = array();
  568. $this->num_rows = null;
  569. $this->return_value = null;
  570. $this->extracted_variables = array();
  571. $this->error_messages = array();
  572. $this->is_error = false;
  573. $this->queries = array();
  574. $this->param_num = 0;
  575. }
  576. /**
  577. * Method to include the apropreate class files.
  578. *
  579. * It is not a good habit to change the include files programatically.
  580. * Needs to be fixed some other way.
  581. *
  582. * @param string $query_type
  583. * @return object reference to apropreate driver
  584. */
  585. private function prepare_engine($query_type = null) {
  586. if (stripos($query_type, 'create') !== false) {
  587. require_once PDODIR . 'query_create.class.php';
  588. $engine = new CreateQuery();
  589. } elseif (stripos($query_type, 'alter') !== false) {
  590. require_once PDODIR . 'query_alter.class.php';
  591. $engine = new AlterQuery();
  592. } else {
  593. require_once PDODIR . 'query.class.php';
  594. $engine = new PDOSQLiteDriver();
  595. }
  596. return $engine;
  597. }
  598. /**
  599. * Method to create a PDO statement object from the query string.
  600. *
  601. * @return PDOStatement
  602. */
  603. private function prepare_query(){
  604. $this->queries[] = "Prepare:\n" . $this->prepared_query;
  605. $reason = 0;
  606. $message = '';
  607. $statement = null;
  608. do {
  609. try {
  610. $statement = $this->pdo->prepare($this->prepared_query);
  611. } catch (PDOException $err) {
  612. $reason = $err->getCode();
  613. $message = $err->getMessage();
  614. }
  615. } while (5 == $reason || 6 == $reason);
  616. if ($reason > 0){
  617. $err_message = sprintf("Problem preparing the PDO SQL Statement. Error was: %s", $message);
  618. $this->set_error(__LINE__, __FUNCTION__, $err_message);
  619. }
  620. return $statement;
  621. }
  622. /**
  623. * Method to execute PDO statement object.
  624. *
  625. * This function executes query and sets the variables to give back to WordPress.
  626. * The variables are class fields. So if success, no return value. If failure, it
  627. * returns void and stops.
  628. *
  629. * @param object $statement of PDO statement
  630. * @return boolean
  631. */
  632. private function execute_query($statement) {
  633. $reason = 0;
  634. $message = '';
  635. if (!is_object($statement))
  636. return false;
  637. if (count($this->extracted_variables) > 0) {
  638. $this->queries[] = "Executing:\n" . var_export($this->extracted_variables, true);
  639. do {
  640. if ($this->query_type == 'update' || $this->query_type == 'replace') {
  641. try {
  642. $this->beginTransaction();
  643. $statement->execute($this->extracted_variables);
  644. $this->commit();
  645. } catch (PDOException $err) {
  646. $reason = $err->getCode();
  647. $message = $err->getMessage();
  648. $this->rollBack();
  649. }
  650. } else {
  651. try {
  652. $statement->execute($this->extracted_variables);
  653. } catch (PDOException $err) {
  654. $reason = $err->getCode();
  655. $message = $err->getMessage();
  656. }
  657. }
  658. } while (5 == $reason || 6 == $reason);
  659. } else {
  660. $this->queries[] = 'Executing: (no parameters)';
  661. do{
  662. if ($this->query_type == 'update' || $this->query_type == 'replace') {
  663. try {
  664. $this->beginTransaction();
  665. $statement->execute();
  666. $this->commit();
  667. } catch (PDOException $err) {
  668. $reason = $err->getCode();
  669. $message = $err->getMessage();
  670. $this->rollBack();
  671. }
  672. } else {
  673. try {
  674. $statement->execute();
  675. } catch (PDOException $err) {
  676. $reason = $err->getCode();
  677. $message = $err->getMessage();
  678. }
  679. }
  680. } while (5 == $reason || 6 == $reason);
  681. }
  682. if ($reason > 0) {
  683. $err_message = sprintf("Error while executing query! Error message was: %s", $message);
  684. $this->set_error(__LINE__, __FUNCTION__, $err_message);
  685. return false;
  686. } else {
  687. $this->_results = $statement->fetchAll(PDO::FETCH_OBJ);
  688. }
  689. //generate the results that $wpdb will want to see
  690. switch ($this->query_type) {
  691. case 'insert':
  692. case 'update':
  693. case 'replace':
  694. $this->last_insert_id = $this->pdo->lastInsertId();
  695. $this->affected_rows = $statement->rowCount();
  696. $this->return_value = $this->affected_rows;
  697. break;
  698. case 'select':
  699. case 'show':
  700. case 'showcolumns':
  701. case 'showindex':
  702. case 'describe':
  703. case 'desc':
  704. case 'check':
  705. case 'analyze':
  706. //case "foundrows":
  707. $this->num_rows = count($this->_results);
  708. $this->return_value = $this->num_rows;
  709. break;
  710. case 'delete':
  711. $this->affected_rows = $statement->rowCount();
  712. $this->return_value = $this->affected_rows;
  713. break;
  714. case 'alter':
  715. case 'drop':
  716. case 'create':
  717. case 'optimize':
  718. case 'truncate':
  719. if ($this->is_error) {
  720. $this->return_value = false;
  721. } else {
  722. $this->return_value = true;
  723. }
  724. break;
  725. }
  726. }
  727. /**
  728. * Method to extract field data to an array and prepare the query statement.
  729. *
  730. * If original SQL statement is CREATE query, this function do nothing and return
  731. * true. This returned value is not used.
  732. *
  733. * @return boolean
  734. */
  735. private function extract_variables() {
  736. if ($this->query_type == 'create') {
  737. $this->prepared_query = $this->rewritten_query;
  738. return true;
  739. }
  740. //long queries can really kill this
  741. $pattern = '/(?<!\\\\)([\'"])(.*?)(?<!\\\\)\\1/imsx';
  742. $_limit = $limit = ini_get('pcre.backtrack_limit');
  743. // if user's setting is more than default * 10, make PHP do the job.
  744. if ($limit > 10000000) {
  745. $query = preg_replace_callback($pattern, array($this, 'replace_variables_with_placeholders'), $this->rewritten_query);
  746. } else {
  747. do {
  748. if ($limit > 10000000) {
  749. $message = 'The query is too big to parse properly';
  750. $this->set_error(__LINE__, __FUNCTION__, $message);
  751. break; //no point in continuing execution, would get into a loop
  752. } else {
  753. ini_set('pcre.backtrack_limit', $limit);
  754. $query = preg_replace_callback($pattern, array($this,'replace_variables_with_placeholders'), $this->rewritten_query);
  755. }
  756. $limit = $limit * 10;
  757. } while (is_null($query));
  758. //reset the pcre.backtrack_limist
  759. ini_set('pcre.backtrack_limit', $_limit);
  760. }
  761. $this->queries[] = "With Placeholders:\n" . $query;
  762. $this->prepared_query = $query;
  763. }
  764. /**
  765. * Call back function to replace field data with PDO parameter.
  766. *
  767. * @param string $matches
  768. * @return string
  769. */
  770. private function replace_variables_with_placeholders($matches) {
  771. //remove the wordpress escaping mechanism
  772. $param = stripslashes($matches[0]);
  773. //remove trailing spaces
  774. $param = trim($param);
  775. //remove the quotes at the end and the beginning
  776. if (in_array($param{strlen($param)-1}, array("'",'"'))) {
  777. $param = substr($param,0,-1) ;//end
  778. }
  779. if (in_array($param{0}, array("'",'"'))) {
  780. $param = substr($param, 1); //start
  781. }
  782. //$this->extracted_variables[] = $param;
  783. $key = ':param_'.$this->param_num++;
  784. $this->extracted_variables[] = $param;
  785. //return the placeholder
  786. //return ' ? ';
  787. return ' '.$key.' ';
  788. }
  789. /**
  790. * Method to determine which query type the argument is.
  791. *
  792. * It takes the query string ,determines the type and returns the type string.
  793. * If the query is the type that SQLite Integration can't executes, returns false.
  794. *
  795. * @param string $query
  796. * @return boolean|string
  797. */
  798. private function determine_query_type($query) {
  799. $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);
  800. if (!$result) {
  801. return false;
  802. }
  803. $this->query_type = strtolower($match[1]);
  804. if (stripos($this->query_type, 'found') !== false) {
  805. $this->query_type = 'foundrows';
  806. }
  807. if (stripos($this->query_type, 'show') !== false) {
  808. if (stripos($this->query_type, 'show table status') !== false) {
  809. $this->query_type = 'showstatus';
  810. } elseif (stripos($this->query_type, 'show tables') !== false || stripos($this->query_type, 'show full tables') !== false) {
  811. $this->query_type = 'show';
  812. } elseif (stripos($this->query_type, 'show columns') !== false || stripos($this->query_type, 'show fields') !== false || stripos($this->query_type, 'show full columns') !== false) {
  813. $this->query_type = 'showcolumns';
  814. } elseif (stripos($this->query_type, 'show index') !== false || stripos($this->query_type, 'show indexes') !== false || stripos($this->query_type, 'show keys') !== false) {
  815. $this->query_type = 'showindex';
  816. } elseif (stripos($this->query_type, 'show variables') !== false || stripos($this->query_type, 'show global variables') !== false || stripos($this->query_type, 'show session variables') !== false) {
  817. $this->query_type = 'show_variables';
  818. } else {
  819. return false;
  820. }
  821. }
  822. if (stripos($this->query_type, 'drop index') !== false) {
  823. $this->query_type = 'drop_index';
  824. }
  825. return true;
  826. }
  827. /**
  828. * Method to execute INSERT query for SQLite version 3.7.11 or later.
  829. *
  830. * SQLite version 3.7.11 began to support multiple rows insert with values
  831. * clause. This is for that version or later.
  832. *
  833. * @param string $query
  834. */
  835. private function execute_insert_query_new($query) {
  836. $engine = $this->prepare_engine($this->query_type);
  837. $this->rewritten_query = $engine->rewrite_query($query, $this->query_type);
  838. $this->queries[] = "Rewritten:\n" . $this->rewritten_query;
  839. $this->extract_variables();
  840. $statement = $this->prepare_query();
  841. $this->execute_query($statement);
  842. }
  843. /**
  844. * Method to execute INSERT query for SQLite version 3.7.10 or lesser.
  845. *
  846. * It executes the INSERT query for SQLite version 3.7.10 or lesser. It is
  847. * necessary to rewrite multiple row values.
  848. *
  849. * @param string $query
  850. */
  851. private function execute_insert_query($query) {
  852. global $wpdb;
  853. $multi_insert = false;
  854. $statement = null;
  855. $engine = $this->prepare_engine($this->query_type);
  856. if (preg_match('/(INSERT.*?VALUES\\s*)(\(.*\))/imsx', $query, $matched)) {
  857. $query_prefix = $matched[1];
  858. $values_data = $matched[2];
  859. if (stripos($values_data, 'ON DUPLICATE KEY') !== false) {
  860. $exploded_parts = $values_data;
  861. } elseif (stripos($query_prefix, "INSERT INTO $wpdb->comments") !== false) {
  862. $exploded_parts = $values_data;
  863. } else {
  864. $exploded_parts = $this->parse_multiple_inserts($values_data);
  865. }
  866. $count = count($exploded_parts);
  867. if ($count > 1) {
  868. $multi_insert = true;
  869. }
  870. }
  871. if ($multi_insert) {
  872. $first = true;
  873. foreach ($exploded_parts as $value) {
  874. if (substr($value, -1, 1) === ')') {
  875. $suffix = '';
  876. } else {
  877. $suffix = ')';
  878. }
  879. $query_string = $query_prefix . ' ' . $value . $suffix;
  880. $this->rewritten_query = $engine->rewrite_query($query_string, $this->query_type);
  881. $this->queries[] = "Rewritten:\n" . $this->rewritten_query;
  882. $this->extracted_variables = array();
  883. $this->extract_variables();
  884. if ($first) {
  885. $statement = $this->prepare_query();
  886. $this->execute_query($statement);
  887. $first = false;
  888. } else {
  889. $this->execute_query($statement);
  890. }
  891. }
  892. } else {
  893. $this->rewritten_query = $engine->rewrite_query($query, $this->query_type);
  894. $this->queries[] = "Rewritten:\n" . $this->rewritten_query;
  895. $this->extract_variables();
  896. $statement = $this->prepare_query();
  897. $this->execute_query($statement);
  898. }
  899. }
  900. /**
  901. * Method to help rewriting multiple row values insert query.
  902. *
  903. * It splits the values clause into an array to execute separately.
  904. *
  905. * @param string $values
  906. * @return array
  907. */
  908. private function parse_multiple_inserts($values) {
  909. $tokens = preg_split("/(''|(?<!\\\\)'|(?<!\()\),(?=\s*\())/s", $values, -1, PREG_SPLIT_DELIM_CAPTURE);
  910. $exploded_parts = array();
  911. $part = '';
  912. $literal = false;
  913. foreach ($tokens as $token) {
  914. switch ($token) {
  915. case "),":
  916. if (!$literal) {
  917. $exploded_parts[] = $part;
  918. $part = '';
  919. } else {
  920. $part .= $token;
  921. }
  922. break;
  923. case "'":
  924. if ($literal) {
  925. $literal = false;
  926. } else {
  927. $literal = true;
  928. }
  929. $part .= $token;
  930. break;
  931. default:
  932. $part .= $token;
  933. break;
  934. }
  935. }
  936. if (!empty($part)) {
  937. $exploded_parts[] = $part;
  938. }
  939. return $exploded_parts;
  940. }
  941. /**
  942. * Method to execute CREATE query.
  943. *
  944. * @param string
  945. * @return boolean
  946. */
  947. private function execute_create_query($query) {
  948. $engine = $this->prepare_engine($this->query_type);
  949. $rewritten_query = $engine->rewrite_query($query);
  950. $reason = 0;
  951. $message = '';
  952. //$queries = explode(";", $this->rewritten_query);
  953. try {
  954. $this->beginTransaction();
  955. foreach ($rewritten_query as $single_query) {
  956. $this->queries[] = "Executing:\n" . $single_query;
  957. $single_query = trim($single_query);
  958. if (empty($single_query)) continue;
  959. $this->pdo->exec($single_query);
  960. }
  961. $this->commit();
  962. } catch (PDOException $err) {
  963. $reason = $err->getCode();
  964. $message = $err->getMessage();
  965. if (5 == $reason || 6 == $reason) {
  966. $this->commit();
  967. } else {
  968. $this->rollBack();
  969. }
  970. }
  971. if ($reason > 0) {
  972. $err_message = sprintf("Problem in creating table or index. Error was: %s", $message);
  973. $this->set_error(__LINE__, __FUNCTION__, $err_message);
  974. return false;
  975. }
  976. return true;
  977. }
  978. /**
  979. * Method to execute ALTER TABLE query.
  980. *
  981. * @param string
  982. * @return boolean
  983. */
  984. private function execute_alter_query($query) {
  985. $engine = $this->prepare_engine($this->query_type);
  986. $reason = 0;
  987. $message = '';
  988. $re_query = '';
  989. $rewritten_query = $engine->rewrite_query($query, $this->query_type);
  990. if (is_array($rewritten_query) && array_key_exists('recursion', $rewritten_query)) {
  991. $re_query = $rewritten_query['recursion'];
  992. unset($rewritten_query['recursion']);
  993. }
  994. try {
  995. $this->beginTransaction();
  996. if (is_array($rewritten_query)) {
  997. foreach ($rewritten_query as $single_query) {
  998. $this->queries[] = "Executing:\n" . $single_query;
  999. $single_query = trim($single_query);
  1000. if (empty($single_query)) continue;
  1001. $this->pdo->exec($single_query);
  1002. }
  1003. } else {
  1004. $this->queries[] = "Executing:\n" . $rewritten_query;
  1005. $rewritten_query = trim($rewritten_query);
  1006. $this->pdo->exec($rewritten_query);
  1007. }
  1008. $this->commit();
  1009. } catch (PDOException $err) {
  1010. $reason = $err->getCode();
  1011. $message = $err->getMessage();
  1012. if (5 == $reason || 6 == $reason) {
  1013. $this->commit();
  1014. usleep(10000);
  1015. } else {
  1016. $this->rollBack();
  1017. }
  1018. }
  1019. if ($re_query != '') {
  1020. $this->query($re_query);
  1021. }
  1022. if ($reason > 0) {
  1023. $err_message = sprintf("Problem in executing alter query. Error was: %s", $message);
  1024. $this->set_error(__LINE__, __FUNCTION__, $err_message);
  1025. return false;
  1026. }
  1027. return true;
  1028. }
  1029. /**
  1030. * Method to execute SHOW VARIABLES query
  1031. *
  1032. * This query is meaningless for SQLite. This function returns null data with some
  1033. * exceptions and only avoids the error message.
  1034. *
  1035. * @param string
  1036. * @return ObjectArray
  1037. */
  1038. private function show_variables_workaround($query) {
  1039. $dummy_data = array('Variable_name' => '', 'Value' => null);
  1040. $pattern = '/SHOW\\s*VARIABLES\\s*LIKE\\s*(.*)?$/im';
  1041. if (preg_match($pattern, $query, $match)) {
  1042. $value = str_replace("'", '', $match[1]);
  1043. $dummy_data['Variable_name'] = trim($value);
  1044. // this is set for Wordfence Security Plugin
  1045. if ($value == 'max_allowed_packet') {
  1046. $dummy_data['Value'] = 1047552;
  1047. } else {
  1048. $dummy_data['Value'] = '';
  1049. }
  1050. }
  1051. $_results[] = new ObjectArray($dummy_data);
  1052. $this->results = $_results;
  1053. $this->num_rows = count($this->results);
  1054. $this->return_value = $this->num_rows;
  1055. return true;
  1056. }
  1057. /**
  1058. * Method to execute SHOW TABLE STATUS query.
  1059. *
  1060. * This query is meaningless for SQLite. This function return dummy data.
  1061. *
  1062. * @param string
  1063. * @return ObjectArray
  1064. */
  1065. private function show_status_workaround($query) {
  1066. $pattern = '/^SHOW\\s*TABLE\\s*STATUS\\s*LIKE\\s*(.*?)$/im';
  1067. if (preg_match($pattern, $query, $match)) {
  1068. $table_name = str_replace("'", '', $match[1]);
  1069. } else {
  1070. $table_name = '';
  1071. }
  1072. $dummy_data = array(
  1073. 'Name' => $table_name, 'Engine' => '', 'Version' => '',
  1074. 'Row_format' => '', 'Rows' => 0, 'Avg_row_length' => 0,
  1075. 'Data_length' => 0, 'Max_data_length' => 0, 'Index_length' => 0,
  1076. 'Data_free' => 0, 'Auto_increment' => 0, 'Create_time' => '',
  1077. 'Update_time' => '', 'Check_time' => '', 'Collation' => '',
  1078. 'Checksum' => '', 'Create_options' => '', 'Comment' => ''
  1079. );
  1080. $_results[] = new ObjectArray($dummy_data);
  1081. $this->results = $_results;
  1082. $this->num_rows = count($this->results);
  1083. $this->return_value = $this->num_rows;
  1084. return true;
  1085. }
  1086. /**
  1087. * Method to format the queried data to that of MySQL.
  1088. *
  1089. * @param string $engine
  1090. */
  1091. private function process_results($engine) {
  1092. if (in_array($this->query_type, array('describe', 'desc', 'showcolumns'))) {
  1093. $this->convert_to_columns_object();
  1094. } elseif ('showindex' === $this->query_type){
  1095. $this->convert_to_index_object();
  1096. } elseif (in_array($this->query_type, array('check', 'analyze'))) {
  1097. $this->convert_result_check_or_analyze();
  1098. } else {
  1099. $this->results = $this->_results;
  1100. }
  1101. }
  1102. /**
  1103. * Method to format the error messages and put out to the file.
  1104. *
  1105. * When $wpdb::suppress_errors is set to true or $wpdb::show_errors is set to false,
  1106. * the error messages are ignored.
  1107. *
  1108. * @param string $line where the error occurred.
  1109. * @param string $function to indicate the function name where the error occurred.
  1110. * @param string $message
  1111. * @return boolean
  1112. */
  1113. private function set_error ($line, $function, $message){
  1114. global $wpdb;
  1115. $this->errors[] = array("line"=>$line, "function"=>$function);
  1116. $this->error_messages[] = $message;
  1117. $this->is_error = true;
  1118. if ($wpdb->suppress_errors) return false;
  1119. if (!$wpdb->show_errors) return false;
  1120. file_put_contents (FQDBDIR .'debug.txt', "Line $line, Function: $function, Message: $message \n", FILE_APPEND);
  1121. }
  1122. /**
  1123. * Method to change the queried data to PHP object format.
  1124. *
  1125. * It takes the associative array of query results and creates a numeric
  1126. * array of anonymous objects
  1127. *
  1128. * @access private
  1129. */
  1130. private function convert_to_object(){
  1131. $_results = array();
  1132. if (count ($this->results) === 0){
  1133. echo $this->get_error_message();
  1134. } else {
  1135. foreach($this->results as $row){
  1136. $_results[] = new ObjectArray($row);
  1137. }
  1138. }
  1139. $this->results = $_results;
  1140. }
  1141. /**
  1142. * Method to convert the SHOW COLUMNS query data to an object.
  1143. *
  1144. * It rewrites pragma results to mysql compatible array
  1145. * when query_type is describe, we use sqlite pragma function.
  1146. *
  1147. * @access private
  1148. */
  1149. private function convert_to_columns_object() {
  1150. $_results = array();
  1151. $_columns = array( //Field names MySQL SHOW COLUMNS returns
  1152. 'Field' => "",
  1153. 'Type' => "",
  1154. 'Null' => "",
  1155. 'Key' => "",
  1156. 'Default' => "",
  1157. 'Extra' => ""
  1158. );
  1159. if (count($this->_results) === 0) {
  1160. echo $this->get_error_message();
  1161. } else {
  1162. foreach ($this->_results as $row) {
  1163. $_columns['Field'] = $row->name;
  1164. $_columns['Type'] = $row->type;
  1165. $_columns['Null'] = $row->notnull ? "NO" : "YES";
  1166. $_columns['Key'] = $row->pk ? "PRI" : "";
  1167. $_columns['Default'] = $row->dflt_value;
  1168. $_results[] = new ObjectArray($_columns);
  1169. }
  1170. }
  1171. $this->results = $_results;
  1172. }
  1173. /**
  1174. * Method to convert SHOW INDEX query data to PHP object.
  1175. *
  1176. * It rewrites the result of SHOW INDEX to the Object compatible with MySQL
  1177. * added the WHERE clause manipulation (ver 1.3.1)
  1178. *
  1179. * @access private
  1180. */
  1181. private function convert_to_index_object() {
  1182. $_results = array();
  1183. $_columns = array(
  1184. 'Table' => "",
  1185. 'Non_unique' => "",// unique -> 0, not unique -> 1
  1186. 'Key_name' => "",// the name of the index
  1187. 'Seq_in_index' => "",// column sequence number in the index. begins at 1
  1188. 'Column_name' => "",
  1189. 'Collation' => "",//A(scend) or NULL
  1190. 'Cardinality' => "",
  1191. 'Sub_part' => "",// set to NULL
  1192. 'Packed' => "",// How to pack key or else NULL
  1193. 'Null' => "",// If column contains null, YES. If not, NO.
  1194. 'Index_type' => "",// BTREE, FULLTEXT, HASH, RTREE
  1195. 'Comment' => ""
  1196. );
  1197. if (count($this->_results) == 0) {
  1198. echo $this->get_error_message();
  1199. } else {
  1200. foreach ($this->_results as $row) {
  1201. if ($row->type == 'table' && !stripos($row->sql, 'primary'))
  1202. continue;
  1203. if ($row->type == 'index' && stripos($row->name, 'sqlite_autoindex') !== false)
  1204. continue;
  1205. switch ($row->type) {
  1206. case 'table':
  1207. $pattern1 = '/^\\s*PRIMARY.*\((.*)\)/im';
  1208. $pattern2 = '/^\\s*(\\w+)?\\s*.*PRIMARY.*(?!\()/im';
  1209. if (preg_match($pattern1, $row->sql, $match)) {
  1210. $col_name = trim($match[1]);
  1211. $_columns['Key_name'] = 'PRIMARY';
  1212. $_columns['Non_unique'] = 0;
  1213. $_columns['Column_name'] = $col_name;
  1214. } elseif (preg_match($pattern2, $row->sql, $match)) {
  1215. $col_name = trim($match[1]);
  1216. $_columns['Key_name'] = 'PRIMARY';
  1217. $_columns['Non_unique'] = 0;
  1218. $_columns['Column_name'] = $col_name;
  1219. }
  1220. break;
  1221. case 'index':
  1222. if (stripos($row->sql, 'unique') !== false) {
  1223. $_columns['Non_unique'] = 0;
  1224. } else {
  1225. $_columns['Non_unique'] = 1;
  1226. }
  1227. if (preg_match('/^.*\((.*)\)/i', $row->sql, $match)) {
  1228. $col_name = str_replace("'", '', $match[1]);
  1229. $_columns['Column_name'] = trim($col_name);
  1230. }
  1231. $_columns['Key_name'] = $row->name;
  1232. break;
  1233. default:
  1234. break;
  1235. }
  1236. $_columns['Table'] = $row->tbl_name;
  1237. $_columns['Collation'] = NULL;
  1238. $_columns['Cardinality'] = 0;
  1239. $_columns['Sub_part'] = NULL;
  1240. $_columns['Packed'] = NULL;
  1241. $_columns['Null'] = 'NO';
  1242. $_columns['Index_type'] = 'BTREE';
  1243. $_columns['Comment'] = '';
  1244. $_results[] = new ObjectArray($_columns);
  1245. }
  1246. if (stripos($this->queries[0], 'WHERE') !== false) {
  1247. preg_match('/WHERE\\s*(.*)$/im', $this->queries[0], $match);
  1248. list($key, $value) = explode('=', $match[1]);
  1249. $key = trim($key);
  1250. $value = preg_replace("/[\';]/", '', $value);
  1251. $value = trim($value);
  1252. foreach ($_results as $result) {
  1253. if (stripos($value, $result->$key) !== false) {
  1254. unset($_results);
  1255. $_results[] = $result;
  1256. break;
  1257. }
  1258. }
  1259. }
  1260. }
  1261. $this->results = $_results;
  1262. }
  1263. /**
  1264. * Method to the CHECK query data to an object.
  1265. *
  1266. * @access private
  1267. */
  1268. private function convert_result_check_or_analyze() {
  1269. $results = array();
  1270. if ($this->query_type == 'check') {
  1271. $_columns = array(
  1272. 'Table' => '',
  1273. 'Op' => 'check',
  1274. 'Msg_type' => 'status',
  1275. 'Msg_text' => 'OK'
  1276. );
  1277. } else {
  1278. $_columns = array(
  1279. 'Table' => '',
  1280. 'Op' => 'analyze',
  1281. 'Msg_type' => 'status',
  1282. 'Msg_text' => 'Table is already up to date'
  1283. );
  1284. }
  1285. $_results[] = new ObjectArray($_columns);
  1286. $this->results = $_results;
  1287. }
  1288. /**
  1289. * Method to check SQLite library version.
  1290. *
  1291. * This is used for checking if SQLite can execute multiple rows insert.
  1292. *
  1293. * @return version number string or 0
  1294. * @access private
  1295. */
  1296. private function get_sqlite_version() {
  1297. try {
  1298. $statement = $this->pdo->prepare('SELECT sqlite_version()');
  1299. $statement->execute();
  1300. $result = $statement->fetch(PDO::FETCH_NUM);
  1301. return $result[0];
  1302. } catch (PDOException $err) {
  1303. return '0';
  1304. }
  1305. }
  1306. /**
  1307. * Method to call PDO::beginTransaction().
  1308. *
  1309. * @see PDO::beginTransaction()
  1310. * @return boolean
  1311. */
  1312. public function beginTransaction() {
  1313. if ($this->has_active_transaction) {
  1314. return false;
  1315. } else {
  1316. $this->has_active_transaction = $this->pdo->beginTransaction();
  1317. return $this->has_active_transaction;
  1318. }
  1319. }
  1320. /**
  1321. * Method to call PDO::commit().
  1322. *
  1323. * @see PDO::commit()
  1324. */
  1325. public function commit() {
  1326. $this->pdo->commit();
  1327. $this->has_active_transaction = false;
  1328. }
  1329. /**
  1330. * Method to call PDO::rollBack().
  1331. *
  1332. * @see PDO::rollBack()
  1333. */
  1334. public function rollBack() {
  1335. $this->pdo->rollBack();
  1336. $this->has_active_transaction = false;
  1337. }
  1338. }
  1339. /**
  1340. * Class to change queried data to PHP object.
  1341. *
  1342. * @author kjm
  1343. */
  1344. class ObjectArray {
  1345. function __construct($data = null,&$node= null) {
  1346. foreach ($data as $key => $value) {
  1347. if ( is_array($value) ) {
  1348. if (!$node) {
  1349. $node =& $this;
  1350. }
  1351. $node->$key = new stdClass();
  1352. self::__construct($value,$node->$key);
  1353. } else {
  1354. if (!$node) {
  1355. $node =& $this;
  1356. }
  1357. $node->$key = $value;
  1358. }
  1359. }
  1360. }
  1361. }
  1362. ?>