]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend/ADODB.php
Add comments
[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     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;
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     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 = (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     // The only thing we might be interested in updating which we can
594     // do fast in the flags (minor_edit).   I think the default
595     // update_versiondata will work fine...
596     //function update_versiondata($pagename, $version, $data) {
597     //}
598
599     /*
600      * Update link table.
601      * on DEBUG: delete old, deleted links from page
602      */
603     function set_links($pagename, $links)
604     {
605         // Update link table.
606         // FIXME: optimize: mysql can do this all in one big INSERT/REPLACE.
607
608         $dbh = &$this->_dbh;
609         extract($this->_table_names);
610
611         $this->lock(array('link'));
612         $pageid = $this->_get_pageid($pagename, true);
613
614         $oldlinks = $dbh->getAssoc("SELECT $link_tbl.linkto as id, page.pagename FROM $link_tbl"
615             . " JOIN page ON ($link_tbl.linkto = page.id)"
616             . " WHERE linkfrom=$pageid");
617         // Delete current links,
618         $dbh->Execute("DELETE FROM $link_tbl WHERE linkfrom=$pageid");
619         // and insert new links. Faster than checking for all single links
620         if ($links) {
621             foreach ($links as $link) {
622                 $linkto = $link['linkto'];
623                 if (isset($link['relation']))
624                     $relation = $this->_get_pageid($link['relation'], true);
625                 else
626                     $relation = 0;
627                 if ($linkto === "") { // ignore attributes
628                     continue;
629                 }
630                 // avoid duplicates
631                 if (isset($linkseen[$linkto]) and !$relation) {
632                     continue;
633                 }
634                 if (!$relation) {
635                     $linkseen[$linkto] = true;
636                 }
637                 $linkid = $this->_get_pageid($linkto, true);
638                 assert($linkid);
639                 if ($relation) {
640                     $dbh->Execute("INSERT INTO $link_tbl (linkfrom, linkto, relation)"
641                         . " VALUES ($pageid, $linkid, $relation)");
642                 } else {
643                     $dbh->Execute("INSERT INTO $link_tbl (linkfrom, linkto)"
644                         . " VALUES ($pageid, $linkid)");
645                 }
646                 if ($oldlinks and array_key_exists($linkid, $oldlinks)) {
647                     // This was also in the previous page
648                     unset($oldlinks[$linkid]);
649                 }
650             }
651         }
652         // purge page table: delete all non-referenced pages
653         // for all previously linked pages, which have no other linkto links
654         if (DEBUG and $oldlinks) {
655             // trigger_error("purge page table: delete all non-referenced pages...", E_USER_NOTICE);
656             foreach ($oldlinks as $id => $name) {
657                 // ...check if the page is empty and has no version
658                 $result = $dbh->getRow("SELECT $page_tbl.id FROM $page_tbl"
659                     . " LEFT JOIN $nonempty_tbl USING (id) "
660                     . " LEFT JOIN $version_tbl USING (id)"
661                     . " WHERE $nonempty_tbl.id is NULL"
662                     . " AND $version_tbl.id is NULL"
663                     . " AND $page_tbl.id=$id");
664                 $linkto = $dbh->getRow("SELECT linkfrom FROM $link_tbl WHERE linkto=$id");
665                 if ($result and empty($linkto)) {
666                     trigger_error("delete empty and non-referenced link $name ($id)", E_USER_NOTICE);
667                     $dbh->Execute("DELETE FROM $recent_tbl WHERE id=$id"); // may fail
668                     $dbh->Execute("DELETE FROM $link_tbl WHERE linkto=$id");
669                     $dbh->Execute("DELETE FROM $page_tbl WHERE id=$id"); // this purges the link
670                 }
671             }
672         }
673         $this->unlock(array('link'));
674         return true;
675     }
676
677     /* get all oldlinks in hash => id, relation
678        check for all new links
679      */
680     function set_links1($pagename, $links)
681     {
682
683         $dbh = &$this->_dbh;
684         extract($this->_table_names);
685
686         $this->lock(array('link'));
687         $pageid = $this->_get_pageid($pagename, true);
688
689         $oldlinks = $dbh->getAssoc("SELECT $link_tbl.linkto as linkto, $link_tbl.relation, page.pagename"
690             . " FROM $link_tbl"
691             . " JOIN page ON ($link_tbl.linkto = page.id)"
692             . " WHERE linkfrom=$pageid");
693         /*      old                  new
694          *      X => [1,0 2,0 1,1]   X => [1,1 3,0]
695          * => delete 1,0 2,0 + insert 3,0
696          */
697         if ($links) {
698             foreach ($links as $link) {
699                 $linkto = $link['linkto'];
700                 if ($link['relation'])
701                     $relation = $this->_get_pageid($link['relation'], true);
702                 else
703                     $relation = 0;
704                 // avoid duplicates
705                 if (isset($linkseen[$linkto]) and !$relation) {
706                     continue;
707                 }
708                 if (!$relation) {
709                     $linkseen[$linkto] = true;
710                 }
711                 $linkid = $this->_get_pageid($linkto, true);
712                 assert($linkid);
713                 $skip = 0;
714                 // find linkfrom,linkto,relation triple in oldlinks
715                 foreach ($oldlinks as $l) {
716                     if ($relation) { // relation NOT NULL
717                         if ($l['linkto'] == $linkid and $l['relation'] == $relation) {
718                             // found and skip
719                             $skip = 1;
720                         }
721                     }
722                 }
723                 if (!$skip) {
724                     if ($update) {
725                     }
726                     if ($relation) {
727                         $dbh->Execute("INSERT INTO $link_tbl (linkfrom, linkto, relation)"
728                             . " VALUES ($pageid, $linkid, $relation)");
729                     } else {
730                         $dbh->Execute("INSERT INTO $link_tbl (linkfrom, linkto)"
731                             . " VALUES ($pageid, $linkid)");
732                     }
733                 }
734
735                 if (array_key_exists($linkid, $oldlinks)) {
736                     // This was also in the previous page
737                     unset($oldlinks[$linkid]);
738                 }
739             }
740         }
741         // purge page table: delete all non-referenced pages
742         // for all previously linked pages...
743         if (DEBUG and $oldlinks) {
744             // trigger_error("purge page table: delete all non-referenced pages...", E_USER_NOTICE);
745             foreach ($oldlinks as $id => $name) {
746                 // ...check if the page is empty and has no version
747                 if ($dbh->getRow("SELECT $page_tbl.id FROM $page_tbl"
748                     . " LEFT JOIN $nonempty_tbl USING (id) "
749                     . " LEFT JOIN $version_tbl USING (id)"
750                     . " WHERE $nonempty_tbl.id is NULL"
751                     . " AND $version_tbl.id is NULL"
752                     . " AND $page_tbl.id=$id")
753                 ) {
754                     trigger_error("delete empty and non-referenced link $name ($id)", E_USER_NOTICE);
755                     $dbh->Execute("DELETE FROM $page_tbl WHERE id=$id"); // this purges the link
756                     $dbh->Execute("DELETE FROM $recent_tbl WHERE id=$id"); // may fail
757                 }
758             }
759         }
760         $this->unlock(array('link'));
761         return true;
762     }
763
764     /*
765      * Find pages which link to or are linked from a page.
766      *
767      * Optimization: save request->_dbi->_iwpcache[] to avoid further iswikipage checks
768      * (linkExistingWikiWord or linkUnknownWikiWord)
769      * This is called on every page header GleanDescription, so we can store all the
770      * existing links.
771      *
772      * relations: $backend->get_links is responsible to add the relation to the pagehash
773      * as 'linkrelation' key as pagename. See WikiDB_PageIterator::next
774      *   if (isset($next['linkrelation']))
775      */
776     function get_links($pagename, $reversed = true, $include_empty = false,
777                        $sortby = '', $limit = '', $exclude = '',
778                        $want_relations = false)
779     {
780         $dbh = &$this->_dbh;
781         extract($this->_table_names);
782
783         if ($reversed)
784             list($have, $want) = array('linkee', 'linker');
785         else
786             list($have, $want) = array('linker', 'linkee');
787         $orderby = $this->sortby($sortby, 'db', array('pagename'));
788         if ($orderby) $orderby = " ORDER BY $want." . $orderby;
789         if ($exclude) // array of pagenames
790             $exclude = " AND $want.pagename NOT IN " . $this->_sql_set($exclude);
791         else
792             $exclude = '';
793
794         $qpagename = $dbh->qstr($pagename);
795         // removed ref to FETCH_MODE in next line
796         $sql = "SELECT $want.id AS id, $want.pagename AS pagename, "
797             . ($want_relations ? " related.pagename as linkrelation" : " $want.hits AS hits")
798             . " FROM "
799             . (!$include_empty ? "$nonempty_tbl, " : '')
800             . " $page_tbl linkee, $page_tbl linker, $link_tbl "
801             . ($want_relations ? " JOIN $page_tbl related ON ($link_tbl.relation=related.id)" : '')
802             . " WHERE linkfrom=linker.id AND linkto=linkee.id"
803             . " AND $have.pagename=$qpagename"
804             . (!$include_empty ? " AND $nonempty_tbl.id=$want.id" : "")
805             //. " GROUP BY $want.id"
806             . $exclude
807             . $orderby;
808         /*
809           echo "SELECT linkee.id AS id, linkee.pagename AS pagename, related.pagename as linkrelation FROM link, page linkee, page linker JOIN page related ON (link.relation=related.id) WHERE linkfrom=linker.id AND linkto=linkee.id AND linker.pagename='SanDiego'" | mysql phpwiki
810         id      pagename        linkrelation
811         2268    California      located_in
812         */
813         if ($limit) {
814             // extract from,count from limit
815             list($offset, $count) = $this->limit($limit);
816             $result = $dbh->SelectLimit($sql, $count, $offset);
817         } else {
818             $result = $dbh->Execute($sql);
819         }
820         $fields = $this->links_field_list;
821         if ($want_relations) // instead of hits
822             $fields[2] = 'linkrelation';
823         return new WikiDB_backend_ADODB_iter($this, $result, $fields);
824     }
825
826     /*
827      * Find if a page links to another page
828      */
829     function exists_link($pagename, $link, $reversed = false)
830     {
831         $dbh = &$this->_dbh;
832         extract($this->_table_names);
833
834         if ($reversed)
835             list($have, $want) = array('linkee', 'linker');
836         else
837             list($have, $want) = array('linker', 'linkee');
838         $qpagename = $dbh->qstr($pagename);
839         $qlink = $dbh->qstr($link);
840         $row = $dbh->GetRow("SELECT CASE WHEN $want.pagename=$qlink THEN 1 ELSE 0 END"
841             . " FROM $link_tbl, $page_tbl linker, $page_tbl linkee, $nonempty_tbl"
842             . " WHERE linkfrom=linker.id AND linkto=linkee.id"
843             . " AND $have.pagename=$qpagename"
844             . " AND $want.pagename=$qlink");
845         return $row[0];
846     }
847
848     public function get_all_pages($include_empty = false,
849                                   $sortby = '', $limit = '', $exclude = '')
850     {
851         $dbh = &$this->_dbh;
852         extract($this->_table_names);
853         $orderby = $this->sortby($sortby, 'db');
854         if ($orderby) $orderby = ' ORDER BY ' . $orderby;
855         $and = '';
856         if ($exclude) { // array of pagenames
857             $and = ' AND ';
858             $exclude = " $page_tbl.pagename NOT IN " . $this->_sql_set($exclude);
859         } else {
860             $exclude = '';
861         }
862
863         //$dbh->SetFetchMode(ADODB_FETCH_ASSOC);
864         if (strstr($orderby, 'mtime ')) { // was ' mtime'
865             if ($include_empty) {
866                 $sql = "SELECT "
867                     . $this->page_tbl_fields
868                     . " FROM $page_tbl, $recent_tbl, $version_tbl"
869                     . " WHERE $page_tbl.id=$recent_tbl.id"
870                     . " AND $page_tbl.id=$version_tbl.id AND latestversion=version"
871                     . " $and$exclude"
872                     . $orderby;
873             } else {
874                 $sql = "SELECT "
875                     . $this->page_tbl_fields
876                     . " FROM $nonempty_tbl, $page_tbl, $recent_tbl, $version_tbl"
877                     . " WHERE $nonempty_tbl.id=$page_tbl.id"
878                     . " AND $page_tbl.id=$recent_tbl.id"
879                     . " AND $page_tbl.id=$version_tbl.id AND latestversion=version"
880                     . " $and$exclude"
881                     . $orderby;
882             }
883         } else {
884             if ($include_empty) {
885                 $sql = "SELECT "
886                     . $this->page_tbl_fields
887                     . " FROM $page_tbl"
888                     . ($exclude ? " WHERE $exclude" : '')
889                     . $orderby;
890             } else {
891                 $sql = "SELECT "
892                     . $this->page_tbl_fields
893                     . " FROM $nonempty_tbl, $page_tbl"
894                     . " WHERE $nonempty_tbl.id=$page_tbl.id"
895                     . " $and$exclude"
896                     . $orderby;
897             }
898         }
899         if ($limit) {
900             // extract from,count from limit
901             list($offset, $count) = $this->limit($limit);
902             $result = $dbh->SelectLimit($sql, $count, $offset);
903         } else {
904             $result = $dbh->Execute($sql);
905         }
906         //$dbh->SetFetchMode(ADODB_FETCH_NUM);
907         return new WikiDB_backend_ADODB_iter($this, $result, $this->page_tbl_field_list);
908     }
909
910     /*
911      * Title and fulltext search.
912      */
913     public function text_search($search, $fulltext = false,
914                                 $sortby = '', $limit = '', $exclude = '')
915     {
916         $dbh = &$this->_dbh;
917         extract($this->_table_names);
918         $orderby = $this->sortby($sortby, 'db');
919         if ($orderby) $orderby = ' ORDER BY ' . $orderby;
920
921         $table = "$nonempty_tbl, $page_tbl";
922         $join_clause = "$nonempty_tbl.id=$page_tbl.id";
923         $fields = $this->page_tbl_fields;
924         $field_list = $this->page_tbl_field_list;
925         $searchobj = new WikiDB_backend_ADODB_search($search, $dbh);
926
927         if ($fulltext) {
928             $table .= ", $recent_tbl";
929             $join_clause .= " AND $page_tbl.id=$recent_tbl.id";
930
931             $table .= ", $version_tbl";
932             $join_clause .= " AND $page_tbl.id=$version_tbl.id AND latestversion=version";
933
934             $fields .= ",$page_tbl.pagedata as pagedata," . $this->version_tbl_fields;
935             $field_list = array_merge($field_list, array('pagedata'),
936                 $this->version_tbl_field_list);
937             $callback = new WikiMethodCb($searchobj, "_fulltext_match_clause");
938         } else {
939             $callback = new WikiMethodCb($searchobj, "_pagename_match_clause");
940         }
941         $search_clause = $search->makeSqlClauseObj($callback);
942         $sql = "SELECT $fields FROM $table"
943             . " WHERE $join_clause"
944             . "  AND ($search_clause)"
945             . $orderby;
946         if ($limit) {
947             // extract from,count from limit
948             list($offset, $count) = $this->limit($limit);
949             $result = $dbh->SelectLimit($sql, $count, $offset);
950         } else {
951             $result = $dbh->Execute($sql);
952         }
953         $iter = new WikiDB_backend_ADODB_iter($this, $result, $field_list);
954         if ($fulltext)
955             $iter->stoplisted = $searchobj->stoplisted;
956         return $iter;
957     }
958
959     /*
960      * TODO: efficiently handle wildcards exclusion: exclude=Php* => 'Php%',
961      *       not sets. See above, but the above methods find too much.
962      * This is only for already resolved wildcards:
963      * " WHERE $page_tbl.pagename NOT IN ".$this->_sql_set(array('page1','page2'));
964      */
965     function _sql_set(&$pagenames)
966     {
967         $s = '(';
968         foreach ($pagenames as $p) {
969             $s .= ($this->_dbh->qstr($p) . ",");
970         }
971         return substr($s, 0, -1) . ")";
972     }
973
974     /*
975      * Find highest or lowest hit counts.
976      */
977     public function most_popular($limit = 20, $sortby = '-hits')
978     {
979         $dbh = &$this->_dbh;
980         extract($this->_table_names);
981         $order = "DESC";
982         if ($limit < 0) {
983             $order = "ASC";
984             $limit = -$limit;
985             $where = "";
986         } else {
987             $where = " AND hits > 0";
988         }
989         if ($sortby != '-hits') {
990             if ($order = $this->sortby($sortby, 'db')) $orderby = " ORDER BY " . $order;
991             else $orderby = "";
992         } else
993             $orderby = " ORDER BY hits $order";
994         $sql = "SELECT "
995             . $this->page_tbl_fields
996             . " FROM $nonempty_tbl, $page_tbl"
997             . " WHERE $nonempty_tbl.id=$page_tbl.id"
998             . $where
999             . $orderby;
1000         if ($limit) {
1001             // extract from,count from limit
1002             list($offset, $count) = $this->limit($limit);
1003             $result = $dbh->SelectLimit($sql, $count, $offset);
1004         } else {
1005             $result = $dbh->Execute($sql);
1006         }
1007         return new WikiDB_backend_ADODB_iter($this, $result, $this->page_tbl_field_list);
1008     }
1009
1010     /*
1011      * Find recent changes.
1012      */
1013     public function most_recent($params)
1014     {
1015         $limit = 0;
1016         $since = 0;
1017         $include_minor_revisions = false;
1018         $exclude_major_revisions = false;
1019         $include_all_revisions = false;
1020         extract($params);
1021
1022         $dbh = &$this->_dbh;
1023         extract($this->_table_names);
1024
1025         $pick = array();
1026         if ($since)
1027             $pick[] = "mtime >= $since";
1028
1029         if ($include_all_revisions) {
1030             // Include all revisions of each page.
1031             $table = "$page_tbl, $version_tbl";
1032             $join_clause = "$page_tbl.id=$version_tbl.id";
1033
1034             if ($exclude_major_revisions) {
1035                 // Include only minor revisions
1036                 $pick[] = "minor_edit <> 0";
1037             } elseif (!$include_minor_revisions) {
1038                 // Include only major revisions
1039                 $pick[] = "minor_edit = 0";
1040             }
1041         } else {
1042             $table = "$page_tbl, $recent_tbl";
1043             $join_clause = "$page_tbl.id=$recent_tbl.id";
1044             $table .= ", $version_tbl";
1045             $join_clause .= " AND $version_tbl.id=$page_tbl.id";
1046
1047             if ($exclude_major_revisions) {
1048                 // Include only most recent minor revision
1049                 $pick[] = 'version=latestminor';
1050             } elseif (!$include_minor_revisions) {
1051                 // Include only most recent major revision
1052                 $pick[] = 'version=latestmajor';
1053             } else {
1054                 // Include only the latest revision (whether major or minor).
1055                 $pick[] = 'version=latestversion';
1056             }
1057         }
1058         $order = "DESC";
1059         if ($limit < 0) {
1060             $order = "ASC";
1061             $limit = -$limit;
1062         }
1063         $where_clause = $join_clause;
1064         if ($pick)
1065             $where_clause .= " AND " . join(" AND ", $pick);
1066         $sql = "SELECT "
1067             . $this->page_tbl_fields . ", " . $this->version_tbl_fields
1068             . " FROM $table"
1069             . " WHERE $where_clause"
1070             . " ORDER BY mtime $order";
1071         // FIXME: use SQL_BUFFER_RESULT for mysql?
1072         if ($limit) {
1073             // extract from,count from limit
1074             list($offset, $count) = $this->limit($limit);
1075             $result = $dbh->SelectLimit($sql, $count, $offset);
1076         } else {
1077             $result = $dbh->Execute($sql);
1078         }
1079         //$result->fields['version'] = $result->fields[6];
1080         return new WikiDB_backend_ADODB_iter($this, $result,
1081             array_merge($this->page_tbl_field_list, $this->version_tbl_field_list));
1082     }
1083
1084     /*
1085      * Find referenced empty pages.
1086      */
1087     function wanted_pages($exclude_from = '', $exclude = '', $sortby = '', $limit = '')
1088     {
1089         $dbh = &$this->_dbh;
1090         extract($this->_table_names);
1091         if ($orderby = $this->sortby($sortby, 'db', array('pagename', 'wantedfrom')))
1092             $orderby = 'ORDER BY ' . $orderby;
1093
1094         if ($exclude_from) // array of pagenames
1095             $exclude_from = " AND pp.pagename NOT IN " . $this->_sql_set($exclude_from);
1096         if ($exclude) // array of pagenames
1097             $exclude = " AND p.pagename NOT IN " . $this->_sql_set($exclude);
1098
1099         /*
1100          all empty pages, independent of linkstatus:
1101            select pagename as empty from page left join nonempty using(id) where is null(nonempty.id);
1102          only all empty pages, which have a linkto:
1103            select page.pagename, linked.pagename as wantedfrom from link, page linked
1104              left join page on link.linkto=page.id left join nonempty on link.linkto=nonempty.id
1105              where nonempty.id is null and linked.id=link.linkfrom;
1106         */
1107         $sql = "SELECT p.pagename, pp.pagename AS wantedfrom"
1108             . " FROM $page_tbl p, $link_tbl linked"
1109             . " LEFT JOIN $page_tbl pp ON (linked.linkto = pp.id)"
1110             . " LEFT JOIN $nonempty_tbl ne ON (linked.linkto = ne.id)"
1111             . " WHERE ne.id IS NULL"
1112             . " AND (p.id = linked.linkfrom)"
1113             . $exclude_from
1114             . $exclude
1115             . $orderby;
1116         if ($limit) {
1117             // extract from,count from limit
1118             list($offset, $count) = $this->limit($limit);
1119             $result = $dbh->SelectLimit($sql, $count, $offset);
1120         } else {
1121             $result = $dbh->Execute($sql);
1122         }
1123         return new WikiDB_backend_ADODB_iter($this, $result, array('pagename', 'wantedfrom'));
1124     }
1125
1126     /*
1127      * Rename page in the database.
1128      */
1129     function rename_page($pagename, $to)
1130     {
1131         $dbh = &$this->_dbh;
1132         extract($this->_table_names);
1133
1134         $this->lock(array('page', 'version', 'recent', 'nonempty', 'link'));
1135         if (($id = $this->_get_pageid($pagename, false))) {
1136             if ($new = $this->_get_pageid($to, false)) {
1137                 // Cludge Alert!
1138                 // This page does not exist (already verified before), but exists in the page table.
1139                 // So we delete this page.
1140                 $dbh->query("DELETE FROM $page_tbl WHERE id=$new");
1141                 $dbh->query("DELETE FROM $version_tbl WHERE id=$new");
1142                 $dbh->query("DELETE FROM $recent_tbl WHERE id=$new");
1143                 $dbh->query("DELETE FROM $nonempty_tbl WHERE id=$new");
1144                 // We have to fix all referring tables to the old id
1145                 $dbh->query("UPDATE $link_tbl SET linkfrom=$id WHERE linkfrom=$new");
1146                 $dbh->query("UPDATE $link_tbl SET linkto=$id WHERE linkto=$new");
1147             }
1148             $dbh->query(sprintf("UPDATE $page_tbl SET pagename=%s WHERE id=$id",
1149                 $dbh->qstr($to)));
1150         }
1151         $this->unlock(array('page'));
1152         return $id;
1153     }
1154
1155     function _update_recent_table($pageid = false)
1156     {
1157         $dbh = &$this->_dbh;
1158         extract($this->_table_names);
1159         extract($this->_expressions);
1160
1161         $pageid = (int)$pageid;
1162
1163         // optimize: mysql can do this with one REPLACE INTO.
1164         $backend_type = $this->backendType();
1165         if (substr($backend_type, 0, 5) == 'mysql') {
1166             $dbh->Execute("REPLACE INTO $recent_tbl"
1167                 . " (id, latestversion, latestmajor, latestminor)"
1168                 . " SELECT id, $maxversion, $maxmajor, $maxminor"
1169                 . " FROM $version_tbl"
1170                 . ($pageid ? " WHERE id=$pageid" : "")
1171                 . " GROUP BY id");
1172         } else {
1173             $this->lock(array('recent'));
1174             $dbh->Execute("DELETE FROM $recent_tbl"
1175                 . ($pageid ? " WHERE id=$pageid" : ""));
1176             $dbh->Execute("INSERT INTO $recent_tbl"
1177                 . " (id, latestversion, latestmajor, latestminor)"
1178                 . " SELECT id, $maxversion, $maxmajor, $maxminor"
1179                 . " FROM $version_tbl"
1180                 . ($pageid ? " WHERE id=$pageid" : "")
1181                 . " GROUP BY id");
1182             $this->unlock(array('recent'));
1183         }
1184     }
1185
1186     function _update_nonempty_table($pageid = false)
1187     {
1188         $dbh = &$this->_dbh;
1189         extract($this->_table_names);
1190         extract($this->_expressions);
1191
1192         $pageid = (int)$pageid;
1193
1194         extract($this->_expressions);
1195         $this->lock(array('nonempty'));
1196         $dbh->Execute("DELETE FROM $nonempty_tbl"
1197             . ($pageid ? " WHERE id=$pageid" : ""));
1198         $dbh->Execute("INSERT INTO $nonempty_tbl (id)"
1199             . " SELECT $recent_tbl.id"
1200             . " FROM $recent_tbl, $version_tbl"
1201             . " WHERE $recent_tbl.id=$version_tbl.id"
1202             . "  AND version=latestversion"
1203             // We have some specifics here (Oracle)
1204             //. "  AND content<>''"
1205             . "  AND content $notempty" // On Oracle not just "<>''"
1206             . ($pageid ? " AND $recent_tbl.id=$pageid" : ""));
1207         $this->unlock(array('nonempty'));
1208     }
1209
1210     /*
1211      * Grab a write lock on the tables in the SQL database.
1212      *
1213      * Calls can be nested.  The tables won't be unlocked until
1214      * _unlock_database() is called as many times as _lock_database().
1215      */
1216     public function lock($tables = array(), $write_lock = true)
1217     {
1218         $this->_dbh->StartTrans();
1219         if ($this->_lock_count++ == 0) {
1220             $this->_current_lock = $tables;
1221             $this->_lock_tables($tables, $write_lock);
1222         }
1223     }
1224
1225     /*
1226      * Overridden by non-transaction safe backends.
1227      */
1228     protected function _lock_tables($tables, $write_lock = true)
1229     {
1230         return $this->_current_lock;
1231     }
1232
1233     /**
1234      * Release a write lock on the tables in the SQL database.
1235      *
1236      * @param array $tables
1237      * @param bool $force Unlock even if not every call to lock() has been matched
1238      * by a call to unlock().
1239      *
1240      * @see _lock_database
1241      */
1242     public function unlock($tables = array(), $force = false)
1243     {
1244         if ($this->_lock_count == 0) {
1245             $this->_current_lock = false;
1246             return;
1247         }
1248         if (--$this->_lock_count <= 0 || $force) {
1249             $this->_unlock_tables($tables);
1250             $this->_current_lock = false;
1251             $this->_lock_count = 0;
1252         }
1253         $this->_dbh->CompleteTrans(!$force);
1254     }
1255
1256     /*
1257      * overridden by non-transaction safe backends
1258      */
1259     protected function _unlock_tables($tables)
1260     {
1261         return;
1262     }
1263
1264     /*
1265      * Serialize data
1266      */
1267     function _serialize($data)
1268     {
1269         if (empty($data))
1270             return '';
1271         assert(is_array($data));
1272         return serialize($data);
1273     }
1274
1275     /*
1276      * Unserialize data
1277      */
1278     function _unserialize($data)
1279     {
1280         return empty($data) ? array() : unserialize($data);
1281     }
1282
1283     /* some variables and functions for DB backend abstraction (action=upgrade) */
1284     function database()
1285     {
1286         return $this->_dbh->database;
1287     }
1288
1289     function backendType()
1290     {
1291         return $this->_dbh->databaseType;
1292     }
1293
1294     function connection()
1295     {
1296         return $this->_dbh->_connectionID;
1297     }
1298
1299     function getRow($query)
1300     {
1301         return $this->_dbh->getRow($query);
1302     }
1303
1304     function listOfTables()
1305     {
1306         return $this->_dbh->MetaTables();
1307     }
1308
1309     // other database needs another connection and other privileges.
1310     function listOfFields($database, $table)
1311     {
1312         $field_list = array();
1313         $old_db = $this->database();
1314         if ($database != $old_db) {
1315             $this->_dbh->Connect($this->_parsedDSN['hostspec'],
1316                 DBADMIN_USER ? DBADMIN_USER : $this->_parsedDSN['username'],
1317                 DBADMIN_PASSWD ? DBADMIN_PASSWD : $this->_parsedDSN['password'],
1318                 $database);
1319         }
1320         foreach ($this->_dbh->MetaColumns($table, false) as $field) {
1321             $field_list[] = $field->name;
1322         }
1323         if ($database != $old_db) {
1324             $this->_dbh->close();
1325             $this->_dbh->Connect($this->_parsedDSN['hostspec'],
1326                 $this->_parsedDSN['username'],
1327                 $this->_parsedDSN['password'],
1328                 $old_db);
1329         }
1330         return $field_list;
1331     }
1332
1333 }
1334
1335 class WikiDB_backend_ADODB_generic_iter
1336     extends WikiDB_backend_iterator
1337 {
1338     function __construct($backend, $query_result, $field_list = NULL)
1339     {
1340         $this->_backend = &$backend;
1341         $this->_result = $query_result;
1342
1343         if (is_null($field_list)) {
1344             // No field list passed, retrieve from DB
1345             // WikiLens is using the iterator behind the scene
1346             $field_list = array();
1347             $fields = $query_result->FieldCount();
1348             for ($i = 0; $i < $fields; $i++) {
1349                 $field_info = $query_result->FetchField($i);
1350                 array_push($field_list, $field_info->name);
1351             }
1352         }
1353
1354         $this->_fields = $field_list;
1355     }
1356
1357     function count()
1358     {
1359         if (!$this->_result) {
1360             return false;
1361         }
1362         $count = $this->_result->numRows();
1363         //$this->_result->Close();
1364         return $count;
1365     }
1366
1367     function next()
1368     {
1369         $result = &$this->_result;
1370         if (!$result || $result->EOF) {
1371             $this->free();
1372             return false;
1373         }
1374
1375         // Convert array to hash
1376         $i = 0;
1377         $rec_num = $result->fields;
1378         foreach ($this->_fields as $field) {
1379             $rec_assoc[$field] = $rec_num[$i++];
1380         }
1381         // check if the cache can be populated here?
1382
1383         $result->MoveNext();
1384         return $rec_assoc;
1385     }
1386
1387     function reset()
1388     {
1389         if ($this->_result) {
1390             $this->_result->MoveFirst();
1391         }
1392     }
1393
1394     function asArray()
1395     {
1396         $result = array();
1397         while ($page = $this->next())
1398             $result[] = $page;
1399         return $result;
1400     }
1401
1402     function free()
1403     {
1404         if ($this->_result) {
1405             /* call mysql_free_result($this->_queryID) */
1406             $this->_result->Close();
1407             $this->_result = false;
1408         }
1409     }
1410 }
1411
1412 class WikiDB_backend_ADODB_iter
1413     extends WikiDB_backend_ADODB_generic_iter
1414 {
1415     function next()
1416     {
1417         $result = &$this->_result;
1418         $backend = &$this->_backend;
1419         if (!$result || $result->EOF) {
1420             $this->free();
1421             return false;
1422         }
1423
1424         // Convert array to hash
1425         $i = 0;
1426         $rec_num = $result->fields;
1427         foreach ($this->_fields as $field) {
1428             $rec_assoc[$field] = $rec_num[$i++];
1429         }
1430
1431         $result->MoveNext();
1432         if (isset($rec_assoc['pagedata']))
1433             $rec_assoc['pagedata'] = $backend->_extract_page_data($rec_assoc['pagedata'], $rec_assoc['hits']);
1434         if (!empty($rec_assoc['version'])) {
1435             $rec_assoc['versiondata'] = $backend->_extract_version_data_assoc($rec_assoc);
1436         }
1437         if (!empty($rec_assoc['linkrelation'])) {
1438             $rec_assoc['linkrelation'] = $rec_assoc['linkrelation']; // pagename enough?
1439         }
1440         return $rec_assoc;
1441     }
1442 }
1443
1444 class WikiDB_backend_ADODB_search extends WikiDB_backend_search_sql
1445 {
1446     // no surrounding quotes because we know it's a string
1447     // function _quote($word) { return $this->_dbh->escapeSimple($word); }
1448 }
1449
1450 // Following function taken from Pear::DB (prev. from adodb-pear.inc.php).
1451 // Eventually, change index.php to provide the relevant information
1452 // directly?
1453 /**
1454  * Parse a data source name.
1455  *
1456  * Additional keys can be added by appending a URI query string to the
1457  * end of the DSN.
1458  *
1459  * The format of the supplied DSN is in its fullest form:
1460  * <code>
1461  *  phptype(dbsyntax)://username:password@protocol+hostspec/database?option=8&another=true
1462  * </code>
1463  *
1464  * Most variations are allowed:
1465  * <code>
1466  *  phptype://username:password@protocol+hostspec:110//usr/db_file.db?mode=0644
1467  *  phptype://username:password@hostspec/database_name
1468  *  phptype://username:password@hostspec
1469  *  phptype://username@hostspec
1470  *  phptype://hostspec/database
1471  *  phptype://hostspec
1472  *  phptype(dbsyntax)
1473  *  phptype
1474  * </code>
1475  *
1476  * @param string $dsn Data Source Name to be parsed
1477  *
1478  * @return array an associative array with the following keys:
1479  *  + phptype:  Database backend used in PHP (mysql, odbc etc.)
1480  *  + dbsyntax: Database used with regards to SQL syntax etc.
1481  *  + protocol: Communication protocol to use (tcp, unix etc.)
1482  *  + hostspec: Host specification (hostname[:port])
1483  *  + database: Database to use on the DBMS server
1484  *  + username: User name for login
1485  *  + password: Password for login
1486  *
1487  * @author Tomas V.V.Cox <cox@idecnet.com>
1488  */
1489 function parseDSN($dsn)
1490 {
1491     $parsed = array(
1492         'phptype' => false,
1493         'dbsyntax' => false,
1494         'username' => false,
1495         'password' => false,
1496         'protocol' => false,
1497         'hostspec' => false,
1498         'port' => false,
1499         'socket' => false,
1500         'database' => false,
1501     );
1502
1503     if (is_array($dsn)) {
1504         $dsn = array_merge($parsed, $dsn);
1505         if (!$dsn['dbsyntax']) {
1506             $dsn['dbsyntax'] = $dsn['phptype'];
1507         }
1508         return $dsn;
1509     }
1510
1511     // Find phptype and dbsyntax
1512     if (($pos = strpos($dsn, '://')) !== false) {
1513         $str = substr($dsn, 0, $pos);
1514         $dsn = substr($dsn, $pos + 3);
1515     } else {
1516         $str = $dsn;
1517         $dsn = null;
1518     }
1519
1520     // Get phptype and dbsyntax
1521     // $str => phptype(dbsyntax)
1522     if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) {
1523         $parsed['phptype'] = $arr[1];
1524         $parsed['dbsyntax'] = !$arr[2] ? $arr[1] : $arr[2];
1525     } else {
1526         $parsed['phptype'] = $str;
1527         $parsed['dbsyntax'] = $str;
1528     }
1529
1530     if (!count($dsn)) {
1531         return $parsed;
1532     }
1533
1534     // Get (if found): username and password
1535     // $dsn => username:password@protocol+hostspec/database
1536     if (($at = strrpos($dsn, '@')) !== false) {
1537         $str = substr($dsn, 0, $at);
1538         $dsn = substr($dsn, $at + 1);
1539         if (($pos = strpos($str, ':')) !== false) {
1540             $parsed['username'] = rawurldecode(substr($str, 0, $pos));
1541             $parsed['password'] = rawurldecode(substr($str, $pos + 1));
1542         } else {
1543             $parsed['username'] = rawurldecode($str);
1544         }
1545     }
1546
1547     // Find protocol and hostspec
1548
1549     // $dsn => proto(proto_opts)/database
1550     if (preg_match('|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match)) {
1551         $proto = $match[1];
1552         $proto_opts = $match[2] ? $match[2] : false;
1553         $dsn = $match[3];
1554
1555         // $dsn => protocol+hostspec/database (old format)
1556     } else {
1557         if (strpos($dsn, '+') !== false) {
1558             list($proto, $dsn) = explode('+', $dsn, 2);
1559         }
1560         if (strpos($dsn, '/') !== false) {
1561             list($proto_opts, $dsn) = explode('/', $dsn, 2);
1562         } else {
1563             $proto_opts = $dsn;
1564             $dsn = null;
1565         }
1566     }
1567
1568     // process the different protocol options
1569     $parsed['protocol'] = (!empty($proto)) ? $proto : 'tcp';
1570     $proto_opts = rawurldecode($proto_opts);
1571     if ($parsed['protocol'] == 'tcp') {
1572         if (strpos($proto_opts, ':') !== false) {
1573             list($parsed['hostspec'], $parsed['port']) = explode(':', $proto_opts);
1574         } else {
1575             $parsed['hostspec'] = $proto_opts;
1576         }
1577     } elseif ($parsed['protocol'] == 'unix') {
1578         $parsed['socket'] = $proto_opts;
1579     }
1580
1581     // Get dabase if any
1582     // $dsn => database
1583     if ($dsn) {
1584         // /database
1585         if (($pos = strpos($dsn, '?')) === false) {
1586             $parsed['database'] = $dsn;
1587             // /database?param1=value1&param2=value2
1588         } else {
1589             $parsed['database'] = substr($dsn, 0, $pos);
1590             $dsn = substr($dsn, $pos + 1);
1591             if (strpos($dsn, '&') !== false) {
1592                 $opts = explode('&', $dsn);
1593             } else { // database?param1=value1
1594                 $opts = array($dsn);
1595             }
1596             foreach ($opts as $opt) {
1597                 list($key, $value) = explode('=', $opt);
1598                 if (!isset($parsed[$key])) {
1599                     // don't allow params overwrite
1600                     $parsed[$key] = rawurldecode($value);
1601                 }
1602             }
1603         }
1604     }
1605
1606     return $parsed;
1607 }
1608
1609 // Local Variables:
1610 // mode: php
1611 // tab-width: 8
1612 // c-basic-offset: 4
1613 // c-hanging-comment-ender-p: nil
1614 // indent-tabs-mode: nil
1615 // End: