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