| [ Index ] |
|
Code source de b2evolution 2.1.0-beta |
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-2007 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: _db.class.php,v 1.2 2007/10/01 19:02:23 fplanque Exp $ 61 * @todo transaction support 62 */ 63 if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' ); 64 65 /** 66 * ezSQL Constants 67 */ 68 define( 'EZSQL_VERSION', '1.25' ); 69 define( 'OBJECT', 'OBJECT', true ); 70 define( 'ARRAY_A', 'ARRAY_A', true); 71 define( 'ARRAY_N', 'ARRAY_N', true); 72 73 if( ! function_exists( 'mysql_real_escape_string' ) ) 74 { // Function only available since PHP 4.3.0 75 function mysql_real_escape_string( $unescaped_string ) 76 { 77 return mysql_escape_string( $unescaped_string ); 78 } 79 } 80 81 /** 82 * The Main Class 83 * 84 * @package evocore 85 */ 86 class DB 87 { 88 /** 89 * Show/Print errors? 90 * @var boolean 91 */ 92 var $show_errors = true; 93 /** 94 * Halt on errors? 95 * @var boolean 96 */ 97 var $halt_on_error = true; 98 /** 99 * Has an error occured? 100 * @var boolean 101 */ 102 var $error = false; 103 /** 104 * Number of done queries. 105 * @var integer 106 */ 107 var $num_queries = 0; 108 /** 109 * last query SQL string 110 * @var string 111 */ 112 var $last_query = ''; 113 /** 114 * last DB error string 115 * @var string 116 */ 117 var $last_error = ''; 118 119 /** 120 * Last insert ID 121 * @var integer 122 */ 123 var $insert_id = 0; 124 125 /** 126 * Last query's resource 127 * @access protected 128 * @var resource 129 */ 130 var $result; 131 132 /** 133 * Last result's rows 134 * @var array 135 */ 136 var $last_result; 137 138 /** 139 * Number of rows in result set (after a select) 140 */ 141 var $num_rows = 0; 142 143 /** 144 * Number of rows affected by insert, delete, update or replace 145 */ 146 var $rows_affected = 0; 147 148 /** 149 * Aliases that will be replaced in queries: 150 */ 151 var $dbaliases = array(); 152 /** 153 * Strings that will replace the aliases in queries: 154 */ 155 var $dbreplaces = array(); 156 157 /** 158 * CREATE TABLE options. 159 * 160 * This gets appended to every "CREATE TABLE" query. 161 * 162 * Edit those if you have control over you MySQL server and want a more professional 163 * database than what is commonly offered by popular hosting providers. 164 * 165 * Recommended settings: ' ENGINE=InnoDB ' 166 * Development settings: ' ENGINE=InnoDB DEFAULT CHARSET=utf8 ' 167 * @var string 168 */ 169 var $table_options = ''; 170 171 /** 172 * Use transactions in DB? 173 * 174 * You need to use InnoDB in order to enable this. See the {@link $db_config "table_options" key}. 175 */ 176 var $use_transactions = false; 177 178 /** 179 * How many transactions are currently nested? 180 */ 181 var $transaction_nesting_level = 0; 182 183 /** 184 * Rememeber if we have to rollback at the end of a nested transaction construct 185 */ 186 var $rollback_nested_transaction = false; 187 188 /** 189 * MySQL Database handle 190 * @var object 191 */ 192 var $dbhandle; 193 194 /** 195 * Database username 196 * @var string 197 */ 198 var $dbuser; 199 200 /** 201 * Database username's password 202 * @var string 203 */ 204 var $dbpassword; 205 206 /** 207 * Database name 208 * @var string 209 * @see select() 210 */ 211 var $dbname; 212 213 /** 214 * Database hostname 215 * @var string 216 */ 217 var $dbhost = 'localhost'; 218 219 /** 220 * Current connection charset 221 * @var string 222 * @see DB::set_connection_charset() 223 */ 224 var $connection_charset; 225 226 227 // DEBUG: 228 229 /** 230 * Do we want to log queries? 231 * This gets set according to {@link $debug}, if it's set. 232 * @todo fp> get rid of this var, use $debug only 233 * @var boolean 234 */ 235 var $log_queries; 236 237 /** 238 * Log of queries: 239 * @var array 240 */ 241 var $queries = array(); 242 243 /** 244 * Do we want to explain joins? 245 * This requires {@link DB::$log_queries} to be true. 246 * @var boolean 247 */ 248 var $debug_explain_joins = false; 249 250 /** 251 * Do we want to output a function backtrace for every query? 252 * Number of stack entries to show (from last to first) (Default: 0); true means 'all'. 253 * 254 * This requires {@link DB::$log_queries} to be true. 255 * 256 * @var integer 257 */ 258 var $debug_dump_function_trace_for_queries = 0; 259 260 /** 261 * Number of rows we want to dump in debug output (0 disables it) 262 * This requires {@link DB::$log_queries} to be true. 263 * @var integer 264 */ 265 var $debug_dump_rows = 0; 266 267 /** 268 * Time in seconds that is considered a fast query (green). 269 * @var float 270 * @see dump_queries() 271 */ 272 var $query_duration_fast = 0.05; 273 274 /** 275 * Time in seconds that is considered a slow query (red). 276 * @var float 277 * @see dump_queries() 278 */ 279 var $query_duration_slow = 0.3; 280 281 282 /** 283 * DB Constructor 284 * 285 * Connects to the server and selects a database. 286 * 287 * @param array An array of parameters. 288 * Manadatory: 289 * - 'user': username to connect with 290 * - 'password': password to connect with 291 * OR 292 * - 'handle': a MySQL Database handle (from a previous {@link mysql_connect()}) 293 * Optional: 294 * - 'name': the name of the default database, see {@link DB::select()} 295 * - 'host': host of the database; Default: 'localhost' 296 * - 'show_errors': Display SQL errors? (true/false); Default: don't change member default ({@link $show_errors}) 297 * - 'halt_on_error': Halt on error? (true/false); Default: don't change member default ({@link $halt_on_error}) 298 * - 'table_options': sets {@link $table_options} 299 * - 'use_transactions': sets {@link $use_transactions} 300 * - 'aliases': Aliases for tables (array( alias => table name )); Default: no aliases. 301 * - 'new_link': create a new link to the DB, even if there was a mysql_connect() with 302 * the same params before. (requires PHP 4.2) 303 * - 'client_flags': optional settings like compression or SSL encryption. See {@link http://www.php.net/manual/en/ref.mysql.php#mysql.client-flags}. 304 * (requires PHP 4.3) 305 */ 306 function DB( $params ) 307 { 308 global $debug; 309 310 // Mandatory parameters: 311 if( isset( $params['handle'] ) ) 312 { // DB-Link provided: 313 $this->dbhandle = $params['handle']; 314 } 315 else 316 { 317 $this->dbuser = $params['user']; 318 $this->dbpassword = $params['password']; 319 } 320 321 // Optional parameters (Allow overriding through $params): 322 if( isset($params['name']) ) $this->dbname = $params['name']; 323 if( isset($params['host']) ) $this->dbhost = $params['host']; 324 if( isset($params['show_errors']) ) $this->show_errors = $params['show_errors']; 325 if( isset($params['halt_on_error']) ) $this->halt_on_error = $params['halt_on_error']; 326 if( isset($params['table_options']) ) $this->table_options = $params['table_options']; 327 if( isset($params['use_transactions']) ) $this->use_transactions = $params['use_transactions']; 328 if( isset($params['debug_dump_rows']) ) $this->debug_dump_rows = $params['debug_dump_rows']; // Nb of rows to dump 329 if( isset($params['debug_explain_joins']) ) $this->debug_explain_joins = $params['debug_explain_joins']; 330 if( isset($params['debug_dump_function_trace_for_queries']) ) $this->debug_dump_function_trace_for_queries = $params['debug_dump_function_trace_for_queries']; 331 332 if( isset($debug) && ! isset($this->log_queries) ) 333 { // $log_queries follows $debug and respects subclasses, which may define it: 334 $this->log_queries = $debug; 335 } 336 337 if( ! extension_loaded('mysql') ) 338 { // The mysql extension is not loaded, try to dynamically load it: 339 $mysql_ext_file = is_windows() ? 'php_mysql.dll' : 'mysql.so'; 340 @dl( $mysql_ext_file ); 341 342 if( ! extension_loaded('mysql') ) 343 { // Still not loaded: 344 $this->print_error( 'The PHP MySQL module could not be loaded.', ' 345 <p>You must edit your php configuration (php.ini) and enable this module ('.$mysql_ext_file.').</p> 346 <p>Do not forget to restart your webserver (if necessary) after editing the PHP conf.</p>', false ); 347 return; 348 } 349 } 350 351 $new_link = isset( $params['new_link'] ) ? $params['new_link'] : false; 352 $client_flags = isset( $params['client_flags'] ) ? $params['client_flags'] : 0; 353 354 if( ! isset($params['handle']) ) 355 { // Connect to the Database: 356 // echo "mysql_connect( $this->dbhost, $this->dbuser, $this->dbpassword, $new_link, $client_flags )"; 357 $this->dbhandle = @mysql_connect( $this->dbhost, $this->dbuser, $this->dbpassword, $new_link, $client_flags ); 358 } 359 360 if( ! $this->dbhandle ) 361 { 362 $mysql_error = mysql_error(); 363 if( empty($mysql_error) ) 364 { // there was a PHP error, like with version below 4.3 which do not support new_link and client_flags; let PHP throw an error: 365 $this->print_error( 'Error establishing a database connection!', ' 366 <p>If you are running a PHP version below 4.3, please upgrade.</p>', false ); // fp> Other causes include: simple tests passing wrong params! 367 368 // Let PHP throw an error: 369 mysql_connect( $this->dbhost, $this->dbuser, $this->dbpassword, $new_link, $client_flags ); 370 } 371 else 372 { 373 $this->print_error( 'Error establishing a database connection!', ' 374 <p>('.$mysql_error.')</p> 375 <ol> 376 <li>Are you sure you have typed the correct user/password?</li> 377 <li>Are you sure that you have typed the correct hostname?</li> 378 <li>Are you sure that the database server is running?</li> 379 </ol>', false ); 380 } 381 } 382 elseif( isset($this->dbname) ) 383 { 384 $this->select($this->dbname); 385 } 386 387 388 if( !empty($params['connection_charset']) ) 389 { // Specify which charset we are using on the client: 390 $this->set_connection_charset($params['connection_charset']); 391 } 392 393 /* 394 echo '<br />Server: '.$this->get_var( 'SELECT @@character_set_server' ); 395 echo '<br />Database: '.$this->get_var( 'SELECT @@character_set_database' ); 396 echo '<br />Connection: '.$this->get_var( 'SELECT @@character_set_connection' ); 397 echo '<br />Client: '.$this->get_var( 'SELECT @@character_set_client' ); 398 echo '<br />Results: '.$this->get_var( 'SELECT @@character_set_results' ); 399 */ 400 401 402 if( isset($params['aliases']) ) 403 { // Prepare aliases for replacements: 404 foreach( $params['aliases'] as $dbalias => $dbreplace ) 405 { 406 $this->dbaliases[] = '#\b'.$dbalias.'\b#'; // \b = word boundary 407 $this->dbreplaces[] = $dbreplace; 408 // echo '<br />'.'#\b'.$dbalias.'\b#'; 409 } 410 // echo count($this->dbaliases); 411 } 412 413 if( $debug ) 414 { // Force MySQL strict mode 415 $this->query( 'SET sql_mode = "TRADITIONAL"' ); 416 } 417 } 418 419 420 /** 421 * Select a DB (if another one needs to be selected) 422 */ 423 function select($db) 424 { 425 if( !@mysql_select_db($db, $this->dbhandle) ) 426 { 427 $this->print_error( 'Error selecting database ['.$db.']!', ' 428 <ol> 429 <li>Are you sure the database exists?</li> 430 <li>Are you sure the DB user is allowed to use that database?</li> 431 <li>Are you sure there is a valid database connection?</li> 432 </ol>', false ); 433 } 434 } 435 436 437 /** 438 * Format a string correctly for safe insert under all PHP conditions 439 */ 440 function escape($str) 441 { 442 return mysql_real_escape_string($str, $this->dbhandle); 443 } 444 445 446 /** 447 * Quote a value, either in single quotes (and escaped) or if it's NULL as 'NULL'. 448 * 449 * @return string Quoted (and escaped) value or 'NULL'. 450 */ 451 function quote($str) 452 { 453 if( is_null( $str ) ) 454 { 455 return 'NULL'; 456 } 457 elseif( is_array( $str ) ) 458 { 459 $r = ''; 460 foreach( $str as $elt ) 461 { 462 $r .= $this->quote($elt).','; 463 } 464 $r = substr( $r, 0, strlen( $r ) - 1 ); 465 return $r; 466 } 467 else 468 { 469 return "'".mysql_real_escape_string($str, $this->dbhandle)."'"; 470 } 471 } 472 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 486 /** 487 * Returns the correct WEEK() function to get the week number for the given date. 488 * 489 * @link http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html 490 * 491 * @todo disable when MySQL < 4 492 * @param string will be used as is 493 * @param integer 0 for sunday, 1 for monday 494 */ 495 function week( $date, $startofweek ) 496 { 497 if( $startofweek == 1 ) 498 { // Week starts on Monday, week 1 must have a monday in this year: 499 return ' WEEK( '.$date.', 5 ) '; 500 } 501 502 // Week starts on Sunday, week 1 must have a sunday in this year: 503 return ' WEEK( '.$date.', 0 ) '; 504 } 505 506 507 /** 508 * Print SQL/DB error. 509 * 510 * TODO: fp> bloated: it probably doesn't make sense to display errors if we don't stop. Any use case? 511 * dh> Sure. Local testing (and test cases). 512 * 513 * @param string Short error (no HTML) 514 * @param string Extended description/help for the error (for HTML) 515 * @param string|false Query title; false if {@link DB::$last_query} should not get displayed 516 */ 517 function print_error( $title = '', $html_str = '', $query_title = '' ) 518 { 519 // All errors go to the global error array $EZSQL_ERROR.. 520 global $EZSQL_ERROR, $is_cli; 521 522 $this->error = true; 523 524 // If no special error string then use mysql default.. 525 $this->last_error = empty($title) ? ( mysql_error($this->dbhandle).'(Errno='.mysql_errno($this->dbhandle).')' ) : $title; 526 527 // Log this error to the global array.. 528 $EZSQL_ERROR[] = array( 529 'query' => $this->last_query, 530 'error_str' => $this->last_error 531 ); 532 533 if( ! ( $this->halt_on_error || $this->show_errors ) ) 534 { // no reason to generate a nice message: 535 return; 536 } 537 538 if( $is_cli ) 539 { // Clean error message for command line interface: 540 $err_msg = "MySQL error! {$this->last_error}\n"; 541 if( ! empty($this->last_query) && $query_title !== false ) 542 { 543 $err_msg .= "Your query: $query_title\n"; 544 $err_msg .= $this->format_query( $this->last_query, false ); 545 } 546 } 547 else 548 { 549 $err_msg = '<p class="error">MySQL error!</p>'."\n"; 550 $err_msg .= "<div><p><strong>{$this->last_error}</strong></p>\n"; 551 $err_msg .= $html_str; 552 if( !empty($this->last_query) && $query_title !== false ) 553 { 554 $err_msg .= '<p class="error">Your query: '.$query_title.'</p>'; 555 $err_msg .= '<pre>'; 556 $err_msg .= $this->format_query( $this->last_query, ! $is_cli ); 557 $err_msg .= '</pre>'; 558 } 559 $err_msg .= "</div>\n"; 560 } 561 562 if( $this->halt_on_error ) 563 { 564 if( function_exists('debug_die') ) 565 { 566 debug_die( $err_msg ); 567 } 568 else 569 { 570 die( $err_msg ); 571 } 572 } 573 elseif( $this->show_errors ) 574 { // If there is an error then take note of it 575 echo '<div class="error">'; 576 echo $err_msg; 577 echo '</div>'; 578 } 579 580 } 581 582 583 /** 584 * Kill cached query results 585 */ 586 function flush() 587 { 588 // Get rid of these 589 $this->last_result = NULL; 590 $this->last_query = NULL; 591 } 592 593 594 /** 595 * Get MYSQL version 596 */ 597 function get_version() 598 { 599 if( isset( $this->version ) ) 600 { 601 return $this->version; 602 } 603 604 $save_show_errors = $this->show_errors; 605 $save_halt_on_error = $this->halt_on_error; 606 // Blatantly ignore any error generated by potentially unknown function... 607 $this->show_errors = false; 608 $this->halt_on_error = false; 609 $last_error = $this->last_error; 610 $error = $this->error; 611 if( ($this->version_long = $this->get_var( 'SELECT VERSION()' ) ) === NULL ) 612 { // Very old version ( < 4.0 ) 613 $this->version = ''; 614 $this->version_long = ''; 615 } 616 else 617 { 618 $this->version = preg_replace( '¤-.*¤', '', $this->version_long ); 619 } 620 $this->show_errors = $save_show_errors; 621 $this->halt_on_error = $save_halt_on_error; 622 $this->last_error = $last_error; 623 $this->error = $error; 624 $this->halt_on_error = false; 625 626 return $this->version; 627 } 628 629 /** 630 * Basic Query 631 * 632 * @param string SQL query 633 * @param string title for debugging 634 * @return mixed # of rows affected or false if error 635 */ 636 function query( $query, $title = '' ) 637 { 638 global $Timer; 639 640 // initialise return 641 $return_val = 0; 642 643 // Flush cached values.. 644 $this->flush(); 645 646 // Log how the function was called 647 $this->func_call = '$db->query("'.$query.'")'; 648 // echo $this->func_call, '<br />'; 649 650 // Replace aliases: 651 if( ! empty($this->dbaliases) ) 652 { 653 // TODO: this should only replace the table name part(s), not the whole query! 654 // blueyed> I've changed it to replace in table name parts for UPDATE, INSERT and REPLACE, because 655 // it corrupted serialized data.. 656 // 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. 657 658 if( preg_match( '~^\s*(UPDATE\s+)(.*?)(\sSET\s.*)$~is', $query, $match ) ) 659 { // replace only between UPDATE and SET: 660 $query = $match[1].preg_replace( $this->dbaliases, $this->dbreplaces, $match[2] ).$match[3]; 661 } 662 elseif( preg_match( '~^\s*(INSERT|REPLACE\s+)(.*?)(\s(VALUES|SET)\s.*)$~is', $query, $match ) ) 663 { // replace only between INSERT|REPLACE and VALUES|SET: 664 $query = $match[1].preg_replace( $this->dbaliases, $this->dbreplaces, $match[2] ).$match[3]; 665 } 666 else 667 { // replace in whole query: 668 $query = preg_replace( $this->dbaliases, $this->dbreplaces, $query ); 669 670 if( ! empty($this->table_options) && preg_match( '#^ \s* create \s* table \s #ix', $query) ) 671 { // Query is a table creation, we add table options: 672 $query = preg_replace( '~;\s*$~', '', $query ); // remove any ";" at the end 673 $query .= ' '.$this->table_options; 674 } 675 } 676 } 677 elseif( ! empty($this->table_options) ) 678 { // No aliases, but table_options: 679 if( preg_match( '#^ \s* create \s* table \s #ix', $query) ) 680 { // Query is a table creation, we add table options: 681 $query .= $this->table_options; 682 } 683 } 684 // echo '<p>'.$query.'</p>'; 685 686 // Keep track of the last query for debug.. 687 $this->last_query = $query; 688 689 // Perform the query via std mysql_query function.. 690 $this->num_queries++; 691 692 if( $this->log_queries ) 693 { // We want to log queries: 694 $this->queries[ $this->num_queries - 1 ] = array( 695 'title' => $title, 696 'sql' => $query, 697 'rows' => -1, 698 'time' => 'unknown', 699 'results' => 'unknown' ); 700 } 701 702 if( is_object($Timer) ) 703 { 704 // Resume global query timer 705 $Timer->resume( 'sql_queries' ); 706 // Start a timer for this particular query: 707 $Timer->start( 'sql_query', false ); 708 709 // Run query: 710 $this->result = @mysql_query( $query, $this->dbhandle ); 711 712 if( $this->log_queries ) 713 { // We want to log queries: 714 // Get duration for last query: 715 $this->queries[ $this->num_queries - 1 ]['time'] = $Timer->get_duration( 'sql_query', 10 ); 716 } 717 718 // Pause global query timer: 719 $Timer->pause( 'sql_queries' ); 720 } 721 else 722 { 723 // Run query: 724 $this->result = @mysql_query( $query, $this->dbhandle ); 725 } 726 727 // If there is an error then take note of it.. 728 if( mysql_error($this->dbhandle) ) 729 { 730 @mysql_free_result($this->result); 731 $this->print_error( '', '', $title ); 732 return false; 733 } 734 735 if( preg_match( '#^\s*(INSERT|DELETE|UPDATE|REPLACE)\s#i', $query, $match ) ) 736 { // Query was an insert, delete, update, replace: 737 738 $this->rows_affected = mysql_affected_rows($this->dbhandle); 739 if( $this->log_queries ) 740 { // We want to log queries: 741 $this->queries[ $this->num_queries - 1 ]['rows'] = $this->rows_affected; 742 } 743 744 // Take note of the insert_id, for INSERT and REPLACE: 745 $match[1] = strtoupper($match[1]); 746 if( $match[1] == 'INSERT' || $match[1] == 'REPLACE' ) 747 { 748 $this->insert_id = mysql_insert_id($this->dbhandle); 749 } 750 751 // Return number of rows affected 752 $return_val = $this->rows_affected; 753 } 754 else 755 { // Query was a select, alter, etc...: 756 $this->num_rows = 0; 757 758 if( is_resource($this->result) ) 759 { // It's not a resource for CREATE or DROP for example and can even trigger a fatal error (see http://forums.b2evolution.net//viewtopic.php?t=9529) 760 761 // Store Query Results 762 while( $row = mysql_fetch_object($this->result) ) 763 { 764 // Store relults as an objects within main array 765 $this->last_result[$this->num_rows] = $row; 766 $this->num_rows++; 767 } 768 } 769 770 if( $this->log_queries ) 771 { // We want to log queries: 772 $this->queries[ $this->num_queries - 1 ]['rows'] = $this->num_rows; 773 } 774 775 // Return number of rows selected 776 $return_val = $this->num_rows; 777 } 778 779 if( $this->log_queries ) 780 { // We want to log queries: 781 if( $this->debug_dump_function_trace_for_queries ) 782 { 783 $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 784 } 785 786 if( $this->debug_dump_rows ) 787 { 788 $this->queries[ $this->num_queries - 1 ]['results'] = $this->debug_get_rows_table( $this->debug_dump_rows ); 789 } 790 } 791 792 // Free original query's result: 793 @mysql_free_result($this->result); 794 795 // EXPLAIN JOINS ?? 796 if( $this->log_queries && $this->debug_explain_joins && preg_match( '#^ \s* SELECT \s #ix', $query) ) 797 { // Query was a select, let's try to explain joins... 798 799 // save values: 800 $saved_last_result = $this->last_result; 801 $saved_num_rows = $this->num_rows; 802 803 $this->last_result = NULL; 804 $this->num_rows = 0; 805 806 $this->result = @mysql_query( 'EXPLAIN '.$query, $this->dbhandle ); 807 808 // Store Query Results 809 $this->num_rows = 0; 810 while( $row = @mysql_fetch_object($this->result) ) 811 { 812 // Store results as an objects within main array 813 $this->last_result[$this->num_rows] = $row; 814 $this->num_rows++; 815 } 816 817 $this->queries[ $this->num_queries - 1 ]['explain'] = $this->debug_get_rows_table( 100, true ); 818 819 // Free "EXPLAIN" result resource: 820 @mysql_free_result($this->result); 821 822 // Restore: 823 $this->last_result = $saved_last_result; 824 $this->num_rows = $saved_num_rows; 825 } 826 827 return $return_val; 828 } 829 830 831 /** 832 * Get one variable from the DB - see docs for more detail 833 * 834 * Note: To be sure that you received NULL from the DB and not "no rows" check 835 * for {@link $num_rows}. 836 * 837 * @return mixed NULL if not found, the value otherwise (which may also be NULL). 838 */ 839 function get_var( $query = NULL, $x = 0, $y = 0, $title = '' ) 840 { 841 // Log how the function was called 842 $this->func_call = "\$db->get_var(\"$query\",$x,$y)"; 843 844 // If there is a query then perform it if not then use cached results.. 845 if( $query ) 846 { 847 $this->query($query, $title); 848 } 849 850 // Extract var out of cached results based x,y vals 851 if( $this->last_result[$y] ) 852 { 853 $values = array_values(get_object_vars($this->last_result[$y])); 854 } 855 856 if( isset($values[$x]) ) 857 { 858 return $values[$x]; 859 } 860 861 return NULL; 862 } 863 864 865 /** 866 * Get one row from the DB - see docs for more detail 867 * 868 * @return array|object 869 */ 870 function get_row( $query = NULL, $output = OBJECT, $y = 0, $title = '' ) 871 { 872 // If there is a query then perform it if not then use cached results.. 873 if( $query ) 874 { 875 $this->query($query, $title); 876 } 877 878 // If the output is an object then return object using the row offset.. 879 if( $output == OBJECT ) 880 { 881 return $this->last_result[$y] 882 ? $this->last_result[$y] 883 : NULL; 884 } 885 // If the output is an associative array then return row as such.. 886 elseif( $output == ARRAY_A ) 887 { 888 return $this->last_result[$y] 889 ? get_object_vars( $this->last_result[$y] ) 890 : array(); 891 } 892 // If the output is an numerical array then return row as such.. 893 elseif( $output == ARRAY_N ) 894 { 895 return $this->last_result[$y] 896 ? array_values( get_object_vars($this->last_result[$y]) ) 897 : array(); 898 } 899 // If invalid output type was specified.. 900 else 901 { 902 $this->print_error('DB::get_row(string query, output type, int offset) -- Output type must be one of: OBJECT, ARRAY_A, ARRAY_N', '', false); 903 } 904 } 905 906 907 /** 908 * Function to get 1 column from the cached result set based in X index 909 * see docs for usage and info 910 * 911 * @return array 912 */ 913 function get_col( $query = NULL, $x = 0, $title = '' ) 914 { 915 // If there is a query then perform it if not then use cached results.. 916 if( $query ) 917 { 918 $this->query( $query, $title ); 919 } 920 921 // Extract the column values 922 $new_array = array(); 923 for( $i = 0, $count = count($this->last_result); $i < $count; $i++ ) 924 { 925 $new_array[$i] = $this->get_var( NULL, $x, $i ); 926 } 927 928 return $new_array; 929 } 930 931 932 /** 933 * Function to get the second column from the cached result indexed by the first column 934 * 935 * @return array [col_0] => col_1 936 */ 937 function get_assoc( $query = NULL, $title = '' ) 938 { 939 // If there is a query then perform it if not then use cached results.. 940 if( $query ) 941 { 942 $this->query( $query, $title ); 943 } 944 945 // Extract the column values 946 $new_array = array(); 947 for( $i = 0, $count = count($this->last_result); $i < $count; $i++ ) 948 { 949 $key = $this->get_var( NULL, 0, $i ); 950 951 $new_array[$key] = $this->get_var( NULL, 1, $i ); 952 } 953 954 return $new_array; 955 } 956 957 958 /** 959 * Return the the query as a result set - see docs for more details 960 * 961 * @return array 962 */ 963 function get_results( $query = NULL, $output = OBJECT, $title = '' ) 964 { 965 // If there is a query then perform it if not then use cached results.. 966 if( $query ) 967 { 968 $this->query($query, $title); 969 } 970 971 // Send back array of objects. Each row is an object 972 if( $output == OBJECT ) 973 { 974 return $this->last_result ? $this->last_result : array(); 975 } 976 elseif( $output == ARRAY_A || $output == ARRAY_N ) 977 { 978 $new_array = array(); 979 980 if( $this->last_result ) 981 { 982 $i = 0; 983 984 foreach( $this->last_result as $row ) 985 { 986 $new_array[$i] = get_object_vars($row); 987 988 if( $output == ARRAY_N ) 989 { 990 $new_array[$i] = array_values($new_array[$i]); 991 } 992 993 $i++; 994 } 995 996 return $new_array; 997 } 998 else 999 { 1000 return array(); 1001 } 1002 } 1003 } 1004 1005 1006 /** 1007 * Get a table (or "<p>No Results.</p>") for the SELECT query results. 1008 * 1009 * @return string HTML table or "No Results" if the 1010 */ 1011 function debug_get_rows_table( $max_lines, $break_at_comma = false ) 1012 { 1013 $r = ''; 1014 1015 if( ! is_resource($this->result) ) 1016 { 1017 return '<p>No Results.</p>'; 1018 } 1019 1020 // Get column info: 1021 $col_info = array(); 1022 $n = mysql_num_fields($this->result); 1023 $i = 0; 1024 while( $i < $n ) 1025 { 1026 $col_info[$i] = mysql_fetch_field($this->result, $i); 1027 $i++; 1028 } 1029 1030 // ===================================================== 1031 // Results top rows 1032 $r .= '<table cellspacing="0" summary="Results for query"><tr>'; 1033 for( $i = 0, $count = count($col_info); $i < $count; $i++ ) 1034 { 1035 $r .= '<th><span class="type">'.$col_info[$i]->type.' '.$col_info[$i]->max_length.'</span><br />' 1036 .$col_info[$i]->name.'</th>'; 1037 } 1038 $r .= '</tr>'; 1039 1040 $i=0; 1041 1042 // ====================================================== 1043 // print main results 1044 if( $this->last_result ) 1045 { 1046 foreach( $this->get_results(NULL,ARRAY_N) as $one_row ) 1047 { 1048 $i++; 1049 if( $i >= $max_lines ) 1050 { 1051 break; 1052 } 1053 $r .= '<tr>'; 1054 foreach( $one_row as $item ) 1055 { 1056 if( $i % 2 ) 1057 { 1058 $r .= '<td class="odd">'; 1059 } 1060 else 1061 { 1062 $r .= '<td>'; 1063 } 1064 1065 if( $break_at_comma ) 1066 { 1067 $item = str_replace( ',', '<br />', $item ); 1068 $item = str_replace( ';', '<br />', $item ); 1069 $r .= $item; 1070 } 1071 else 1072 { 1073 if( strlen( $item ) > 50 ) 1074 { 1075 $item = substr( $item, 0, 50 ).'...'; 1076 } 1077 $r .= htmlspecialchars($item); 1078 } 1079 $r .= '</td>'; 1080 } 1081 1082 $r .= '</tr>'; 1083 } 1084 1085 } // if last result 1086 else 1087 { 1088 $r .= '<tr><td colspan="'.(count($col_info)+1).'">No Results</td></tr>'; 1089 } 1090 if( $i >= $max_lines ) 1091 { 1092 $r .= '<tr><td colspan="'.(count($col_info)+1).'">Max number of dumped rows has been reached.</td></tr>'; 1093 } 1094 1095 $r .= '</table>'; 1096 1097 return $r; 1098 } 1099 1100 1101 /** 1102 * Format a SQL query 1103 * @static 1104 * @todo dh> Steal the code from phpMyAdmin :) 1105 * @param string SQL 1106 * @param boolean Format with/for HTML? 1107 */ 1108 function format_query( $sql, $html = true ) 1109 { 1110 $sql = str_replace("\t", ' ', $sql ); 1111 if( $html ) 1112 { 1113 $sql = htmlspecialchars( $sql ); 1114 $replace_prefix = "<br />\n"; 1115 } 1116 else 1117 { 1118 $replace_prefix = "\n"; 1119 } 1120 1121 $search = array( 1122 '~(FROM|WHERE|GROUP BY|ORDER BY|LIMIT|VALUES)~', 1123 '~(AND |OR )~', 1124 ); 1125 $replace = array( 1126 $replace_prefix.'$1', 1127 $replace_prefix.' $1', 1128 ); 1129 $sql = preg_replace( $search, $replace, $sql ); 1130 1131 return $sql; 1132 } 1133 1134 1135 /** 1136 * Displays all queries that have been executed 1137 */ 1138 function dump_queries() 1139 { 1140 global $Timer; 1141 if( is_object( $Timer ) ) 1142 { 1143 $time_queries = $Timer->get_duration( 'sql_queries' ); 1144 } 1145 else 1146 { 1147 $time_queries = 0; 1148 } 1149 1150 $count_queries = 0; 1151 $count_rows = 0; 1152 1153 echo '<strong>DB queries:</strong> '.$this->num_queries."<br />\n"; 1154 1155 if( ! $this->log_queries ) 1156 { // nothing more to do here.. 1157 return; 1158 } 1159 1160 foreach( $this->queries as $query ) 1161 { 1162 $count_queries++; 1163 echo '<h4>Query #'.$count_queries.': '.$query['title']."</h4>\n"; 1164 echo '<code>'; 1165 echo $this->format_query( $query['sql'] ); 1166 echo "</code>\n"; 1167 1168 // Color-Format duration: long => red, fast => green, normal => black 1169 if( $query['time'] > $this->query_duration_slow ) 1170 { 1171 $style_time_text = 'color:red;font-weight:bold;'; 1172 $style_time_graph = 'background-color:red;'; 1173 } 1174 elseif( $query['time'] < $this->query_duration_fast ) 1175 { 1176 $style_time_text = 'color:green;'; 1177 $style_time_graph = 'background-color:green;'; 1178 } 1179 else 1180 { 1181 $style_time_text = ''; 1182 $style_time_graph = 'background-color:black;'; 1183 } 1184 1185 // Number of rows with time (percentage and graph, if total time available) 1186 echo '<div class="query_info">'; 1187 echo 'Rows: '.$query['rows']; 1188 1189 echo ' – Time: '; 1190 if( $style_time_text ) 1191 { 1192 echo '<span style="'.$style_time_text.'">'; 1193 } 1194 echo number_format( $query['time'], 4 ).'s'; 1195 1196 if( $time_queries > 0 ) 1197 { // We have a total time we can use to calculate percentage: 1198 echo ' ('.number_format( 100/$time_queries * $query['time'], 2 ).'%)'; 1199 } 1200 1201 if( $style_time_text ) 1202 { 1203 echo '</span>'; 1204 } 1205 1206 if( $time_queries > 0 ) 1207 { // We have a total time we can use to display a graph/bar: 1208 echo '<div style="margin:0; padding:0; height:12px; width:'.( round( 100/$time_queries * $query['time'] ) ).'%;'.$style_time_graph.'"></div>'; 1209 } 1210 echo '</div>'; 1211 1212 1213 // Explain: 1214 if( isset($query['explain']) ) 1215 { 1216 echo $query['explain']; 1217 } 1218 1219 // Results: 1220 if( $query['results'] != 'unknown' ) 1221 { 1222 echo $query['results']; 1223 } 1224 1225 // Function trace: 1226 if( isset($query['function_trace']) ) 1227 { 1228 echo $query['function_trace']; 1229 } 1230 1231 $count_rows += $query['rows']; 1232 } 1233 echo "\n<strong>Total rows:</strong> $count_rows<br />\n"; 1234 } 1235 1236 1237 /** 1238 * BEGIN A TRANSCATION 1239 * 1240 * Note: By default, MySQL runs with autocommit mode enabled. 1241 * This means that as soon as you execute a statement that updates (modifies) 1242 * a table, MySQL stores the update on disk. 1243 * Once you execute a BEGIN, the updates are "pending" until you execute a 1244 * {@link DB::commit() COMMIT} or a {@link DB:rollback() ROLLBACK} 1245 * 1246 * Note 2: standard syntax would be START TRANSACTION but it's not supported by older 1247 * MySQL versions whereas BEGIN is... 1248 * 1249 * Note 3: The default isolation level is REPEATABLE READ. 1250 */ 1251 function begin() 1252 { 1253 if( $this->use_transactions ) 1254 { 1255 $this->query( 'BEGIN', 'BEGIN transaction' ); 1256 1257 $this->transaction_nesting_level++; 1258 } 1259 } 1260 1261 1262 /** 1263 * Commit current transaction 1264 */ 1265 function commit() 1266 { 1267 if( $this->use_transactions ) 1268 { 1269 if( $this->transaction_nesting_level == 1 ) 1270 { // Only COMMIT if there are no remaining nested transactions: 1271 if( $this->rollback_nested_transaction ) 1272 { 1273 $this->query( 'ROLLBACK', 'ROLLBACK transaction because there was a failure somewhere in the nesting of transactions' ); 1274 } 1275 else 1276 { 1277 $this->query( 'COMMIT', 'COMMIT transaction' ); 1278 } 1279 $this->rollback_nested_transaction = false; 1280 } 1281 if( $this->transaction_nesting_level ) 1282 { 1283 $this->transaction_nesting_level--; 1284 } 1285 } 1286 } 1287 1288 1289 /** 1290 * Rollback current transaction 1291 */ 1292 function rollback() 1293 { 1294 if( $this->use_transactions ) 1295 { 1296 if( $this->transaction_nesting_level == 1 ) 1297 { // Only ROLLBACK if there are no remaining nested transactions: 1298 $this->query( 'ROLLBACK', 'ROLLBACK transaction' ); 1299 $this->rollback_nested_transaction = false; 1300 } 1301 else 1302 { // Remember we'll have to roll back at the end! 1303 $this->rollback_nested_transaction = true; 1304 } 1305 if( $this->transaction_nesting_level ) 1306 { 1307 $this->transaction_nesting_level--; 1308 } 1309 } 1310 } 1311 1312 1313 /** 1314 * Set the charset of the connection. 1315 * 1316 * WARNING: this will fail on MySQL 3.23 1317 * 1318 * @staticvar array "regular charset => mysql charset map" 1319 * @param string Charset 1320 * @param boolean Use the "regular charset => mysql charset map"? 1321 * @return boolean true on success, false on failure 1322 */ 1323 function set_connection_charset( $charset, $use_map = false ) 1324 { 1325 global $Debuglog; 1326 1327 /** 1328 * This is taken from phpMyAdmin (libraries/select_lang.lib.php). 1329 */ 1330 static $mysql_charset_map = array( 1331 'big5' => 'big5', 1332 'cp-866' => 'cp866', 1333 'euc-jp' => 'ujis', 1334 'euc-kr' => 'euckr', 1335 'gb2312' => 'gb2312', 1336 'gbk' => 'gbk', 1337 'iso-8859-1' => 'latin1', 1338 'iso-8859-2' => 'latin2', 1339 'iso-8859-7' => 'greek', 1340 'iso-8859-8' => 'hebrew', 1341 'iso-8859-8-i' => 'hebrew', 1342 'iso-8859-9' => 'latin5', 1343 'iso-8859-13' => 'latin7', 1344 'iso-8859-15' => 'latin1', 1345 'koi8-r' => 'koi8r', 1346 'shift_jis' => 'sjis', 1347 'tis-620' => 'tis620', 1348 'utf-8' => 'utf8', 1349 'windows-1250' => 'cp1250', 1350 'windows-1251' => 'cp1251', 1351 'windows-1252' => 'latin1', 1352 'windows-1256' => 'cp1256', 1353 'windows-1257' => 'cp1257', 1354 ); 1355 1356 $charset = strtolower($charset); 1357 1358 if( $use_map ) 1359 { 1360 if( ! isset($mysql_charset_map[$charset]) ) 1361 { 1362 return false; 1363 } 1364 1365 $charset = $mysql_charset_map[$charset]; 1366 } 1367 1368 $r = true; 1369 if( $charset != $this->connection_charset ) 1370 { 1371 // SET NAMES is not supported by MySQL 3.23 and for a non-supported charset even not in MySQL 5 probably.. 1372 1373 $save_show_errors = $this->show_errors; 1374 $save_halt_on_error = $this->halt_on_error; 1375 $this->show_errors = false; 1376 $this->halt_on_error = false; 1377 $last_error = $this->last_error; 1378 $error = $this->error; 1379 if( $this->query( 'SET NAMES '.$charset ) === false ) 1380 { 1381 $Debuglog->add( 'Could not "SET NAMES '.$charset.'"! (MySQL error: '.strip_tags($this->last_error).')', 'locale' ); 1382 1383 $r = false; 1384 } 1385 else 1386 { 1387 $Debuglog->add( 'Set DB connection charset: '.$charset, 'locale' ); 1388 } 1389 $this->show_errors = $save_show_errors; 1390 $this->halt_on_error = $save_halt_on_error; 1391 // Blatantly ignore any error generated by SET NAMES... 1392 $this->last_error = $last_error; 1393 $this->error = $error; 1394 1395 // dh> TODO: this should only get set in case of success, I'd say.. 1396 $this->connection_charset = $charset; 1397 } 1398 1399 return $r; 1400 } 1401 1402 } 1403 1404 1405 /* 1406 * $Log: _db.class.php,v $ 1407 * Revision 1.2 2007/10/01 19:02:23 fplanque 1408 * MySQL version check 1409 * 1410 * Revision 1.1 2007/06/25 10:58:58 fplanque 1411 * MODULES (refactored MVC) 1412 * 1413 * Revision 1.61 2007/06/19 23:17:52 blueyed 1414 * Force MySQL strict mode, if $debug 1415 * 1416 * Revision 1.60 2007/06/19 23:15:08 blueyed 1417 * doc fixes 1418 * 1419 * Revision 1.59 2007/05/14 02:44:14 fplanque 1420 * allow quoting of arrays 1421 * 1422 * Revision 1.58 2007/04/26 00:11:07 fplanque 1423 * (c) 2007 1424 * 1425 * Revision 1.57 2007/03/11 22:30:08 fplanque 1426 * cleaned up group perms 1427 * 1428 * Revision 1.56 2007/02/09 17:28:56 blueyed 1429 * doc 1430 * 1431 * Revision 1.55 2007/01/29 01:21:22 blueyed 1432 * Do not let $transaction_nesting_level become negative! 1433 * 1434 * Revision 1.54 2007/01/25 05:14:13 fplanque 1435 * rollback 1436 * 1437 * Revision 1.52 2006/12/14 00:42:04 fplanque 1438 * A little bit of windows detection / normalization 1439 * 1440 * Revision 1.51 2006/12/07 23:12:21 fplanque 1441 * @var needs to have only one argument: the variable type 1442 * Otherwise, I can't code! 1443 * 1444 * Revision 1.50 2006/12/03 21:27:21 blueyed 1445 * Save and reset $error with set_connection_charset(); TODO 1446 * 1447 * Revision 1.49 2006/11/28 02:52:26 fplanque 1448 * doc 1449 * 1450 * Revision 1.48 2006/11/28 00:33:01 blueyed 1451 * Removed DB::compString() (never used) and DB::get_list() (just a macro and better to have in the 4 used places directly; Cleanup/normalization; no extended regexp, when not needed! 1452 * 1453 * Revision 1.47 2006/11/27 20:54:07 fplanque 1454 * doc 1455 * 1456 * Revision 1.46 2006/11/27 01:35:47 blueyed 1457 * Removed get_col_info() and free mysql_result in query() always again 1458 * 1459 * Revision 1.45 2006/11/26 11:12:38 fplanque 1460 * doc / todo 1461 * 1462 * Revision 1.44 2006/11/26 03:17:53 blueyed 1463 * doc about resource freeing and flush() in general 1464 * 1465 * Revision 1.43 2006/11/26 02:30:39 fplanque 1466 * doc / todo 1467 * 1468 * Revision 1.42 2006/11/24 18:27:27 blueyed 1469 * Fixed link to b2evo CVS browsing interface in file docblocks 1470 * 1471 * Revision 1.41 2006/11/23 15:33:58 blueyed 1472 * Small opt 1473 * 1474 * Revision 1.40 2006/11/20 12:23:28 blueyed 1475 * Optimized col_info handling: obsoleted DB::col_info: use DB::get_col_info() instead (lazy-loading of column info) 1476 * 1477 * Revision 1.39 2006/11/19 23:30:38 fplanque 1478 * made simpletest almost installable by almost bozos almost like me 1479 * 1480 * Revision 1.38 2006/11/18 03:44:48 fplanque 1481 * reverted to optimized col info 1482 * 1483 * Revision 1.36 2006/11/17 01:44:38 fplanque 1484 * A function should NEVER FAIL SILENTLY! 1485 * 1486 * Revision 1.35 2006/11/14 17:35:39 blueyed 1487 * small opt 1488 * 1489 * Revision 1.34 2006/11/04 18:39:15 blueyed 1490 * Normalized 1491 * 1492 * Revision 1.33 2006/11/04 18:11:42 fplanque 1493 * comments 1494 * 1495 * Revision 1.32 2006/11/04 01:29:55 blueyed 1496 * Better error displaying. Fix: use $html_str in print_error() 1497 * 1498 * Revision 1.31 2006/11/04 01:22:29 blueyed 1499 * Proposed fix for users with PHP < 4.3: let them get the PHP error. 1500 * 1501 * Revision 1.30 2006/11/03 00:22:21 blueyed 1502 * $log_queries follows $debug global; Removed dumpvar() and vardump() - use pre_dump() 1503 * 1504 * Revision 1.29 2006/11/02 19:49:22 fplanque 1505 * no message 1506 * 1507 * Revision 1.28 2006/10/28 15:05:25 blueyed 1508 * CLI/non-HTML support for print_error() and format_query() 1509 * 1510 * Revision 1.27 2006/10/14 03:05:59 blueyed 1511 * MFB: fix 1512 * 1513 * Revision 1.26 2006/10/10 21:42:42 blueyed 1514 * Optimization: only collect $col_info, if $log_queries is enabled. TODO. 1515 * 1516 * Revision 1.25 2006/10/10 21:24:29 blueyed 1517 * Fix for the optimization 1518 * 1519 * Revision 1.24 2006/10/10 21:21:40 blueyed 1520 * Fixed possible SQL error, if table_options get used and theres a semicolon at the end of query; +optimization 1521 * 1522 * Revision 1.23 2006/10/10 21:17:42 blueyed 1523 * Fixed possible fatal error while collecting col_info for CREATE and DROP queries 1524 */ 1525 ?>
titre
Description
Corps
titre
Description
Corps
titre
Description
Corps
titre
Corps
| Généré le : Thu Nov 29 23:58:50 2007 | par Balluche grâce à PHPXref 0.7 |
|