source: subversion/trunk/roundcubemail/program/include/rcube_db.inc @ 164

Last change on this file since 164 was 164, checked in by roundcube, 7 years ago

Improved error handling in DB connection failure

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
File size: 14.1 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_db.inc                                          |
6 |                                                                       |
7 | This file is part of the RoundCube Webmail client                     |
8 | Copyright (C) 2005, RoundCube Dev. - Switzerland                      |
9 | Licensed under the GNU GPL                                            |
10 |                                                                       |
11 | PURPOSE:                                                              |
12 |   PEAR:DB wrapper class that implements PEAR DB functions             |
13 |   See http://pear.php.net/package/DB                                  |
14 |                                                                       |
15 +-----------------------------------------------------------------------+
16 | Author: David Saez Padros <david@ols.es>                              |
17 +-----------------------------------------------------------------------+
18
19 $Id$
20
21*/
22
23
24/**
25 * Obtain the PEAR::DB class that is used for abstraction
26 */
27require_once('DB.php');
28
29
30/**
31 * Database independent query interface
32 *
33 * This is a wrapper for the PEAR::DB class
34 *
35 * @package    RoundCube Webmail
36 * @author     David Saez Padros <david@ols.es>
37 * @author     Thomas Bruederli <roundcube@gmail.com>
38 * @version    1.17
39 * @link       http://pear.php.net/package/DB
40 */
41class rcube_db
42  {
43  var $db_dsnw;               // DSN for write operations
44  var $db_dsnr;               // DSN for read operations
45  var $db_connected = false;  // Already connected ?
46  var $db_mode = '';          // Connection mode
47  var $db_handle = 0;         // Connection handle
48  var $db_pconn = false;      // Use persistent connections
49  var $db_error = false;
50  var $db_error_msg = '';
51
52  var $a_query_results = array('dummy');
53  var $last_res_id = 0;
54
55
56  /**
57   * Object constructor
58   *
59   * @param  string  DSN for read/write operations
60   * @param  string  Optional DSN for read only operations
61   */
62  function __construct($db_dsnw, $db_dsnr='', $pconn=false)
63    {
64    if ($db_dsnr=='')
65      $db_dsnr=$db_dsnw;
66       
67    $this->db_dsnw = $db_dsnw;
68    $this->db_dsnr = $db_dsnr;
69    $this->db_pconn = $pconn;
70       
71    $dsn_array = DB::parseDSN($db_dsnw);
72    $this->db_provider = $dsn_array['phptype'];       
73    }
74
75
76  /**
77   * PHP 4 object constructor
78   *
79   * @see  rcube_db::__construct
80   */
81  function rcube_db($db_dsnw, $db_dsnr='', $pconn=false)
82    {
83    $this->__construct($db_dsnw, $db_dsnr);
84    }
85
86
87  /**
88   * Connect to specific database
89   *
90   * @param  string  DSN for DB connections
91   * @return object  PEAR database handle
92   * @access private
93   */
94  function dsn_connect($dsn)
95    {
96    // Use persistent connections if available
97    $dbh = DB::connect($dsn, array('persistent' => $this->db_pconn));
98       
99    if (DB::isError($dbh))
100      {
101      $this->db_error = TRUE;
102      $this->db_error_msg = $dbh->getMessage();
103
104      raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
105                        'message' => $this->db_error_msg), TRUE, FALSE);
106                       
107      return FALSE;
108      }
109
110    else if ($this->db_provider=='sqlite')
111      {
112      $dsn_array = DB::parseDSN($dsn);
113      if (!filesize($dsn_array['database']) && !empty($this->sqlite_initials))
114        $this->_sqlite_create_database($dbh, $this->sqlite_initials);
115      }
116       
117    return $dbh;
118    }
119
120
121  /**
122   * Connect to appropiate databse
123   * depending on the operation
124   *
125   * @param  string  Connection mode (r|w)
126   * @access public
127   */
128  function db_connect($mode)
129    {
130    $this->db_mode = $mode;
131
132    // Already connected
133    if ($this->db_connected)
134      {
135      // no replication, current connection is ok
136      if ($this->db_dsnw==$this->db_dsnr)
137        return;
138           
139      // connected to master, current connection is ok
140      if ($this->db_mode=='w')
141        return;
142
143      // Same mode, current connection is ok
144      if ($this->db_mode==$mode)
145        return;
146      }
147     
148    if ($mode=='r')
149      $dsn = $this->db_dsnr;
150    else
151      $dsn = $this->db_dsnw;
152
153    $this->db_handle = $this->dsn_connect($dsn);
154    $this->db_connected = $this->db_handle ? TRUE : FALSE;
155    }
156   
157   
158  /**
159   * Getter for error state
160   *
161   * @param  boolean  True on error
162   */
163  function is_error()
164    {
165    return $this->db_error ? $this->db_error_msg : FALSE;
166    }
167
168
169  /**
170   * Execute a SQL query
171   *
172   * @param  string  SQL query to execute
173   * @param  mixed   Values to be inserted in query
174   * @return number  Query handle identifier
175   * @access public
176   */
177  function query()
178    {
179    $params = func_get_args();
180    $query = array_shift($params);
181
182    return $this->_query($query, 0, 0, $params);
183    }
184
185
186  /**
187   * Execute a SQL query with limits
188   *
189   * @param  string  SQL query to execute
190   * @param  number  Offset for LIMIT statement
191   * @param  number  Number of rows for LIMIT statement
192   * @param  mixed   Values to be inserted in query
193   * @return number  Query handle identifier
194   * @access public
195   */
196  function limitquery()
197    {
198    $params = func_get_args();
199    $query = array_shift($params);
200    $offset = array_shift($params);
201    $numrows = array_shift($params);
202               
203    return $this->_query($query, $offset, $numrows, $params);
204    }
205
206
207  /**
208   * Execute a SQL query with limits
209   *
210   * @param  string  SQL query to execute
211   * @param  number  Offset for LIMIT statement
212   * @param  number  Number of rows for LIMIT statement
213   * @param  array   Values to be inserted in query
214   * @return number  Query handle identifier
215   * @access private
216   */
217  function _query($query, $offset, $numrows, $params)
218    {
219    // Read or write ?
220    if (strtolower(trim(substr($query,0,6)))=='select')
221      $mode='r';
222    else
223      $mode='w';
224       
225    $this->db_connect($mode);
226   
227    if (!$this->db_connected)
228      return FALSE;
229
230    if ($this->db_provider == 'sqlite')
231      $this->_sqlite_prepare();
232
233    if ($numrows || $offset)
234      $result = $this->db_handle->limitQuery($query,$offset,$numrows,$params);
235    else   
236      $result = $this->db_handle->query($query, $params);
237       
238    // add result, even if it's an error
239    return $this->_add_result($result);
240    }
241
242
243  /**
244   * Get number of rows for a SQL query
245   * If no query handle is specified, the last query will be taken as reference
246   *
247   * @param  number  Optional query handle identifier
248   * @return mixed   Number of rows or FALSE on failure
249   * @access public
250   */
251  function num_rows($res_id=NULL)
252    {
253    if (!$this->db_handle)
254      return FALSE;
255
256    if ($result = $this->_get_result($res_id))   
257      return $result->numRows();
258    else
259      return FALSE;
260    }
261
262
263  /**
264   * Get number of affected rows fort he last query
265   *
266   * @return mixed   Number of rows or FALSE on failure
267   * @access public
268   */
269  function affected_rows()
270    {
271    if (!$this->db_handle)
272      return FALSE;
273
274    return $this->db_handle->affectedRows();
275    }
276
277
278  /**
279   * Get last inserted record ID
280   * For Postgres databases, a sequence name is required
281   *
282   * @param  string  Sequence name for increment
283   * @return mixed   ID or FALSE on failure
284   * @access public
285   */
286  function insert_id($sequence = '')
287    {
288    if (!$this->db_handle || $this->db_mode=='r')
289      return FALSE;
290
291    switch($this->db_provider)
292      {
293      case 'pgsql':
294        // PostgreSQL uses sequences
295        $result = &$this->db_handle->getOne("SELECT CURRVAL('$sequence')");
296        if (DB::isError($result))
297          {
298          raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
299                            'message' => $result->getMessage()), TRUE, FALSE);
300          }
301
302        return $result;
303               
304      case 'mysql': // This is unfortuneate
305        return mysql_insert_id($this->db_handle->connection);
306
307      case 'mysqli':
308        return mysqli_insert_id($this->db_handle->connection);
309       
310      case 'sqlite':
311        return sqlite_last_insert_rowid($this->db_handle->connection);
312
313      default:
314        die("portability issue with this database, please have the developer fix");
315      }
316    }
317
318
319  /**
320   * Get an associative array for one row
321   * If no query handle is specified, the last query will be taken as reference
322   *
323   * @param  number  Optional query handle identifier
324   * @return mixed   Array with col values or FALSE on failure
325   * @access public
326   */
327  function fetch_assoc($res_id=NULL)
328    {
329    $result = $this->_get_result($res_id);
330    return $this->_fetch_row($result, DB_FETCHMODE_ASSOC);
331    }
332
333
334  /**
335   * Get an index array for one row
336   * If no query handle is specified, the last query will be taken as reference
337   *
338   * @param  number  Optional query handle identifier
339   * @return mixed   Array with col values or FALSE on failure
340   * @access public
341   */
342  function fetch_array($res_id=NULL)
343    {
344    $result = $this->_get_result($res_id);
345    return $this->_fetch_row($result, DB_FETCHMODE_ORDERED);
346    }
347
348
349  /**
350   * Get co values for a result row
351   *
352   * @param  object  Query result handle
353   * @param  number  Fetch mode identifier
354   * @return mixed   Array with col values or FALSE on failure
355   * @access private
356   */
357  function _fetch_row($result, $mode)
358    {
359    if (DB::isError($result))
360      {
361      raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
362                        'message' => $this->db_link->getMessage()), TRUE, FALSE);
363      return FALSE;
364      }
365                         
366    return $result->fetchRow($mode);
367    }
368   
369
370  /**
371   * Formats input so it can be safely used in a query
372   *
373   * @param  mixed   Value to quote
374   * @return string  Quoted/converted string for use in query
375   * @access public
376   */
377  function quote($input)
378    {
379    // create DB handle if not available
380    if (!$this->db_handle)
381      $this->db_connect('r');
382     
383    // escape pear identifier chars
384    $rep_chars = array('?' => '\?',
385                       '!' => '\!',
386                       '&' => '\&');
387     
388    return $this->db_handle->quoteSmart(strtr($input, $rep_chars));
389    }
390   
391
392  /**
393   * Quotes a string so it can be safely used as a table or column name
394   *
395   * @param  string  Value to quote
396   * @return string  Quoted string for use in query
397   * @deprecated     Replaced by rcube_db::quote_identifier
398   * @see            rcube_db::quote_identifier
399   * @access public
400   */
401  function quoteIdentifier($str)
402        {
403    return $this->quote_identifier($str);
404        }
405
406
407  /**
408   * Quotes a string so it can be safely used as a table or column name
409   *
410   * @param  string  Value to quote
411   * @return string  Quoted string for use in query
412   * @access public
413   */
414  function quote_identifier($str)
415    {
416    if (!$this->db_handle)
417      $this->db_connect('r');
418                       
419    return $this->db_handle->quoteIdentifier($str);
420    }
421
422
423  /**
424   * Return SQL statement to convert a field value into a unix timestamp
425   *
426   * @param  string  Field name
427   * @return string  SQL statement to use in query
428   * @access public
429   */
430  function unixtimestamp($field)
431    {
432    switch($this->db_provider)
433      {
434      case 'pgsql':
435        return "EXTRACT (EPOCH FROM $field)";
436        break;
437
438      default:
439        return "UNIX_TIMESTAMP($field)";
440      }
441    }
442
443
444  /**
445   * Return SQL statement to convert from a unix timestamp
446   *
447   * @param  string  Field name
448   * @return string  SQL statement to use in query
449   * @access public
450   */
451  function fromunixtime($timestamp)
452    {
453    switch($this->db_provider)
454      {
455      case 'mysqli':
456      case 'mysql':
457      case 'sqlite':
458        return "FROM_UNIXTIME($timestamp)";
459
460      default:
461        return date("'Y-m-d H:i:s'", $timestamp);
462      }
463    }
464
465
466  /**
467   * Adds a query result and returns a handle ID
468   *
469   * @param  object  Query handle
470   * @return mixed   Handle ID or FALE on failure
471   * @access private
472   */
473  function _add_result($res)
474    {
475    // sql error occured
476    if (DB::isError($res))
477      {
478      raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
479                        'message' => $res->getMessage() . " Query: " . substr(preg_replace('/[\r\n]+\s*/', ' ', $res->userinfo), 0, 512)), TRUE, FALSE);
480      return FALSE;
481      }
482    else
483      {
484      $res_id = sizeof($this->a_query_results);
485      $this->a_query_results[$res_id] = $res;
486      $this->last_res_id = $res_id;
487      return $res_id;
488      }
489    }
490
491
492  /**
493   * Resolves a given handle ID and returns the according query handle
494   * If no ID is specified, the last ressource handle will be returned
495   *
496   * @param  number  Handle ID
497   * @return mixed   Ressource handle or FALE on failure
498   * @access private
499   */
500  function _get_result($res_id=NULL)
501    {
502    if ($res_id==NULL)
503      $res_id = $this->last_res_id;
504   
505     if ($res_id && isset($this->a_query_results[$res_id]))
506       return $this->a_query_results[$res_id];
507     else
508       return FALSE;
509    }
510
511
512  /**
513   * Create a sqlite database from a file
514   *
515   * @param  object  SQLite database handle
516   * @param  string  File path to use for DB creation
517   * @access private
518   */
519  function _sqlite_create_database($dbh, $file_name)
520    {
521    if (empty($file_name) || !is_string($file_name))
522      return;
523
524    $data = '';
525    if ($fd = fopen($file_name, 'r'))
526      {
527      $data = fread($fd, filesize($file_name));
528      fclose($fd);
529      }
530
531    if (strlen($data))
532      sqlite_exec($dbh->connection, $data);
533    }
534
535
536  /**
537   * Add some proprietary database functions to the current SQLite handle
538   * in order to make it MySQL compatible
539   *
540   * @access private
541   */
542  function _sqlite_prepare()
543    {
544    include_once('include/rcube_sqlite.inc');
545
546    // we emulate via callback some missing MySQL function
547    sqlite_create_function($this->db_handle->connection, "from_unixtime", "rcube_sqlite_from_unixtime");
548    sqlite_create_function($this->db_handle->connection, "unix_timestamp", "rcube_sqlite_unix_timestamp");
549    sqlite_create_function($this->db_handle->connection, "now", "rcube_sqlite_now");
550    sqlite_create_function($this->db_handle->connection, "md5", "rcube_sqlite_md5");   
551    }
552
553
554  }  // end class rcube_db
555
556?>
Note: See TracBrowser for help on using the repository browser.