source: subversion/trunk/roundcubemail/program/include/rcube_mdb2.php @ 3450

Last change on this file since 3450 was 3450, checked in by thomasb, 3 years ago

List of tables mustn't be static (wtf?)

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 18.6 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_mdb2.php                                        |
6 |                                                                       |
7 | This file is part of the RoundCube Webmail client                     |
8 | Copyright (C) 2005-2009, RoundCube Dev. - Switzerland                 |
9 | Licensed under the GNU GPL                                            |
10 |                                                                       |
11 | PURPOSE:                                                              |
12 |   PEAR:DB wrapper class that implements PEAR MDB2 functions           |
13 |   See http://pear.php.net/package/MDB2                                |
14 |                                                                       |
15 +-----------------------------------------------------------------------+
16 | Author: Lukas Kahwe Smith <smith@pooteeweet.org>                      |
17 +-----------------------------------------------------------------------+
18
19 $Id$
20
21*/
22
23
24/**
25 * Database independent query interface
26 *
27 * This is a wrapper for the PEAR::MDB2 class
28 *
29 * @package    Database
30 * @author     David Saez Padros <david@ols.es>
31 * @author     Thomas Bruederli <roundcube@gmail.com>
32 * @author     Lukas Kahwe Smith <smith@pooteeweet.org>
33 * @version    1.16
34 * @link       http://pear.php.net/package/MDB2
35 */
36class rcube_mdb2
37  {
38  var $db_dsnw;               // DSN for write operations
39  var $db_dsnr;               // DSN for read operations
40  var $db_connected = false;  // Already connected ?
41  var $db_mode = '';          // Connection mode
42  var $db_handle = 0;         // Connection handle
43  var $db_error = false;
44  var $db_error_msg = '';
45  var $debug_mode = false;
46
47  var $a_query_results = array('dummy');
48  var $last_res_id = 0;
49 
50  private $tables;
51
52
53  /**
54   * Object constructor
55   *
56   * @param  string  DSN for read/write operations
57   * @param  string  Optional DSN for read only operations
58   */
59  function __construct($db_dsnw, $db_dsnr='', $pconn=false)
60    {
61    if ($db_dsnr=='')
62      $db_dsnr=$db_dsnw;
63
64    $this->db_dsnw = $db_dsnw;
65    $this->db_dsnr = $db_dsnr;
66    $this->db_pconn = $pconn;
67   
68    $dsn_array = MDB2::parseDSN($db_dsnw);
69    $this->db_provider = $dsn_array['phptype'];
70    }
71
72
73  /**
74   * Connect to specific database
75   *
76   * @param  string  DSN for DB connections
77   * @return object  PEAR database handle
78   * @access private
79   */
80  function dsn_connect($dsn)
81    {
82    // Use persistent connections if available
83    $db_options = array(
84        'persistent' => $this->db_pconn,
85        'emulate_prepared' => $this->debug_mode,
86        'debug' => $this->debug_mode,
87        'debug_handler' => 'mdb2_debug_handler',
88        'portability' => MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_EMPTY_TO_NULL);
89
90    if ($this->db_provider == 'pgsql') {
91      $db_options['disable_smart_seqname'] = true;
92      $db_options['seqname_format'] = '%s';
93    }
94
95    $dbh = MDB2::connect($dsn, $db_options);
96
97    if (MDB2::isError($dbh))
98      {
99      $this->db_error = TRUE;
100      $this->db_error_msg = $dbh->getMessage();
101     
102      raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__,
103        'file' => __FILE__, 'message' => $dbh->getUserInfo()), TRUE, FALSE);
104      }
105    else if ($this->db_provider=='sqlite')
106      {
107      $dsn_array = MDB2::parseDSN($dsn);
108      if (!filesize($dsn_array['database']) && !empty($this->sqlite_initials))
109        $this->_sqlite_create_database($dbh, $this->sqlite_initials);
110      }
111    else if ($this->db_provider!='mssql' && $this->db_provider!='sqlsrv')
112      $dbh->setCharset('utf8');
113
114    return $dbh;
115    }
116
117
118  /**
119   * Connect to appropiate databse
120   * depending on the operation
121   *
122   * @param  string  Connection mode (r|w)
123   * @access public
124   */
125  function db_connect($mode)
126    {
127    $this->db_mode = $mode;
128
129    // Already connected
130    if ($this->db_connected)
131      {
132      // no replication, current connection is ok
133      if ($this->db_dsnw==$this->db_dsnr)
134        return;
135
136      // connected to master, current connection is ok
137      if ($this->db_mode=='w')
138        return;
139
140      // Same mode, current connection is ok
141      if ($this->db_mode==$mode)
142        return;
143      }
144
145    if ($mode=='r')
146      $dsn = $this->db_dsnr;
147    else
148      $dsn = $this->db_dsnw;
149
150    $this->db_handle = $this->dsn_connect($dsn);
151    $this->db_connected = true;
152    }
153
154
155  /**
156   * Activate/deactivate debug mode
157   *
158   * @param boolean True if SQL queries should be logged
159   */
160  function set_debug($dbg = true)
161  {
162    $this->debug_mode = $dbg;
163    if ($this->db_connected)
164    {
165      $this->db_handle->setOption('debug', $dbg);
166      $this->db_handle->setOption('emulate_prepared', $dbg);
167    }
168  }
169
170   
171  /**
172   * Getter for error state
173   *
174   * @param  boolean  True on error
175   */
176  function is_error()
177    {
178    return $this->db_error ? $this->db_error_msg : FALSE;
179    }
180   
181
182  /**
183   * Connection state checker
184   *
185   * @param  boolean  True if in connected state
186   */
187  function is_connected()
188    {
189    return PEAR::isError($this->db_handle) ? false : true;
190    }
191
192
193  /**
194   * Execute a SQL query
195   *
196   * @param  string  SQL query to execute
197   * @param  mixed   Values to be inserted in query
198   * @return number  Query handle identifier
199   * @access public
200   */
201  function query()
202    {
203    if (!$this->is_connected())
204      return NULL;
205   
206    $params = func_get_args();
207    $query = array_shift($params);
208
209    return $this->_query($query, 0, 0, $params);
210    }
211
212
213  /**
214   * Execute a SQL query with limits
215   *
216   * @param  string  SQL query to execute
217   * @param  number  Offset for LIMIT statement
218   * @param  number  Number of rows for LIMIT statement
219   * @param  mixed   Values to be inserted in query
220   * @return number  Query handle identifier
221   * @access public
222   */
223  function limitquery()
224    {
225    $params = func_get_args();
226    $query = array_shift($params);
227    $offset = array_shift($params);
228    $numrows = array_shift($params);
229
230    return $this->_query($query, $offset, $numrows, $params);
231    }
232
233
234  /**
235   * Execute a SQL query with limits
236   *
237   * @param  string  SQL query to execute
238   * @param  number  Offset for LIMIT statement
239   * @param  number  Number of rows for LIMIT statement
240   * @param  array   Values to be inserted in query
241   * @return number  Query handle identifier
242   * @access private
243   */
244  function _query($query, $offset, $numrows, $params)
245    {
246    // Read or write ?
247    if (strtolower(substr(trim($query),0,6))=='select')
248      $mode='r';
249    else
250      $mode='w';
251
252    $this->db_connect($mode);
253
254    if ($this->db_provider == 'sqlite')
255      $this->_sqlite_prepare();
256
257    if ($numrows || $offset)
258      $result = $this->db_handle->setLimit($numrows,$offset);
259
260    if (empty($params))
261      $result = $mode=='r' ? $this->db_handle->query($query) : $this->db_handle->exec($query);
262    else
263      {
264      $params = (array)$params;
265      $q = $this->db_handle->prepare($query, null, $mode=='w' ? MDB2_PREPARE_MANIP : null);
266      if ($this->db_handle->isError($q))
267        {
268        $this->db_error = TRUE;
269        $this->db_error_msg = $q->userinfo;
270
271        raise_error(array('code' => 500, 'type' => 'db',
272          'line' => __LINE__, 'file' => __FILE__,
273          'message' => $this->db_error_msg), TRUE, TRUE);
274        }
275      else
276        {
277        $result = $q->execute($params);
278        $q->free();
279        }
280      }
281
282    // add result, even if it's an error
283    return $this->_add_result($result);
284    }
285
286
287  /**
288   * Get number of rows for a SQL query
289   * If no query handle is specified, the last query will be taken as reference
290   *
291   * @param  number  Optional query handle identifier
292   * @return mixed   Number of rows or FALSE on failure
293   * @access public
294   */
295  function num_rows($res_id=NULL)
296    {
297    if (!$this->db_handle)
298      return FALSE;
299
300    if ($result = $this->_get_result($res_id))
301      return $result->numRows();
302    else
303      return FALSE;
304    }
305
306
307  /**
308   * Get number of affected rows for the last query
309   *
310   * @param  number  Optional query handle identifier
311   * @return mixed   Number of rows or FALSE on failure
312   * @access public
313   */
314  function affected_rows($res_id = null)
315    {
316    if (!$this->db_handle)
317      return FALSE;
318
319    return (int) $this->_get_result($res_id);
320    }
321
322
323  /**
324   * Get last inserted record ID
325   * For Postgres databases, a sequence name is required
326   *
327   * @param  string  Table name (to find the incremented sequence)
328   * @return mixed   ID or FALSE on failure
329   * @access public
330   */
331  function insert_id($table = '')
332    {
333    if (!$this->db_handle || $this->db_mode=='r')
334      return FALSE;
335
336    if ($table) {
337      if ($this->db_provider == 'pgsql')
338        // find sequence name
339        $table = get_sequence_name($table);
340      else
341        // resolve table name
342        $table = get_table_name($table);
343    }
344   
345    $id = $this->db_handle->lastInsertID($table);
346   
347    return $this->db_handle->isError($id) ? null : $id;
348    }
349
350
351  /**
352   * Get an associative array for one row
353   * If no query handle is specified, the last query will be taken as reference
354   *
355   * @param  number  Optional query handle identifier
356   * @return mixed   Array with col values or FALSE on failure
357   * @access public
358   */
359  function fetch_assoc($res_id=NULL)
360    {
361    $result = $this->_get_result($res_id);
362    return $this->_fetch_row($result, MDB2_FETCHMODE_ASSOC);
363    }
364
365
366  /**
367   * Get an index array for one row
368   * If no query handle is specified, the last query will be taken as reference
369   *
370   * @param  number  Optional query handle identifier
371   * @return mixed   Array with col values or FALSE on failure
372   * @access public
373   */
374  function fetch_array($res_id=NULL)
375    {
376    $result = $this->_get_result($res_id);
377    return $this->_fetch_row($result, MDB2_FETCHMODE_ORDERED);
378    }
379
380
381  /**
382   * Get col values for a result row
383   *
384   * @param  object  Query result handle
385   * @param  number  Fetch mode identifier
386   * @return mixed   Array with col values or FALSE on failure
387   * @access private
388   */
389  function _fetch_row($result, $mode)
390    {
391    if ($result === FALSE || PEAR::isError($result) || !$this->is_connected())
392      return FALSE;
393
394    return $result->fetchRow($mode);
395    }
396
397
398  /**
399   * Wrapper for the SHOW TABLES command
400   *
401   * @return array List of all tables of the current database
402   */
403  function list_tables()
404  {
405    // get tables if not cached
406    if (!$this->tables) {
407      $this->tables = array();
408
409      switch ($this->db_provider) {
410        case 'sqlite':
411          $result = $this->db_handle->query("SELECT name FROM sqlite_master WHERE type='table'");
412          break;
413        default:
414          $result = $this->db_handle->query("SHOW TABLES");
415      }
416
417      if ($result !== false && !PEAR::isError($result))
418        while ($rec = $result->fetchRow(MDB2_FETCHMODE_ORDERED))
419          $this->tables[] = $rec[0];
420    }
421
422    return $this->tables;
423  }
424
425
426  /**
427   * Formats input so it can be safely used in a query
428   *
429   * @param  mixed   Value to quote
430   * @return string  Quoted/converted string for use in query
431   * @access public
432   */
433  function quote($input, $type = null)
434    {
435    // create DB handle if not available
436    if (!$this->db_handle)
437      $this->db_connect('r');
438
439    // escape pear identifier chars
440    $rep_chars = array('?' => '\?',
441                       '!' => '\!',
442                       '&' => '\&');
443
444    return $this->db_handle->quote($input, $type);
445    }
446
447
448  /**
449   * Quotes a string so it can be safely used as a table or column name
450   *
451   * @param  string  Value to quote
452   * @return string  Quoted string for use in query
453   * @deprecated     Replaced by rcube_MDB2::quote_identifier
454   * @see            rcube_mdb2::quote_identifier
455   * @access public
456   */
457  function quoteIdentifier($str)
458    {
459    return $this->quote_identifier($str);
460    }
461
462
463  /**
464   * Quotes a string so it can be safely used as a table or column name
465   *
466   * @param  string  Value to quote
467   * @return string  Quoted string for use in query
468   * @access public
469   */
470  function quote_identifier($str)
471    {
472    if (!$this->db_handle)
473      $this->db_connect('r');
474
475    return $this->db_handle->quoteIdentifier($str);
476    }
477
478  /**
479   * Escapes a string
480   *
481   * @param  string  The string to be escaped
482   * @return string  The escaped string
483   * @access public
484   * @since  0.1.1
485   */
486  function escapeSimple($str)
487    {
488    if (!$this->db_handle)
489      $this->db_connect('r');
490   
491    return $this->db_handle->escape($str);
492    }
493
494
495  /**
496   * Return SQL function for current time and date
497   *
498   * @return string SQL function to use in query
499   * @access public
500   */
501  function now()
502    {
503    switch($this->db_provider)
504      {
505      case 'mssql':
506      case 'sqlsrv':
507        return "getdate()";
508
509      default:
510        return "now()";
511      }
512    }
513
514
515  /**
516   * Return list of elements for use with SQL's IN clause
517   *
518   * @param  string Input array
519   * @return string Elements list string
520   * @access public
521   */
522  function array2list($arr, $type=null)
523    {
524    if (!is_array($arr))
525      return $this->quote($arr, $type);
526   
527    $res = array();
528    foreach ($arr as $item)
529      $res[] = $this->quote($item, $type);
530
531    return implode(',', $res);
532    }
533
534
535  /**
536   * Return SQL statement to convert a field value into a unix timestamp
537   *
538   * @param  string  Field name
539   * @return string  SQL statement to use in query
540   * @access public
541   */
542  function unixtimestamp($field)
543    {
544    switch($this->db_provider)
545      {
546      case 'pgsql':
547        return "EXTRACT (EPOCH FROM $field)";
548        break;
549
550      case 'mssql':
551      case 'sqlsrv':
552        return "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())";
553
554      default:
555        return "UNIX_TIMESTAMP($field)";
556      }
557    }
558
559
560  /**
561   * Return SQL statement to convert from a unix timestamp
562   *
563   * @param  string  Field name
564   * @return string  SQL statement to use in query
565   * @access public
566   */
567  function fromunixtime($timestamp)
568    {
569    switch($this->db_provider)
570      {
571      case 'mysqli':
572      case 'mysql':
573      case 'sqlite':
574        return sprintf("FROM_UNIXTIME(%d)", $timestamp);
575
576      default:
577        return date("'Y-m-d H:i:s'", $timestamp);
578      }
579    }
580
581
582  /**
583   * Return SQL statement for case insensitive LIKE
584   *
585   * @param  string  Field name
586   * @param  string  Search value
587   * @return string  SQL statement to use in query
588   * @access public
589   */
590  function ilike($column, $value)
591    {
592    // TODO: use MDB2's matchPattern() function
593    switch($this->db_provider)
594      {
595      case 'pgsql':
596        return $this->quote_identifier($column).' ILIKE '.$this->quote($value);
597      default:
598        return $this->quote_identifier($column).' LIKE '.$this->quote($value);
599      }
600    }
601
602
603  /**
604   * Encodes non-UTF-8 characters in string/array/object (recursive)
605   *
606   * @param  mixed  Data to fix
607   * @return mixed  Properly UTF-8 encoded data
608   * @access public
609   */
610  function encode($input)
611    {
612    if (is_object($input)) {
613      foreach (get_object_vars($input) as $idx => $value)
614        $input->$idx = $this->encode($value);
615      return $input;
616      }
617    else if (is_array($input)) {
618      foreach ($input as $idx => $value)
619        $input[$idx] = $this->encode($value);
620      return $input;   
621      }
622
623    return utf8_encode($input);
624    }
625
626
627  /**
628   * Decodes encoded UTF-8 string/object/array (recursive)
629   *
630   * @param  mixed  Input data
631   * @return mixed  Decoded data
632   * @access public
633   */
634  function decode($input)
635    {
636    if (is_object($input)) {
637      foreach (get_object_vars($input) as $idx => $value)
638        $input->$idx = $this->decode($value);
639      return $input;
640      }
641    else if (is_array($input)) {
642      foreach ($input as $idx => $value)
643        $input[$idx] = $this->decode($value);
644      return $input;   
645      }
646
647    return utf8_decode($input);
648    }
649
650
651  /**
652   * Adds a query result and returns a handle ID
653   *
654   * @param  object  Query handle
655   * @return mixed   Handle ID
656   * @access private
657   */
658  function _add_result($res)
659    {
660    // sql error occured
661    if (PEAR::isError($res))
662      {
663      $this->db_error = TRUE;
664      $this->db_error_msg = $res->getMessage();
665      raise_error(array('code' => 500, 'type' => 'db',
666            'line' => __LINE__, 'file' => __FILE__,
667            'message' => $res->getMessage() . " Query: " 
668            . substr(preg_replace('/[\r\n]+\s*/', ' ', $res->userinfo), 0, 512)),
669            TRUE, FALSE);
670      }
671   
672    $res_id = sizeof($this->a_query_results);
673    $this->last_res_id = $res_id;
674    $this->a_query_results[$res_id] = $res;
675    return $res_id;
676    }
677
678
679  /**
680   * Resolves a given handle ID and returns the according query handle
681   * If no ID is specified, the last resource handle will be returned
682   *
683   * @param  number  Handle ID
684   * @return mixed   Resource handle or FALSE on failure
685   * @access private
686   */
687  function _get_result($res_id=NULL)
688    {
689    if ($res_id==NULL)
690      $res_id = $this->last_res_id;
691
692    if (isset($this->a_query_results[$res_id]))
693      if (!PEAR::isError($this->a_query_results[$res_id]))
694        return $this->a_query_results[$res_id];
695   
696    return FALSE;
697    }
698
699
700  /**
701   * Create a sqlite database from a file
702   *
703   * @param  object  SQLite database handle
704   * @param  string  File path to use for DB creation
705   * @access private
706   */
707  function _sqlite_create_database($dbh, $file_name)
708    {
709    if (empty($file_name) || !is_string($file_name))
710      return;
711
712    $data = file_get_contents($file_name);
713
714    if (strlen($data))
715      if (!sqlite_exec($dbh->connection, $data, $error) || MDB2::isError($dbh)) 
716        raise_error(array('code' => 500, 'type' => 'db',
717            'line' => __LINE__, 'file' => __FILE__, 'message' => $error), TRUE, FALSE); 
718    }
719
720
721  /**
722   * Add some proprietary database functions to the current SQLite handle
723   * in order to make it MySQL compatible
724   *
725   * @access private
726   */
727  function _sqlite_prepare()
728    {
729    include_once('include/rcube_sqlite.inc');
730
731    // we emulate via callback some missing MySQL function
732    sqlite_create_function($this->db_handle->connection, "from_unixtime", "rcube_sqlite_from_unixtime");
733    sqlite_create_function($this->db_handle->connection, "unix_timestamp", "rcube_sqlite_unix_timestamp");
734    sqlite_create_function($this->db_handle->connection, "now", "rcube_sqlite_now");
735    sqlite_create_function($this->db_handle->connection, "md5", "rcube_sqlite_md5");
736    }
737
738
739  }  // end class rcube_db
740
741
742/* this is our own debug handler for the MDB2 connection */
743function mdb2_debug_handler(&$db, $scope, $message, $context = array())
744{
745  if ($scope != 'prepare')
746  {
747    $debug_output = $scope . '('.$db->db_index.'): ';
748    $debug_output .= $message . $db->getOption('log_line_break');
749    write_log('sql', $debug_output);
750  }
751}
Note: See TracBrowser for help on using the repository browser.