]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend/PearDB.php
extra_empty_lines
[SourceForge/phpwiki.git] / lib / WikiDB / backend / PearDB.php
1 <?php // -*-php-*-
2 // $Id$
3
4 require_once('lib/WikiDB/backend.php');
5 //require_once('lib/FileFinder.php');
6 //require_once('lib/ErrorManager.php');
7
8 class WikiDB_backend_PearDB
9 extends WikiDB_backend
10 {
11     var $_dbh;
12
13     function WikiDB_backend_PearDB ($dbparams) {
14         // Find and include PEAR's DB.php. maybe we should force our private version again...
15         // if DB would have exported its version number, it would be easier.
16         @require_once('DB/common.php'); // Either our local pear copy or the system one
17         // check the version!
18         $name = check_php_version(5) ? "escapeSimple" : strtolower("escapeSimple");
19         // TODO: apparently some Pear::Db version adds LIMIT 1,0 to getOne(),
20         // which is invalid for "select version()"
21         if (!in_array($name, get_class_methods("DB_common"))) {
22             $finder = new FileFinder;
23             $dir = dirname(__FILE__)."/../../pear";
24             $finder->_prepend_to_include_path($dir);
25             include_once("$dir/DB/common.php"); // use our version instead.
26             if (!in_array($name, get_class_methods("DB_common"))) {
27                 $pearFinder = new PearFileFinder("lib/pear");
28                 $pearFinder->includeOnce('DB.php');
29             } else {
30                 include_once("$dir/DB.php");
31             }
32         } else {
33           include_once("DB.php");
34         }
35
36         // Install filter to handle bogus error notices from buggy DB.php's.
37         // TODO: check the Pear_DB version, but how?
38         if (DEBUG) {
39             global $ErrorManager;
40             $ErrorManager->pushErrorHandler(new WikiMethodCb($this, '_pear_notice_filter'));
41             $this->_pearerrhandler = true;
42         }
43
44         // Open connection to database
45         $this->_dsn = $dbparams['dsn'];
46     $this->_dbparams = $dbparams;
47         $this->_lock_count = 0;
48
49         // persistent is usually a DSN option: we override it with a config value.
50         //   phptype://username:password@hostspec/database?persistent=false
51         $dboptions = array('persistent' => DATABASE_PERSISTENT,
52                            'debug' => 2);
53         //if (preg_match('/^pgsql/', $this->_dsn)) $dboptions['persistent'] = false;
54         $this->_dbh = DB::connect($this->_dsn, $dboptions);
55         $dbh = &$this->_dbh;
56         if (DB::isError($dbh)) {
57             trigger_error(sprintf("Can't connect to database: %s",
58                                   $this->_pear_error_message($dbh)),
59                           isset($dbparams['_tryroot_from_upgrade']) // hack!
60                             ? E_USER_WARNING : E_USER_ERROR);
61             if (isset($dbparams['_tryroot_from_upgrade']))
62                 return;
63         }
64         $dbh->setErrorHandling(PEAR_ERROR_CALLBACK,
65                                array($this, '_pear_error_callback'));
66         $dbh->setFetchMode(DB_FETCHMODE_ASSOC);
67
68         $prefix = isset($dbparams['prefix']) ? $dbparams['prefix'] : '';
69         $this->_table_names
70             = array('page_tbl'     => $prefix . 'page',
71                     'version_tbl'  => $prefix . 'version',
72                     'link_tbl'     => $prefix . 'link',
73                     'recent_tbl'   => $prefix . 'recent',
74                     'nonempty_tbl' => $prefix . 'nonempty');
75         $page_tbl = $this->_table_names['page_tbl'];
76         $version_tbl = $this->_table_names['version_tbl'];
77         $this->page_tbl_fields = "$page_tbl.id AS id, $page_tbl.pagename AS pagename, $page_tbl.hits AS hits";
78         $this->version_tbl_fields = "$version_tbl.version AS version, $version_tbl.mtime AS mtime, ".
79             "$version_tbl.minor_edit AS minor_edit, $version_tbl.content AS content, $version_tbl.versiondata AS versiondata";
80
81         $this->_expressions
82             = array('maxmajor'     => "MAX(CASE WHEN minor_edit=0 THEN version END)",
83                     'maxminor'     => "MAX(CASE WHEN minor_edit<>0 THEN version END)",
84                     'maxversion'   => "MAX(version)",
85                     'notempty'     => "<>''",
86                     'iscontent'    => "content<>''");
87
88     }
89
90     /**
91      * Close database connection.
92      */
93     function close () {
94         if (!$this->_dbh)
95             return;
96         if ($this->_lock_count) {
97             trigger_error( "WARNING: database still locked " . '(lock_count = $this->_lock_count)' . "\n<br />",
98                           E_USER_WARNING);
99         }
100         $this->_dbh->setErrorHandling(PEAR_ERROR_PRINT);        // prevent recursive loops.
101         $this->unlock('force');
102
103         $this->_dbh->disconnect();
104
105         if (!empty($this->_pearerrhandler)) {
106             $GLOBALS['ErrorManager']->popErrorHandler();
107         }
108     }
109
110     /*
111      * Test fast wikipage.
112      */
113     function is_wiki_page($pagename) {
114         $dbh = &$this->_dbh;
115         extract($this->_table_names);
116         return $dbh->getOne(sprintf("SELECT $page_tbl.id as id"
117                                     . " FROM $nonempty_tbl, $page_tbl"
118                                     . " WHERE $nonempty_tbl.id=$page_tbl.id"
119                                     . "   AND pagename='%s'",
120                                     $dbh->escapeSimple($pagename)));
121     }
122
123     function get_all_pagenames() {
124         $dbh = &$this->_dbh;
125         extract($this->_table_names);
126         return $dbh->getCol("SELECT pagename"
127                             . " FROM $nonempty_tbl, $page_tbl"
128                             . " WHERE $nonempty_tbl.id=$page_tbl.id");
129     }
130
131     function numPages($filter=false, $exclude='') {
132         $dbh = &$this->_dbh;
133         extract($this->_table_names);
134         return $dbh->getOne("SELECT count(*)"
135                             . " FROM $nonempty_tbl, $page_tbl"
136                             . " WHERE $nonempty_tbl.id=$page_tbl.id");
137     }
138
139     function increaseHitCount($pagename) {
140         $dbh = &$this->_dbh;
141         // Hits is the only thing we can update in a fast manner.
142         // Note that this will fail silently if the page does not
143         // have a record in the page table.  Since it's just the
144         // hit count, who cares?
145         $dbh->query(sprintf("UPDATE %s SET hits=hits+1 WHERE pagename='%s'",
146                             $this->_table_names['page_tbl'],
147                             $dbh->escapeSimple($pagename)));
148         return;
149     }
150
151     /**
152      * Read page information from database.
153      */
154     function get_pagedata($pagename) {
155         $dbh = &$this->_dbh;
156         //trigger_error("GET_PAGEDATA $pagename", E_USER_NOTICE);
157         $result = $dbh->getRow(sprintf("SELECT hits,pagedata FROM %s WHERE pagename='%s'",
158                                        $this->_table_names['page_tbl'],
159                                        $dbh->escapeSimple($pagename)),
160                                DB_FETCHMODE_ASSOC);
161         return $result ? $this->_extract_page_data($result) : false;
162     }
163
164     function  _extract_page_data($data) {
165         if (empty($data)) return array();
166         elseif (empty($data['pagedata'])) return $data;
167         else {
168             $data = array_merge($data, $this->_unserialize($data['pagedata']));
169             unset($data['pagedata']);
170             return $data;
171         }
172     }
173
174     function update_pagedata($pagename, $newdata) {
175         $dbh = &$this->_dbh;
176         $page_tbl = $this->_table_names['page_tbl'];
177
178         // Hits is the only thing we can update in a fast manner.
179         if (count($newdata) == 1 && isset($newdata['hits'])) {
180             // Note that this will fail silently if the page does not
181             // have a record in the page table.  Since it's just the
182             // hit count, who cares?
183             $dbh->query(sprintf("UPDATE $page_tbl SET hits=%d WHERE pagename='%s'",
184                                 $newdata['hits'], $dbh->escapeSimple($pagename)));
185             return;
186         }
187
188         $this->lock(array($page_tbl), true);
189         $data = $this->get_pagedata($pagename);
190         if (!$data) {
191             $data = array();
192             $this->_get_pageid($pagename, true); // Creates page record
193         }
194
195         $hits = !empty($data['hits']) ? (int)$data['hits'] : 0;
196         unset($data['hits']);
197
198         foreach ($newdata as $key => $val) {
199             if ($key == 'hits')
200                 $hits = (int)$val;
201             else if (empty($val))
202                 unset($data[$key]);
203             else
204                 $data[$key] = $val;
205         }
206
207         /* Portability issue -- not all DBMS supports huge strings
208          * so we need to 'bind' instead of building a simple SQL statment.
209          * Note that we do not need to escapeSimple when we bind
210         $dbh->query(sprintf("UPDATE $page_tbl"
211                             . " SET hits=%d, pagedata='%s'"
212                             . " WHERE pagename='%s'",
213                             $hits,
214                             $dbh->escapeSimple($this->_serialize($data)),
215                             $dbh->escapeSimple($pagename)));
216         */
217         $dbh->query("UPDATE $page_tbl"
218                     . " SET hits=?, pagedata=?"
219                     . " WHERE pagename=?",
220                     array($hits, $this->_serialize($data), $pagename));
221         $this->unlock(array($page_tbl));
222     }
223
224     function get_cached_html($pagename) {
225         $dbh = &$this->_dbh;
226         $page_tbl = $this->_table_names['page_tbl'];
227         return $dbh->GetOne(sprintf("SELECT cached_html FROM $page_tbl WHERE pagename='%s'",
228                                     $dbh->escapeSimple($pagename)));
229     }
230
231     function set_cached_html($pagename, $data) {
232         $dbh = &$this->_dbh;
233         $page_tbl = $this->_table_names['page_tbl'];
234         $dbh->query("UPDATE $page_tbl"
235                     . " SET cached_html=?"
236                     . " WHERE pagename=?",
237                     array($data, $pagename));
238     }
239
240     function _get_pageid($pagename, $create_if_missing = false) {
241
242         // check id_cache
243         global $request;
244         $cache =& $request->_dbi->_cache->_id_cache;
245         if (isset($cache[$pagename])) {
246             if ($cache[$pagename] or !$create_if_missing) {
247                 return $cache[$pagename];
248             }
249         }
250
251     // attributes play this game.
252         if ($pagename === '') return 0;
253
254         $dbh = &$this->_dbh;
255         $page_tbl = $this->_table_names['page_tbl'];
256
257         $query = sprintf("SELECT id FROM $page_tbl WHERE pagename='%s'",
258                          $dbh->escapeSimple($pagename));
259
260         if (!$create_if_missing)
261             return $dbh->getOne($query);
262
263         $id = $dbh->getOne($query);
264         if (empty($id)) {
265             $this->lock(array($page_tbl), true); // write lock
266             $max_id = $dbh->getOne("SELECT MAX(id) FROM $page_tbl");
267             $id = $max_id + 1;
268             // requires createSequence and on mysql lock the interim table ->getSequenceName
269             //$id = $dbh->nextId($page_tbl . "_id");
270             $dbh->query(sprintf("INSERT INTO $page_tbl"
271                                 . " (id,pagename,hits)"
272                                 . " VALUES (%d,'%s',0)",
273                                 $id, $dbh->escapeSimple($pagename)));
274             $this->unlock(array($page_tbl));
275         }
276         return $id;
277     }
278
279     function get_latest_version($pagename) {
280         $dbh = &$this->_dbh;
281         extract($this->_table_names);
282         return
283             (int)$dbh->getOne(sprintf("SELECT latestversion"
284                                       . " FROM $page_tbl, $recent_tbl"
285                                       . " WHERE $page_tbl.id=$recent_tbl.id"
286                                       . "  AND pagename='%s'",
287                                       $dbh->escapeSimple($pagename)));
288     }
289
290     function get_previous_version($pagename, $version) {
291         $dbh = &$this->_dbh;
292         extract($this->_table_names);
293
294         return
295             (int)$dbh->getOne(sprintf("SELECT version"
296                                       . " FROM $version_tbl, $page_tbl"
297                                       . " WHERE $version_tbl.id=$page_tbl.id"
298                                       . "  AND pagename='%s'"
299                                       . "  AND version < %d"
300                                       . " ORDER BY version DESC",
301                                       /* Non portable and useless anyway with getOne
302                                       . " LIMIT 1",
303                                       */
304                                       $dbh->escapeSimple($pagename),
305                                       $version));
306     }
307
308     /**
309      * Get version data.
310      *
311      * @param $version int Which version to get.
312      *
313      * @return hash The version data, or false if specified version does not
314      *              exist.
315      */
316     function get_versiondata($pagename, $version, $want_content = false) {
317         $dbh = &$this->_dbh;
318         extract($this->_table_names);
319         extract($this->_expressions);
320
321         assert(is_string($pagename) and $pagename != "");
322         assert($version > 0);
323
324         //trigger_error("GET_REVISION $pagename $version $want_content", E_USER_NOTICE);
325         // FIXME: optimization: sometimes don't get page data?
326         if ($want_content) {
327             $fields = $this->page_tbl_fields
328                 . ",$page_tbl.pagedata as pagedata,"
329                 . $this->version_tbl_fields;
330         }
331         else {
332             $fields = $this->page_tbl_fields . ","
333                 . "mtime, minor_edit, versiondata,"
334                 . "$iscontent AS have_content";
335         }
336
337         $result = $dbh->getRow(sprintf("SELECT $fields"
338                                        . " FROM $page_tbl, $version_tbl"
339                                        . " WHERE $page_tbl.id=$version_tbl.id"
340                                        . "  AND pagename='%s'"
341                                        . "  AND version=%d",
342                                        $dbh->escapeSimple($pagename), $version),
343                                DB_FETCHMODE_ASSOC);
344
345         return $this->_extract_version_data($result);
346     }
347
348     function _extract_version_data($query_result) {
349         if (!$query_result)
350             return false;
351
352         /* Earlier versions (<= 1.3.7) stored the version data in base64.
353            This could be done here or in upgrade.
354         */
355         if (!strstr($query_result['versiondata'], ":")) {
356             $query_result['versiondata'] =
357                 base64_decode($query_result['versiondata']);
358         }
359         $data = $this->_unserialize($query_result['versiondata']);
360
361         $data['mtime'] = $query_result['mtime'];
362         $data['is_minor_edit'] = !empty($query_result['minor_edit']);
363
364         if (isset($query_result['content']))
365             $data['%content'] = $query_result['content'];
366         elseif ($query_result['have_content'])
367             $data['%content'] = true;
368         else
369             $data['%content'] = '';
370
371         // FIXME: this is ugly.
372         if (isset($query_result['pagedata'])) {
373             // Query also includes page data.
374             // We might as well send that back too...
375             unset($query_result['versiondata']);
376             $data['%pagedata'] = $this->_extract_page_data($query_result);
377         }
378
379         return $data;
380     }
381
382
383     /**
384      * Create a new revision of a page.
385      */
386     function set_versiondata($pagename, $version, $data) {
387         $dbh = &$this->_dbh;
388         $version_tbl = $this->_table_names['version_tbl'];
389
390         $minor_edit = (int) !empty($data['is_minor_edit']);
391         unset($data['is_minor_edit']);
392
393         $mtime = (int)$data['mtime'];
394         unset($data['mtime']);
395         assert(!empty($mtime));
396
397         $content = isset($data['%content']) ? (string)$data['%content'] : '';
398         unset($data['%content']);
399
400         unset($data['%pagedata']);
401
402         $this->lock();
403         $id = $this->_get_pageid($pagename, true);
404
405         $dbh->query(sprintf("DELETE FROM $version_tbl"
406                             . " WHERE id=%d AND version=%d",
407                             $id, $version));
408         // generic slow PearDB bind eh quoting.
409         $dbh->query("INSERT INTO $version_tbl"
410                     . " (id,version,mtime,minor_edit,content,versiondata)"
411                     . " VALUES(?, ?, ?, ?, ?, ?)",
412                     array($id, $version, $mtime, $minor_edit, $content,
413                     $this->_serialize($data)));
414
415         $this->_update_recent_table($id);
416         $this->_update_nonempty_table($id);
417
418         $this->unlock();
419     }
420
421     /**
422      * Delete an old revision of a page.
423      */
424     function delete_versiondata($pagename, $version) {
425         $dbh = &$this->_dbh;
426         extract($this->_table_names);
427
428         $this->lock();
429         if ( ($id = $this->_get_pageid($pagename)) ) {
430             $dbh->query("DELETE FROM $version_tbl"
431                         . " WHERE id=$id AND version=$version");
432
433             $this->_update_recent_table($id);
434             // This shouldn't be needed (as long as the latestversion
435             // never gets deleted.)  But, let's be safe.
436             $this->_update_nonempty_table($id);
437         }
438         $this->unlock();
439     }
440
441     /**
442      * Delete page from the database with backup possibility.
443      * i.e save_page('') and DELETE nonempty id
444      * Can be undone and is seen in RecentChanges.
445      */
446     /* // see parent backend.php
447     function delete_page($pagename) {
448         $mtime = time();
449         $user =& $GLOBALS['request']->_user;
450         $vdata = array('author' => $user->getId(),
451                        'author_id' => $user->getAuthenticatedId(),
452                        'mtime' => $mtime);
453
454         $this->lock();
455         $version = $this->get_latest_version($pagename);
456         $this->set_versiondata($pagename, $version+1, $vdata);
457         $this->set_links($pagename, false);
458         $pagedata = get_pagedata($pagename);
459         $this->update_pagedata($pagename, array('hits' => $pagedata['hits']));
460         $this->unlock();
461     }
462     */
463
464     /**
465      * Delete page completely from the database.
466      * I'm not sure if this is what we want. Maybe just delete the revisions
467      */
468     function purge_page($pagename) {
469         $dbh = &$this->_dbh;
470         extract($this->_table_names);
471
472         $this->lock();
473         if ( ($id = $this->_get_pageid($pagename, false)) ) {
474             $dbh->query("DELETE FROM $nonempty_tbl WHERE id=$id");
475             $dbh->query("DELETE FROM $recent_tbl   WHERE id=$id");
476             $dbh->query("DELETE FROM $version_tbl  WHERE id=$id");
477             $dbh->query("DELETE FROM $link_tbl     WHERE linkfrom=$id");
478             $nlinks = $dbh->getOne("SELECT COUNT(*) FROM $link_tbl WHERE linkto=$id");
479             if ($nlinks) {
480                 // We're still in the link table (dangling link) so we can't delete this
481                 // altogether.
482                 $dbh->query("UPDATE $page_tbl SET hits=0, pagedata='' WHERE id=$id");
483                 $result = 0;
484             }
485             else {
486                 $dbh->query("DELETE FROM $page_tbl WHERE id=$id");
487                 $result = 1;
488             }
489             $this->_update_recent_table();
490             $this->_update_nonempty_table();
491         } else {
492             $result = -1; // already purged or not existing
493         }
494         $this->unlock();
495         return $result;
496     }
497
498     // The only thing we might be interested in updating which we can
499     // do fast in the flags (minor_edit).   I think the default
500     // update_versiondata will work fine...
501     //function update_versiondata($pagename, $version, $data) {
502     //}
503
504     function set_links($pagename, $links) {
505         // Update link table.
506         // FIXME: optimize: mysql can do this all in one big INSERT.
507
508         $dbh = &$this->_dbh;
509         extract($this->_table_names);
510
511         $this->lock();
512         $pageid = $this->_get_pageid($pagename, true);
513
514         $dbh->query("DELETE FROM $link_tbl WHERE linkfrom=$pageid");
515     if ($links) {
516         $linkseen = array();
517             foreach ($links as $link) {
518                 $linkto = $link['linkto'];
519                 if ($linkto === "") { // ignore attributes
520                     continue;
521                 }
522                 if (isset($link['relation']))
523                     $relation = $this->_get_pageid($link['relation'], true);
524                 else
525                     $relation = 0;
526                 // avoid duplicates
527                 if (isset($linkseen[$linkto]) and !$relation)
528                     continue;
529                 if (!$relation)
530                     $linkseen[$linkto] = true;
531                 $linkid = $this->_get_pageid($linkto, true);
532                   if (!$linkid) {
533                        echo("No link for $linkto on page $pagename");
534                        //printSimpleTrace(debug_backtrace());
535                        trigger_error("No link for $linkto on page $pagename");
536                 }
537                 assert($linkid);
538                 $dbh->query("INSERT INTO $link_tbl (linkfrom, linkto, relation)"
539                             . " VALUES ($pageid, $linkid, $relation)");
540             }
541         unset($linkseen);
542     }
543         $this->unlock();
544     }
545
546     /**
547      * Find pages which link to or are linked from a page.
548      *
549      * TESTME relations: get_links is responsible to add the relation to the pagehash
550      * as 'linkrelation' key as pagename. See WikiDB_PageIterator::next
551      *   if (isset($next['linkrelation']))
552      */
553     function get_links($pagename, $reversed=true, $include_empty=false,
554                        $sortby='', $limit='', $exclude='',
555                        $want_relations = false)
556     {
557         $dbh = &$this->_dbh;
558         extract($this->_table_names);
559
560         if ($reversed)
561             list($have,$want) = array('linkee', 'linker');
562         else
563             list($have,$want) = array('linker', 'linkee');
564         $orderby = $this->sortby($sortby, 'db', array('pagename'));
565         if ($orderby) $orderby = " ORDER BY $want." . $orderby;
566         if ($exclude) // array of pagenames
567             $exclude = " AND $want.pagename NOT IN ".$this->_sql_set($exclude);
568         else
569             $exclude='';
570
571         $qpagename = $dbh->escapeSimple($pagename);
572         $sql = "SELECT $want.id AS id, $want.pagename AS pagename, "
573             . ($want_relations ? " related.pagename as linkrelation" : " $want.hits AS hits")
574             . " FROM "
575             . (!$include_empty ? "$nonempty_tbl, " : '')
576             . " $page_tbl linkee, $page_tbl linker, $link_tbl "
577             . ($want_relations ? " JOIN $page_tbl related ON ($link_tbl.relation=related.id)" : '')
578             . " WHERE linkfrom=linker.id AND linkto=linkee.id"
579             . " AND $have.pagename='$qpagename'"
580             . (!$include_empty ? " AND $nonempty_tbl.id=$want.id" : "")
581             //. " GROUP BY $want.id"
582             . $exclude
583             . $orderby;
584         if ($limit) {
585             // extract from,count from limit
586             list($from,$count) = $this->limit($limit);
587             $result = $dbh->limitQuery($sql, $from, $count);
588         } else {
589             $result = $dbh->query($sql);
590         }
591
592         return new WikiDB_backend_PearDB_iter($this, $result);
593     }
594
595     /**
596      * Find if a page links to another page
597      */
598     function exists_link($pagename, $link, $reversed=false) {
599         $dbh = &$this->_dbh;
600         extract($this->_table_names);
601
602         if ($reversed)
603             list($have, $want) = array('linkee', 'linker');
604         else
605             list($have, $want) = array('linker', 'linkee');
606         $qpagename = $dbh->escapeSimple($pagename);
607         $qlink = $dbh->escapeSimple($link);
608         $row = $dbh->GetRow("SELECT CASE WHEN $want.pagename='$qlink' THEN 1 ELSE 0 END as result"
609                             . " FROM $link_tbl, $page_tbl linker, $page_tbl linkee, $nonempty_tbl"
610                             . " WHERE linkfrom=linker.id AND linkto=linkee.id"
611                             . " AND $have.pagename='$qpagename'"
612                             . " AND $want.pagename='$qlink'");
613         return $row['result'];
614     }
615
616     function get_all_pages($include_empty=false, $sortby='', $limit='', $exclude='') {
617         $dbh = &$this->_dbh;
618         extract($this->_table_names);
619         $orderby = $this->sortby($sortby, 'db');
620         if ($orderby) $orderby = ' ORDER BY ' . $orderby;
621         if ($exclude) // array of pagenames
622             $exclude = " AND $page_tbl.pagename NOT IN ".$this->_sql_set($exclude);
623         else
624             $exclude='';
625
626         if (strstr($orderby, 'mtime ')) { // multiple columns possible
627             if ($include_empty) {
628                 $sql = "SELECT "
629                     . $this->page_tbl_fields
630                     . " FROM $page_tbl, $recent_tbl, $version_tbl"
631                     . " WHERE $page_tbl.id=$recent_tbl.id"
632                     . " AND $page_tbl.id=$version_tbl.id AND latestversion=version"
633                     . $exclude
634                     . $orderby;
635             }
636             else {
637                 $sql = "SELECT "
638                     . $this->page_tbl_fields
639                     . " FROM $nonempty_tbl, $page_tbl, $recent_tbl, $version_tbl"
640                     . " WHERE $nonempty_tbl.id=$page_tbl.id"
641                     . " AND $page_tbl.id=$recent_tbl.id"
642                     . " AND $page_tbl.id=$version_tbl.id AND latestversion=version"
643                     . $exclude
644                     . $orderby;
645             }
646         } else {
647             if ($include_empty) {
648                 $sql = "SELECT "
649                     . $this->page_tbl_fields
650                     ." FROM $page_tbl"
651                     . ($exclude ? " WHERE $exclude" : '')
652                     . $orderby;
653             }
654             else {
655                 $sql = "SELECT "
656                     . $this->page_tbl_fields
657                     . " FROM $nonempty_tbl, $page_tbl"
658                     . " WHERE $nonempty_tbl.id=$page_tbl.id"
659                     . $exclude
660                     . $orderby;
661             }
662         }
663         if ($limit && $orderby) {
664             // extract from,count from limit
665             list($from,$count) = $this->limit($limit);
666             $result = $dbh->limitQuery($sql, $from, $count);
667             $options = array('limit_by_db' => 1);
668         } else {
669             $result = $dbh->query($sql);
670             $options = array('limit_by_db' => 0);
671         }
672         return new WikiDB_backend_PearDB_iter($this, $result, $options);
673     }
674
675     /**
676      * Title search.
677      * Todo: exclude
678      */
679     function text_search($search, $fulltext=false, $sortby='', $limit='',
680                          $exclude='')
681     {
682         $dbh = &$this->_dbh;
683         extract($this->_table_names);
684         $orderby = $this->sortby($sortby, 'db');
685         if ($orderby) $orderby = ' ORDER BY ' . $orderby;
686         //else " ORDER BY rank($field, to_tsquery('$searchon')) DESC";
687
688         $searchclass = get_class($this)."_search";
689         // no need to define it everywhere and then fallback. memory!
690         if (!class_exists($searchclass))
691             $searchclass = "WikiDB_backend_PearDB_search";
692         $searchobj = new $searchclass($search, $dbh);
693
694         $table = "$nonempty_tbl, $page_tbl";
695         $join_clause = "$nonempty_tbl.id=$page_tbl.id";
696         $fields = $this->page_tbl_fields;
697
698         if ($fulltext) {
699             $table .= ", $recent_tbl";
700             $join_clause .= " AND $page_tbl.id=$recent_tbl.id";
701
702             $table .= ", $version_tbl";
703             $join_clause .= " AND $page_tbl.id=$version_tbl.id AND latestversion=version";
704
705             $fields .= ", $page_tbl.pagedata as pagedata, " . $this->version_tbl_fields;
706             $callback = new WikiMethodCb($searchobj, "_fulltext_match_clause");
707         } else {
708             $callback = new WikiMethodCb($searchobj, "_pagename_match_clause");
709         }
710         $search_clause = $search->makeSqlClauseObj($callback);
711
712         $sql = "SELECT $fields FROM $table"
713             . " WHERE $join_clause"
714             . "  AND ($search_clause)"
715             . $orderby;
716          if ($limit) {
717              list($from, $count) = $this->limit($limit);
718              $result = $dbh->limitQuery($sql, $from, $count);
719          } else {
720              $result = $dbh->query($sql);
721          }
722
723         $iter = new WikiDB_backend_PearDB_iter($this, $result);
724         $iter->stoplisted = @$searchobj->stoplisted;
725         return $iter;
726     }
727
728     //Todo: check if the better Mysql MATCH operator is supported,
729     // (ranked search) and also google like expressions.
730     function _sql_match_clause($word) {
731         $word = preg_replace('/(?=[%_\\\\])/', "\\", $word);
732         $word = $this->_dbh->escapeSimple($word);
733         //$page_tbl = $this->_table_names['page_tbl'];
734         //Note: Mysql 4.1.0 has a bug which fails with binary fields.
735         //      e.g. if word is lowercased.
736         // http://bugs.mysql.com/bug.php?id=1491
737         return "LOWER(pagename) LIKE '%$word%'";
738     }
739     function _sql_casematch_clause($word) {
740         $word = preg_replace('/(?=[%_\\\\])/', "\\", $word);
741         $word = $this->_dbh->escapeSimple($word);
742         return "pagename LIKE '%$word%'";
743     }
744     function _fullsearch_sql_match_clause($word) {
745         $word = preg_replace('/(?=[%_\\\\])/', "\\", $word);
746         $word = $this->_dbh->escapeSimple($word);
747         //$page_tbl = $this->_table_names['page_tbl'];
748         //Mysql 4.1.1 has a bug which fails here if word is lowercased.
749         return "LOWER(pagename) LIKE '%$word%' OR content LIKE '%$word%'";
750     }
751     function _fullsearch_sql_casematch_clause($word) {
752         $word = preg_replace('/(?=[%_\\\\])/', "\\", $word);
753         $word = $this->_dbh->escapeSimple($word);
754         return "pagename LIKE '%$word%' OR content LIKE '%$word%'";
755     }
756
757     /**
758      * Find highest or lowest hit counts.
759      */
760     function most_popular($limit=20, $sortby='-hits') {
761         $dbh = &$this->_dbh;
762         extract($this->_table_names);
763         if ($limit < 0){
764             $order = "hits ASC";
765             $limit = -$limit;
766             $where = "";
767         } else {
768             $order = "hits DESC";
769             $where = " AND hits > 0";
770         }
771         $orderby = '';
772         if ($sortby != '-hits') {
773             if ($order = $this->sortby($sortby, 'db'))
774                 $orderby = " ORDER BY " . $order;
775         } else {
776             $orderby = " ORDER BY $order";
777         }
778         //$limitclause = $limit ? " LIMIT $limit" : '';
779         $sql = "SELECT "
780             . $this->page_tbl_fields
781             . " FROM $nonempty_tbl, $page_tbl"
782             . " WHERE $nonempty_tbl.id=$page_tbl.id"
783             . $where
784             . $orderby;
785          if ($limit) {
786              list($from, $count) = $this->limit($limit);
787              $result = $dbh->limitQuery($sql, $from, $count);
788          } else {
789              $result = $dbh->query($sql);
790          }
791
792         return new WikiDB_backend_PearDB_iter($this, $result);
793     }
794
795     /**
796      * Find recent changes.
797      */
798     function most_recent($params) {
799         $limit = 0;
800         $since = 0;
801         $include_minor_revisions = false;
802         $exclude_major_revisions = false;
803         $include_all_revisions = false;
804         extract($params);
805
806         $dbh = &$this->_dbh;
807         extract($this->_table_names);
808
809         $pick = array();
810         if ($since)
811             $pick[] = "mtime >= $since";
812
813         if ($include_all_revisions) {
814             // Include all revisions of each page.
815             $table = "$page_tbl, $version_tbl";
816             $join_clause = "$page_tbl.id=$version_tbl.id";
817
818             if ($exclude_major_revisions) {
819         // Include only minor revisions
820                 $pick[] = "minor_edit <> 0";
821             }
822             elseif (!$include_minor_revisions) {
823         // Include only major revisions
824                 $pick[] = "minor_edit = 0";
825             }
826         }
827         else {
828             $table = "$page_tbl, $recent_tbl";
829             $join_clause = "$page_tbl.id=$recent_tbl.id";
830             $table .= ", $version_tbl";
831             $join_clause .= " AND $version_tbl.id=$page_tbl.id";
832
833             if ($exclude_major_revisions) {
834                 // Include only most recent minor revision
835                 $pick[] = 'version=latestminor';
836             }
837             elseif (!$include_minor_revisions) {
838                 // Include only most recent major revision
839                 $pick[] = 'version=latestmajor';
840             }
841             else {
842                 // Include only the latest revision (whether major or minor).
843                 $pick[] ='version=latestversion';
844             }
845         }
846         $order = "DESC";
847         if($limit < 0){
848             $order = "ASC";
849             $limit = -$limit;
850         }
851         // $limitclause = $limit ? " LIMIT $limit" : '';
852         $where_clause = $join_clause;
853         if ($pick)
854             $where_clause .= " AND " . join(" AND ", $pick);
855
856         // FIXME: use SQL_BUFFER_RESULT for mysql?
857         $sql = "SELECT "
858                . $this->page_tbl_fields . ", " . $this->version_tbl_fields
859                . " FROM $table"
860                . " WHERE $where_clause"
861                . " ORDER BY mtime $order";
862         if ($limit) {
863              list($from, $count) = $this->limit($limit);
864              $result = $dbh->limitQuery($sql, $from, $count);
865         } else {
866             $result = $dbh->query($sql);
867         }
868         return new WikiDB_backend_PearDB_iter($this, $result);
869     }
870
871     /**
872      * Find referenced empty pages.
873      */
874     function wanted_pages($exclude_from='', $exclude='', $sortby='', $limit='') {
875         $dbh = &$this->_dbh;
876         extract($this->_table_names);
877         if ($orderby = $this->sortby($sortby, 'db', array('pagename','wantedfrom')))
878             $orderby = 'ORDER BY ' . $orderby;
879
880         if ($exclude_from) // array of pagenames
881             $exclude_from = " AND pp.pagename NOT IN ".$this->_sql_set($exclude_from);
882         if ($exclude) // array of pagenames
883             $exclude = " AND p.pagename NOT IN ".$this->_sql_set($exclude);
884         $sql = "SELECT p.pagename, pp.pagename AS wantedfrom"
885             . " FROM $page_tbl p, $link_tbl linked"
886             .   " LEFT JOIN $page_tbl pp ON linked.linkto = pp.id"
887             .   " LEFT JOIN $nonempty_tbl ne ON linked.linkto = ne.id"
888             . " WHERE ne.id IS NULL"
889             .       " AND p.id = linked.linkfrom"
890             . $exclude_from
891             . $exclude
892             . $orderby;
893         if ($limit) {
894             // oci8 error: WHERE NULL = NULL appended
895             list($from, $count) = $this->limit($limit);
896             $result = $dbh->limitQuery($sql, $from, $count * 3);
897         } else {
898             $result = $dbh->query($sql);
899         }
900         return new WikiDB_backend_PearDB_generic_iter($this, $result);
901     }
902
903     function _sql_set(&$pagenames) {
904         $s = '(';
905         foreach ($pagenames as $p) {
906             $s .= ("'".$this->_dbh->escapeSimple($p)."',");
907         }
908         return substr($s,0,-1).")";
909     }
910
911     /**
912      * Rename page in the database.
913      */
914     function rename_page ($pagename, $to) {
915         $dbh = &$this->_dbh;
916         extract($this->_table_names);
917
918         $this->lock();
919         if (($id = $this->_get_pageid($pagename, false)) ) {
920             if ($new = $this->_get_pageid($to, false)) {
921                 // Cludge Alert!
922                 // This page does not exist (already verified before), but exists in the page table.
923                 // So we delete this page.
924                 $dbh->query("DELETE FROM $nonempty_tbl WHERE id=$new");
925                 $dbh->query("DELETE FROM $recent_tbl WHERE id=$new");
926                 $dbh->query("DELETE FROM $version_tbl WHERE id=$new");
927                 // We have to fix all referring tables to the old id
928                 $dbh->query("UPDATE $link_tbl SET linkfrom=$id WHERE linkfrom=$new");
929                 $dbh->query("UPDATE $link_tbl SET linkto=$id WHERE linkto=$new");
930                 $dbh->query("DELETE FROM $page_tbl WHERE id=$new");
931             }
932             $dbh->query(sprintf("UPDATE $page_tbl SET pagename='%s' WHERE id=$id",
933                                 $dbh->escapeSimple($to)));
934         }
935         $this->unlock();
936         return $id;
937     }
938
939     function _update_recent_table($pageid = false) {
940         $dbh = &$this->_dbh;
941         extract($this->_table_names);
942         extract($this->_expressions);
943
944         $pageid = (int)$pageid;
945
946         $this->lock();
947         $dbh->query("DELETE FROM $recent_tbl"
948                     . ( $pageid ? " WHERE id=$pageid" : ""));
949         $dbh->query( "INSERT INTO $recent_tbl"
950                      . " (id, latestversion, latestmajor, latestminor)"
951                      . " SELECT id, $maxversion, $maxmajor, $maxminor"
952                      . " FROM $version_tbl"
953                      . ( $pageid ? " WHERE id=$pageid" : "")
954                      . " GROUP BY id" );
955         $this->unlock();
956     }
957
958     function _update_nonempty_table($pageid = false) {
959         $dbh = &$this->_dbh;
960         extract($this->_table_names);
961
962         $pageid = (int)$pageid;
963
964         extract($this->_expressions);
965         $this->lock();
966         $dbh->query("DELETE FROM $nonempty_tbl"
967                     . ( $pageid ? " WHERE id=$pageid" : ""));
968         $dbh->query("INSERT INTO $nonempty_tbl (id)"
969                     . " SELECT $recent_tbl.id"
970                     . " FROM $recent_tbl, $version_tbl"
971                     . " WHERE $recent_tbl.id=$version_tbl.id"
972                     . "       AND version=latestversion"
973                     // We have some specifics here (Oracle)
974                     //. "  AND content<>''"
975                     . "  AND content $notempty"
976                     . ( $pageid ? " AND $recent_tbl.id=$pageid" : ""));
977
978         $this->unlock();
979     }
980
981     /**
982      * Grab a write lock on the tables in the SQL database.
983      *
984      * Calls can be nested.  The tables won't be unlocked until
985      * _unlock_database() is called as many times as _lock_database().
986      *
987      * @access protected
988      */
989     function lock($tables = false, $write_lock = true) {
990         if ($this->_lock_count++ == 0)
991             $this->_lock_tables($write_lock);
992     }
993
994     /**
995      * Actually lock the required tables.
996      */
997     function _lock_tables($write_lock) {
998         trigger_error("virtual", E_USER_ERROR);
999     }
1000
1001     /**
1002      * Release a write lock on the tables in the SQL database.
1003      *
1004      * @access protected
1005      *
1006      * @param $force boolean Unlock even if not every call to lock() has been matched
1007      * by a call to unlock().
1008      *
1009      * @see _lock_database
1010      */
1011     function unlock($tables = false, $force = false) {
1012         if ($this->_lock_count == 0)
1013             return;
1014         if (--$this->_lock_count <= 0 || $force) {
1015             $this->_unlock_tables();
1016             $this->_lock_count = 0;
1017         }
1018     }
1019
1020     /**
1021      * Actually unlock the required tables.
1022      */
1023     function _unlock_tables($write_lock) {
1024         trigger_error("virtual", E_USER_ERROR);
1025     }
1026
1027
1028     /**
1029      * Serialize data
1030      */
1031     function _serialize($data) {
1032         if (empty($data))
1033             return '';
1034         assert(is_array($data));
1035         return serialize($data);
1036     }
1037
1038     /**
1039      * Unserialize data
1040      */
1041     function _unserialize($data) {
1042         return empty($data) ? array() : unserialize($data);
1043     }
1044
1045     /**
1046      * Callback for PEAR (DB) errors.
1047      *
1048      * @access protected
1049      *
1050      * @param A PEAR_error object.
1051      */
1052     function _pear_error_callback($error) {
1053         if ($this->_is_false_error($error))
1054             return;
1055
1056         $this->_dbh->setErrorHandling(PEAR_ERROR_PRINT);        // prevent recursive loops.
1057         $this->close();
1058         trigger_error($this->_pear_error_message($error), E_USER_ERROR);
1059     }
1060
1061     /**
1062      * Detect false errors messages from PEAR DB.
1063      *
1064      * The version of PEAR DB which ships with PHP 4.0.6 has a bug in that
1065      * it doesn't recognize "LOCK" and "UNLOCK" as SQL commands which don't
1066      * return any data.  (So when a "LOCK" command doesn't return any data,
1067      * DB reports it as an error, when in fact, it's not.)
1068      *
1069      * @access private
1070      * @return bool True iff error is not really an error.
1071      */
1072     function _is_false_error($error) {
1073         if ($error->getCode() != DB_ERROR)
1074             return false;
1075
1076         $query = $this->_dbh->last_query;
1077
1078         if (! preg_match('/^\s*"?(INSERT|UPDATE|DELETE|REPLACE|CREATE'
1079                          . '|DROP|ALTER|GRANT|REVOKE|LOCK|UNLOCK)\s/', $query)) {
1080             // Last query was not of the sort which doesn't return any data.
1081             //" <--kludge for brain-dead syntax coloring
1082             return false;
1083         }
1084
1085         if (! in_array('ismanip', get_class_methods('DB'))) {
1086             // Pear shipped with PHP 4.0.4pl1 (and before, presumably)
1087             // does not have the DB::isManip method.
1088             return true;
1089         }
1090
1091         if (DB::isManip($query)) {
1092             // If Pear thinks it's an isManip then it wouldn't have thrown
1093             // the error we're testing for....
1094             return false;
1095         }
1096
1097         return true;
1098     }
1099
1100     function _pear_error_message($error) {
1101         $class = get_class($this);
1102         $message = "$class: fatal database error\n"
1103              . "\t" . $error->getMessage() . "\n"
1104              . "\t(" . $error->getDebugInfo() . ")\n";
1105
1106         // Prevent password from being exposed during a connection error
1107         $safe_dsn = preg_replace('| ( :// .*? ) : .* (?=@) |xs',
1108                                  '\\1:XXXXXXXX', $this->_dsn);
1109         return str_replace($this->_dsn, $safe_dsn, $message);
1110     }
1111
1112     /**
1113      * Filter PHP errors notices from PEAR DB code.
1114      *
1115      * The PEAR DB code which ships with PHP 4.0.6 produces spurious
1116      * errors and notices.  This is an error callback (for use with
1117      * ErrorManager which will filter out those spurious messages.)
1118      * @see _is_false_error, ErrorManager
1119      * @access private
1120      */
1121     function _pear_notice_filter($err) {
1122         return ( $err->isNotice()
1123                  && preg_match('|DB[/\\\\]common.php$|', $err->errfile)
1124                  && $err->errline == 126
1125                  && preg_match('/Undefined offset: +0\b/', $err->errstr) );
1126     }
1127
1128     /* some variables and functions for DB backend abstraction (action=upgrade) */
1129     function database () {
1130         return $this->_dbh->dsn['database'];
1131     }
1132     function backendType() {
1133         return $this->_dbh->phptype;
1134     }
1135     function connection() {
1136         return $this->_dbh->connection;
1137     }
1138     function getRow($query) {
1139         return $this->_dbh->getRow($query);
1140     }
1141
1142     function listOfTables() {
1143         return $this->_dbh->getListOf('tables');
1144     }
1145     function listOfFields($database,$table) {
1146         if ($this->backendType() == 'mysql') {
1147             $fields = array();
1148             assert(!empty($database));
1149             assert(!empty($table));
1150           $result = mysql_list_fields($database, $table, $this->_dbh->connection) or
1151               trigger_error(__FILE__.':'.__LINE__.' '.mysql_error(), E_USER_WARNING);
1152           if (!$result) return array();
1153               $columns = mysql_num_fields($result);
1154             for ($i = 0; $i < $columns; $i++) {
1155                 $fields[] = mysql_field_name($result, $i);
1156             }
1157             mysql_free_result($result);
1158             return $fields;
1159         } else {
1160             // TODO: try ADODB version?
1161             trigger_error("Unsupported dbtype and backend. Either switch to ADODB or check it manually.");
1162             return false;
1163         }
1164     }
1165 };
1166
1167 /**
1168  * This class is a generic iterator.
1169  *
1170  * WikiDB_backend_PearDB_iter only iterates over things that have
1171  * 'pagename', 'pagedata', etc. etc.
1172  *
1173  * Probably WikiDB_backend_PearDB_iter and this class should be merged
1174  * (most of the code is cut-and-paste :-( ), but I am trying to make
1175  * changes that could be merged easily.
1176  *
1177  * @author: Dan Frankowski
1178  */
1179 class WikiDB_backend_PearDB_generic_iter
1180 extends WikiDB_backend_iterator
1181 {
1182     function WikiDB_backend_PearDB_generic_iter($backend, $query_result, $field_list = NULL) {
1183         if (DB::isError($query_result)) {
1184             // This shouldn't happen, I thought.
1185             $backend->_pear_error_callback($query_result);
1186         }
1187
1188         $this->_backend = &$backend;
1189         $this->_result = $query_result;
1190         $this->_options = $field_list;
1191     }
1192
1193     function count() {
1194         if (!$this->_result)
1195             return false;
1196         return $this->_result->numRows();
1197     }
1198
1199     function next() {
1200         if (!$this->_result)
1201             return false;
1202
1203         $record = $this->_result->fetchRow(DB_FETCHMODE_ASSOC);
1204         if (!$record) {
1205             $this->free();
1206             return false;
1207         }
1208
1209         return $record;
1210     }
1211
1212     function reset () {
1213         if ($this->_result) {
1214             $this->_result->MoveFirst();
1215         }
1216     }
1217
1218     function free () {
1219         if ($this->_result) {
1220             $this->_result->free();
1221             $this->_result = false;
1222         }
1223     }
1224
1225     function asArray () {
1226         $result = array();
1227         while ($page = $this->next())
1228             $result[] = $page;
1229         return $result;
1230     }
1231 }
1232
1233 class WikiDB_backend_PearDB_iter
1234 extends WikiDB_backend_PearDB_generic_iter
1235 {
1236
1237     function next() {
1238         $backend = &$this->_backend;
1239         if (!$this->_result)
1240             return false;
1241
1242         $record = $this->_result->fetchRow(DB_FETCHMODE_ASSOC);
1243         if (!$record) {
1244             $this->free();
1245             return false;
1246         }
1247
1248         $pagedata = $backend->_extract_page_data($record);
1249         $rec = array('pagename' => $record['pagename'],
1250                      'pagedata' => $pagedata);
1251
1252         if (!empty($record['version'])) {
1253             $rec['versiondata'] = $backend->_extract_version_data($record);
1254             $rec['version'] = $record['version'];
1255         }
1256
1257         return $rec;
1258     }
1259 }
1260
1261 class WikiDB_backend_PearDB_search extends WikiDB_backend_search_sql
1262 {
1263     // no surrounding quotes because we know it's a string
1264     // function _quote($word) { return $this->_dbh->addq($word); }
1265 }
1266
1267 // Local Variables:
1268 // mode: php
1269 // tab-width: 8
1270 // c-basic-offset: 4
1271 // c-hanging-comment-ender-p: nil
1272 // indent-tabs-mode: nil
1273 // End:
1274 ?>