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