Mercurial > hg > rc2
comparison program/lib/Roundcube/rcube_db.php @ 0:4681f974d28b
vanilla 1.3.3 distro, I hope
author | Charlie Root |
---|---|
date | Thu, 04 Jan 2018 15:52:31 -0500 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4681f974d28b |
---|---|
1 <?php | |
2 | |
3 /** | |
4 +-----------------------------------------------------------------------+ | |
5 | This file is part of the Roundcube Webmail client | | |
6 | Copyright (C) 2005-2012, The Roundcube Dev Team | | |
7 | | | |
8 | Licensed under the GNU General Public License version 3 or | | |
9 | any later version with exceptions for skins & plugins. | | |
10 | See the README file for a full license statement. | | |
11 | | | |
12 | PURPOSE: | | |
13 | Database wrapper class that implements PHP PDO functions | | |
14 +-----------------------------------------------------------------------+ | |
15 | Author: Aleksander Machniak <alec@alec.pl> | | |
16 +-----------------------------------------------------------------------+ | |
17 */ | |
18 | |
19 /** | |
20 * Database independent query interface. | |
21 * This is a wrapper for the PHP PDO. | |
22 * | |
23 * @package Framework | |
24 * @subpackage Database | |
25 */ | |
26 class rcube_db | |
27 { | |
28 public $db_provider; | |
29 | |
30 protected $db_dsnw; // DSN for write operations | |
31 protected $db_dsnr; // DSN for read operations | |
32 protected $db_connected = false; // Already connected ? | |
33 protected $db_mode; // Connection mode | |
34 protected $dbh; // Connection handle | |
35 protected $dbhs = array(); | |
36 protected $table_connections = array(); | |
37 | |
38 protected $db_error = false; | |
39 protected $db_error_msg = ''; | |
40 protected $conn_failure = false; | |
41 protected $db_index = 0; | |
42 protected $last_result; | |
43 protected $tables; | |
44 protected $variables; | |
45 | |
46 protected $options = array( | |
47 // column/table quotes | |
48 'identifier_start' => '"', | |
49 'identifier_end' => '"', | |
50 // date/time input format | |
51 'datetime_format' => 'Y-m-d H:i:s', | |
52 ); | |
53 | |
54 const DEBUG_LINE_LENGTH = 4096; | |
55 const DEFAULT_QUOTE = '`'; | |
56 | |
57 /** | |
58 * Factory, returns driver-specific instance of the class | |
59 * | |
60 * @param string $db_dsnw DSN for read/write operations | |
61 * @param string $db_dsnr Optional DSN for read only operations | |
62 * @param bool $pconn Enables persistent connections | |
63 * | |
64 * @return rcube_db Object instance | |
65 */ | |
66 public static function factory($db_dsnw, $db_dsnr = '', $pconn = false) | |
67 { | |
68 $driver = strtolower(substr($db_dsnw, 0, strpos($db_dsnw, ':'))); | |
69 $driver_map = array( | |
70 'sqlite2' => 'sqlite', | |
71 'sybase' => 'mssql', | |
72 'dblib' => 'mssql', | |
73 'mysqli' => 'mysql', | |
74 'oci' => 'oracle', | |
75 'oci8' => 'oracle', | |
76 ); | |
77 | |
78 $driver = isset($driver_map[$driver]) ? $driver_map[$driver] : $driver; | |
79 $class = "rcube_db_$driver"; | |
80 | |
81 if (!$driver || !class_exists($class)) { | |
82 rcube::raise_error(array('code' => 600, 'type' => 'db', | |
83 'line' => __LINE__, 'file' => __FILE__, | |
84 'message' => "Configuration error. Unsupported database driver: $driver"), | |
85 true, true); | |
86 } | |
87 | |
88 return new $class($db_dsnw, $db_dsnr, $pconn); | |
89 } | |
90 | |
91 /** | |
92 * Object constructor | |
93 * | |
94 * @param string $db_dsnw DSN for read/write operations | |
95 * @param string $db_dsnr Optional DSN for read only operations | |
96 * @param bool $pconn Enables persistent connections | |
97 */ | |
98 public function __construct($db_dsnw, $db_dsnr = '', $pconn = false) | |
99 { | |
100 if (empty($db_dsnr)) { | |
101 $db_dsnr = $db_dsnw; | |
102 } | |
103 | |
104 $this->db_dsnw = $db_dsnw; | |
105 $this->db_dsnr = $db_dsnr; | |
106 $this->db_pconn = $pconn; | |
107 | |
108 $this->db_dsnw_array = self::parse_dsn($db_dsnw); | |
109 $this->db_dsnr_array = self::parse_dsn($db_dsnr); | |
110 | |
111 $config = rcube::get_instance()->config; | |
112 | |
113 $this->options['table_prefix'] = $config->get('db_prefix'); | |
114 $this->options['dsnw_noread'] = $config->get('db_dsnw_noread', false); | |
115 $this->options['table_dsn_map'] = array_map(array($this, 'table_name'), $config->get('db_table_dsn', array())); | |
116 } | |
117 | |
118 /** | |
119 * Connect to specific database | |
120 * | |
121 * @param array $dsn DSN for DB connections | |
122 * @param string $mode Connection mode (r|w) | |
123 */ | |
124 protected function dsn_connect($dsn, $mode) | |
125 { | |
126 $this->db_error = false; | |
127 $this->db_error_msg = null; | |
128 | |
129 // return existing handle | |
130 if ($this->dbhs[$mode]) { | |
131 $this->dbh = $this->dbhs[$mode]; | |
132 $this->db_mode = $mode; | |
133 return $this->dbh; | |
134 } | |
135 | |
136 // connect to database | |
137 if ($dbh = $this->conn_create($dsn)) { | |
138 $this->dbh = $dbh; | |
139 $this->dbhs[$mode] = $dbh; | |
140 $this->db_mode = $mode; | |
141 $this->db_connected = true; | |
142 } | |
143 } | |
144 | |
145 /** | |
146 * Create PDO connection | |
147 */ | |
148 protected function conn_create($dsn) | |
149 { | |
150 // Get database specific connection options | |
151 $dsn_string = $this->dsn_string($dsn); | |
152 $dsn_options = $this->dsn_options($dsn); | |
153 | |
154 // Connect | |
155 try { | |
156 // with this check we skip fatal error on PDO object creation | |
157 if (!class_exists('PDO', false)) { | |
158 throw new Exception('PDO extension not loaded. See http://php.net/manual/en/intro.pdo.php'); | |
159 } | |
160 | |
161 $this->conn_prepare($dsn); | |
162 | |
163 $dbh = new PDO($dsn_string, $dsn['username'], $dsn['password'], $dsn_options); | |
164 | |
165 // don't throw exceptions or warnings | |
166 $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); | |
167 | |
168 $this->conn_configure($dsn, $dbh); | |
169 } | |
170 catch (Exception $e) { | |
171 $this->db_error = true; | |
172 $this->db_error_msg = $e->getMessage(); | |
173 | |
174 rcube::raise_error(array('code' => 500, 'type' => 'db', | |
175 'line' => __LINE__, 'file' => __FILE__, | |
176 'message' => $this->db_error_msg), true, false); | |
177 | |
178 return null; | |
179 } | |
180 | |
181 return $dbh; | |
182 } | |
183 | |
184 /** | |
185 * Driver-specific preparation of database connection | |
186 * | |
187 * @param array $dsn DSN for DB connections | |
188 */ | |
189 protected function conn_prepare($dsn) | |
190 { | |
191 } | |
192 | |
193 /** | |
194 * Driver-specific configuration of database connection | |
195 * | |
196 * @param array $dsn DSN for DB connections | |
197 * @param PDO $dbh Connection handler | |
198 */ | |
199 protected function conn_configure($dsn, $dbh) | |
200 { | |
201 } | |
202 | |
203 /** | |
204 * Connect to appropriate database depending on the operation | |
205 * | |
206 * @param string $mode Connection mode (r|w) | |
207 * @param boolean $force Enforce using the given mode | |
208 */ | |
209 public function db_connect($mode, $force = false) | |
210 { | |
211 // previous connection failed, don't attempt to connect again | |
212 if ($this->conn_failure) { | |
213 return; | |
214 } | |
215 | |
216 // no replication | |
217 if ($this->db_dsnw == $this->db_dsnr) { | |
218 $mode = 'w'; | |
219 } | |
220 | |
221 // Already connected | |
222 if ($this->db_connected) { | |
223 // connected to db with the same or "higher" mode (if allowed) | |
224 if ($this->db_mode == $mode || $this->db_mode == 'w' && !$force && !$this->options['dsnw_noread']) { | |
225 return; | |
226 } | |
227 } | |
228 | |
229 $dsn = ($mode == 'r') ? $this->db_dsnr_array : $this->db_dsnw_array; | |
230 $this->dsn_connect($dsn, $mode); | |
231 | |
232 // use write-master when read-only fails | |
233 if (!$this->db_connected && $mode == 'r' && $this->is_replicated()) { | |
234 $this->dsn_connect($this->db_dsnw_array, 'w'); | |
235 } | |
236 | |
237 $this->conn_failure = !$this->db_connected; | |
238 } | |
239 | |
240 /** | |
241 * Analyze the given SQL statement and select the appropriate connection to use | |
242 */ | |
243 protected function dsn_select($query) | |
244 { | |
245 // no replication | |
246 if ($this->db_dsnw == $this->db_dsnr) { | |
247 return 'w'; | |
248 } | |
249 | |
250 // Read or write ? | |
251 $mode = preg_match('/^(select|show|set)/i', $query) ? 'r' : 'w'; | |
252 | |
253 $start = '[' . $this->options['identifier_start'] . self::DEFAULT_QUOTE . ']'; | |
254 $end = '[' . $this->options['identifier_end'] . self::DEFAULT_QUOTE . ']'; | |
255 $regex = '/(?:^|\s)(from|update|into|join)\s+'.$start.'?([a-z0-9._]+)'.$end.'?\s+/i'; | |
256 | |
257 // find tables involved in this query | |
258 if (preg_match_all($regex, $query, $matches, PREG_SET_ORDER)) { | |
259 foreach ($matches as $m) { | |
260 $table = $m[2]; | |
261 | |
262 // always use direct mapping | |
263 if ($this->options['table_dsn_map'][$table]) { | |
264 $mode = $this->options['table_dsn_map'][$table]; | |
265 break; // primary table rules | |
266 } | |
267 else if ($mode == 'r') { | |
268 // connected to db with the same or "higher" mode for this table | |
269 $db_mode = $this->table_connections[$table]; | |
270 if ($db_mode == 'w' && !$this->options['dsnw_noread']) { | |
271 $mode = $db_mode; | |
272 } | |
273 } | |
274 } | |
275 | |
276 // remember mode chosen (for primary table) | |
277 $table = $matches[0][2]; | |
278 $this->table_connections[$table] = $mode; | |
279 } | |
280 | |
281 return $mode; | |
282 } | |
283 | |
284 /** | |
285 * Activate/deactivate debug mode | |
286 * | |
287 * @param boolean $dbg True if SQL queries should be logged | |
288 */ | |
289 public function set_debug($dbg = true) | |
290 { | |
291 $this->options['debug_mode'] = $dbg; | |
292 } | |
293 | |
294 /** | |
295 * Writes debug information/query to 'sql' log file | |
296 * | |
297 * @param string $query SQL query | |
298 */ | |
299 protected function debug($query) | |
300 { | |
301 if ($this->options['debug_mode']) { | |
302 if (($len = strlen($query)) > self::DEBUG_LINE_LENGTH) { | |
303 $diff = $len - self::DEBUG_LINE_LENGTH; | |
304 $query = substr($query, 0, self::DEBUG_LINE_LENGTH) | |
305 . "... [truncated $diff bytes]"; | |
306 } | |
307 rcube::write_log('sql', '[' . (++$this->db_index) . '] ' . $query . ';'); | |
308 } | |
309 } | |
310 | |
311 /** | |
312 * Getter for error state | |
313 * | |
314 * @param mixed $result Optional query result | |
315 * | |
316 * @return string Error message | |
317 */ | |
318 public function is_error($result = null) | |
319 { | |
320 if ($result !== null) { | |
321 return $result === false ? $this->db_error_msg : null; | |
322 } | |
323 | |
324 return $this->db_error ? $this->db_error_msg : null; | |
325 } | |
326 | |
327 /** | |
328 * Connection state checker | |
329 * | |
330 * @return boolean True if in connected state | |
331 */ | |
332 public function is_connected() | |
333 { | |
334 return !is_object($this->dbh) ? false : $this->db_connected; | |
335 } | |
336 | |
337 /** | |
338 * Is database replication configured? | |
339 * | |
340 * @return bool Returns true if dsnw != dsnr | |
341 */ | |
342 public function is_replicated() | |
343 { | |
344 return !empty($this->db_dsnr) && $this->db_dsnw != $this->db_dsnr; | |
345 } | |
346 | |
347 /** | |
348 * Get database runtime variables | |
349 * | |
350 * @param string $varname Variable name | |
351 * @param mixed $default Default value if variable is not set | |
352 * | |
353 * @return mixed Variable value or default | |
354 */ | |
355 public function get_variable($varname, $default = null) | |
356 { | |
357 // to be implemented by driver class | |
358 return rcube::get_instance()->config->get('db_' . $varname, $default); | |
359 } | |
360 | |
361 /** | |
362 * Execute a SQL query | |
363 * | |
364 * @param string SQL query to execute | |
365 * @param mixed Values to be inserted in query | |
366 * | |
367 * @return number Query handle identifier | |
368 */ | |
369 public function query() | |
370 { | |
371 $params = func_get_args(); | |
372 $query = array_shift($params); | |
373 | |
374 // Support one argument of type array, instead of n arguments | |
375 if (count($params) == 1 && is_array($params[0])) { | |
376 $params = $params[0]; | |
377 } | |
378 | |
379 return $this->_query($query, 0, 0, $params); | |
380 } | |
381 | |
382 /** | |
383 * Execute a SQL query with limits | |
384 * | |
385 * @param string SQL query to execute | |
386 * @param int Offset for LIMIT statement | |
387 * @param int Number of rows for LIMIT statement | |
388 * @param mixed Values to be inserted in query | |
389 * | |
390 * @return PDOStatement|bool Query handle or False on error | |
391 */ | |
392 public function limitquery() | |
393 { | |
394 $params = func_get_args(); | |
395 $query = array_shift($params); | |
396 $offset = array_shift($params); | |
397 $numrows = array_shift($params); | |
398 | |
399 return $this->_query($query, $offset, $numrows, $params); | |
400 } | |
401 | |
402 /** | |
403 * Execute a SQL query with limits | |
404 * | |
405 * @param string $query SQL query to execute | |
406 * @param int $offset Offset for LIMIT statement | |
407 * @param int $numrows Number of rows for LIMIT statement | |
408 * @param array $params Values to be inserted in query | |
409 * | |
410 * @return PDOStatement|bool Query handle or False on error | |
411 */ | |
412 protected function _query($query, $offset, $numrows, $params) | |
413 { | |
414 $query = ltrim($query); | |
415 | |
416 $this->db_connect($this->dsn_select($query), true); | |
417 | |
418 // check connection before proceeding | |
419 if (!$this->is_connected()) { | |
420 return $this->last_result = false; | |
421 } | |
422 | |
423 if ($numrows || $offset) { | |
424 $query = $this->set_limit($query, $numrows, $offset); | |
425 } | |
426 | |
427 // replace self::DEFAULT_QUOTE with driver-specific quoting | |
428 $query = $this->query_parse($query); | |
429 | |
430 // Because in Roundcube we mostly use queries that are | |
431 // executed only once, we will not use prepared queries | |
432 $pos = 0; | |
433 $idx = 0; | |
434 | |
435 if (count($params)) { | |
436 while ($pos = strpos($query, '?', $pos)) { | |
437 if ($query[$pos+1] == '?') { // skip escaped '?' | |
438 $pos += 2; | |
439 } | |
440 else { | |
441 $val = $this->quote($params[$idx++]); | |
442 unset($params[$idx-1]); | |
443 $query = substr_replace($query, $val, $pos, 1); | |
444 $pos += strlen($val); | |
445 } | |
446 } | |
447 } | |
448 | |
449 $query = rtrim($query, " \t\n\r\0\x0B;"); | |
450 | |
451 // replace escaped '?' and quotes back to normal, see self::quote() | |
452 $query = str_replace( | |
453 array('??', self::DEFAULT_QUOTE.self::DEFAULT_QUOTE), | |
454 array('?', self::DEFAULT_QUOTE), | |
455 $query | |
456 ); | |
457 | |
458 // log query | |
459 $this->debug($query); | |
460 | |
461 return $this->query_execute($query); | |
462 } | |
463 | |
464 /** | |
465 * Query execution | |
466 */ | |
467 protected function query_execute($query) | |
468 { | |
469 // destroy reference to previous result, required for SQLite driver (#1488874) | |
470 $this->last_result = null; | |
471 $this->db_error_msg = null; | |
472 | |
473 // send query | |
474 $result = $this->dbh->query($query); | |
475 | |
476 if ($result === false) { | |
477 $result = $this->handle_error($query); | |
478 } | |
479 | |
480 return $this->last_result = $result; | |
481 } | |
482 | |
483 /** | |
484 * Parse SQL query and replace identifier quoting | |
485 * | |
486 * @param string $query SQL query | |
487 * | |
488 * @return string SQL query | |
489 */ | |
490 protected function query_parse($query) | |
491 { | |
492 $start = $this->options['identifier_start']; | |
493 $end = $this->options['identifier_end']; | |
494 $quote = self::DEFAULT_QUOTE; | |
495 | |
496 if ($start == $quote) { | |
497 return $query; | |
498 } | |
499 | |
500 $pos = 0; | |
501 $in = false; | |
502 | |
503 while ($pos = strpos($query, $quote, $pos)) { | |
504 if ($query[$pos+1] == $quote) { // skip escaped quote | |
505 $pos += 2; | |
506 } | |
507 else { | |
508 if ($in) { | |
509 $q = $end; | |
510 $in = false; | |
511 } | |
512 else { | |
513 $q = $start; | |
514 $in = true; | |
515 } | |
516 | |
517 $query = substr_replace($query, $q, $pos, 1); | |
518 $pos++; | |
519 } | |
520 } | |
521 | |
522 return $query; | |
523 } | |
524 | |
525 /** | |
526 * Helper method to handle DB errors. | |
527 * This by default logs the error but could be overridden by a driver implementation | |
528 * | |
529 * @param string $query Query that triggered the error | |
530 * | |
531 * @return mixed Result to be stored and returned | |
532 */ | |
533 protected function handle_error($query) | |
534 { | |
535 $error = $this->dbh->errorInfo(); | |
536 | |
537 if (empty($this->options['ignore_key_errors']) || !in_array($error[0], array('23000', '23505'))) { | |
538 $this->db_error = true; | |
539 $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]); | |
540 | |
541 if (empty($this->options['ignore_errors'])) { | |
542 rcube::raise_error(array( | |
543 'code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__, | |
544 'message' => $this->db_error_msg . " (SQL Query: $query)" | |
545 ), true, false); | |
546 } | |
547 } | |
548 | |
549 return false; | |
550 } | |
551 | |
552 /** | |
553 * Get number of affected rows for the last query | |
554 * | |
555 * @param mixed $result Optional query handle | |
556 * | |
557 * @return int Number of (matching) rows | |
558 */ | |
559 public function affected_rows($result = null) | |
560 { | |
561 if ($result || ($result === null && ($result = $this->last_result))) { | |
562 if ($result !== true) { | |
563 return $result->rowCount(); | |
564 } | |
565 } | |
566 | |
567 return 0; | |
568 } | |
569 | |
570 /** | |
571 * Get number of rows for a SQL query | |
572 * If no query handle is specified, the last query will be taken as reference | |
573 * | |
574 * @param mixed $result Optional query handle | |
575 * | |
576 * @return mixed Number of rows or false on failure | |
577 * @deprecated This method shows very poor performance and should be avoided. | |
578 */ | |
579 public function num_rows($result = null) | |
580 { | |
581 if (($result || ($result === null && ($result = $this->last_result))) && $result !== true) { | |
582 // repeat query with SELECT COUNT(*) ... | |
583 if (preg_match('/^SELECT\s+(?:ALL\s+|DISTINCT\s+)?(?:.*?)\s+FROM\s+(.*)$/ims', $result->queryString, $m)) { | |
584 $query = $this->dbh->query('SELECT COUNT(*) FROM ' . $m[1], PDO::FETCH_NUM); | |
585 return $query ? intval($query->fetchColumn(0)) : false; | |
586 } | |
587 else { | |
588 $num = count($result->fetchAll()); | |
589 $result->execute(); // re-execute query because there's no seek(0) | |
590 return $num; | |
591 } | |
592 } | |
593 | |
594 return false; | |
595 } | |
596 | |
597 /** | |
598 * Get last inserted record ID | |
599 * | |
600 * @param string $table Table name (to find the incremented sequence) | |
601 * | |
602 * @return mixed ID or false on failure | |
603 */ | |
604 public function insert_id($table = '') | |
605 { | |
606 if (!$this->db_connected || $this->db_mode == 'r') { | |
607 return false; | |
608 } | |
609 | |
610 if ($table) { | |
611 // resolve table name | |
612 $table = $this->table_name($table); | |
613 } | |
614 | |
615 $id = $this->dbh->lastInsertId($table); | |
616 | |
617 return $id; | |
618 } | |
619 | |
620 /** | |
621 * Get an associative array for one row | |
622 * If no query handle is specified, the last query will be taken as reference | |
623 * | |
624 * @param mixed $result Optional query handle | |
625 * | |
626 * @return mixed Array with col values or false on failure | |
627 */ | |
628 public function fetch_assoc($result = null) | |
629 { | |
630 return $this->_fetch_row($result, PDO::FETCH_ASSOC); | |
631 } | |
632 | |
633 /** | |
634 * Get an index array for one row | |
635 * If no query handle is specified, the last query will be taken as reference | |
636 * | |
637 * @param mixed $result Optional query handle | |
638 * | |
639 * @return mixed Array with col values or false on failure | |
640 */ | |
641 public function fetch_array($result = null) | |
642 { | |
643 return $this->_fetch_row($result, PDO::FETCH_NUM); | |
644 } | |
645 | |
646 /** | |
647 * Get col values for a result row | |
648 * | |
649 * @param mixed $result Optional query handle | |
650 * @param int $mode Fetch mode identifier | |
651 * | |
652 * @return mixed Array with col values or false on failure | |
653 */ | |
654 protected function _fetch_row($result, $mode) | |
655 { | |
656 if ($result || ($result === null && ($result = $this->last_result))) { | |
657 if ($result !== true) { | |
658 return $result->fetch($mode); | |
659 } | |
660 } | |
661 | |
662 return false; | |
663 } | |
664 | |
665 /** | |
666 * Adds LIMIT,OFFSET clauses to the query | |
667 * | |
668 * @param string $query SQL query | |
669 * @param int $limit Number of rows | |
670 * @param int $offset Offset | |
671 * | |
672 * @return string SQL query | |
673 */ | |
674 protected function set_limit($query, $limit = 0, $offset = 0) | |
675 { | |
676 if ($limit) { | |
677 $query .= ' LIMIT ' . intval($limit); | |
678 } | |
679 | |
680 if ($offset) { | |
681 $query .= ' OFFSET ' . intval($offset); | |
682 } | |
683 | |
684 return $query; | |
685 } | |
686 | |
687 /** | |
688 * Returns list of tables in a database | |
689 * | |
690 * @return array List of all tables of the current database | |
691 */ | |
692 public function list_tables() | |
693 { | |
694 // get tables if not cached | |
695 if ($this->tables === null) { | |
696 $q = $this->query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES" | |
697 . " WHERE TABLE_TYPE = 'BASE TABLE'" | |
698 . " ORDER BY TABLE_NAME"); | |
699 | |
700 $this->tables = $q ? $q->fetchAll(PDO::FETCH_COLUMN, 0) : array(); | |
701 } | |
702 | |
703 return $this->tables; | |
704 } | |
705 | |
706 /** | |
707 * Returns list of columns in database table | |
708 * | |
709 * @param string $table Table name | |
710 * | |
711 * @return array List of table cols | |
712 */ | |
713 public function list_cols($table) | |
714 { | |
715 $q = $this->query('SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?', | |
716 array($table)); | |
717 | |
718 if ($q) { | |
719 return $q->fetchAll(PDO::FETCH_COLUMN, 0); | |
720 } | |
721 | |
722 return array(); | |
723 } | |
724 | |
725 /** | |
726 * Start transaction | |
727 * | |
728 * @return bool True on success, False on failure | |
729 */ | |
730 public function startTransaction() | |
731 { | |
732 $this->db_connect('w', true); | |
733 | |
734 // check connection before proceeding | |
735 if (!$this->is_connected()) { | |
736 return $this->last_result = false; | |
737 } | |
738 | |
739 $this->debug('BEGIN TRANSACTION'); | |
740 | |
741 return $this->last_result = $this->dbh->beginTransaction(); | |
742 } | |
743 | |
744 /** | |
745 * Commit transaction | |
746 * | |
747 * @return bool True on success, False on failure | |
748 */ | |
749 public function endTransaction() | |
750 { | |
751 $this->db_connect('w', true); | |
752 | |
753 // check connection before proceeding | |
754 if (!$this->is_connected()) { | |
755 return $this->last_result = false; | |
756 } | |
757 | |
758 $this->debug('COMMIT TRANSACTION'); | |
759 | |
760 return $this->last_result = $this->dbh->commit(); | |
761 } | |
762 | |
763 /** | |
764 * Rollback transaction | |
765 * | |
766 * @return bool True on success, False on failure | |
767 */ | |
768 public function rollbackTransaction() | |
769 { | |
770 $this->db_connect('w', true); | |
771 | |
772 // check connection before proceeding | |
773 if (!$this->is_connected()) { | |
774 return $this->last_result = false; | |
775 } | |
776 | |
777 $this->debug('ROLLBACK TRANSACTION'); | |
778 | |
779 return $this->last_result = $this->dbh->rollBack(); | |
780 } | |
781 | |
782 /** | |
783 * Release resources related to the last query result. | |
784 * When we know we don't need to access the last query result we can destroy it | |
785 * and release memory. Useful especially if the query returned big chunk of data. | |
786 */ | |
787 public function reset() | |
788 { | |
789 $this->last_result = null; | |
790 } | |
791 | |
792 /** | |
793 * Terminate database connection. | |
794 */ | |
795 public function closeConnection() | |
796 { | |
797 $this->db_connected = false; | |
798 $this->db_index = 0; | |
799 | |
800 // release statement and connection resources | |
801 $this->last_result = null; | |
802 $this->dbh = null; | |
803 $this->dbhs = array(); | |
804 } | |
805 | |
806 /** | |
807 * Formats input so it can be safely used in a query | |
808 * | |
809 * @param mixed $input Value to quote | |
810 * @param string $type Type of data (integer, bool, ident) | |
811 * | |
812 * @return string Quoted/converted string for use in query | |
813 */ | |
814 public function quote($input, $type = null) | |
815 { | |
816 // handle int directly for better performance | |
817 if ($type == 'integer' || $type == 'int') { | |
818 return intval($input); | |
819 } | |
820 | |
821 if (is_null($input)) { | |
822 return 'NULL'; | |
823 } | |
824 | |
825 if ($input instanceof DateTime) { | |
826 return $this->quote($input->format($this->options['datetime_format'])); | |
827 } | |
828 | |
829 if ($type == 'ident') { | |
830 return $this->quote_identifier($input); | |
831 } | |
832 | |
833 // create DB handle if not available | |
834 if (!$this->dbh) { | |
835 $this->db_connect('r'); | |
836 } | |
837 | |
838 if ($this->dbh) { | |
839 $map = array( | |
840 'bool' => PDO::PARAM_BOOL, | |
841 'integer' => PDO::PARAM_INT, | |
842 ); | |
843 | |
844 $type = isset($map[$type]) ? $map[$type] : PDO::PARAM_STR; | |
845 | |
846 return strtr($this->dbh->quote($input, $type), | |
847 // escape ? and ` | |
848 array('?' => '??', self::DEFAULT_QUOTE => self::DEFAULT_QUOTE.self::DEFAULT_QUOTE) | |
849 ); | |
850 } | |
851 | |
852 return 'NULL'; | |
853 } | |
854 | |
855 /** | |
856 * Escapes a string so it can be safely used in a query | |
857 * | |
858 * @param string $str A string to escape | |
859 * | |
860 * @return string Escaped string for use in a query | |
861 */ | |
862 public function escape($str) | |
863 { | |
864 if (is_null($str)) { | |
865 return 'NULL'; | |
866 } | |
867 | |
868 return substr($this->quote($str), 1, -1); | |
869 } | |
870 | |
871 /** | |
872 * Quotes a string so it can be safely used as a table or column name | |
873 * | |
874 * @param string $str Value to quote | |
875 * | |
876 * @return string Quoted string for use in query | |
877 * @deprecated Replaced by rcube_db::quote_identifier | |
878 * @see rcube_db::quote_identifier | |
879 */ | |
880 public function quoteIdentifier($str) | |
881 { | |
882 return $this->quote_identifier($str); | |
883 } | |
884 | |
885 /** | |
886 * Escapes a string so it can be safely used in a query | |
887 * | |
888 * @param string $str A string to escape | |
889 * | |
890 * @return string Escaped string for use in a query | |
891 * @deprecated Replaced by rcube_db::escape | |
892 * @see rcube_db::escape | |
893 */ | |
894 public function escapeSimple($str) | |
895 { | |
896 return $this->escape($str); | |
897 } | |
898 | |
899 /** | |
900 * Quotes a string so it can be safely used as a table or column name | |
901 * | |
902 * @param string $str Value to quote | |
903 * | |
904 * @return string Quoted string for use in query | |
905 */ | |
906 public function quote_identifier($str) | |
907 { | |
908 $start = $this->options['identifier_start']; | |
909 $end = $this->options['identifier_end']; | |
910 $name = array(); | |
911 | |
912 foreach (explode('.', $str) as $elem) { | |
913 $elem = str_replace(array($start, $end), '', $elem); | |
914 $name[] = $start . $elem . $end; | |
915 } | |
916 | |
917 return implode($name, '.'); | |
918 } | |
919 | |
920 /** | |
921 * Return SQL function for current time and date | |
922 * | |
923 * @param int $interval Optional interval (in seconds) to add/subtract | |
924 * | |
925 * @return string SQL function to use in query | |
926 */ | |
927 public function now($interval = 0) | |
928 { | |
929 if ($interval) { | |
930 $add = ' ' . ($interval > 0 ? '+' : '-') . ' INTERVAL '; | |
931 $add .= $interval > 0 ? intval($interval) : intval($interval) * -1; | |
932 $add .= ' SECOND'; | |
933 } | |
934 | |
935 return "now()" . $add; | |
936 } | |
937 | |
938 /** | |
939 * Return list of elements for use with SQL's IN clause | |
940 * | |
941 * @param array $arr Input array | |
942 * @param string $type Type of data (integer, bool, ident) | |
943 * | |
944 * @return string Comma-separated list of quoted values for use in query | |
945 */ | |
946 public function array2list($arr, $type = null) | |
947 { | |
948 if (!is_array($arr)) { | |
949 return $this->quote($arr, $type); | |
950 } | |
951 | |
952 foreach ($arr as $idx => $item) { | |
953 $arr[$idx] = $this->quote($item, $type); | |
954 } | |
955 | |
956 return implode(',', $arr); | |
957 } | |
958 | |
959 /** | |
960 * Return SQL statement to convert a field value into a unix timestamp | |
961 * | |
962 * This method is deprecated and should not be used anymore due to limitations | |
963 * of timestamp functions in Mysql (year 2038 problem) | |
964 * | |
965 * @param string $field Field name | |
966 * | |
967 * @return string SQL statement to use in query | |
968 * @deprecated | |
969 */ | |
970 public function unixtimestamp($field) | |
971 { | |
972 return "UNIX_TIMESTAMP($field)"; | |
973 } | |
974 | |
975 /** | |
976 * Return SQL statement to convert from a unix timestamp | |
977 * | |
978 * @param int $timestamp Unix timestamp | |
979 * | |
980 * @return string Date string in db-specific format | |
981 * @deprecated | |
982 */ | |
983 public function fromunixtime($timestamp) | |
984 { | |
985 return $this->quote(date($this->options['datetime_format'], $timestamp)); | |
986 } | |
987 | |
988 /** | |
989 * Return SQL statement for case insensitive LIKE | |
990 * | |
991 * @param string $column Field name | |
992 * @param string $value Search value | |
993 * | |
994 * @return string SQL statement to use in query | |
995 */ | |
996 public function ilike($column, $value) | |
997 { | |
998 return $this->quote_identifier($column).' LIKE '.$this->quote($value); | |
999 } | |
1000 | |
1001 /** | |
1002 * Abstract SQL statement for value concatenation | |
1003 * | |
1004 * @return string SQL statement to be used in query | |
1005 */ | |
1006 public function concat(/* col1, col2, ... */) | |
1007 { | |
1008 $args = func_get_args(); | |
1009 if (is_array($args[0])) { | |
1010 $args = $args[0]; | |
1011 } | |
1012 | |
1013 return '(' . join(' || ', $args) . ')'; | |
1014 } | |
1015 | |
1016 /** | |
1017 * Encodes non-UTF-8 characters in string/array/object (recursive) | |
1018 * | |
1019 * @param mixed $input Data to fix | |
1020 * @param bool $serialized Enable serialization | |
1021 * | |
1022 * @return mixed Properly UTF-8 encoded data | |
1023 */ | |
1024 public static function encode($input, $serialized = false) | |
1025 { | |
1026 // use Base64 encoding to workaround issues with invalid | |
1027 // or null characters in serialized string (#1489142) | |
1028 if ($serialized) { | |
1029 return base64_encode(serialize($input)); | |
1030 } | |
1031 | |
1032 if (is_object($input)) { | |
1033 foreach (get_object_vars($input) as $idx => $value) { | |
1034 $input->$idx = self::encode($value); | |
1035 } | |
1036 return $input; | |
1037 } | |
1038 else if (is_array($input)) { | |
1039 foreach ($input as $idx => $value) { | |
1040 $input[$idx] = self::encode($value); | |
1041 } | |
1042 | |
1043 return $input; | |
1044 } | |
1045 | |
1046 return utf8_encode($input); | |
1047 } | |
1048 | |
1049 /** | |
1050 * Decodes encoded UTF-8 string/object/array (recursive) | |
1051 * | |
1052 * @param mixed $input Input data | |
1053 * @param bool $serialized Enable serialization | |
1054 * | |
1055 * @return mixed Decoded data | |
1056 */ | |
1057 public static function decode($input, $serialized = false) | |
1058 { | |
1059 // use Base64 encoding to workaround issues with invalid | |
1060 // or null characters in serialized string (#1489142) | |
1061 if ($serialized) { | |
1062 // Keep backward compatybility where base64 wasn't used | |
1063 if (strpos(substr($input, 0, 16), ':') !== false) { | |
1064 return self::decode(@unserialize($input)); | |
1065 } | |
1066 | |
1067 return @unserialize(base64_decode($input)); | |
1068 } | |
1069 | |
1070 if (is_object($input)) { | |
1071 foreach (get_object_vars($input) as $idx => $value) { | |
1072 $input->$idx = self::decode($value); | |
1073 } | |
1074 return $input; | |
1075 } | |
1076 else if (is_array($input)) { | |
1077 foreach ($input as $idx => $value) { | |
1078 $input[$idx] = self::decode($value); | |
1079 } | |
1080 return $input; | |
1081 } | |
1082 | |
1083 return utf8_decode($input); | |
1084 } | |
1085 | |
1086 /** | |
1087 * Return correct name for a specific database table | |
1088 * | |
1089 * @param string $table Table name | |
1090 * @param bool $quoted Quote table identifier | |
1091 * | |
1092 * @return string Translated table name | |
1093 */ | |
1094 public function table_name($table, $quoted = false) | |
1095 { | |
1096 // let plugins alter the table name (#1489837) | |
1097 $plugin = rcube::get_instance()->plugins->exec_hook('db_table_name', array('table' => $table)); | |
1098 $table = $plugin['table']; | |
1099 | |
1100 // add prefix to the table name if configured | |
1101 if (($prefix = $this->options['table_prefix']) && strpos($table, $prefix) !== 0) { | |
1102 $table = $prefix . $table; | |
1103 } | |
1104 | |
1105 if ($quoted) { | |
1106 $table = $this->quote_identifier($table); | |
1107 } | |
1108 | |
1109 return $table; | |
1110 } | |
1111 | |
1112 /** | |
1113 * Set class option value | |
1114 * | |
1115 * @param string $name Option name | |
1116 * @param mixed $value Option value | |
1117 */ | |
1118 public function set_option($name, $value) | |
1119 { | |
1120 $this->options[$name] = $value; | |
1121 } | |
1122 | |
1123 /** | |
1124 * Set DSN connection to be used for the given table | |
1125 * | |
1126 * @param string $table Table name | |
1127 * @param string $mode DSN connection ('r' or 'w') to be used | |
1128 */ | |
1129 public function set_table_dsn($table, $mode) | |
1130 { | |
1131 $this->options['table_dsn_map'][$this->table_name($table)] = $mode; | |
1132 } | |
1133 | |
1134 /** | |
1135 * MDB2 DSN string parser | |
1136 * | |
1137 * @param string $sequence Secuence name | |
1138 * | |
1139 * @return array DSN parameters | |
1140 */ | |
1141 public static function parse_dsn($dsn) | |
1142 { | |
1143 if (empty($dsn)) { | |
1144 return null; | |
1145 } | |
1146 | |
1147 // Find phptype and dbsyntax | |
1148 if (($pos = strpos($dsn, '://')) !== false) { | |
1149 $str = substr($dsn, 0, $pos); | |
1150 $dsn = substr($dsn, $pos + 3); | |
1151 } | |
1152 else { | |
1153 $str = $dsn; | |
1154 $dsn = null; | |
1155 } | |
1156 | |
1157 // Get phptype and dbsyntax | |
1158 // $str => phptype(dbsyntax) | |
1159 if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) { | |
1160 $parsed['phptype'] = $arr[1]; | |
1161 $parsed['dbsyntax'] = !$arr[2] ? $arr[1] : $arr[2]; | |
1162 } | |
1163 else { | |
1164 $parsed['phptype'] = $str; | |
1165 $parsed['dbsyntax'] = $str; | |
1166 } | |
1167 | |
1168 if (empty($dsn)) { | |
1169 return $parsed; | |
1170 } | |
1171 | |
1172 // Get (if found): username and password | |
1173 // $dsn => username:password@protocol+hostspec/database | |
1174 if (($at = strrpos($dsn,'@')) !== false) { | |
1175 $str = substr($dsn, 0, $at); | |
1176 $dsn = substr($dsn, $at + 1); | |
1177 if (($pos = strpos($str, ':')) !== false) { | |
1178 $parsed['username'] = rawurldecode(substr($str, 0, $pos)); | |
1179 $parsed['password'] = rawurldecode(substr($str, $pos + 1)); | |
1180 } | |
1181 else { | |
1182 $parsed['username'] = rawurldecode($str); | |
1183 } | |
1184 } | |
1185 | |
1186 // Find protocol and hostspec | |
1187 | |
1188 // $dsn => proto(proto_opts)/database | |
1189 if (preg_match('|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match)) { | |
1190 $proto = $match[1]; | |
1191 $proto_opts = $match[2] ? $match[2] : false; | |
1192 $dsn = $match[3]; | |
1193 } | |
1194 // $dsn => protocol+hostspec/database (old format) | |
1195 else { | |
1196 if (strpos($dsn, '+') !== false) { | |
1197 list($proto, $dsn) = explode('+', $dsn, 2); | |
1198 } | |
1199 if ( strpos($dsn, '//') === 0 | |
1200 && strpos($dsn, '/', 2) !== false | |
1201 && $parsed['phptype'] == 'oci8' | |
1202 ) { | |
1203 //oracle's "Easy Connect" syntax: | |
1204 //"username/password@[//]host[:port][/service_name]" | |
1205 //e.g. "scott/tiger@//mymachine:1521/oracle" | |
1206 $proto_opts = $dsn; | |
1207 $pos = strrpos($proto_opts, '/'); | |
1208 $dsn = substr($proto_opts, $pos + 1); | |
1209 $proto_opts = substr($proto_opts, 0, $pos); | |
1210 } | |
1211 else if (strpos($dsn, '/') !== false) { | |
1212 list($proto_opts, $dsn) = explode('/', $dsn, 2); | |
1213 } | |
1214 else { | |
1215 $proto_opts = $dsn; | |
1216 $dsn = null; | |
1217 } | |
1218 } | |
1219 | |
1220 // process the different protocol options | |
1221 $parsed['protocol'] = $proto ?: 'tcp'; | |
1222 $proto_opts = rawurldecode($proto_opts); | |
1223 if (strpos($proto_opts, ':') !== false) { | |
1224 list($proto_opts, $parsed['port']) = explode(':', $proto_opts); | |
1225 } | |
1226 if ($parsed['protocol'] == 'tcp') { | |
1227 $parsed['hostspec'] = $proto_opts; | |
1228 } | |
1229 else if ($parsed['protocol'] == 'unix') { | |
1230 $parsed['socket'] = $proto_opts; | |
1231 } | |
1232 | |
1233 // Get dabase if any | |
1234 // $dsn => database | |
1235 if ($dsn) { | |
1236 // /database | |
1237 if (($pos = strpos($dsn, '?')) === false) { | |
1238 $parsed['database'] = rawurldecode($dsn); | |
1239 // /database?param1=value1¶m2=value2 | |
1240 } | |
1241 else { | |
1242 $parsed['database'] = rawurldecode(substr($dsn, 0, $pos)); | |
1243 $dsn = substr($dsn, $pos + 1); | |
1244 if (strpos($dsn, '&') !== false) { | |
1245 $opts = explode('&', $dsn); | |
1246 } | |
1247 else { // database?param1=value1 | |
1248 $opts = array($dsn); | |
1249 } | |
1250 foreach ($opts as $opt) { | |
1251 list($key, $value) = explode('=', $opt); | |
1252 if (!array_key_exists($key, $parsed) || false === $parsed[$key]) { | |
1253 // don't allow params overwrite | |
1254 $parsed[$key] = rawurldecode($value); | |
1255 } | |
1256 } | |
1257 } | |
1258 } | |
1259 | |
1260 return $parsed; | |
1261 } | |
1262 | |
1263 /** | |
1264 * Returns PDO DSN string from DSN array | |
1265 * | |
1266 * @param array $dsn DSN parameters | |
1267 * | |
1268 * @return string DSN string | |
1269 */ | |
1270 protected function dsn_string($dsn) | |
1271 { | |
1272 $params = array(); | |
1273 $result = $dsn['phptype'] . ':'; | |
1274 | |
1275 if ($dsn['hostspec']) { | |
1276 $params[] = 'host=' . $dsn['hostspec']; | |
1277 } | |
1278 | |
1279 if ($dsn['port']) { | |
1280 $params[] = 'port=' . $dsn['port']; | |
1281 } | |
1282 | |
1283 if ($dsn['database']) { | |
1284 $params[] = 'dbname=' . $dsn['database']; | |
1285 } | |
1286 | |
1287 if (!empty($params)) { | |
1288 $result .= implode(';', $params); | |
1289 } | |
1290 | |
1291 return $result; | |
1292 } | |
1293 | |
1294 /** | |
1295 * Returns driver-specific connection options | |
1296 * | |
1297 * @param array $dsn DSN parameters | |
1298 * | |
1299 * @return array Connection options | |
1300 */ | |
1301 protected function dsn_options($dsn) | |
1302 { | |
1303 $result = array(); | |
1304 | |
1305 if ($this->db_pconn) { | |
1306 $result[PDO::ATTR_PERSISTENT] = true; | |
1307 } | |
1308 | |
1309 if (!empty($dsn['prefetch'])) { | |
1310 $result[PDO::ATTR_PREFETCH] = (int) $dsn['prefetch']; | |
1311 } | |
1312 | |
1313 if (!empty($dsn['timeout'])) { | |
1314 $result[PDO::ATTR_TIMEOUT] = (int) $dsn['timeout']; | |
1315 } | |
1316 | |
1317 return $result; | |
1318 } | |
1319 | |
1320 /** | |
1321 * Execute the given SQL script | |
1322 * | |
1323 * @param string $sql SQL queries to execute | |
1324 * | |
1325 * @return boolen True on success, False on error | |
1326 */ | |
1327 public function exec_script($sql) | |
1328 { | |
1329 $sql = $this->fix_table_names($sql); | |
1330 $buff = ''; | |
1331 $exec = ''; | |
1332 | |
1333 foreach (explode("\n", $sql) as $line) { | |
1334 $trimmed = trim($line); | |
1335 if ($trimmed == '' || preg_match('/^--/', $trimmed)) { | |
1336 continue; | |
1337 } | |
1338 | |
1339 if ($trimmed == 'GO') { | |
1340 $exec = $buff; | |
1341 } | |
1342 else if ($trimmed[strlen($trimmed)-1] == ';') { | |
1343 $exec = $buff . substr(rtrim($line), 0, -1); | |
1344 } | |
1345 | |
1346 if ($exec) { | |
1347 $this->query($exec); | |
1348 $buff = ''; | |
1349 $exec = ''; | |
1350 if ($this->db_error) { | |
1351 break; | |
1352 } | |
1353 } | |
1354 else { | |
1355 $buff .= $line . "\n"; | |
1356 } | |
1357 } | |
1358 | |
1359 return !$this->db_error; | |
1360 } | |
1361 | |
1362 /** | |
1363 * Parse SQL file and fix table names according to table prefix | |
1364 */ | |
1365 protected function fix_table_names($sql) | |
1366 { | |
1367 if (!$this->options['table_prefix']) { | |
1368 return $sql; | |
1369 } | |
1370 | |
1371 $sql = preg_replace_callback( | |
1372 '/((TABLE|TRUNCATE|(?<!ON )UPDATE|INSERT INTO|FROM' | |
1373 . '| ON(?! (DELETE|UPDATE))|REFERENCES|CONSTRAINT|FOREIGN KEY|INDEX)' | |
1374 . '\s+(IF (NOT )?EXISTS )?[`"]*)([^`"\( \r\n]+)/', | |
1375 array($this, 'fix_table_names_callback'), | |
1376 $sql | |
1377 ); | |
1378 | |
1379 return $sql; | |
1380 } | |
1381 | |
1382 /** | |
1383 * Preg_replace callback for fix_table_names() | |
1384 */ | |
1385 protected function fix_table_names_callback($matches) | |
1386 { | |
1387 return $matches[1] . $this->options['table_prefix'] . $matches[count($matches)-1]; | |
1388 } | |
1389 } |