PageRenderTime 156ms CodeModel.GetById 35ms RepoModel.GetById 1ms app.codeStats 0ms

/blogs/inc/_core/model/db/_db.class.php

https://github.com/whissip/whissip
PHP | 1954 lines | 996 code | 195 blank | 763 comment | 179 complexity | 326b68ca8d720fa9a7a67ba5169d3f1b MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * This file implements the DB class.
  4. *
  5. * Based on ezSQL - Class to make it very easy to deal with MySQL database connections.
  6. * b2evo Additions:
  7. * - nested transactions
  8. * - symbolic table names
  9. * - query log
  10. * - get_list
  11. * - dynamic extension loading
  12. * - Debug features (EXPLAIN...)
  13. * and more...
  14. *
  15. * This file is part of the b2evolution/evocms project - {@link http://b2evolution.net/}.
  16. * See also {@link http://sourceforge.net/projects/evocms/}.
  17. *
  18. * @copyright (c)2003-2010 by Francois PLANQUE - {@link http://fplanque.net/}.
  19. * Parts of this file are copyright (c)2004 by Justin Vincent - {@link http://php.justinvincent.com}
  20. * Parts of this file are copyright (c)2004-2005 by Daniel HAHLER - {@link http://thequod.de/contact}.
  21. *
  22. * {@internal License choice
  23. * - If you have received this file as part of a package, please find the license.txt file in
  24. * the same folder or the closest folder above for complete license terms.
  25. * - If you have received this file individually (e-g: from http://evocms.cvs.sourceforge.net/)
  26. * then you must choose one of the following licenses before using the file:
  27. * - GNU General Public License 2 (GPL) - http://www.opensource.org/licenses/gpl-license.php
  28. * - Mozilla Public License 1.1 (MPL) - http://www.opensource.org/licenses/mozilla1.1.php
  29. * }}
  30. *
  31. * {@internal Origin:
  32. * This file is based on the following package (excerpt from ezSQL's readme.txt):
  33. * =======================================================================
  34. * Author: Justin Vincent (justin@visunet.ie)
  35. * Web: http://php.justinvincent.com
  36. * Name: ezSQL
  37. * Desc: Class to make it very easy to deal with database connections.
  38. * License: FREE / Donation (LGPL - You may do what you like with ezSQL - no exceptions.)
  39. * =======================================================================
  40. * A $10 donation has been made to Justin VINCENT on behalf of the b2evolution team.
  41. * The package has been relicensed as GPL based on
  42. * "You may do what you like with ezSQL - no exceptions."
  43. * 2004-10-14 (email): Justin VINCENT grants Francois PLANQUE the right to relicense
  44. * this modified class under other licenses. "Just include a link to where you got it from."
  45. * }}
  46. *
  47. * {@internal Open Source relicensing agreement:
  48. * Daniel HAHLER grants Francois PLANQUE the right to license
  49. * Daniel HAHLER's contributions to this file and the b2evolution project
  50. * under any OSI approved OSS license (http://www.opensource.org/licenses/).
  51. * }}
  52. *
  53. * @package evocore
  54. *
  55. * {@internal Below is a list of authors who have contributed to design/coding of this file: }}
  56. * @author blueyed: Daniel HAHLER
  57. * @author fplanque: Francois PLANQUE
  58. * @author Justin VINCENT
  59. *
  60. * @version $Id$
  61. * @todo transaction support
  62. */
  63. if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );
  64. /**
  65. * ezSQL Constants
  66. */
  67. define( 'EZSQL_VERSION', '1.25' );
  68. define( 'OBJECT', 'OBJECT', true );
  69. define( 'ARRAY_A', 'ARRAY_A', true);
  70. define( 'ARRAY_N', 'ARRAY_N', true);
  71. /**
  72. * The Main Class
  73. *
  74. * @package evocore
  75. */
  76. class DB
  77. {
  78. /**
  79. * Show/Print errors?
  80. * @var boolean
  81. */
  82. var $show_errors = true;
  83. /**
  84. * Halt on errors?
  85. * @var boolean
  86. */
  87. var $halt_on_error = true;
  88. /**
  89. * Log errors using {@link error_log()}?
  90. * There's no reason to disable this, apart from when you are expecting
  91. * to get an error, like with {@link get_db_version()}.
  92. * @var boolean
  93. */
  94. var $log_errors = true;
  95. /**
  96. * Has an error occured?
  97. * @var boolean
  98. */
  99. var $error = false;
  100. /**
  101. * Number of done queries.
  102. * @var integer
  103. */
  104. var $num_queries = 0;
  105. /**
  106. * last query SQL string
  107. * @var string
  108. */
  109. var $last_query = '';
  110. /**
  111. * last DB error string
  112. * @var string
  113. */
  114. var $last_error = '';
  115. /**
  116. * Last insert ID
  117. * @var integer
  118. */
  119. var $insert_id = 0;
  120. /**
  121. * Last query's resource
  122. * @access protected
  123. * @var object
  124. */
  125. var $result;
  126. /**
  127. * Number of rows in result set
  128. */
  129. var $num_rows = 0;
  130. /**
  131. * Number of rows affected by insert, delete, update or replace
  132. */
  133. var $rows_affected = 0;
  134. /**
  135. * Aliases that will be replaced in queries:
  136. */
  137. var $dbaliases = array();
  138. /**
  139. * Strings that will replace the aliases in queries:
  140. */
  141. var $dbreplaces = array();
  142. /**
  143. * CREATE TABLE options.
  144. *
  145. * This gets appended to every "CREATE TABLE" query.
  146. *
  147. * Edit those if you have control over you MySQL server and want a more professional
  148. * database than what is commonly offered by popular hosting providers.
  149. *
  150. * @todo dh> If the query itself uses already e.g. "CHARACTER SET latin1" it should not get overridden..
  151. * @var string
  152. */
  153. var $table_options = '';
  154. /**
  155. * Use transactions in DB?
  156. *
  157. * You need to use InnoDB in order to enable this. See the {@link $db_config "table_options" key}.
  158. */
  159. var $use_transactions = false;
  160. /**
  161. * How many transactions are currently nested?
  162. */
  163. var $transaction_nesting_level = 0;
  164. /**
  165. * Rememeber if we have to rollback at the end of a nested transaction construct
  166. */
  167. var $rollback_nested_transaction = false;
  168. /**
  169. * MySQL Database handle
  170. * @var object mysqli
  171. */
  172. var $dbhandle;
  173. /**
  174. * Database username
  175. * @var string
  176. */
  177. var $dbuser;
  178. /**
  179. * Database username's password
  180. * @var string
  181. */
  182. var $dbpassword;
  183. /**
  184. * Database name
  185. * @var string
  186. * @see select()
  187. */
  188. var $dbname;
  189. /**
  190. * Database hostname
  191. * @var string
  192. */
  193. var $dbhost = 'localhost';
  194. // DEBUG:
  195. /**
  196. * Do we want to log queries?
  197. * If null, it gets set according to {@link $debug}.
  198. * A subclass may set it by default (e.g. DbUnitTestCase_DB).
  199. * This requires {@link $debug} to be true.
  200. * @var boolean
  201. */
  202. var $log_queries;
  203. /**
  204. * Log of queries:
  205. * @var array
  206. */
  207. var $queries = array();
  208. /**
  209. * Do we want to explain joins?
  210. * This requires {@link DB::$log_queries} to be true.
  211. *
  212. * @todo fp> we'd probably want to group all the advanced debug vars under a single setting now. We might even auto enable it when $debug=2. (And we might actually want to include a $debug="cookie" mode for easy switching with bookmarks or a bookmarklet)
  213. *
  214. * @var boolean
  215. */
  216. var $debug_explain_joins = true;
  217. /**
  218. * Do we want to profile queries?
  219. * This requires {@link DB::$log_queries} to be true.
  220. *
  221. * This sets "profiling=1" for the session and queries "SHOW PROFILE" after
  222. * each query.
  223. *
  224. * @var boolean
  225. */
  226. var $debug_profile_queries = false;
  227. /**
  228. * Do we want to output a function backtrace for every query?
  229. * Number of stack entries to show (from last to first) (Default: 0); true means 'all'.
  230. *
  231. * This requires {@link DB::$log_queries} to be true.
  232. *
  233. * @var integer
  234. */
  235. var $debug_dump_function_trace_for_queries = true;
  236. /**
  237. * Number of rows we want to dump in debug output (0 disables it)
  238. * This requires {@link DB::$log_queries} to be true.
  239. * @var integer
  240. */
  241. var $debug_dump_rows = 0;
  242. /**
  243. * Time in seconds that is considered a fast query (green).
  244. * @var float
  245. * @see dump_queries()
  246. */
  247. var $query_duration_fast = 0.05;
  248. /**
  249. * Time in seconds that is considered a slow query (red).
  250. * @var float
  251. * @see dump_queries()
  252. */
  253. var $query_duration_slow = 0.3;
  254. /**
  255. * DB Constructor
  256. *
  257. * Connects to the server and selects a database.
  258. *
  259. * @param array An array of parameters.
  260. * Manadatory:
  261. * - 'user': username to connect with
  262. * - 'password': password to connect with
  263. * OR
  264. * - 'handle': a MySQLi database handle/object (from a previous {@link mysqli_connect()})
  265. * Optional:
  266. * - 'name': the name of the default database, see {@link DB::select()}
  267. * - 'host': host of the database; Default: 'localhost'
  268. * - 'show_errors': Display SQL errors? (true/false); Default: don't change member default ({@link $show_errors})
  269. * - 'halt_on_error': Halt on error? (true/false); Default: don't change member default ({@link $halt_on_error})
  270. * - 'table_options': sets {@link $table_options}
  271. * - 'use_transactions': sets {@link $use_transactions}
  272. * - 'aliases': Aliases for tables (array( alias => table name )); Default: no aliases.
  273. * - 'new_link': create a new link to the DB, even if there was a mysqli_connect() with
  274. * the same params before. (requires PHP 4.2)
  275. * - 'client_flags': optional settings like compression or SSL encryption. See {@link http://www.php.net/manual/en/ref.mysql.php#mysql.client-flags}.
  276. * (requires PHP 4.3)
  277. * - 'strict_mode': use MySQL strict mode (SET sql_mode="TRADITIONAL") (Default: false)
  278. * - 'log_queries': should queries get logged internally? (follows $debug by default, and requires it to be enabled otherwise)
  279. * This is a requirement for the following options:
  280. * - 'debug_dump_rows': Number of rows to dump
  281. * - 'debug_explain_joins': Explain JOINS? (calls "EXPLAIN $query")
  282. * - 'debug_profile_queries': Profile queries? (calls "SHOW PROFILE" after each query)
  283. * - 'debug_dump_function_trace_for_queries': Collect call stack for queries? (showing where queries have been called)
  284. */
  285. function DB( $params )
  286. {
  287. global $debug;
  288. // Mandatory parameters:
  289. if( isset( $params['handle'] ) )
  290. { // DB-Link provided:
  291. $this->dbhandle = $params['handle'];
  292. }
  293. else
  294. {
  295. $this->dbuser = $params['user'];
  296. $this->dbpassword = $params['password'];
  297. }
  298. // Optional parameters (Allow overriding through $params):
  299. if( isset($params['name']) ) $this->dbname = $params['name'];
  300. if( isset($params['host']) ) $this->dbhost = $params['host'];
  301. if( isset($params['show_errors']) ) $this->show_errors = $params['show_errors'];
  302. if( isset($params['halt_on_error']) ) $this->halt_on_error = $params['halt_on_error'];
  303. if( isset($params['table_options']) ) $this->table_options = $params['table_options'];
  304. if( isset($params['use_transactions']) ) $this->use_transactions = $params['use_transactions'];
  305. if( isset($params['debug_dump_rows']) ) $this->debug_dump_rows = $params['debug_dump_rows']; // Nb of rows to dump
  306. if( isset($params['debug_explain_joins']) ) $this->debug_explain_joins = $params['debug_explain_joins'];
  307. if( isset($params['debug_profile_queries']) ) $this->debug_profile_queries = $params['debug_profile_queries'];
  308. if( isset($params['debug_dump_function_trace_for_queries']) ) $this->debug_dump_function_trace_for_queries = $params['debug_dump_function_trace_for_queries'];
  309. if( isset($params['log_queries']) )
  310. {
  311. $this->log_queries = $debug && $params['log_queries'];
  312. }
  313. elseif( isset($debug) && ! isset($this->log_queries) )
  314. { // $log_queries follows $debug and respects subclasses, which may define it:
  315. $this->log_queries = (bool)$debug;
  316. }
  317. if( ! extension_loaded('mysql') )
  318. { // The mysql extension is not loaded, try to dynamically load it:
  319. $mysql_ext_file = is_windows() ? 'php_mysqli.dll' : 'mysqli.so';
  320. if( function_exists('dl') )
  321. {
  322. $php_errormsg = null;
  323. $old_track_errors = ini_set('track_errors', 1);
  324. $old_html_errors = ini_set('html_errors', 0);
  325. @dl( $mysql_ext_file );
  326. $error_msg = $php_errormsg;
  327. if( $old_track_errors !== false ) ini_set('track_errors', $old_track_errors);
  328. if( $old_html_errors !== false ) ini_set('html_errors', $old_html_errors);
  329. }
  330. else
  331. {
  332. $error_msg = 'The PHP mysql extension is not installed and we cannot load it dynamically.';
  333. }
  334. if( ! extension_loaded('mysql') )
  335. { // Still not loaded:
  336. $this->print_error( 'The PHP MySQLi module could not be loaded.', '
  337. <p><strong>Error:</strong> '.$error_msg.'</p>
  338. <p>You probably have to edit your php configuration (php.ini) and enable this module ('.$mysql_ext_file.').</p>
  339. <p>You may have to install it first (e.g. php5-mysql on Debian/Ubuntu).</p>
  340. <p>Do not forget to restart your webserver (if necessary) after editing the PHP conf.</p>', false );
  341. return;
  342. }
  343. }
  344. if( isset($params['new_link']) && ! $params['new_link'] ) {
  345. debug_die('DB: new_link=false is not supported anymore.');
  346. }
  347. if( isset($params['client_flags']) ) {
  348. debug_die('DB: client_flags is not supported anymore.');
  349. }
  350. #$new_link = isset( $params['new_link'] ) ? $params['new_link'] : false;
  351. #$client_flags = isset( $params['client_flags'] ) ? $params['client_flags'] : 0;
  352. if( ! $this->dbhandle )
  353. { // Connect to the Database:
  354. // echo "mysqli_connect( $this->dbhost, $this->dbuser, $this->dbpassword, $new_link, $client_flags )";
  355. // mysqli_error() is tied to an established connection
  356. // if the connection fails we need a different method to get the error message
  357. $php_errormsg = null;
  358. $old_track_errors = ini_set('track_errors', 1);
  359. $old_html_errors = ini_set('html_errors', 0);
  360. #$this->dbhandle = mysqli_connect( $this->dbhost, $this->dbuser, $this->dbpassword, $new_link, $client_flags );
  361. $this->dbhandle = mysqli_connect( $this->dbhost, $this->dbuser, $this->dbpassword );
  362. $mysql_error = $php_errormsg;
  363. if( $old_track_errors !== false ) ini_set('track_errors', $old_track_errors);
  364. if( $old_html_errors !== false ) ini_set('html_errors', $old_html_errors);
  365. }
  366. if( ! $this->dbhandle )
  367. {
  368. $this->print_error( 'Error establishing a database connection!',
  369. ( $mysql_error ? '<p>('.$mysql_error.')</p>' : '' ).'
  370. <ol>
  371. <li>Are you sure you have typed the correct user/password?</li>
  372. <li>Are you sure that you have typed the correct hostname?</li>
  373. <li>Are you sure that the database server is running?</li>
  374. </ol>', false );
  375. }
  376. elseif( isset($this->dbname) )
  377. {
  378. $this->select($this->dbname);
  379. }
  380. if( $this->query( 'SET NAMES UTF8' ) === false )
  381. {
  382. debug_die( 'Could not "SET NAMES UTF8"! (MySQL error: '.strip_tags($this->last_error).')' );
  383. }
  384. /*
  385. echo '<br />Server: '.$this->get_var( 'SELECT @@character_set_server' );
  386. echo '<br />Database: '.$this->get_var( 'SELECT @@character_set_database' );
  387. echo '<br />Connection: '.$this->get_var( 'SELECT @@character_set_connection' );
  388. echo '<br />Client: '.$this->get_var( 'SELECT @@character_set_client' );
  389. echo '<br />Results: '.$this->get_var( 'SELECT @@character_set_results' );
  390. */
  391. if( isset($params['aliases']) )
  392. { // Prepare aliases for replacements:
  393. foreach( $params['aliases'] as $dbalias => $dbreplace )
  394. {
  395. $this->dbaliases[] = '#\b'.$dbalias.'\b#'; // \b = word boundary
  396. $this->dbreplaces[] = $dbreplace;
  397. // echo '<br />'.'#\b'.$dbalias.'\b#';
  398. }
  399. // echo count($this->dbaliases);
  400. }
  401. if( ! empty($params['strict_mode']) )
  402. { // Force MySQL strict mode
  403. $this->query( 'SET sql_mode = "TRADITIONAL"', 'we do this in DEBUG mode only' );
  404. }
  405. if( $this->debug_profile_queries )
  406. {
  407. // dh> this will fail, if it is not supported, but has to be enabled manually anyway.
  408. $this->query('SET profiling = 1'); // Requires 5.0.37.
  409. }
  410. }
  411. /**
  412. * Select a DB (if another one needs to be selected)
  413. */
  414. function select($db)
  415. {
  416. if( !@mysqli_select_db($this->dbhandle, $db) )
  417. {
  418. $this->print_error( 'Error selecting database ['.$db.']!', '
  419. <ol>
  420. <li>Are you sure the database exists?</li>
  421. <li>Are you sure the DB user is allowed to use that database?</li>
  422. <li>Are you sure there is a valid database connection?</li>
  423. </ol>', false );
  424. }
  425. $this->dbname = $db;
  426. }
  427. /**
  428. * Format a string correctly for safe insert under all PHP conditions
  429. */
  430. function escape($str)
  431. {
  432. return mysqli_real_escape_string($this->dbhandle, $str);
  433. }
  434. /**
  435. * Quote a value, either in single quotes (and escaped) or if it's NULL as 'NULL'.
  436. *
  437. * @param string|array|null
  438. * @return string Quoted (and escaped) value or 'NULL'.
  439. */
  440. function quote($str)
  441. {
  442. if( $str === NULL )
  443. {
  444. return 'NULL';
  445. }
  446. $type = gettype($str);
  447. if( $type == 'array' )
  448. {
  449. return implode(',', array_map( array('DB', 'quote'), $str )); // TODO: should be 'self' (PHP 5.3?)
  450. }
  451. // Add Debuglog warning when quoting integers (not necessary):
  452. // if( $type == 'integer' )
  453. // {
  454. // global $Debuglog;
  455. // if( $Debuglog ) {
  456. // $Debuglog->add('DB::quote: quoting integer: '.$str.' (performance drawback) '.debug_get_backtrace(), 'debug');
  457. // }
  458. // }
  459. return "'".$this->escape($str)."'";
  460. }
  461. /**
  462. * Surround identifier with backticks, such as table or column names.
  463. * Escape quote char in the identifier itself by duplicating them.
  464. * @see http://dev.mysql.com/doc/refman/5.1/en/identifiers.html
  465. *
  466. * @param string
  467. * @return string Quoted value
  468. */
  469. function quote_identifier($str)
  470. {
  471. $str = str_replace('`', '``', $str);
  472. return '`'.$str.'`';
  473. }
  474. /**
  475. * @return string Return the given value or 'NULL', if it's === NULL.
  476. */
  477. function null($val)
  478. {
  479. if( $val === NULL )
  480. return 'NULL';
  481. else
  482. return $val;
  483. }
  484. /**
  485. * Returns the correct WEEK() function to get the week number for the given date.
  486. *
  487. * @link http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html
  488. *
  489. * @todo disable when MySQL < 4
  490. * @param string will be used as is
  491. * @param integer 0 for sunday, 1 for monday
  492. */
  493. function week( $date, $startofweek )
  494. {
  495. if( $startofweek == 1 )
  496. { // Week starts on Monday, week 1 must have a monday in this year:
  497. return ' WEEK( '.$date.', 5 ) ';
  498. }
  499. // Week starts on Sunday, week 1 must have a sunday in this year:
  500. return ' WEEK( '.$date.', 0 ) ';
  501. }
  502. /**
  503. * Print SQL/DB error.
  504. *
  505. * TODO: fp> bloated: it probably doesn't make sense to display errors if we don't stop. Any use case?
  506. * dh> Sure. Local testing (and test cases).
  507. *
  508. * @param string Short error (no HTML)
  509. * @param string Extended description/help for the error (for HTML)
  510. * @param string|false Query title; false if {@link DB::$last_query} should not get displayed
  511. */
  512. function print_error( $title = '', $html_str = '', $query_title = '' )
  513. {
  514. // All errors go to the global error array $EZSQL_ERROR..
  515. global $EZSQL_ERROR, $is_cli;
  516. $this->error = true;
  517. // If no special error string then use mysql default..
  518. if( ! strlen($title) )
  519. {
  520. if( is_object($this->dbhandle) )
  521. { // use mysql_error:
  522. $this->last_error = mysqli_error($this->dbhandle).'(Errno='.mysqli_errno($this->dbhandle).')';
  523. }
  524. else
  525. {
  526. $this->last_error = 'Unknown (and no $dbhandle available)';
  527. }
  528. }
  529. else
  530. {
  531. $this->last_error = $title;
  532. }
  533. // Log this error to the global array..
  534. $EZSQL_ERROR[] = array(
  535. 'query' => $this->last_query,
  536. 'error_str' => $this->last_error
  537. );
  538. // Send error to PHP's system logger.
  539. if( $this->log_errors )
  540. {
  541. // TODO: dh> respect $log_app_errors? Create a wrapper, e.g. evo_error_log, which can be used later to write into e.g. a DB table?!
  542. if( isset($_SERVER['REQUEST_URI']) )
  543. {
  544. $req_url = ( (isset($_SERVER['HTTPS']) && ( $_SERVER['HTTPS'] != 'off' ) ) ? 'https://' : 'http://' )
  545. .$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
  546. }
  547. else
  548. {
  549. $req_url = '-';
  550. }
  551. $error_text = 'SQL ERROR: '. $this->last_error
  552. . ', QUERY: "'.trim($this->last_query).'"'
  553. . ', BACKTRACE: '.trim(strip_tags(debug_get_backtrace()))
  554. . ', URL: '.$req_url;
  555. error_log( preg_replace( '#\s+#', ' ', $error_text ) );
  556. }
  557. if( ! ( $this->halt_on_error || $this->show_errors ) )
  558. { // no reason to generate a nice message:
  559. return;
  560. }
  561. if( $this->halt_on_error && ! $this->show_errors )
  562. { // do not show errors, just die:
  563. die();
  564. }
  565. if( $is_cli )
  566. { // Clean error message for command line interface:
  567. $err_msg = "MySQL error!\n{$this->last_error}\n";
  568. if( ! empty($this->last_query) && $query_title !== false )
  569. {
  570. $err_msg .= "Your query: $query_title\n";
  571. $err_msg .= $this->format_query( $this->last_query, false );
  572. }
  573. }
  574. else
  575. {
  576. $err_msg = '<p class="error">MySQL error!</p>'."\n";
  577. $err_msg .= "<div><p><strong>{$this->last_error}</strong></p>\n";
  578. $err_msg .= $html_str;
  579. if( !empty($this->last_query) && $query_title !== false )
  580. {
  581. $err_msg .= '<p class="error">Your query: '.$query_title.'</p>';
  582. $err_msg .= '<pre>';
  583. $err_msg .= $this->format_query( $this->last_query, ! $is_cli );
  584. $err_msg .= '</pre>';
  585. }
  586. $err_msg .= "</div>\n";
  587. }
  588. if( $this->halt_on_error )
  589. {
  590. if( function_exists('debug_die') )
  591. {
  592. debug_die( $err_msg );
  593. }
  594. else
  595. {
  596. die( $err_msg );
  597. }
  598. }
  599. elseif( $this->show_errors )
  600. { // If there is an error then take note of it
  601. echo '<div class="error">';
  602. echo $err_msg;
  603. echo '</div>';
  604. }
  605. }
  606. /**
  607. * Kill cached query results
  608. */
  609. function flush()
  610. {
  611. $this->result = NULL;
  612. $this->last_query = NULL;
  613. $this->num_rows = 0;
  614. if( $this->result && is_object($this->result) )
  615. { // Free last result object
  616. mysqli_free_result($this->result);
  617. }
  618. }
  619. /**
  620. * Get MYSQL version
  621. */
  622. function get_version( $query_title = NULL )
  623. {
  624. if( isset( $this->version ) )
  625. {
  626. return $this->version;
  627. }
  628. $this->save_error_state();
  629. // Blatantly ignore any error generated by potentially unknown function...
  630. $this->show_errors = false;
  631. $this->halt_on_error = false;
  632. if( ($this->version_long = $this->get_var( 'SELECT VERSION()', 0, 0, $query_title ) ) === NULL )
  633. { // Very old version ( < 4.0 )
  634. $this->version = '';
  635. $this->version_long = '';
  636. }
  637. else
  638. {
  639. $this->version = preg_replace( '¤-.*¤', '', $this->version_long );
  640. }
  641. $this->restore_error_state();
  642. return $this->version;
  643. }
  644. /**
  645. * Save the vars responsible for error handling.
  646. * This can be chained.
  647. * @see DB::restore_error_state()
  648. */
  649. function save_error_state()
  650. {
  651. $this->saved_error_states[] = array(
  652. 'show_errors' => $this->show_errors,
  653. 'halt_on_error' => $this->halt_on_error,
  654. 'last_error' => $this->last_error,
  655. 'error' => $this->error,
  656. 'log_errors' => $this->log_errors,
  657. );
  658. }
  659. /**
  660. * Call this after {@link save_halt_on_error()} to
  661. * restore the previous error state.
  662. * This can be chained.
  663. * @see DB::save_error_state()
  664. */
  665. function restore_error_state()
  666. {
  667. if( empty($this->saved_error_states)
  668. || ! is_array($this->saved_error_states) )
  669. {
  670. return false;
  671. }
  672. $state = array_pop($this->saved_error_states);
  673. foreach( $state as $k => $v )
  674. $this->$k = $v;
  675. }
  676. /**
  677. * Basic Query
  678. *
  679. * @param string SQL query
  680. * @param string title for debugging
  681. * @return mixed # of rows affected or false if error
  682. */
  683. function query( $query, $title = '' )
  684. {
  685. global $Timer;
  686. // initialise return
  687. $return_val = 0;
  688. // Flush cached values..
  689. $this->flush();
  690. // Replace aliases:
  691. if( ! empty($this->dbaliases) )
  692. {
  693. // TODO: this should only replace the table name part(s), not the whole query!
  694. // blueyed> I've changed it to replace in table name parts for UPDATE, INSERT and REPLACE, because
  695. // it corrupted serialized data..
  696. // IMHO, a cleaner solution would be to use {T_xxx} in the queries and replace it here. In object properties (e.g. DataObject::$dbtablename), only "T_xxx" would get used and surrounded by "{..}" in the queries it creates.
  697. if( preg_match( '~^\s*(UPDATE\s+)(.*?)(\sSET\s.*)$~is', $query, $match ) )
  698. { // replace only between UPDATE and SET:
  699. $query = $match[1].preg_replace( $this->dbaliases, $this->dbreplaces, $match[2] ).$match[3];
  700. }
  701. elseif( preg_match( '~^\s*(INSERT|REPLACE\s+)(.*?)(\s(VALUES|SET)\s.*)$~is', $query, $match ) )
  702. { // replace only between INSERT|REPLACE and VALUES|SET:
  703. $query = $match[1].preg_replace( $this->dbaliases, $this->dbreplaces, $match[2] ).$match[3];
  704. }
  705. else
  706. { // replace in whole query:
  707. $query = preg_replace( $this->dbaliases, $this->dbreplaces, $query );
  708. if( ! empty($this->table_options) && preg_match( '#^ \s* create \s* table \s #ix', $query) )
  709. { // Query is a table creation, we add table options:
  710. $query = preg_replace( '~;\s*$~', '', $query ); // remove any ";" at the end
  711. $query .= ' '.$this->table_options;
  712. }
  713. }
  714. }
  715. elseif( ! empty($this->table_options) )
  716. { // No aliases, but table_options:
  717. if( preg_match( '#^ \s* create \s* table \s #ix', $query) )
  718. { // Query is a table creation, we add table options:
  719. $query = preg_replace( '~;\s*$~', '', $query ); // remove any ";" at the end
  720. $query .= $this->table_options;
  721. }
  722. }
  723. // echo '<p>'.$query.'</p>';
  724. // Keep track of the last query for debug..
  725. $this->last_query = $query;
  726. // Perform the query via std mysql_query function..
  727. $this->num_queries++;
  728. if( $this->log_queries )
  729. { // We want to log queries:
  730. $this->queries[ $this->num_queries - 1 ] = array(
  731. 'title' => $title,
  732. 'sql' => $query,
  733. 'rows' => -1,
  734. 'time' => 'unknown',
  735. 'results' => 'unknown' );
  736. }
  737. if( is_object($Timer) )
  738. {
  739. // Resume global query timer
  740. $Timer->resume( 'SQL QUERIES' , false );
  741. // Start a timer for this particular query:
  742. $Timer->start( 'sql_query', false );
  743. // Run query:
  744. $this->result = mysqli_query( $this->dbhandle, $query );
  745. if( $this->log_queries )
  746. { // We want to log queries:
  747. // Get duration for last query:
  748. $this->queries[ $this->num_queries - 1 ]['time'] = $Timer->get_duration( 'sql_query', 10 );
  749. }
  750. // Pause global query timer:
  751. $Timer->pause( 'SQL QUERIES' , false );
  752. }
  753. else
  754. {
  755. // Run query:
  756. $this->result = @mysqli_query( $this->dbhandle, $query );
  757. }
  758. if( $this->log_queries && $this->debug_dump_function_trace_for_queries )
  759. { // Log backtrace, also for invalid queries:
  760. $this->queries[ $this->num_queries - 1 ]['function_trace'] = debug_get_backtrace( $this->debug_dump_function_trace_for_queries, array( array( 'class' => 'DB' ) ), 1 ); // including first stack entry from class DB
  761. }
  762. // If there is an error then take note of it..
  763. if( is_object($this->dbhandle) && mysqli_error($this->dbhandle) )
  764. {
  765. if( is_object($this->result) )
  766. {
  767. mysqli_free_result($this->result);
  768. }
  769. $this->print_error( '', '', $title );
  770. return false;
  771. }
  772. if( preg_match( '#^\s*(INSERT|DELETE|UPDATE|REPLACE)\s#i', $query, $match ) )
  773. { // Query was an insert, delete, update, replace:
  774. $this->rows_affected = mysqli_affected_rows($this->dbhandle);
  775. if( $this->log_queries )
  776. { // We want to log queries:
  777. $this->queries[ $this->num_queries - 1 ]['rows'] = $this->rows_affected;
  778. }
  779. // Take note of the insert_id, for INSERT and REPLACE:
  780. $match[1] = strtoupper($match[1]);
  781. if( $match[1] == 'INSERT' || $match[1] == 'REPLACE' )
  782. {
  783. $this->insert_id = mysqli_insert_id($this->dbhandle);
  784. }
  785. // Return number of rows affected
  786. $return_val = $this->rows_affected;
  787. }
  788. else
  789. { // Query was a select, alter, etc...:
  790. if( is_object($this->result) )
  791. { // It's not a object for CREATE or DROP for example and can even trigger a fatal error (see http://forums.b2evolution.net//viewtopic.php?t=9529)
  792. $this->num_rows = mysqli_num_rows($this->result);
  793. }
  794. if( $this->log_queries )
  795. { // We want to log queries:
  796. $this->queries[ $this->num_queries - 1 ]['rows'] = $this->num_rows;
  797. }
  798. // Return number of rows selected
  799. $return_val = $this->num_rows;
  800. }
  801. if( $this->log_queries )
  802. { // We want to log queries:
  803. if( $this->debug_dump_rows && $this->num_rows )
  804. {
  805. $this->queries[ $this->num_queries - 1 ]['results'] = $this->debug_get_rows_table( $this->debug_dump_rows );
  806. }
  807. // Profile queries
  808. if( $this->debug_profile_queries )
  809. {
  810. // save values:
  811. $saved_last_result = $this->result;
  812. $saved_num_rows = $this->num_rows;
  813. $this->num_rows = 0;
  814. $this->result = @mysqli_query( $this->dbhandle, 'SHOW PROFILE' );
  815. $this->num_rows = mysqli_num_rows($this->result);
  816. if( $this->num_rows )
  817. {
  818. $this->queries[$this->num_queries-1]['profile'] = $this->debug_get_rows_table( 100, true );
  819. // Get time information from PROFILING table (which corresponds to "SHOW PROFILE")
  820. $this->result = mysqli_query( $this->dbhandle, 'SELECT FORMAT(SUM(DURATION), 6) AS DURATION FROM INFORMATION_SCHEMA.PROFILING GROUP BY QUERY_ID ORDER BY QUERY_ID DESC LIMIT 1' );
  821. $this->queries[$this->num_queries-1]['time_profile'] = array_shift(mysqli_fetch_row($this->result));
  822. }
  823. // Free "PROFILE" result resource:
  824. mysqli_free_result($this->result);
  825. // Restore:
  826. $this->result = $saved_last_result;
  827. $this->num_rows = $saved_num_rows;
  828. }
  829. }
  830. return $return_val;
  831. }
  832. /**
  833. * Get one variable from the DB - see docs for more detail
  834. *
  835. * Note: To be sure that you received NULL from the DB and not "no rows" check
  836. * for {@link $num_rows}.
  837. *
  838. * @param string Optional query to execute
  839. * @param integer Column number (starting at and defaulting to 0)
  840. * @param integer Row (defaults to NULL for "next"/"do not seek")
  841. * @param string Optional title of query
  842. * @return mixed NULL if not found, the value otherwise (which may also be NULL).
  843. */
  844. function get_var( $query = NULL, $x = 0, $y = NULL, $title = '' )
  845. {
  846. // If there is a query then perform it if not then use cached results..
  847. if( $query )
  848. {
  849. $this->query($query, $title);
  850. }
  851. if( $this->num_rows
  852. && ( $y === NULL || mysqli_data_seek($this->result, $y) ) )
  853. {
  854. $row = mysqli_fetch_row($this->result);
  855. if( isset($row[$x]) )
  856. {
  857. return $row[$x];
  858. }
  859. }
  860. return NULL;
  861. }
  862. /**
  863. * Get one row from the DB.
  864. *
  865. * @param string Query (or NULL for previous query)
  866. * @param string Output type ("OBJECT", "ARRAY_A", "ARRAY_N")
  867. * @param int Row to fetch (or NULL for next - useful with $query=NULL)
  868. * @param string Optional title for $query (if any)
  869. * @return mixed
  870. */
  871. function get_row( $query = NULL, $output = OBJECT, $y = NULL, $title = '' )
  872. {
  873. // If there is a query then perform it if not then use cached results..
  874. if( $query )
  875. {
  876. $this->query($query, $title);
  877. }
  878. if( ! $this->num_rows
  879. || ( isset($y) && ! mysqli_data_seek($this->result, $y) ) )
  880. {
  881. if( $output == OBJECT )
  882. return NULL;
  883. else
  884. return array();
  885. }
  886. // If the output is an object then return object using the row offset..
  887. switch( $output )
  888. {
  889. case OBJECT:
  890. return mysqli_fetch_object($this->result);
  891. case ARRAY_A:
  892. return mysqli_fetch_array($this->result, MYSQL_ASSOC);
  893. case ARRAY_N:
  894. return mysqli_fetch_array($this->result, MYSQL_NUM);
  895. default:
  896. $this->print_error('DB::get_row(string query, output type, int offset) -- Output type must be one of: OBJECT, ARRAY_A, ARRAY_N', '', false);
  897. break;
  898. }
  899. }
  900. /**
  901. * Function to get 1 column from the cached result set based on X index
  902. * see docs for usage and info
  903. *
  904. * @return array
  905. */
  906. function get_col( $query = NULL, $x = 0, $title = '' )
  907. {
  908. // If there is a query then perform it if not then use cached results..
  909. if( $query )
  910. {
  911. $this->query( $query, $title );
  912. }
  913. // Extract the column values
  914. $new_array = array();
  915. for( $i = 0; $i < $this->num_rows; $i++ )
  916. {
  917. $new_array[$i] = $this->get_var( NULL, $x, $i );
  918. }
  919. return $new_array;
  920. }
  921. /**
  922. * Function to get the second column from the cached result indexed by the first column
  923. *
  924. * @return array [col_0] => col_1
  925. */
  926. function get_assoc( $query = NULL, $title = '' )
  927. {
  928. // If there is a query then perform it if not then use cached results..
  929. if( $query )
  930. {
  931. $this->query( $query, $title );
  932. }
  933. // Extract the column values
  934. $new_array = array();
  935. for( $i = 0; $i < $this->num_rows; $i++ )
  936. {
  937. $key = $this->get_var( NULL, 0, $i );
  938. $new_array[$key] = $this->get_var( NULL, 1, $i );
  939. }
  940. return $new_array;
  941. }
  942. /**
  943. * Return the the query as a result set - see docs for more details
  944. *
  945. * @return mixed
  946. */
  947. function get_results( $query = NULL, $output = OBJECT, $title = '' )
  948. {
  949. // If there is a query then perform it if not then use cached results..
  950. if( $query )
  951. {
  952. $this->query($query, $title);
  953. }
  954. $r = array();
  955. if( $this->num_rows )
  956. {
  957. mysqli_data_seek($this->result, 0);
  958. switch( $output )
  959. {
  960. case OBJECT:
  961. while( $row = mysqli_fetch_object($this->result) )
  962. {
  963. $r[] = $row;
  964. }
  965. break;
  966. case ARRAY_A:
  967. while( $row = mysqli_fetch_array($this->result, MYSQLI_ASSOC) )
  968. {
  969. $r[] = $row;
  970. }
  971. break;
  972. case ARRAY_N:
  973. while( $row = mysqli_fetch_array($this->result, MYSQLI_NUM) )
  974. {
  975. $r[] = $row;
  976. }
  977. break;
  978. }
  979. }
  980. return $r;
  981. }
  982. /**
  983. * Function to get column meta data info pertaining to the last query.
  984. *
  985. * NOTE: not used in whissip/b2evo anymore, but maintained still anyway.
  986. *
  987. * @param string|NULL Key of info, see {@link http://php.net/mysqli_fetch_field_direct} for a list;
  988. * empty/NULL for an array with all entries
  989. * @param integer Column offset; -1 for all
  990. */
  991. function get_col_info( $info_type = 'name', $col_offset = -1 )
  992. {
  993. if( ! is_object($this->result) )
  994. { // fp> A function should NEVER FAIL SILENTLY!
  995. debug_die( 'DB::get_col_info() cannot return a value because no result resource is available!' );
  996. }
  997. // Get column info:
  998. if( $col_offset == -1 )
  999. { // all columns:
  1000. $n = mysqli_num_fields($this->result);
  1001. $i = 0;
  1002. while( $i < $n )
  1003. {
  1004. $col_info[$i] = mysqli_fetch_field_direct($this->result, $i);
  1005. $i++;
  1006. }
  1007. }
  1008. else
  1009. {
  1010. $col_info = mysqli_fetch_field_direct($this->result, $col_offset);
  1011. }
  1012. if( empty($info_type) )
  1013. { // all field properties:
  1014. return $col_info;
  1015. }
  1016. else
  1017. { // a specific column field property
  1018. if( $col_offset == -1 )
  1019. {
  1020. $new_array = array();
  1021. $i = 0;
  1022. foreach( $col_info as $col )
  1023. {
  1024. $new_array[$i] = $col->{$info_type};
  1025. $i++;
  1026. }
  1027. return $new_array;
  1028. }
  1029. else
  1030. {
  1031. return $col_info->{$info_type};
  1032. }
  1033. }
  1034. }
  1035. /**
  1036. * Get a table (or "<p>No Results.</p>") for the SELECT query results.
  1037. *
  1038. * @return string HTML table or "No Results" if the
  1039. */
  1040. function debug_get_rows_table( $max_lines, $break_at_comma = false )
  1041. {
  1042. $r = '';
  1043. if( ! $this->result || ! $this->num_rows ) {
  1044. return '<p>No Results.</p>';
  1045. }
  1046. // Get column info:
  1047. $col_info = array();
  1048. $n = mysqli_num_fields($this->result);
  1049. $i = 0;
  1050. while( $i < $n ) {
  1051. $col_info[$i] = mysqli_fetch_field_direct($this->result, $i);
  1052. $i++;
  1053. }
  1054. // =====================================================
  1055. // Results top rows
  1056. $r .= '<table cellspacing="0" summary="Results for query"><tr>';
  1057. for( $i = 0, $count = count($col_info); $i < $count; $i++ )
  1058. {
  1059. $r .= '<th><span class="type">'.$col_info[$i]->type.' '.$col_info[$i]->max_length.'</span><br />'
  1060. .$col_info[$i]->name.'</th>';
  1061. }
  1062. $r .= '</tr>';
  1063. // ======================================================
  1064. // Print main results
  1065. $i=0;
  1066. // Rewind to first row (should be there already).
  1067. mysqli_data_seek($this->result, 0);
  1068. while( $one_row = $this->get_row(NULL, ARRAY_N) )
  1069. {
  1070. $i++;
  1071. if( $i >= $max_lines ) {
  1072. break;
  1073. }
  1074. $r .= '<tr>';
  1075. foreach( $one_row as $item ) {
  1076. if( $i % 2 ) {
  1077. $r .= '<td class="odd">';
  1078. } else {
  1079. $r .= '<td>';
  1080. }
  1081. if( $break_at_comma ) {
  1082. $r .= str_replace( array(',', ';'), '<br />', htmlspecialchars($item) );
  1083. } else {
  1084. $r .= strmaxlen($item, 100, NULL, 'htmlspecialchars');
  1085. }
  1086. $r .= '</td>';
  1087. }
  1088. $r .= '</tr>';
  1089. }
  1090. // Rewind to first row again.
  1091. mysqli_data_seek($this->result, 0);
  1092. if( $i >= $max_lines ) {
  1093. $r .= '<tr><td colspan="'.(count($col_info)+1).'">Max number of dumped rows has been reached.</td></tr>';
  1094. }
  1095. $r .= '</table>';
  1096. return $r;
  1097. }
  1098. /**
  1099. * Format a SQL query
  1100. * @static
  1101. * @param string SQL
  1102. * @param boolean Format with/for HTML?
  1103. */
  1104. function format_query( $sql, $html = true, $maxlen = NULL )
  1105. {
  1106. $sql = trim( str_replace("\t", ' ', $sql ) );
  1107. if( $maxlen )
  1108. {
  1109. $sql = strmaxlen($sql, $maxlen, '...');
  1110. }
  1111. // Format query (adding newlines/indent)
  1112. $new = '';
  1113. $word = '';
  1114. $in_comment = false;
  1115. $in_literal = false;
  1116. for( $i = 0, $n = strlen($sql); $i < $n; $i++ )
  1117. {
  1118. $c = $sql[$i];
  1119. if( $in_comment )
  1120. {
  1121. if( $in_comment === '/*' && substr($sql, $i, 2) == '*/' )
  1122. $in_comment = false;
  1123. elseif( $c == "\n" )
  1124. $in_comment = false;
  1125. }
  1126. elseif( $in_literal )
  1127. {
  1128. if( $c == $in_literal )
  1129. $in_literal = false;
  1130. }
  1131. elseif( $c == '#' || ($c == '-' && substr($sql, $i, 3) == '-- ') )
  1132. {
  1133. $in_comment = true;
  1134. }
  1135. elseif( ctype_space($c) )
  1136. {
  1137. $uword = strtoupper($word);
  1138. if( in_array($uword, array('SELECT', 'FROM', 'WHERE', 'GROUP', 'ORDER', 'LIMIT', 'VALUES', 'AND', 'OR', 'LEFT', 'RIGHT', 'INNER')) )
  1139. {
  1140. $new = rtrim($new)."\n".str_pad($word, 6, ' ', STR_PAD_LEFT).' ';
  1141. # Remove any trailing whitespace after keywords
  1142. while( ctype_space($sql[$i+1]) ) {
  1143. ++$i;
  1144. }
  1145. }
  1146. else
  1147. {
  1148. $new .= $word.$c;
  1149. }
  1150. $word = '';
  1151. continue;
  1152. }
  1153. $word .= $c;
  1154. }
  1155. $sql = trim($new.$word);
  1156. if( ! $html )
  1157. {
  1158. return $sql;
  1159. }
  1160. if( empty($GLOBALS['db_use_geshi_highlighting']) )
  1161. { // poor man's indent
  1162. $sql = htmlspecialchars($sql);
  1163. $sql = preg_replace_callback("~^(\s+)~m", create_function('$m', 'return str_replace(" ", "&nbsp;", $m[1]);'), $sql);
  1164. $sql = nl2br($sql);
  1165. return $sql;
  1166. }
  1167. # Parse/Highlight SQL using GeSHi
  1168. static $geshi;
  1169. if( ! isset($geshi) ) {
  1170. load_funcs( '_ext/geshi/geshi.php' );
  1171. $geshi = new GeSHi($sql, 'mysql');
  1172. $geshi->set_header_type(GESHI_HEADER_NONE);
  1173. $geshi->set_tab_width(2);
  1174. } else {
  1175. $geshi->set_source($sql);
  1176. }
  1177. return $geshi->parse_code();
  1178. }
  1179. /**
  1180. * Displays all queries that have been executed
  1181. *
  1182. * @param boolean Use HTML.
  1183. */
  1184. function dump_queries( $html = true )
  1185. {
  1186. if ( $html )
  1187. {
  1188. echo '<strong>DB queries:</strong> '.$this->num_queries."<br />\n";
  1189. }
  1190. else
  1191. {
  1192. echo 'DB queries: '.$this->num_queries."\n\n";
  1193. }
  1194. if( ! $this->log_queries )
  1195. { // nothing more to do here..
  1196. return;
  1197. }
  1198. global $Timer;
  1199. if( is_object( $Timer ) )
  1200. {
  1201. $time_queries = $Timer->get_duration( 'SQL QUERIES' , 4 );
  1202. }
  1203. else
  1204. {
  1205. $time_queries = 0;
  1206. }
  1207. $count_queries = 0;
  1208. $count_rows = 0;
  1209. $time_queries_profiled = 0;
  1210. // Javascript function to toggle DIVs (EXPLAIN, results, backtraces).
  1211. if( $html )
  1212. {
  1213. global $rsc_url;
  1214. echo '<script type="text/javascript" src="'.$rsc_url.'js/debug.js"></script>';
  1215. }
  1216. foreach( $this->queries as $i => $query )
  1217. {
  1218. $count_queries++;
  1219. $get_md5_query = create_function( '', '
  1220. static $r; if( isset($r) ) return $r;
  1221. global $query;
  1222. $r = md5(serialize($query))."-".rand();
  1223. return $r;' );
  1224. if ( $html )
  1225. {
  1226. echo '<h4>Query #'.$count_queries.': '.$query['title']."</h4>\n";
  1227. $div_id = 'db_query_sql_'.$i.'_'.$get_md5_query();
  1228. if( strlen($query['sql']) > 512 )
  1229. {
  1230. $sql_short = $this->format_query( $query['sql'], true, 512 );
  1231. $sql = $this->format_query( $query['sql'], true );
  1232. echo '<code id="'.$div_id.'" style="display:none">'.$sql_short.'</code>';
  1233. echo '<code id="'.$div_id.'_full">'.$sql.'</code>';
  1234. echo '<script type="text/javascript">debug_onclick_toggle_div("'.$div_id.','.$div_id.'_full", "Hide full SQL", "Show full SQL");</script>';
  1235. }
  1236. else
  1237. {
  1238. echo '<code>'.$this->format_query( $query['sql'] ).'</code>';
  1239. }
  1240. echo "\n";
  1241. }
  1242. else
  1243. {
  1244. echo '= Query #'.$count_queries.': '.$query['title']." =\n";
  1245. echo $this->format_query( $query['sql'], false )."\n\n";
  1246. }
  1247. // Color-Format duration: long => red, fast => green, normal => black
  1248. if( $query['time'] > $this->query_duration_slow )
  1249. {
  1250. $style_time_text = 'color:red;font-weight:bold;';
  1251. $style_time_graph = 'background-color:red;';
  1252. $plain_time_text = ' [slow]';
  1253. }
  1254. elseif( $query['time'] < $this->query_duration_fast )
  1255. {
  1256. $style_time_text = 'color:green;';
  1257. $style_time_graph = 'background-color:green;';
  1258. $plain_time_text = ' [fast]';
  1259. }
  1260. else
  1261. {
  1262. $style_time_text = '';
  1263. $style_time_graph = 'background-color:black;';
  1264. $plain_time_text = '';
  1265. }
  1266. // Number of rows with time (percentage and graph, if total time available)
  1267. if ( $html )
  1268. {
  1269. echo '<div class="query_info">';
  1270. echo 'Rows: '.$query['rows'];
  1271. echo ' &ndash; Time: ';
  1272. }
  1273. else
  1274. {
  1275. echo 'Rows: '.$query['rows'].' - Time: ';
  1276. }
  1277. if( $html && $style_time_text )
  1278. {
  1279. echo '<span style="'.$style_time_text.'">';
  1280. }
  1281. echo number_format( $query['time'], 4 ).'s';
  1282. if( $time_queries > 0 )
  1283. { // We have a total time we can use to calculate percentage:
  1284. echo ' ('.number_format( 100/$time_queries * $query['time'], 2 ).'%)';
  1285. }
  1286. if( isset($query['time_profile']) )
  1287. {
  1288. echo ' (real: '.number_format($query['time_profile'], 4).'s)';
  1289. $time_queries_profiled += $query['time_profile'];
  1290. }
  1291. if( $style_time_text || $plain_time_text )
  1292. {
  1293. echo $html ? '</span>' : $plain_time_text;
  1294. }
  1295. if( $time_queries > 0 )
  1296. { // We have a total time we can use to display a graph/bar:
  1297. $perc = round( 100/$time_queries * $query['time'] );
  1298. if ( $html )
  1299. {
  1300. echo '<div style="margin:0; padding:0; height:12px; width:'.$perc.'%;'.$style_time_graph.'"></div>';
  1301. }
  1302. else
  1303. { // display an ASCII bar
  1304. printf( "\n".'[%-50s]', str_repeat( '=', $perc / 2 ) );
  1305. }
  1306. }
  1307. echo $html ? '</div>' : "\n\n";
  1308. // EXPLAIN JOINS ??
  1309. if( $this->debug_explain_joins && preg_match( '#^ [\s(]* SELECT \s #ix', $query['sql']) )
  1310. { // Query was a select, let's try to explain joins...
  1311. $this->result = mysqli_query( $this->dbhandle, 'EXPLAIN '.$query['sql'] );
  1312. if( is_object($this->result) )
  1313. { // will be false for invalid SQL
  1314. $this->num_rows = mysqli_num_rows($this->result);
  1315. if( $html )
  1316. {
  1317. $div_id = 'db_query_explain_'.$i.'_'.$get_md5_query();
  1318. echo '<div id="'.$div_id.'">';
  1319. echo $this->debug_get_rows_table( 100, true );
  1320. echo '</div>';
  1321. echo '<script type="text/javascript">debug_onclick_toggle_div("'.$div_id.'", "Show EXPLAIN", "Hide EXPLAIN");</script>';
  1322. }
  1323. else
  1324. { // TODO: dh> contains html.
  1325. echo $this->debug_get_rows_table( 100, true );
  1326. }
  1327. mysqli_free_result($this->result);
  1328. }
  1329. }
  1330. // Profile:
  1331. if( isset($query['profile']) )
  1332. {
  1333. if( $html )
  1334. {
  1335. $div_id = 'db_query_profile_'.$i.'_'.$get_md5_query();
  1336. echo '<div id="'.$div_id.'">';
  1337. echo $query['profile'];
  1338. echo '</div>';
  1339. echo '<script type="text/javascript">debug_onclick_toggle_div("'.$div_id.'", "Show PROFILE", "Hide PROFILE");</script>';
  1340. }
  1341. else
  1342. { // TODO: dh> contains html.
  1343. echo $this->debug_get_rows_table( 100, true );
  1344. }
  1345. }
  1346. // Results:
  1347. if( $query['results'] != 'unknown' )
  1348. {
  1349. if( $html )
  1350. {
  1351. $div_id = 'db_query_results_'.$i.'_'.$get_md5_query();
  1352. echo '<div id="'.$div_id.'">';
  1353. echo $query['results'];
  1354. echo '</div>';
  1355. echo '<script type="text/javascript">debug_onclick_toggle_div("'.$div_id.'", "Show results", "Hide results");</script>';
  1356. }
  1357. else
  1358. { // TODO: dh> contains html.
  1359. echo $query['results'];
  1360. }
  1361. }
  1362. // Function trace:
  1363. if( isset($query['function_trace']) )
  1364. {
  1365. if( $html )
  1366. {
  1367. $div_id = 'db_query_backtrace_'.$i.'_'.$get_md5_query();
  1368. echo '<div id="'.$div_id.'">';
  1369. echo $query['function_trace'];
  1370. echo '</div>';
  1371. echo '<script type="text/javascript">debug_onclick_toggle_div("'.$div_id.'", "Show function trace", "Hide function trace");</script>';
  1372. }
  1373. else
  1374. { // TODO: dh> contains html.
  1375. echo $query['function_trace'];
  1376. }
  1377. }
  1378. echo $html ? '<hr />' : "=============================================\n";
  1379. $count_rows += $query['rows'];
  1380. }
  1381. $time_queries_profiled = number_format($time_queries_profiled, 4);
  1382. $time_diff_percentage = $time_queries_profiled != 0 ? round($time_queries / $time_queries_profiled * 100) : false;
  1383. if ( $html )
  1384. {
  1385. echo "\nTotal rows: $count_rows<br />\n";
  1386. echo "\nMeasured time: {$time_queries}s<br />\n";
  1387. echo "\nProfiled time: {$time_queries_profiled}s<br />\n";
  1388. if( $time_diff_percentage !== false )
  1389. {
  1390. echo "\nTime difference: {$time_diff_percentage}%<br />\n";
  1391. }
  1392. }
  1393. else
  1394. {
  1395. echo 'Total rows: '.$count_rows."\n";
  1396. echo "Measured time: {$time_queries}s\n";
  1397. echo "Profiled time: {$time_queries_profiled}s\n";
  1398. if( $time_diff_percentage !== false )
  1399. {
  1400. echo "Time difference: {$time_diff_percentage}%\n";
  1401. }
  1402. }
  1403. }
  1404. /**
  1405. * BEGIN A TRANSCATION
  1406. *
  1407. * Note: By default, MySQL runs with autocommit mode enabled.
  1408. * This means that as soon as you execute a statement that updates (modifies)
  1409. * a table, MySQL stores the update on disk.
  1410. * Once you execute a BEGIN, the updates are "pending" until you execute a
  1411. * {@link DB::commit() COMMIT} or a {@link DB:rollback() ROLLBACK}
  1412. *
  1413. * Note 2: standard syntax would be START TRANSACTION but it's not supported by older
  1414. * MySQL versions whereas BEGIN is...
  1415. *
  1416. * Note 3: The default isolation level is REPEATABLE READ.
  1417. */
  1418. function begin()
  1419. {
  1420. if( $this->use_transactions )
  1421. {
  1422. $this->query( 'BEGIN', 'BEGIN transaction' );
  1423. $this->transaction_nesting_level++;
  1424. }
  1425. }
  1426. /**
  1427. * Commit current transaction
  1428. */
  1429. function commit()
  1430. {
  1431. if( $this->use_transactions )
  1432. {
  1433. if( $this->transaction_nesting_level == 1 )
  1434. { // Only COMMIT if there are no remaining nested transactions:
  1435. if( $this->rollback_nested_transaction )
  1436. {
  1437. $this->query( 'ROLLBACK', 'ROLLBACK transaction because there was a failure somewhere in the nesting of transactions' );
  1438. }
  1439. else
  1440. {
  1441. $this->query( 'COMMIT', 'COMMIT transaction' );
  1442. }
  1443. $this->rollback_nested_transaction = false;
  1444. }
  1445. if( $this->transaction_nesting_level )
  1446. {
  1447. $this->transaction_nesting_level--;
  1448. }
  1449. }
  1450. }
  1451. /**
  1452. * Rollback current transaction
  1453. */
  1454. function rollback()
  1455. {
  1456. if( $this->use_transactions )
  1457. {
  1458. if( $this->transaction_nesting_level == 1 )
  1459. { // Only ROLLBACK if there are no remaining nested transactions:
  1460. $this->query( 'ROLLBACK', 'ROLLBACK transaction' );
  1461. $this->rollback_nested_transaction = false;
  1462. }
  1463. else
  1464. { // Remember we'll have to roll back at the end!
  1465. $this->rollback_nested_transaction = true;
  1466. }
  1467. if( $this->transaction_nesting_level )
  1468. {
  1469. $this->transaction_nesting_level--;
  1470. }
  1471. }
  1472. }
  1473. }
  1474. /*
  1475. * $Log$
  1476. * Revision 1.52 2010/05/02 00:02:30 blueyed
  1477. * DB::dump_queries: Fix mysql_free_result when using profiling/explain.
  1478. *
  1479. * Revision 1.51 2010/03/29 19:02:00 blueyed
  1480. * DB class: improve debugging
  1481. * - Improve format_query parsing
  1482. * - dump_queries: crop long queries (toggable)
  1483. * - Move toggle JS to rsc/js/debug.js
  1484. *
  1485. * Revision 1.50 2010/02/08 17:51:51 efy-yury
  1486. * copyright 2009 -> 2010
  1487. *
  1488. * Revision 1.49 2010/01/16 05:21:25 sam2kb
  1489. * Deleted crap text at the bottom
  1490. *
  1491. * Revision 1.48 2010/01/15 18:34:12 blueyed
  1492. * The dl function is deprecated and not available in PHP 5.3. Do not make us produce white pages.
  1493. *
  1494. * Revision 1.47 2009/12/10 20:13:24 blueyed
  1495. * Add log_errors property to DB and set it to false in get_db_version to not
  1496. * log SQL errors which are expected during install.
  1497. *
  1498. * Revision 1.46 2009/12/06 01:52:54 blueyed
  1499. * Add 'htmlspecialchars' type to format_to_output, same as formvalue, but less irritating. Useful for strmaxlen, which is being used in more places now.
  1500. *
  1501. * Revision 1.45 2009/12/01 20:37:11 blueyed
  1502. * DB::select: set dbname
  1503. *
  1504. * Revision 1.44 2009/11/30 00:22:04 fplanque
  1505. * clean up debug info
  1506. * show more timers in view of block caching
  1507. *
  1508. * Revision 1.43 2009/11/16 20:44:07 blueyed
  1509. * - Use escape in quote (makes mocking in tests easier)
  1510. * - get_var: add support for $y=NULL (next row)
  1511. *
  1512. * Revision 1.42 2009/10/27 21:57:43 fplanque
  1513. * minor/doc
  1514. *
  1515. * Revision 1.41 2009/10/19 21:56:01 blueyed
  1516. * error_log SQL errors
  1517. *
  1518. * Revision 1.40 2009/10/04 21:10:16 blueyed
  1519. * Merge db-noresultcache via whissip.
  1520. *
  1521. * Revision 1.39 2009/09/20 22:35:56 blueyed
  1522. * whoops.
  1523. *
  1524. * Revision 1.38 2009/09/20 22:05:34 blueyed
  1525. * DB:
  1526. * - log_queries requires $debug to be enabled, otherwise you won't see
  1527. * any results, but the performance drawback.
  1528. * - Save two IFs
  1529. *
  1530. * Revision 1.37 2009/09/16 20:50:52 tblue246
  1531. * Do not divide by zero; style fix
  1532. *
  1533. * Revision 1.36 2009/09/13 21:32:42 blueyed
  1534. * DB: display toggle links below dumped queries inline, saving some screen space.
  1535. *
  1536. * Revision 1.35 2009/09/13 21:32:16 blueyed
  1537. * DB: add "debug_profile_queries" option, which uses MySQL profiling. Info is displayed when dumping queries and total time is compared to measured time.
  1538. *
  1539. * Revision 1.34 2009/09/13 21:29:59 blueyed
  1540. * DB: fix debug_get_rows_table, which returned 'No results' since 1.32. Only display result related info if there are any rows now.
  1541. *
  1542. * Revision 1.33 2009/07/25 00:47:21 blueyed
  1543. * Add log_queries param to DB constructor. Used from tests for performance reason.
  1544. *
  1545. * Revision 1.32 2009/07/25 00:39:00 blueyed
  1546. * DB::debug_get_rows_table: only print result rows, if there is a result.
  1547. *
  1548. * Revision 1.31 2009/07/24 23:36:47 blueyed
  1549. * doc
  1550. *
  1551. * Revision 1.30 2009/07/22 20:51:18 blueyed
  1552. * Only display P and brackets if there is a DB error, which is not the case with wrong user/pass (oddly)
  1553. *
  1554. * Revision 1.29 2009/07/12 23:18:22 fplanque
  1555. * upgrading tables to innodb
  1556. *
  1557. * Revision 1.28 2009/07/10 15:59:04 sam2kb
  1558. * Change DB connection charset only if SET NAMES worked
  1559. *
  1560. * Revision 1.27 2009/07/10 10:54:06 tblue246
  1561. * Doc
  1562. *
  1563. * Revision 1.26 2009/07/09 23:23:40 fplanque
  1564. * Check that DB supports proper charset before installing.
  1565. *
  1566. * Revision 1.25 2009/07/09 22:57:32 fplanque
  1567. * Fixed init of connection_charset, especially during install.
  1568. *
  1569. * Revision 1.24 2009/04/22 19:43:02 blueyed
  1570. * debug_get_rows_table: use get_row (and properly for HEAD, where it does not default to NULL/next row (fixing r1.23)
  1571. *
  1572. * Revision 1.23 2009/04/22 19:27:36 blueyed
  1573. * debug_get_rows_table: use get_row instead of get_results, since it stops after 'max rows'.
  1574. *
  1575. * Revision 1.22 2009/03/08 23:57:40 fplanque
  1576. * 2009
  1577. *
  1578. * Revision 1.21 2009/03/03 00:59:10 fplanque
  1579. *…

Large files files are truncated, but you can click here to view the full file