initial import from onelab svn codebase
[plewww.git] / includes / database.inc
1 <?php
2 // $Id: database.inc 1119 2008-01-04 11:08:59Z thierry $
3
4 /**
5  * @file
6  * Wrapper for database interface code.
7  */
8
9 /**
10  * @defgroup database Database abstraction layer
11  * @{
12  * Allow the use of different database servers using the same code base.
13  *
14  * Drupal provides a slim database abstraction layer to provide developers with
15  * the ability to support multiple database servers easily. The intent of this
16  * layer is to preserve the syntax and power of SQL as much as possible, while
17  * letting Drupal control the pieces of queries that need to be written
18  * differently for different servers and provide basic security checks.
19  *
20  * Most Drupal database queries are performed by a call to db_query() or
21  * db_query_range(). Module authors should also consider using pager_query() for
22  * queries that return results that need to be presented on multiple pages, and
23  * tablesort_sql() for generating appropriate queries for sortable tables.
24  *
25  * For example, one might wish to return a list of the most recent 10 nodes
26  * authored by a given user. Instead of directly issuing the SQL query
27  * @code
28  *   SELECT n.title, n.body, n.created FROM node n WHERE n.uid = $uid LIMIT 0, 10;
29  * @endcode
30  * one would instead call the Drupal functions:
31  * @code
32  *   $result = db_query_range('SELECT n.title, n.body, n.created
33  *     FROM {node} n WHERE n.uid = %d', $uid, 0, 10);
34  *   while ($node = db_fetch_object($result)) {
35  *     // Perform operations on $node->body, etc. here.
36  *   }
37  * @endcode
38  * Curly braces are used around "node" to provide table prefixing via
39  * db_prefix_tables(). The explicit use of a user ID is pulled out into an
40  * argument passed to db_query() so that SQL injection attacks from user input
41  * can be caught and nullified. The LIMIT syntax varies between database servers,
42  * so that is abstracted into db_query_range() arguments. Finally, note the
43  * common pattern of iterating over the result set using db_fetch_object().
44  */
45
46 /**
47  * Append a database prefix to all tables in a query.
48  *
49  * Queries sent to Drupal should wrap all table names in curly brackets. This
50  * function searches for this syntax and adds Drupal's table prefix to all
51  * tables, allowing Drupal to coexist with other systems in the same database if
52  * necessary.
53  *
54  * @param $sql
55  *   A string containing a partial or entire SQL query.
56  * @return
57  *   The properly-prefixed string.
58  */
59 function db_prefix_tables($sql) {
60   global $db_prefix;
61
62   if (is_array($db_prefix)) {
63     if (array_key_exists('default', $db_prefix)) {
64       $tmp = $db_prefix;
65       unset($tmp['default']);
66       foreach ($tmp as $key => $val) {
67         $sql = strtr($sql, array('{'. $key. '}' => $val. $key));
68       }
69       return strtr($sql, array('{' => $db_prefix['default'], '}' => ''));
70     }
71     else {
72       foreach ($db_prefix as $key => $val) {
73         $sql = strtr($sql, array('{'. $key. '}' => $val. $key));
74       }
75       return strtr($sql, array('{' => '', '}' => ''));
76     }
77   }
78   else {
79     return strtr($sql, array('{' => $db_prefix, '}' => ''));
80   }
81 }
82
83 /**
84  * Fixed parse_url() function.
85  *
86  * The builtin parse_url() cannot handle passwords with @ in them.
87  */
88 function db_parse_url($url, $component = NULL) {
89   // scheme://user:pass@host:port/path?query#fragment
90
91   $pattern = '(.*)://'; // scheme (before ://)
92   $pattern .= '((.*)?:(.*)?@)?'; // user:pass@ (optional, before @, separated by :)
93   $pattern .= '([^:]*)'; // host (until :)
94   $pattern .= '(:(.*))?'; // port (optional, after :)
95   $pattern .= '(/[^?]*)'; // path (after and including /, until ?)
96   $pattern .= '(\?([^#]*))?'; // query (optional, after ?, until #)
97   $pattern .= '(#(.*))?'; // fragment (optional, after #)
98
99   preg_match('|' . $pattern . '|', $url, $matches);
100
101   if ($matches) {
102     $url = array();
103
104     list($full_match,
105          $url['scheme'],
106          $user_pass, $url['user'], $url['pass'],
107          $url['host'],
108          $optional_port, $url['port'],
109          $url['path']) = $matches;
110
111     if (count($matches) > 9)
112       list($optional_query, $url['query']) = array_slice($matches, 9);
113
114     if (count($matches) > 10)
115       list($optional_fragment, $url['fragment']) = array_slice($matches, 10);
116
117     /* thierry : on fc6 an empty component matches PHP_URL_SCHEME=0 ! */
118     if ($component) {
119      if (defined('PHP_URL_SCHEME')) {
120       switch ($component) {
121       case PHP_URL_SCHEME: return $url['scheme'];
122       case PHP_URL_HOST: return $url['host'];
123       case PHP_URL_PORT: return $url['port'];
124       case PHP_URL_USER: return $url['user'];
125       case PHP_URL_PASS: return $url['pass'];
126       case PHP_URL_PATH: return $url['path'];
127       case PHP_URL_QUERY: return $url['query'];
128       case PHP_URL_FRAGMENT: return $url['fragment'];
129       }
130      }
131     }
132
133     # Remove unmatched fields
134     $url = array_filter($url, 'strlen');
135
136     return $url;
137   }
138
139   return FALSE;
140 }
141
142 /**
143  * Activate a database for future queries.
144  *
145  * If it is necessary to use external databases in a project, this function can
146  * be used to change where database queries are sent. If the database has not
147  * yet been used, it is initialized using the URL specified for that name in
148  * Drupal's configuration file. If this name is not defined, a duplicate of the
149  * default connection is made instead.
150  *
151  * Be sure to change the connection back to the default when done with custom
152  * code.
153  *
154  * @param $name
155  *   The name assigned to the newly active database connection. If omitted, the
156  *   default connection will be made active.
157  *
158  * @return the name of the previously active database or FALSE if non was found.
159  */
160 function db_set_active($name = 'default') {
161   global $db_url, $db_type, $active_db;
162   static $db_conns;
163
164   if (!isset($db_conns[$name])) {
165     // Initiate a new connection, using the named DB URL specified.
166     if (is_array($db_url)) {
167       $connect_url = array_key_exists($name, $db_url) ? $db_url[$name] : $db_url['default'];
168     }
169     else {
170       $connect_url = $db_url;
171     }
172
173     $db_type = substr($connect_url, 0, strpos($connect_url, '://'));
174     $handler = "./includes/database.$db_type.inc";
175
176     if (is_file($handler)) {
177       include_once $handler;
178     }
179     else {
180       drupal_maintenance_theme();
181       drupal_set_title('Unsupported database type');
182       print theme('maintenance_page', '<p>The database type '. theme('placeholder', $db_type) .' is unsupported. Please use either <var>mysql</var> for MySQL 3.x &amp; 4.0.x databases, <var>mysqli</var> for MySQL 4.1.x+ databases, or <var>pgsql</var> for PostgreSQL databases. The database information is in your <code>settings.php</code> file.</p>
183 <p>For more help, see the <a href="http://drupal.org/node/258">Installation and upgrading handbook</a>. If you are unsure what these terms mean you should probably contact your hosting provider.</p>');
184       exit;
185     }
186
187     $db_conns[$name] = db_connect($connect_url);
188   }
189
190   $previous_db = $active_db;
191   // Set the active connection.
192   $active_db = $db_conns[$name];
193
194   return array_search($previous_db, $db_conns);
195 }
196
197 /**
198  * Helper function for db_query().
199  */
200 function _db_query_callback($match, $init = FALSE) {
201   static $args = NULL;
202   if ($init) {
203     $args = $match;
204     return;
205   }
206
207   switch ($match[1]) {
208     case '%d': // We must use type casting to int to convert false/null/(true?)
209       return (int) array_shift($args); // We don't need db_escape_string as numbers are db-safe
210     case '%s':
211       return db_escape_string(array_shift($args));
212     case '%%':
213       return '%';
214     case '%f':
215       return (float) array_shift($args);
216     case '%b': // binary data
217       return db_encode_blob(array_shift($args));
218   }
219 }
220
221 define('DB_QUERY_REGEXP', '/(%d|%s|%%|%f|%b)/');
222
223 /**
224  * Runs a basic query in the active database.
225  *
226  * User-supplied arguments to the query should be passed in as separate
227  * parameters so that they can be properly escaped to avoid SQL injection
228  * attacks.
229  *
230  * @param $query
231  *   A string containing an SQL query.
232  * @param ...
233  *   A variable number of arguments which are substituted into the query
234  *   using printf() syntax. Instead of a variable number of query arguments,
235  *   you may also pass a single array containing the query arguments.
236
237  *   Valid %-modifiers are: %s, %d, %f, %b (binary data, do not enclose
238  *   in '') and %%.
239  *
240  *   NOTE: using this syntax will cast NULL and FALSE values to decimal 0,
241  *   and TRUE values to decimal 1.
242  *
243  * @return
244  *   A database query result resource, or FALSE if the query was not
245  *   executed correctly.
246  */
247 function db_query($query) {
248   $args = func_get_args();
249   array_shift($args);
250   $query = db_prefix_tables($query);
251   if (isset($args[0]) and is_array($args[0])) { // 'All arguments in one array' syntax
252     $args = $args[0];
253   }
254   _db_query_callback($args, TRUE);
255   $query = preg_replace_callback(DB_QUERY_REGEXP, '_db_query_callback', $query);
256   return _db_query($query);
257 }
258
259 /**
260  * Debugging version of db_query().
261  *
262  * Echoes the query to the browser.
263  */
264 function db_queryd($query) {
265   $args = func_get_args();
266   array_shift($args);
267   $query = db_prefix_tables($query);
268   if (isset($args[0]) and is_array($args[0])) { // 'All arguments in one array' syntax
269     $args = $args[0];
270   }
271   _db_query_callback($args, TRUE);
272   $query = preg_replace_callback(DB_QUERY_REGEXP, '_db_query_callback', $query);
273   return _db_query($query, 1);
274 }
275
276 /**
277  * Helper function for db_rewrite_sql.
278  *
279  * Collects JOIN and WHERE statements via hook_sql.
280  * Decides whether to select primary_key or DISTINCT(primary_key)
281  *
282  * @param $query
283  *   Query to be rewritten.
284  * @param $primary_table
285  *   Name or alias of the table which has the primary key field for this query. Possible values are: comments, forum, node, menu, term_data, vocabulary.
286  * @param $primary_field
287  *   Name of the primary field.
288  * @param $args
289  *   Array of additional arguments.
290  * @return
291  *   An array: join statements, where statements, field or DISTINCT(field).
292  */
293 function _db_rewrite_sql($query = '', $primary_table = 'n', $primary_field = 'nid', $args = array()) {
294   $where = array();
295   $join = array();
296   $distinct = FALSE;
297   foreach (module_implements('db_rewrite_sql') as $module) {
298     $result = module_invoke($module, 'db_rewrite_sql', $query, $primary_table, $primary_field, $args);
299     if (isset($result) && is_array($result)) {
300       if (isset($result['where'])) {
301         $where[] = $result['where'];
302       }
303       if (isset($result['join'])) {
304         $join[] = $result['join'];
305       }
306       if (isset($result['distinct']) && $result['distinct']) {
307         $distinct = TRUE;
308       }
309     }
310     elseif (isset($result)) {
311       $where[] = $result;
312     }
313   }
314
315   $where = empty($where) ? '' : '('. implode(') AND (', $where) .')';
316   $join = empty($join) ? '' : implode(' ', $join);
317
318   return array($join, $where, $distinct);
319 }
320
321 /**
322  * Rewrites node, taxonomy and comment queries. Use it for listing queries. Do not
323  * use FROM table1, table2 syntax, use JOIN instead.
324  *
325  * @param $query
326  *   Query to be rewritten.
327  * @param $primary_table
328  *   Name or alias of the table which has the primary key field for this query. Possible values are: comments, forum, node, menu, term_data, vocabulary.
329  * @param $primary_field
330  *   Name of the primary field.
331  * @param $args
332  *   An array of arguments, passed to the implementations of hook_db_rewrite_sql.
333  * @return
334  *   The original query with JOIN and WHERE statements inserted from hook_db_rewrite_sql implementations. nid is rewritten if needed.
335  */
336 function db_rewrite_sql($query, $primary_table = 'n', $primary_field = 'nid',  $args = array()) {
337   list($join, $where, $distinct) = _db_rewrite_sql($query, $primary_table, $primary_field, $args);
338
339   if ($distinct) {
340     $field_to_select = 'DISTINCT('. $primary_table .'.'. $primary_field .')';
341     // (?<!text) is a negative look-behind (no need to rewrite queries that already use DISTINCT).
342     $query = preg_replace('/(SELECT.*)('. $primary_table .'\.)?(?<!DISTINCT\()(?<!DISTINCT\('. $primary_table .'\.)'. $primary_field .'(.*FROM)/AUsi', '\1'. $field_to_select .'\3', $query);
343   }
344
345   if (!empty($where) || !empty($join)) {
346     if (!empty($where)) {
347       $new = " WHERE $where ";
348     }
349     $new = " $join $new";
350     if (strpos($query, 'WHERE')) {
351       $replace = 'WHERE';
352       $add = 'AND';
353     }
354     elseif (strpos($query, 'GROUP')) {
355       $replace = 'GROUP';
356       $add = 'GROUP';
357     }
358     elseif (strpos($query, 'ORDER')) {
359       $replace = 'ORDER';
360       $add = 'ORDER';
361     }
362     elseif (strpos($query, 'LIMIT')) {
363       $replace = 'LIMIT';
364       $add = 'LIMIT';
365     }
366     else {
367       $query .= $new;
368     }
369     if (isset($replace)) {
370       $query = str_replace($replace, "$new $add ", $query);
371     }
372   }
373
374   return $query;
375 }
376
377 /**
378  * Restrict a dynamic tablename to safe characters.
379  *
380  * Only keeps alphanumeric and underscores.
381  */
382 function db_escape_table($string) {
383   return preg_replace('/[^A-Za-z0-9_]+/', '', $string);
384 }
385
386 /**
387  * @} End of "defgroup database".
388  */
389
390