]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend/PDO.php
Add comments
[SourceForge/phpwiki.git] / lib / WikiDB / backend / PDO.php
1 <?php
2
3 /*
4  * Copyright 2005 $ThePhpWikiProgrammingTeam
5  *
6  * This file is part of PhpWiki.
7  *
8  * PhpWiki is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * PhpWiki is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License along
19  * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
20  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21  */
22
23 /**
24  * @author: Reini Urban
25  */
26
27 require_once 'lib/WikiDB/backend.php';
28
29 class WikiDB_backend_PDO
30     extends WikiDB_backend
31 {
32
33     function __construct($dbparams)
34     {
35         $this->_dbparams = $dbparams;
36         if (strstr($dbparams['dsn'], "://")) { // pear DB syntax
37             $parsed = parseDSN($dbparams['dsn']);
38             $this->_parsedDSN = $parsed;
39             /* Need to convert the DSN
40              *    dbtype(dbsyntax)://username:password@protocol+hostspec/database?option=value&option2=value2
41              * to dbtype:option1=value1;option2=value2
42              * PDO passes the DSN verbatim to the driver. But we need to extract the username + password
43              * and cannot rely that the parseDSN keys have the same name as needed by the driver.
44              * e.g: odbc:DSN=SAMPLE;UID=db2inst1;PWD=ibmdb2, mysql:host=127.0.0.1;dbname=testdb
45              */
46             $driver = $parsed['phptype'];
47             unset($parsed['phptype']);
48             unset($parsed['dbsyntax']);
49             $dbparams['dsn'] = $driver . ":";
50             $this->_dbh->database = $parsed['database'];
51             // mysql needs to map database=>dbname, hostspec=>host. TODO for the others.
52             $dsnmap = array('mysql' => array('database' => 'dbname', 'hostspec' => 'host')
53             );
54             foreach (array('protocol', 'hostspec', 'port', 'socket', 'database') as $option) {
55                 if (!empty($parsed[$option])) {
56                     $optionname = (isset($dsnmap[$driver][$option]) and !isset($parsed[$optionname]))
57                         ? $dsnmap[$driver][$option]
58                         : $option;
59                     $dbparams['dsn'] .= ($optionname . "=" . $parsed[$option] . ";");
60                     unset($parsed[$optionname]);
61                 }
62                 unset($parsed[$option]);
63             }
64             unset($parsed['username']);
65             unset($parsed['password']);
66             // pass the rest verbatim to the driver.
67             foreach ($parsed as $option => $value) {
68                 $dbparams['dsn'] .= ($option . "=" . $value . ";");
69             }
70         } else {
71             list($driver, $dsn) = explode(":", $dbparams['dsn'], 2);
72             foreach (explode(";", trim($dsn)) as $pair) {
73                 if ($pair) {
74                     list($option, $value) = explode("=", $pair, 2);
75                     $this->_parsedDSN[$option] = $value;
76                 }
77             }
78             $this->_dbh->database = isset($this->_parsedDSN['database'])
79                 ? $this->_parsedDSN['database']
80                 : $this->_parsedDSN['dbname'];
81         }
82         if (empty($this->_parsedDSN['password'])) $this->_parsedDSN['password'] = '';
83
84         try {
85             // try to load it dynamically (unix only)
86             if (!loadPhpExtension("pdo_$driver")) {
87                 echo $GLOBALS['php_errormsg'], "<br>\n";
88                 trigger_error(sprintf("dl() problem: Required extension '%s' could not be loaded!",
89                         "pdo_$driver"),
90                     E_USER_WARNING);
91             }
92
93             // persistent is defined as DSN option, or with a config value.
94             //   phptype://username:password@hostspec/database?persistent=false
95             $this->_dbh = new PDO($dbparams['dsn'],
96                 $this->_parsedDSN['username'],
97                 $this->_parsedDSN['password'],
98                 array(PDO::ATTR_AUTOCOMMIT => true,
99                     PDO::ATTR_TIMEOUT => DATABASE_TIMEOUT,
100                     PDO::ATTR_PERSISTENT => !empty($parsed['persistent'])
101                         or DATABASE_PERSISTENT
102                 ));
103         } catch (PDOException $e) {
104             echo "<br>\nCan't connect to database: %s" . $e->getMessage();
105             if (DEBUG & _DEBUG_VERBOSE or DEBUG & _DEBUG_SQL) {
106                 echo "<br>\nDSN: '", $dbparams['dsn'], "'";
107                 echo "<br>\n_parsedDSN: '", print_r($this->_parsedDSN), "'";
108                 echo "<br>\nparsed: '", print_r($parsed), "'";
109             }
110             if (isset($dbparams['_tryroot_from_upgrade']))
111                 trigger_error(sprintf("Can't connect to database: %s", $e->getMessage()),
112                     E_USER_WARNING);
113             else
114                 exit();
115         }
116         if (DEBUG & _DEBUG_SQL) { // not yet implemented
117             $this->_dbh->debug = true;
118         }
119         $this->_dsn = $dbparams['dsn'];
120         $this->_dbh->databaseType = $driver;
121         $this->_dbh->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);
122         // Use the faster FETCH_NUM, with some special ASSOC based exceptions.
123
124         $this->_hasTransactions = true;
125         try {
126             $this->_dbh->beginTransaction();
127         } catch (PDOException $e) {
128             $this->_hasTransactions = false;
129         }
130         $sth = $this->_dbh->prepare("SELECT version()");
131         $sth->execute();
132         $this->_serverinfo['version'] = $sth->fetchColumn();
133         $this->commit(); // required to match the try catch block above!
134
135         $prefix = isset($dbparams['prefix']) ? $dbparams['prefix'] : '';
136         $this->_table_names
137             = array('page_tbl' => $prefix . 'page',
138             'version_tbl' => $prefix . 'version',
139             'link_tbl' => $prefix . 'link',
140             'recent_tbl' => $prefix . 'recent',
141             'nonempty_tbl' => $prefix . 'nonempty');
142         $page_tbl = $this->_table_names['page_tbl'];
143         $version_tbl = $this->_table_names['version_tbl'];
144         $this->page_tbl_fields = "$page_tbl.id AS id, $page_tbl.pagename AS pagename, "
145             . "$page_tbl.hits hits";
146         $this->page_tbl_field_list = array('id', 'pagename', 'hits');
147         $this->version_tbl_fields = "$version_tbl.version AS version, "
148             . "$version_tbl.mtime AS mtime, "
149             . "$version_tbl.minor_edit AS minor_edit, $version_tbl.content AS content, "
150             . "$version_tbl.versiondata AS versiondata";
151         $this->version_tbl_field_list = array('version', 'mtime', 'minor_edit', 'content',
152             'versiondata');
153
154         $this->_expressions
155             = array('maxmajor' => "MAX(CASE WHEN minor_edit=0 THEN version END)",
156             'maxminor' => "MAX(CASE WHEN minor_edit<>0 THEN version END)",
157             'maxversion' => "MAX(version)",
158             'notempty' => "<>''",
159             'iscontent' => "$version_tbl.content<>''");
160         $this->_lock_count = 0;
161     }
162
163     function beginTransaction()
164     {
165         if ($this->_hasTransactions)
166             $this->_dbh->beginTransaction();
167     }
168
169     function commit()
170     {
171         if ($this->_hasTransactions)
172             $this->_dbh->commit();
173     }
174
175     function rollback()
176     {
177         if ($this->_hasTransactions)
178             $this->_dbh->rollback();
179     }
180
181     /* no result */
182     function query($sql)
183     {
184         $sth = $this->_dbh->prepare($sql);
185         return $sth->execute();
186     }
187
188     /* with one result row */
189     function getRow($sql)
190     {
191         $sth = $this->_dbh->prepare($sql);
192         if ($sth->execute())
193             return $sth->fetch(PDO::FETCH_BOTH);
194         else
195             return false;
196     }
197
198     /**
199      * Close database connection.
200      */
201     function close()
202     {
203         if (!$this->_dbh)
204             return;
205         if ($this->_lock_count) {
206             trigger_error("WARNING: database still locked " .
207                     '(lock_count = $this->_lock_count)' . "\n<br />",
208                 E_USER_WARNING);
209         }
210         $this->unlock(array(), 'force');
211
212         unset($this->_dbh);
213     }
214
215     /*
216      * Fast test for wikipage.
217      */
218     function is_wiki_page($pagename)
219     {
220         $dbh = &$this->_dbh;
221         extract($this->_table_names);
222         $sth = $dbh->prepare("SELECT $page_tbl.id AS id"
223             . " FROM $nonempty_tbl, $page_tbl"
224             . " WHERE $nonempty_tbl.id=$page_tbl.id"
225             . "   AND pagename=?");
226         $sth->bindParam(1, $pagename, PDO::PARAM_STR, 100);
227         if ($sth->execute())
228             return $sth->fetchColumn();
229         else
230             return false;
231     }
232
233     function get_all_pagenames()
234     {
235         $dbh = &$this->_dbh;
236         extract($this->_table_names);
237         $sth = $dbh->exec("SELECT pagename"
238             . " FROM $nonempty_tbl, $page_tbl"
239             . " WHERE $nonempty_tbl.id=$page_tbl.id"
240             . " LIMIT 1");
241         return $sth->fetchAll(PDO::FETCH_NUM);
242     }
243
244     /*
245      * filter (nonempty pages) currently ignored
246      */
247     function numPages($filter = false, $exclude = '')
248     {
249         $dbh = &$this->_dbh;
250         extract($this->_table_names);
251         $sth = $dbh->exec("SELECT count(*)"
252             . " FROM $nonempty_tbl, $page_tbl"
253             . " WHERE $nonempty_tbl.id=$page_tbl.id");
254         return $sth->fetchColumn();
255     }
256
257     function increaseHitCount($pagename)
258     {
259         $dbh = &$this->_dbh;
260         $page_tbl = $this->_table_names['page_tbl'];
261         $sth = $dbh->prepare("UPDATE $page_tbl SET hits=hits+1"
262             . " WHERE pagename=?"
263             . " LIMIT 1");
264         $sth->bindParam(1, $pagename, PDO::PARAM_STR, 100);
265         $sth->execute();
266     }
267
268     /*
269      * Read page information from database.
270      */
271     function get_pagedata($pagename)
272     {
273         $dbh = &$this->_dbh;
274         $page_tbl = $this->_table_names['page_tbl'];
275         $sth = $dbh->prepare("SELECT id,pagename,hits,pagedata FROM $page_tbl"
276             . " WHERE pagename=? LIMIT 1");
277         $sth->bindParam(1, $pagename, PDO::PARAM_STR, 100);
278         $sth->execute();
279         $row = $sth->fetch(PDO::FETCH_NUM);
280         return $row ? $this->_extract_page_data($row[3], $row[2]) : false;
281     }
282
283     function  _extract_page_data($data, $hits)
284     {
285         if (empty($data))
286             return array('hits' => $hits);
287         else
288             return array_merge(array('hits' => $hits), $this->_unserialize($data));
289     }
290
291     function update_pagedata($pagename, $newdata)
292     {
293         $dbh = &$this->_dbh;
294         $page_tbl = $this->_table_names['page_tbl'];
295
296         // Hits is the only thing we can update in a fast manner.
297         if (count($newdata) == 1 && isset($newdata['hits'])) {
298             // Note that this will fail silently if the page does not
299             // have a record in the page table.  Since it's just the
300             // hit count, who cares?
301             $sth = $dbh->prepare("UPDATE $page_tbl SET hits=? WHERE pagename=? LIMIT 1");
302             $sth->bindParam(1, $newdata['hits'], PDO::PARAM_INT);
303             $sth->bindParam(2, $pagename, PDO::PARAM_STR, 100);
304             $sth->execute();
305             return;
306         }
307         $this->beginTransaction();
308         $data = $this->get_pagedata($pagename);
309         if (!$data) {
310             $data = array();
311             $this->_get_pageid($pagename, true); // Creates page record
312         }
313
314         $hits = (empty($data['hits'])) ? 0 : (int)$data['hits'];
315         unset($data['hits']);
316
317         foreach ($newdata as $key => $val) {
318             if ($key == 'hits')
319                 $hits = (int)$val;
320             else if (empty($val))
321                 unset($data[$key]);
322             else
323                 $data[$key] = $val;
324         }
325         $sth = $dbh->prepare("UPDATE $page_tbl"
326             . " SET hits=?, pagedata=?"
327             . " WHERE pagename=?"
328             . " LIMIT 1");
329         $sth->bindParam(1, $hits, PDO::PARAM_INT);
330         $sth->bindParam(2, $this->_serialize($data), PDO::PARAM_LOB);
331         $sth->bindParam(3, $pagename, PDO::PARAM_STR, 100);
332         if ($sth->execute()) {
333             $this->commit();
334             return true;
335         } else {
336             $this->rollBack();
337             return false;
338         }
339     }
340
341     function get_cached_html($pagename)
342     {
343         $dbh = &$this->_dbh;
344         $page_tbl = $this->_table_names['page_tbl'];
345         $sth = $dbh->prepare("SELECT cached_html FROM $page_tbl WHERE pagename=? LIMIT 1");
346         $sth->bindParam(1, $pagename, PDO::PARAM_STR, 100);
347         $sth->execute();
348         return $sth->fetchColumn(PDO::FETCH_NUM);
349     }
350
351     function set_cached_html($pagename, $data)
352     {
353         $dbh = &$this->_dbh;
354         $page_tbl = $this->_table_names['page_tbl'];
355         if (empty($data)) $data = '';
356         $sth = $dbh->prepare("UPDATE $page_tbl"
357             . " SET cached_html=?"
358             . " WHERE pagename=?"
359             . " LIMIT 1");
360         $sth->bindParam(1, $data, PDO::PARAM_STR);
361         $sth->bindParam(2, $pagename, PDO::PARAM_STR, 100);
362         $sth->execute();
363     }
364
365     function _get_pageid($pagename, $create_if_missing = false)
366     {
367         // check id_cache
368         global $request;
369         $cache =& $request->_dbi->_cache->_id_cache;
370         if (isset($cache[$pagename])) {
371             if ($cache[$pagename] or !$create_if_missing) {
372                 return $cache[$pagename];
373             }
374         }
375         // attributes play this game.
376         if ($pagename === '') return 0;
377
378         $dbh = &$this->_dbh;
379         $page_tbl = $this->_table_names['page_tbl'];
380         $sth = $dbh->prepare("SELECT id FROM $page_tbl WHERE pagename=? LIMIT 1");
381         $sth->bindParam(1, $pagename, PDO::PARAM_STR, 100);
382         $id = $sth->fetchColumn();
383         if (!$create_if_missing) {
384             return $id;
385         }
386         if (!$id) {
387             //mysql, mysqli or mysqlt
388             if (substr($dbh->databaseType, 0, 5) == 'mysql') {
389                 // have auto-incrementing, atomic version
390                 $sth = $dbh->prepare("INSERT INTO $page_tbl"
391                     . " (id,pagename)"
392                     . " VALUES (NULL,?)");
393                 $sth->bindParam(1, $pagename, PDO::PARAM_STR, 100);
394                 $sth->execute();
395                 $id = $dbh->lastInsertId();
396             } else {
397                 $this->beginTransaction();
398                 $sth = $dbh->prepare("SELECT MAX(id) FROM $page_tbl");
399                 $sth->execute();
400                 $id = $sth->fetchColumn();
401                 $sth = $dbh->prepare("INSERT INTO $page_tbl"
402                     . " (id,pagename,hits)"
403                     . " VALUES (?,?,0)");
404                 $id++;
405                 $sth->bindParam(1, $id, PDO::PARAM_INT);
406                 $sth->bindParam(2, $pagename, PDO::PARAM_STR, 100);
407                 if ($sth->execute())
408                     $this->commit();
409                 else
410                     $this->rollBack();
411             }
412         }
413         assert($id);
414         return $id;
415     }
416
417     function get_latest_version($pagename)
418     {
419         $dbh = &$this->_dbh;
420         extract($this->_table_names);
421         $sth = $dbh->prepare("SELECT latestversion"
422             . " FROM $page_tbl, $recent_tbl"
423             . " WHERE $page_tbl.id=$recent_tbl.id"
424             . "  AND pagename=?"
425             . " LIMIT 1");
426         $sth->bindParam(1, $pagename, PDO::PARAM_STR, 100);
427         $sth->execute();
428         return $sth->fetchColumn();
429     }
430
431     function get_previous_version($pagename, $version)
432     {
433         $dbh = &$this->_dbh;
434         extract($this->_table_names);
435         $sth = $dbh->prepare("SELECT version"
436             . " FROM $version_tbl, $page_tbl"
437             . " WHERE $version_tbl.id=$page_tbl.id"
438             . "  AND pagename=?"
439             . "  AND version < ?"
440             . " ORDER BY version DESC"
441             . " LIMIT 1");
442         $sth->bindParam(1, $pagename, PDO::PARAM_STR, 100);
443         $sth->bindParam(2, $version, PDO::PARAM_INT);
444         $sth->execute();
445         return $sth->fetchColumn();
446     }
447
448     /**
449      * Get version data.
450      *
451      * @param string $pagename Name of the page
452      * @param int $version Which version to get
453      * @param bool $want_content Do we need content?
454      *
455      * @return array hash The version data, or false if specified version does not
456      *              exist.
457      */
458     function get_versiondata($pagename, $version, $want_content = false)
459     {
460         $dbh = &$this->_dbh;
461         extract($this->_table_names);
462         extract($this->_expressions);
463
464         assert(is_string($pagename) and $pagename != '');
465         assert($version > 0);
466
467         // FIXME: optimization: sometimes don't get page data?
468         if ($want_content) {
469             $fields = $this->page_tbl_fields . ", $page_tbl.pagedata AS pagedata"
470                 . ', ' . $this->version_tbl_fields;
471         } else {
472             $fields = $this->page_tbl_fields . ", '' AS pagedata"
473                 . ", $version_tbl.version AS version, $version_tbl.mtime AS mtime, "
474                 . "$version_tbl.minor_edit AS minor_edit, $iscontent AS have_content, "
475                 . "$version_tbl.versiondata as versiondata";
476         }
477         $sth = $dbh->prepare("SELECT $fields"
478             . " FROM $page_tbl, $version_tbl"
479             . " WHERE $page_tbl.id=$version_tbl.id"
480             . "  AND pagename=?"
481             . "  AND version=?"
482             . " LIMIT 1");
483         $sth->bindParam(1, $pagename, PDO::PARAM_STR, 100);
484         $sth->bindParam(2, $version, PDO::PARAM_INT);
485         $sth->execute();
486         $row = $sth->fetch(PDO::FETCH_NUM);
487         return $row ? $this->_extract_version_data_num($row, $want_content) : false;
488     }
489
490     function _extract_version_data_num($row, $want_content)
491     {
492         if (!$row)
493             return false;
494
495         //$id       &= $row[0];
496         //$pagename &= $row[1];
497         $data = empty($row[8]) ? array() : $this->_unserialize($row[8]);
498         $data['mtime'] = $row[5];
499         $data['is_minor_edit'] = !empty($row[6]);
500         if ($want_content) {
501             $data['%content'] = $row[7];
502         } else {
503             $data['%content'] = !empty($row[7]);
504         }
505         if (!empty($row[3])) {
506             $data['%pagedata'] = $this->_extract_page_data($row[3], $row[2]);
507         }
508         return $data;
509     }
510
511     function _extract_version_data_assoc($row)
512     {
513         if (!$row)
514             return false;
515
516         extract($row);
517         $data = empty($versiondata) ? array() : $this->_unserialize($versiondata);
518         $data['mtime'] = $mtime;
519         $data['is_minor_edit'] = !empty($minor_edit);
520         if (isset($content))
521             $data['%content'] = $content;
522         elseif ($have_content)
523             $data['%content'] = true; else
524             $data['%content'] = '';
525         if (!empty($pagedata)) {
526             $data['%pagedata'] = $this->_extract_page_data($pagedata, $hits);
527         }
528         return $data;
529     }
530
531     /*
532      * Create a new revision of a page.
533      */
534     function set_versiondata($pagename, $version, $data)
535     {
536         $dbh = &$this->_dbh;
537         $version_tbl = $this->_table_names['version_tbl'];
538
539         $minor_edit = (int)!empty($data['is_minor_edit']);
540         unset($data['is_minor_edit']);
541
542         $mtime = (int)$data['mtime'];
543         unset($data['mtime']);
544         assert(!empty($mtime));
545
546         @$content = (string)$data['%content'];
547         unset($data['%content']);
548         unset($data['%pagedata']);
549
550         $this->lock(array('page', 'recent', 'version', 'nonempty'));
551         $this->beginTransaction();
552         $id = $this->_get_pageid($pagename, true);
553         $backend_type = $this->backendType();
554         // optimize: mysql can do this with one REPLACE INTO.
555         if (substr($backend_type, 0, 5) == 'mysql') {
556             $sth = $dbh->prepare("REPLACE INTO $version_tbl"
557                 . " (id,version,mtime,minor_edit,content,versiondata)"
558                 . " VALUES(?,?,?,?,?,?)");
559             $sth->bindParam(1, $id, PDO::PARAM_INT);
560             $sth->bindParam(2, $version, PDO::PARAM_INT);
561             $sth->bindParam(3, $mtime, PDO::PARAM_INT);
562             $sth->bindParam(4, $minor_edit, PDO::PARAM_INT);
563             $sth->bindParam(5, $content, PDO::PARAM_STR, 100);
564             $sth->bindParam(6, $this->_serialize($data), PDO::PARAM_STR, 100);
565             $rs = $sth->execute();
566         } else {
567             $sth = $dbh->prepare("DELETE FROM $version_tbl"
568                 . " WHERE id=? AND version=?");
569             $sth->bindParam(1, $id, PDO::PARAM_INT);
570             $sth->bindParam(2, $version, PDO::PARAM_INT);
571             $sth->execute();
572             $sth = $dbh->prepare("INSERT INTO $version_tbl"
573                 . " (id,version,mtime,minor_edit,content,versiondata)"
574                 . " VALUES(?,?,?,?,?,?)");
575             $sth->bindParam(1, $id, PDO::PARAM_INT);
576             $sth->bindParam(2, $version, PDO::PARAM_INT);
577             $sth->bindParam(3, $mtime, PDO::PARAM_INT);
578             $sth->bindParam(4, $minor_edit, PDO::PARAM_INT);
579             $sth->bindParam(5, $content, PDO::PARAM_STR, 100);
580             $sth->bindParam(6, $this->_serialize($data), PDO::PARAM_STR, 100);
581             $rs = $sth->execute();
582         }
583         $this->_update_recent_table($id);
584         $this->_update_nonempty_table($id);
585         if ($rs) $this->commit();
586         else $this->rollBack();
587         $this->unlock(array('page', 'recent', 'version', 'nonempty'));
588     }
589
590     /*
591      * Delete an old revision of a page.
592      */
593     function delete_versiondata($pagename, $version)
594     {
595         $dbh = &$this->_dbh;
596         extract($this->_table_names);
597
598         $this->lock(array('version'));
599         if (($id = $this->_get_pageid($pagename))) {
600             $dbh->query("DELETE FROM $version_tbl"
601                 . " WHERE id=$id AND version=$version");
602             $this->_update_recent_table($id);
603             // This shouldn't be needed (as long as the latestversion
604             // never gets deleted.)  But, let's be safe.
605             $this->_update_nonempty_table($id);
606         }
607         $this->unlock(array('version'));
608     }
609
610     /*
611      * Delete page from the database with backup possibility.
612      * i.e save_page('') and DELETE nonempty id
613      *
614      * deletePage increments latestversion in recent to a non-existent version,
615      * and removes the nonempty row,
616      * so that get_latest_version returns id+1 and get_previous_version returns prev id
617      * and page->exists returns false.
618      */
619     function delete_page($pagename)
620     {
621         /**
622          * @var WikiRequest $request
623          */
624         global $request;
625
626         $dbh = &$this->_dbh;
627         extract($this->_table_names);
628
629         $this->beginTransaction();
630         //$dbh->CommitLock($recent_tbl);
631         if (($id = $this->_get_pageid($pagename, false)) === false) {
632             $this->rollback();
633             return false;
634         }
635         $mtime = time();
636         $user =& $request->_user;
637         $meta = array('author' => $user->getId(),
638             'author_id' => $user->getAuthenticatedId(),
639             'mtime' => $mtime);
640         $this->lock(array('version', 'recent', 'nonempty', 'page', 'link'));
641         $version = $this->get_latest_version($pagename);
642         if ($dbh->query("UPDATE $recent_tbl SET latestversion=latestversion+1,"
643             . "latestmajor=latestversion+1,latestminor=NULL WHERE id=$id")
644         ) {
645             $insert = $dbh->prepare("INSERT INTO $version_tbl"
646                 . " (id,version,mtime,minor_edit,content,versiondata)"
647                 . " VALUES(?,?,?,?,?,?)");
648             $version_plus_1 = $version + 1;
649             $zero = 0;
650             $empty_string = '';
651             $insert->bindParam(1, $id, PDO::PARAM_INT);
652             $insert->bindParam(2, $version_plus_1, PDO::PARAM_INT);
653             $insert->bindParam(3, $mtime, PDO::PARAM_INT);
654             $insert->bindParam(4, $zero, PDO::PARAM_INT);
655             $insert->bindParam(5, $empty_string, PDO::PARAM_STR, 100);
656             $insert->bindParam(6, $this->_serialize($meta), PDO::PARAM_STR, 100);
657             if ($insert->execute()
658                 and $dbh->query("DELETE FROM $nonempty_tbl WHERE id=$id")
659                     and $this->set_links($pagename, array())
660             ) {
661                 // need to keep perms and LOCKED, otherwise you can reset the perm
662                 // by action=remove and re-create it with default perms
663                 // keep hits but delete meta-data
664                 //and $dbh->Execute("UPDATE $page_tbl SET pagedata='' WHERE id=$id")
665                 $this->unlock(array('version', 'recent', 'nonempty', 'page', 'link'));
666                 $this->commit();
667                 return true;
668             }
669         } else {
670             $this->unlock(array('version', 'recent', 'nonempty', 'page', 'link'));
671             $this->rollBack();
672             return false;
673         }
674     }
675
676     /*
677      * Delete page completely from the database.
678      */
679     function purge_page($pagename)
680     {
681         $dbh = &$this->_dbh;
682         extract($this->_table_names);
683
684         $this->lock(array('version', 'recent', 'nonempty', 'page', 'link'));
685         if (($id = $this->_get_pageid($pagename, false))) {
686             $dbh->query("DELETE FROM $version_tbl  WHERE id=$id");
687             $dbh->query("DELETE FROM $recent_tbl   WHERE id=$id");
688             $dbh->query("DELETE FROM $nonempty_tbl WHERE id=$id");
689             $this->set_links($pagename, array());
690             $sth = $dbh->prepare("SELECT COUNT(*) FROM $link_tbl WHERE linkto=$id");
691             $sth->execute();
692             if ($sth->fetchColumn()) {
693                 // We're still in the link table (dangling link) so we can't delete this
694                 // altogether.
695                 $dbh->query("UPDATE $page_tbl SET hits=0, pagedata='' WHERE id=$id");
696                 $result = 0;
697             } else {
698                 $dbh->query("DELETE FROM $page_tbl WHERE id=$id");
699                 $result = 1;
700             }
701         } else {
702             $result = -1; // already purged or not existing
703         }
704         $this->unlock(array('version', 'recent', 'nonempty', 'page', 'link'));
705         return $result;
706     }
707
708     // The only thing we might be interested in updating which we can
709     // do fast in the flags (minor_edit).   I think the default
710     // update_versiondata will work fine...
711     //function update_versiondata($pagename, $version, $data) {
712     //}
713
714     /*
715      * Update link table.
716      * on DEBUG: delete old, deleted links from page
717      */
718     function set_links($pagename, $links)
719     {
720         // Update link table.
721         // FIXME: optimize: mysql can do this all in one big INSERT/REPLACE.
722
723         $dbh = &$this->_dbh;
724         extract($this->_table_names);
725
726         $this->lock(array('link'));
727         $pageid = $this->_get_pageid($pagename, true);
728
729         if ($links) {
730             $dbh->query("DELETE FROM $link_tbl WHERE linkfrom=$pageid");
731             foreach ($links as $link) {
732                 if (isset($linkseen[$link]))
733                     continue;
734                 $linkseen[$link] = true;
735                 $linkid = $this->_get_pageid($link, true);
736                 assert($linkid);
737                 $dbh->query("INSERT INTO $link_tbl (linkfrom, linkto)"
738                     . " VALUES ($pageid, $linkid)");
739             }
740         } elseif (DEBUG) {
741             // purge page table: delete all non-referenced pages
742             // for all previously linked pages...
743             $sth = $dbh->prepare("SELECT $link_tbl.linkto as id FROM $link_tbl" .
744                 " WHERE linkfrom=$pageid");
745             $sth->execute();
746             foreach ($sth->fetchAll(PDO::FETCH_NUM) as $id) {
747                 // ...check if the page is empty and has no version
748                 $sth1 = $dbh->prepare("SELECT $page_tbl.id FROM $page_tbl"
749                     . " LEFT JOIN $nonempty_tbl USING (id) "
750                     . " LEFT JOIN $version_tbl USING (id)"
751                     . " WHERE ISNULL($nonempty_tbl.id) AND"
752                     . " ISNULL($version_tbl.id) AND $page_tbl.id=$id");
753                 $sth1->execute();
754                 if ($sth1->fetchColumn()) {
755                     $dbh->query("DELETE FROM $page_tbl WHERE id=$id"); // this purges the link
756                     $dbh->query("DELETE FROM $recent_tbl WHERE id=$id"); // may fail
757                 }
758             }
759             $dbh->query("DELETE FROM $link_tbl WHERE linkfrom=$pageid");
760         }
761         $this->unlock(array('link'));
762         return true;
763     }
764
765     /*
766      * Find pages which link to or are linked from a page.
767      *
768      * Optimization: save request->_dbi->_iwpcache[] to avoid further iswikipage checks
769      * (linkExistingWikiWord or linkUnknownWikiWord)
770      * This is called on every page header GleanDescription, so we can store all the
771      * existing links.
772      */
773     function get_links($pagename, $reversed = true, $include_empty = false,
774                        $sortby = '', $limit = '', $exclude = '')
775     {
776         $dbh = &$this->_dbh;
777         extract($this->_table_names);
778
779         if ($reversed)
780             list($have, $want) = array('linkee', 'linker');
781         else
782             list($have, $want) = array('linker', 'linkee');
783         $orderby = $this->sortby($sortby, 'db', array('pagename'));
784         if ($orderby) $orderby = " ORDER BY $want." . $orderby;
785         if ($exclude) // array of pagenames
786             $exclude = " AND $want.pagename NOT IN " . $this->_sql_set($exclude);
787         else
788             $exclude = '';
789         $limit = $this->_limit_sql($limit);
790
791         $sth = $dbh->prepare("SELECT $want.id AS id, $want.pagename AS pagename,"
792             . " $want.hits AS hits"
793             . " FROM $link_tbl, $page_tbl linker, $page_tbl linkee"
794             . (!$include_empty ? ", $nonempty_tbl" : '')
795             . " WHERE linkfrom=linker.id AND linkto=linkee.id"
796             . " AND $have.pagename=?"
797             . (!$include_empty ? " AND $nonempty_tbl.id=$want.id" : "")
798             //. " GROUP BY $want.id"
799             . $exclude
800             . $orderby
801             . $limit);
802         $sth->bindParam(1, $pagename, PDO::PARAM_STR, 100);
803         $sth->execute();
804         $result = $sth->fetch(PDO::FETCH_BOTH);
805         return new WikiDB_backend_PDO_iter($this, $result, $this->page_tbl_field_list);
806     }
807
808     /*
809      * Find if a page links to another page
810      */
811     function exists_link($pagename, $link, $reversed = false)
812     {
813         $dbh = &$this->_dbh;
814         extract($this->_table_names);
815
816         if ($reversed)
817             list($have, $want) = array('linkee', 'linker');
818         else
819             list($have, $want) = array('linker', 'linkee');
820         $sth = $dbh->prepare("SELECT CASE WHEN $want.pagename THEN 1 ELSE 0 END"
821             . " FROM $link_tbl, $page_tbl linker, $page_tbl linkee, $nonempty_tbl"
822             . " WHERE linkfrom=linker.id AND linkto=linkee.id"
823             . " AND $have.pagename=?"
824             . " AND $want.pagename=?");
825         $sth->bindParam(1, $pagename, PDO::PARAM_STR, 100);
826         $sth->bindParam(2, $link, PDO::PARAM_STR, 100);
827         $sth->execute();
828         return $sth->fetchColumn();
829     }
830
831     public function get_all_pages($include_empty = false,
832                                   $sortby = '', $limit = '', $exclude = '')
833     {
834         $dbh = &$this->_dbh;
835         extract($this->_table_names);
836         $orderby = $this->sortby($sortby, 'db');
837         if ($orderby) $orderby = ' ORDER BY ' . $orderby;
838         if ($exclude) // array of pagenames
839             $exclude = " AND $page_tbl.pagename NOT IN " . $this->_sql_set($exclude);
840         else
841             $exclude = '';
842         $limit = $this->_limit_sql($limit);
843
844         if (strstr($orderby, 'mtime ')) { // was ' mtime'
845             if ($include_empty) {
846                 $sql = "SELECT "
847                     . $this->page_tbl_fields
848                     . " FROM $page_tbl, $recent_tbl, $version_tbl"
849                     . " WHERE $page_tbl.id=$recent_tbl.id"
850                     . " AND $page_tbl.id=$version_tbl.id AND latestversion=version"
851                     . $exclude
852                     . $orderby;
853             } else {
854                 $sql = "SELECT "
855                     . $this->page_tbl_fields
856                     . " FROM $nonempty_tbl, $page_tbl, $recent_tbl, $version_tbl"
857                     . " WHERE $nonempty_tbl.id=$page_tbl.id"
858                     . " AND $page_tbl.id=$recent_tbl.id"
859                     . " AND $page_tbl.id=$version_tbl.id AND latestversion=version"
860                     . $exclude
861                     . $orderby;
862             }
863         } else {
864             if ($include_empty) {
865                 $sql = "SELECT "
866                     . $this->page_tbl_fields
867                     . " FROM $page_tbl"
868                     . ($exclude ? " WHERE $exclude" : '')
869                     . $orderby;
870             } else {
871                 $sql = "SELECT "
872                     . $this->page_tbl_fields
873                     . " FROM $nonempty_tbl, $page_tbl"
874                     . " WHERE $nonempty_tbl.id=$page_tbl.id"
875                     . $exclude
876                     . $orderby;
877             }
878         }
879         $sth = $dbh->prepare($sql . $limit);
880         $sth->execute();
881         $result = $sth->fetch(PDO::FETCH_BOTH);
882         return new WikiDB_backend_PDO_iter($this, $result, $this->page_tbl_field_list);
883     }
884
885     /*
886      * Title and fulltext search.
887      */
888     public function text_search($search, $fulltext = false,
889                                 $sortby = '', $limit = '', $exclude = '')
890     {
891         $dbh = &$this->_dbh;
892         extract($this->_table_names);
893         $orderby = $this->sortby($sortby, 'db');
894         if ($orderby) $orderby = ' ORDER BY ' . $orderby;
895         $limit = $this->_limit_sql($limit);
896
897         $table = "$nonempty_tbl, $page_tbl";
898         $join_clause = "$nonempty_tbl.id=$page_tbl.id";
899         $fields = $this->page_tbl_fields;
900         $field_list = $this->page_tbl_field_list;
901         $searchobj = new WikiDB_backend_PDO_search($search, $dbh);
902
903         if ($fulltext) {
904             $table .= ", $recent_tbl";
905             $join_clause .= " AND $page_tbl.id=$recent_tbl.id";
906
907             $table .= ", $version_tbl";
908             $join_clause .= " AND $page_tbl.id=$version_tbl.id AND latestversion=version";
909
910             $fields .= ",$page_tbl.pagedata as pagedata," . $this->version_tbl_fields;
911             $field_list = array_merge($field_list, array('pagedata'), $this->version_tbl_field_list);
912             $callback = new WikiMethodCb($searchobj, "_fulltext_match_clause");
913         } else {
914             $callback = new WikiMethodCb($searchobj, "_pagename_match_clause");
915         }
916         $search_clause = $search->makeSqlClauseObj($callback);
917         $sth = $dbh->prepare("SELECT $fields FROM $table"
918             . " WHERE $join_clause"
919             . "  AND ($search_clause)"
920             . $orderby
921             . $limit);
922         $sth->execute();
923         $result = $sth->fetch(PDO::FETCH_NUM);
924         $iter = new WikiDB_backend_PDO_iter($this, $result, $field_list);
925         $iter->stoplisted = $searchobj->stoplisted;
926         return $iter;
927     }
928
929     /*
930      * TODO: efficiently handle wildcards exclusion: exclude=Php* => 'Php%',
931      *       not sets. See above, but the above methods find too much.
932      * This is only for already resolved wildcards:
933      * " WHERE $page_tbl.pagename NOT IN ".$this->_sql_set(array('page1','page2'));
934      */
935     function _sql_set(&$pagenames)
936     {
937         $s = '(';
938         foreach ($pagenames as $p) {
939             $s .= ($this->_dbh->qstr($p) . ",");
940         }
941         return substr($s, 0, -1) . ")";
942     }
943
944     /*
945      * Find highest or lowest hit counts.
946      */
947     public function most_popular($limit = 20, $sortby = '-hits')
948     {
949         $dbh = &$this->_dbh;
950         extract($this->_table_names);
951         $order = "DESC";
952         if ($limit < 0) {
953             $order = "ASC";
954             $limit = -$limit;
955             $where = "";
956         } else {
957             $where = " AND hits > 0";
958         }
959         if ($sortby != '-hits') {
960             if ($order = $this->sortby($sortby, 'db')) $orderby = " ORDER BY " . $order;
961             else $orderby = "";
962         } else
963             $orderby = " ORDER BY hits $order";
964         $sql = "SELECT "
965             . $this->page_tbl_fields
966             . " FROM $nonempty_tbl, $page_tbl"
967             . " WHERE $nonempty_tbl.id=$page_tbl.id"
968             . $where
969             . $orderby;
970         if ($limit) {
971             $sth = $dbh->prepare($sql . $this->_limit_sql($limit));
972         } else {
973             $sth = $dbh->prepare($sql);
974         }
975         $sth->execute();
976         $result = $sth->fetch(PDO::FETCH_NUM);
977         return new WikiDB_backend_PDO_iter($this, $result, $this->page_tbl_field_list);
978     }
979
980     /*
981      * Find recent changes.
982      */
983     public function most_recent($params)
984     {
985         $limit = 0;
986         $since = 0;
987         $include_minor_revisions = false;
988         $exclude_major_revisions = false;
989         $include_all_revisions = false;
990         extract($params);
991
992         $dbh = &$this->_dbh;
993         extract($this->_table_names);
994
995         $pick = array();
996         if ($since)
997             $pick[] = "mtime >= $since";
998
999         if ($include_all_revisions) {
1000             // Include all revisions of each page.
1001             $table = "$page_tbl, $version_tbl";
1002             $join_clause = "$page_tbl.id=$version_tbl.id";
1003
1004             if ($exclude_major_revisions) {
1005                 // Include only minor revisions
1006                 $pick[] = "minor_edit <> 0";
1007             } elseif (!$include_minor_revisions) {
1008                 // Include only major revisions
1009                 $pick[] = "minor_edit = 0";
1010             }
1011         } else {
1012             $table = "$page_tbl, $recent_tbl";
1013             $join_clause = "$page_tbl.id=$recent_tbl.id";
1014             $table .= ", $version_tbl";
1015             $join_clause .= " AND $version_tbl.id=$page_tbl.id";
1016
1017             if ($exclude_major_revisions) {
1018                 // Include only most recent minor revision
1019                 $pick[] = 'version=latestminor';
1020             } elseif (!$include_minor_revisions) {
1021                 // Include only most recent major revision
1022                 $pick[] = 'version=latestmajor';
1023             } else {
1024                 // Include only the latest revision (whether major or minor).
1025                 $pick[] = 'version=latestversion';
1026             }
1027         }
1028         $order = "DESC";
1029         if ($limit < 0) {
1030             $order = "ASC";
1031             $limit = -$limit;
1032         }
1033         $where_clause = $join_clause;
1034         if ($pick)
1035             $where_clause .= " AND " . join(" AND ", $pick);
1036         $sql = "SELECT "
1037             . $this->page_tbl_fields . ", " . $this->version_tbl_fields
1038             . " FROM $table"
1039             . " WHERE $where_clause"
1040             . " ORDER BY mtime $order";
1041         if ($limit) {
1042             $sth = $dbh->prepare($sql . $this->_limit_sql($limit));
1043         } else {
1044             $sth = $dbh->prepare($sql);
1045         }
1046         $sth->execute();
1047         $result = $sth->fetch(PDO::FETCH_NUM);
1048         return new WikiDB_backend_PDO_iter($this, $result,
1049             array_merge($this->page_tbl_field_list, $this->version_tbl_field_list));
1050     }
1051
1052     /*
1053      * Find referenced empty pages.
1054      */
1055     function wanted_pages($exclude_from = '', $exclude = '', $sortby = '', $limit = '')
1056     {
1057         $dbh = &$this->_dbh;
1058         extract($this->_table_names);
1059         if ($orderby = $this->sortby($sortby, 'db', array('pagename', 'wantedfrom')))
1060             $orderby = 'ORDER BY ' . $orderby;
1061
1062         if ($exclude_from) // array of pagenames
1063             $exclude_from = " AND linked.pagename NOT IN " . $this->_sql_set($exclude_from);
1064         if ($exclude) // array of pagenames
1065             $exclude = " AND $page_tbl.pagename NOT IN " . $this->_sql_set($exclude);
1066
1067         /*
1068          all empty pages, independent of linkstatus:
1069            select pagename as empty from page left join nonempty using(id) where isnull(nonempty.id);
1070          only all empty pages, which have a linkto:
1071            select page.pagename, linked.pagename as wantedfrom from link, page as linked
1072              left join page on(link.linkto=page.id) left join nonempty on(link.linkto=nonempty.id)
1073              where isnull(nonempty.id) and linked.id=link.linkfrom;
1074         */
1075         $sql = "SELECT p.pagename, pp.pagename AS wantedfrom"
1076             . " FROM $page_tbl p JOIN $link_tbl linked"
1077             . " LEFT JOIN $page_tbl pp ON linked.linkto = pp.id"
1078             . " LEFT JOIN $nonempty_tbl ne ON linked.linkto = ne.id"
1079             . " WHERE ne.id IS NULL"
1080             . " AND p.id = linked.linkfrom"
1081             . $exclude_from
1082             . $exclude
1083             . $orderby;
1084         if ($limit) {
1085             $sth = $dbh->prepare($sql . $this->_limit_sql($limit));
1086         } else {
1087             $sth = $dbh->prepare($sql);
1088         }
1089         $sth->execute();
1090         $result = $sth->fetch(PDO::FETCH_NUM);
1091         return new WikiDB_backend_PDO_iter($this, $result, array('pagename', 'wantedfrom'));
1092     }
1093
1094     /*
1095      * Rename page in the database.
1096      */
1097     function rename_page($pagename, $to)
1098     {
1099         $dbh = &$this->_dbh;
1100         extract($this->_table_names);
1101
1102         $this->lock(array('page', 'version', 'recent', 'nonempty', 'link'));
1103         if (($id = $this->_get_pageid($pagename, false))) {
1104             if ($new = $this->_get_pageid($to, false)) {
1105                 // Cludge Alert!
1106                 // This page does not exist (already verified before), but exists in the page table.
1107                 // So we delete this page.
1108                 $dbh->query("DELETE FROM $page_tbl WHERE id=$new");
1109                 $dbh->query("DELETE FROM $version_tbl WHERE id=$new");
1110                 $dbh->query("DELETE FROM $recent_tbl WHERE id=$new");
1111                 $dbh->query("DELETE FROM $nonempty_tbl WHERE id=$new");
1112                 // We have to fix all referring tables to the old id
1113                 $dbh->query("UPDATE $link_tbl SET linkfrom=$id WHERE linkfrom=$new");
1114                 $dbh->query("UPDATE $link_tbl SET linkto=$id WHERE linkto=$new");
1115             }
1116             $sth = $dbh->prepare("UPDATE $page_tbl SET pagename=? WHERE id=?");
1117             $sth->bindParam(1, $to, PDO::PARAM_STR, 100);
1118             $sth->bindParam(2, $id, PDO::PARAM_INT);
1119             $sth->execute();
1120         }
1121         $this->unlock(array('page'));
1122         return $id;
1123     }
1124
1125     function _update_recent_table($pageid = false)
1126     {
1127         $dbh = &$this->_dbh;
1128         extract($this->_table_names);
1129         extract($this->_expressions);
1130
1131         $pageid = (int)$pageid;
1132
1133         // optimize: mysql can do this with one REPLACE INTO.
1134         $backend_type = $this->backendType();
1135         if (substr($backend_type, 0, 5) == 'mysql') {
1136             $sth = $dbh->prepare("REPLACE INTO $recent_tbl"
1137                 . " (id, latestversion, latestmajor, latestminor)"
1138                 . " SELECT id, $maxversion, $maxmajor, $maxminor"
1139                 . " FROM $version_tbl"
1140                 . ($pageid ? " WHERE id=$pageid" : "")
1141                 . " GROUP BY id");
1142             $sth->execute();
1143         } else {
1144             $this->lock(array('recent'));
1145             $sth = $dbh->prepare("DELETE FROM $recent_tbl"
1146                 . ($pageid ? " WHERE id=$pageid" : ""));
1147             $sth->execute();
1148             $sth = $dbh->prepare("INSERT INTO $recent_tbl"
1149                 . " (id, latestversion, latestmajor, latestminor)"
1150                 . " SELECT id, $maxversion, $maxmajor, $maxminor"
1151                 . " FROM $version_tbl"
1152                 . ($pageid ? " WHERE id=$pageid" : "")
1153                 . " GROUP BY id");
1154             $sth->execute();
1155             $this->unlock(array('recent'));
1156         }
1157     }
1158
1159     function _update_nonempty_table($pageid = false)
1160     {
1161         $dbh = &$this->_dbh;
1162         extract($this->_table_names);
1163         extract($this->_expressions);
1164
1165         $pageid = (int)$pageid;
1166
1167         extract($this->_expressions);
1168         $this->lock(array('nonempty'));
1169         $dbh->query("DELETE FROM $nonempty_tbl"
1170             . ($pageid ? " WHERE id=$pageid" : ""));
1171         $dbh->query("INSERT INTO $nonempty_tbl (id)"
1172             . " SELECT $recent_tbl.id"
1173             . " FROM $recent_tbl, $version_tbl"
1174             . " WHERE $recent_tbl.id=$version_tbl.id"
1175             . "  AND version=latestversion"
1176             // We have some specifics here (Oracle)
1177             //. "  AND content<>''"
1178             . "  AND content $notempty" // On Oracle not just "<>''"
1179             . ($pageid ? " AND $recent_tbl.id=$pageid" : ""));
1180         $this->unlock(array('nonempty'));
1181     }
1182
1183     /*
1184      * Grab a write lock on the tables in the SQL database.
1185      *
1186      * Calls can be nested.  The tables won't be unlocked until
1187      * _unlock_database() is called as many times as _lock_database().
1188      */
1189     public function lock($tables = array(), $write_lock = true)
1190     {
1191         if ($this->_lock_count++ == 0) {
1192             $this->_current_lock = $tables;
1193             if (!$this->_hasTransactions)
1194                 $this->_lock_tables($tables, $write_lock);
1195         }
1196     }
1197
1198     /*
1199      * Overridden by non-transaction safe backends.
1200      */
1201     protected function _lock_tables($tables, $write_lock = true)
1202     {
1203         $lock_type = $write_lock ? "WRITE" : "READ";
1204         foreach ($this->_table_names as $key => $table) {
1205             $locks[] = "$table $lock_type";
1206         }
1207         $this->_dbh->query("LOCK TABLES " . join(",", $locks));
1208     }
1209
1210     /**
1211      * Release a write lock on the tables in the SQL database.
1212      *
1213      * @param array $tables
1214      * @param bool $force Unlock even if not every call to lock() has been matched
1215      * by a call to unlock().
1216      *
1217      * @see _lock_database
1218      */
1219     public function unlock($tables = array(), $force = false)
1220     {
1221         if ($this->_lock_count == 0) {
1222             $this->_current_lock = false;
1223             return;
1224         }
1225         if (--$this->_lock_count <= 0 || $force) {
1226             if (!$this->_hasTransactions)
1227                 $this->_unlock_tables();
1228             $this->_current_lock = false;
1229             $this->_lock_count = 0;
1230         }
1231     }
1232
1233     /*
1234      * overridden by non-transaction safe backends
1235      */
1236     protected function _unlock_tables()
1237     {
1238         $this->_dbh->query("UNLOCK TABLES");
1239     }
1240
1241     /*
1242      * Serialize data
1243      */
1244     function _serialize($data)
1245     {
1246         if (empty($data))
1247             return '';
1248         assert(is_array($data));
1249         return serialize($data);
1250     }
1251
1252     /*
1253      * Unserialize data
1254      */
1255     function _unserialize($data)
1256     {
1257         return empty($data) ? array() : unserialize($data);
1258     }
1259
1260     /* some variables and functions for DB backend abstraction (action=upgrade) */
1261     function database()
1262     {
1263         return $this->_dbh->database;
1264     }
1265
1266     function backendType()
1267     {
1268         return $this->_dbh->databaseType;
1269     }
1270
1271     function connection()
1272     {
1273         trigger_error("PDO: connectionID unsupported", E_USER_ERROR);
1274         return false;
1275     }
1276
1277     function listOfTables()
1278     {
1279         trigger_error("PDO: virtual listOfTables", E_USER_ERROR);
1280         return array();
1281     }
1282
1283     function listOfFields($database, $table)
1284     {
1285         trigger_error("PDO: virtual listOfFields", E_USER_ERROR);
1286         return array();
1287     }
1288
1289     /*
1290      * LIMIT with OFFSET is not SQL specified.
1291      *   mysql: LIMIT $offset, $count
1292      *   pgsql,sqlite: LIMIT $count OFFSET $offset
1293      *   InterBase,FireBird: ROWS $offset TO $last
1294      *   mssql: TOP $rows => TOP $last
1295      *   oci8: ROWNUM
1296      *   IBM DB2: FetchFirst
1297      * See http://search.cpan.org/dist/SQL-Abstract-Limit/lib/SQL/Abstract/Limit.pm
1298
1299      SELECT field_list FROM $table X WHERE where_clause AND
1300      (
1301          SELECT COUNT(*) FROM $table WHERE $pk > X.$pk
1302      )
1303      BETWEEN $offset AND $last
1304      ORDER BY $pk $asc_desc
1305      */
1306     function _limit_sql($limit = false)
1307     {
1308         if ($limit) {
1309             list($offset, $count) = $this->limit($limit);
1310             if ($offset) {
1311                 $limit = " LIMIT $count";
1312                 trigger_error("unsupported OFFSET in SQL ignored", E_USER_WARNING);
1313             } else
1314                 $limit = " LIMIT $count";
1315         } else
1316             $limit = '';
1317         return $limit;
1318     }
1319
1320     function write_accesslog(&$entry)
1321     {
1322         $dbh = &$this->_dbh;
1323         $log_tbl = $entry->_accesslog->logtable;
1324         $dbh->prepare("INSERT INTO $log_tbl"
1325             . " (time_stamp,remote_host,remote_user,request_method,request_line,request_args,"
1326             . "request_file,request_uri,request_time,status,bytes_sent,referer,agent,request_duration)"
1327             . " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)");
1328         // Either use unixtime as %d (long), or the native timestamp format.
1329         $sth->bindParam(1, $entry->time, PDO::PARAM_INT);
1330         $sth->bindParam(2, $entry->host, PDO::PARAM_STR, 100);
1331         $sth->bindParam(3, $entry->user, PDO::PARAM_STR, 50);
1332         $sth->bindParam(4, $entry->request_method, PDO::PARAM_STR, 10);
1333         $sth->bindParam(5, $entry->request, PDO::PARAM_STR, 255);
1334         $sth->bindParam(6, $entry->request_args, PDO::PARAM_STR, 255);
1335         $sth->bindParam(7, $entry->request_uri, PDO::PARAM_STR, 255);
1336         $sth->bindParam(8, $entry->_ncsa_time($entry->time), PDO::PARAM_STR, 28);
1337         $sth->bindParam(9, $entry->time, PDO::PARAM_INT);
1338         $sth->bindParam(10, $entry->status, PDO::PARAM_INT);
1339         $sth->bindParam(11, $entry->size, PDO::PARAM_INT);
1340         $sth->bindParam(12, $entry->referer, PDO::PARAM_STR, 255);
1341         $sth->bindParam(13, $entry->user_agent, PDO::PARAM_STR, 255);
1342         $sth->bindParam(14, $entry->duration, PDO::PARAM_FLOAT);
1343         $sth->execute();
1344     }
1345 }
1346
1347 class WikiDB_backend_PDO_generic_iter
1348     extends WikiDB_backend_iterator
1349 {
1350     function __construct($backend, $query_result, $field_list = NULL)
1351     {
1352         $this->_backend = &$backend;
1353         $this->_result = $query_result;
1354         //$this->_fields = $field_list;
1355     }
1356
1357     function count()
1358     {
1359         if (!is_object($this->_result)) {
1360             return false;
1361         }
1362         $count = $this->_result->rowCount();
1363         return $count;
1364     }
1365
1366     function next()
1367     {
1368         $result = &$this->_result;
1369         if (!is_object($result)) {
1370             return false;
1371         }
1372         return $result->fetch(PDO::FETCH_BOTH);
1373     }
1374
1375     function free()
1376     {
1377         if ($this->_result) {
1378             unset($this->_result);
1379         }
1380     }
1381 }
1382
1383 class WikiDB_backend_PDO_iter
1384     extends WikiDB_backend_PDO_generic_iter
1385 {
1386     function next()
1387     {
1388         $result = &$this->_result;
1389         if (!is_object($result)) {
1390             return false;
1391         }
1392         $this->_backend = &$backend;
1393         $rec = $result->fetch(PDO::FETCH_ASSOC);
1394
1395         if (isset($rec['pagedata']))
1396             $rec['pagedata'] = $backend->_extract_page_data($rec['pagedata'], $rec['hits']);
1397         if (!empty($rec['version'])) {
1398             $rec['versiondata'] = $backend->_extract_version_data_assoc($rec);
1399         }
1400         return $rec;
1401     }
1402 }
1403
1404 class WikiDB_backend_PDO_search extends WikiDB_backend_search_sql
1405 {
1406 }
1407
1408 // Following function taken from Pear::DB (prev. from adodb-pear.inc.php).
1409 // Eventually, change index.php to provide the relevant information
1410 // directly?
1411 /**
1412  * Parse a data source name.
1413  *
1414  * Additional keys can be added by appending a URI query string to the
1415  * end of the DSN.
1416  *
1417  * The format of the supplied DSN is in its fullest form:
1418  * <code>
1419  *  phptype(dbsyntax)://username:password@protocol+hostspec/database?option=8&another=true
1420  * </code>
1421  *
1422  * Most variations are allowed:
1423  * <code>
1424  *  phptype://username:password@protocol+hostspec:110//usr/db_file.db?mode=0644
1425  *  phptype://username:password@hostspec/database_name
1426  *  phptype://username:password@hostspec
1427  *  phptype://username@hostspec
1428  *  phptype://hostspec/database
1429  *  phptype://hostspec
1430  *  phptype(dbsyntax)
1431  *  phptype
1432  * </code>
1433  *
1434  * @param string $dsn Data Source Name to be parsed
1435  *
1436  * @return array an associative array with the following keys:
1437  *  + phptype:  Database backend used in PHP (mysql, odbc etc.)
1438  *  + dbsyntax: Database used with regards to SQL syntax etc. (ignored with PDO)
1439  *  + protocol: Communication protocol to use (tcp, unix, pipe etc.)
1440  *  + hostspec: Host specification (hostname[:port])
1441  *  + database: Database to use on the DBMS server
1442  *  + username: User name for login
1443  *  + password: Password for login
1444  *
1445  * @author Tomas V.V.Cox <cox@idecnet.com>
1446  */
1447 function parseDSN($dsn)
1448 {
1449     $parsed = array(
1450         'phptype' => false,
1451         'dbsyntax' => false,
1452         'username' => false,
1453         'password' => false,
1454         'protocol' => false,
1455         'hostspec' => false,
1456         'port' => false,
1457         'socket' => false,
1458         'database' => false,
1459     );
1460
1461     if (is_array($dsn)) {
1462         $dsn = array_merge($parsed, $dsn);
1463         if (!$dsn['dbsyntax']) {
1464             $dsn['dbsyntax'] = $dsn['phptype'];
1465         }
1466         return $dsn;
1467     }
1468
1469     // Find phptype and dbsyntax
1470     if (($pos = strpos($dsn, '://')) !== false) {
1471         $str = substr($dsn, 0, $pos);
1472         $dsn = substr($dsn, $pos + 3);
1473     } else {
1474         $str = $dsn;
1475         $dsn = null;
1476     }
1477
1478     // Get phptype and dbsyntax
1479     // $str => phptype(dbsyntax)
1480     if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) {
1481         $parsed['phptype'] = $arr[1];
1482         $parsed['dbsyntax'] = !$arr[2] ? $arr[1] : $arr[2];
1483     } else {
1484         $parsed['phptype'] = $str;
1485         $parsed['dbsyntax'] = $str;
1486     }
1487
1488     if (!count($dsn)) {
1489         return $parsed;
1490     }
1491
1492     // Get (if found): username and password
1493     // $dsn => username:password@protocol+hostspec/database
1494     if (($at = strrpos($dsn, '@')) !== false) {
1495         $str = substr($dsn, 0, $at);
1496         $dsn = substr($dsn, $at + 1);
1497         if (($pos = strpos($str, ':')) !== false) {
1498             $parsed['username'] = rawurldecode(substr($str, 0, $pos));
1499             $parsed['password'] = rawurldecode(substr($str, $pos + 1));
1500         } else {
1501             $parsed['username'] = rawurldecode($str);
1502         }
1503     }
1504
1505     // Find protocol and hostspec
1506
1507     // $dsn => proto(proto_opts)/database
1508     if (preg_match('|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match)) {
1509         $proto = $match[1];
1510         $proto_opts = $match[2] ? $match[2] : false;
1511         $dsn = $match[3];
1512
1513         // $dsn => protocol+hostspec/database (old format)
1514     } else {
1515         if (strpos($dsn, '+') !== false) {
1516             list($proto, $dsn) = explode('+', $dsn, 2);
1517         }
1518         if (strpos($dsn, '/') !== false) {
1519             list($proto_opts, $dsn) = explode('/', $dsn, 2);
1520         } else {
1521             $proto_opts = $dsn;
1522             $dsn = null;
1523         }
1524     }
1525
1526     // process the different protocol options
1527     $parsed['protocol'] = (!empty($proto)) ? $proto : 'tcp';
1528     $proto_opts = rawurldecode($proto_opts);
1529     if ($parsed['protocol'] == 'tcp') {
1530         if (strpos($proto_opts, ':') !== false) {
1531             list($parsed['hostspec'], $parsed['port']) = explode(':', $proto_opts);
1532         } else {
1533             $parsed['hostspec'] = $proto_opts;
1534         }
1535     } elseif ($parsed['protocol'] == 'unix') {
1536         $parsed['socket'] = $proto_opts;
1537     }
1538
1539     // Get dabase if any
1540     // $dsn => database
1541     if ($dsn) {
1542         // /database
1543         if (($pos = strpos($dsn, '?')) === false) {
1544             $parsed['database'] = $dsn;
1545             // /database?param1=value1&param2=value2
1546         } else {
1547             $parsed['database'] = substr($dsn, 0, $pos);
1548             $dsn = substr($dsn, $pos + 1);
1549             if (strpos($dsn, '&') !== false) {
1550                 $opts = explode('&', $dsn);
1551             } else { // database?param1=value1
1552                 $opts = array($dsn);
1553             }
1554             foreach ($opts as $opt) {
1555                 list($key, $value) = explode('=', $opt);
1556                 if (!isset($parsed[$key])) {
1557                     // don't allow params overwrite
1558                     $parsed[$key] = rawurldecode($value);
1559                 }
1560             }
1561         }
1562     }
1563
1564     return $parsed;
1565 }
1566
1567 // Local Variables:
1568 // mode: php
1569 // tab-width: 8
1570 // c-basic-offset: 4
1571 // c-hanging-comment-ender-p: nil
1572 // indent-tabs-mode: nil
1573 // End: