]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend/dbaBase.php
Remove useless semicolon
[SourceForge/phpwiki.git] / lib / WikiDB / backend / dbaBase.php
1 <?php
2
3 require_once 'lib/WikiDB/backend.php';
4
5 // FIXME:padding of data?  Is it needed?  dba_optimize() seems to do a good
6 // job at packing 'gdbm' (and 'db2') databases.
7
8 /*
9  * Tables:
10  *
11  *  page:
12  *   Index: 'p' + pagename
13  *  Values: latestversion . ':' . flags . ':' serialized hash of page meta data
14  *           Currently flags = 1 if latest version has empty content.
15  *
16  *  version
17  *   Index: 'v' + version:pagename
18  *   Value: serialized hash of revision meta data, including:
19  *          + quasi-meta-data %content
20  *
21  *  links
22  *   index: 'o' + pagename
23  *   value: serialized list of pages (names) which pagename links to.
24  *   index: 'i' + pagename
25  *   value: serialized list of pages which link to pagename
26  *
27  *  TODO:
28  *  Don't keep tables locked the whole time.
29  *
30  *  More index tables:
31  *   - Yes - RecentChanges support. Lists of most recent edits (major, minor, either).
32  *     't' + mtime => 'a|i' + version+':'+pagename ('a': major, 'i': minor)
33  *     Cost: Currently we have to get_all_pages and sort it by mtime.
34  *     With a seperate t table we have to update this table on every version change.
35  *   - No - list of pagenames for get_all_pages (very cheap: iterate page table)
36  *   - Maybe - mostpopular list? 'h' + pagename => hits
37   *
38  *  Separate hit table, so we don't have to update the whole page entry
39  *  each time we get a hit. Maybe not so important though.
40  */
41
42 require_once 'lib/DbaPartition.php';
43
44 class WikiDB_backend_dbaBase
45     extends WikiDB_backend
46 {
47     function WikiDB_backend_dbaBase(&$dba)
48     {
49         $this->_db = &$dba;
50         // TODO: page and version tables should be in their own files, probably.
51         // We'll pack them all in one for now (testing).
52         // 2004-07-09 10:07:30 rurban: It's fast enough this way.
53         $this->_pagedb = new DbaPartition($dba, 'p');
54         $this->_versiondb = new DbaPartition($dba, 'v');
55         $linkdbpart = new DbaPartition($dba, 'l');
56         $this->_linkdb = new WikiDB_backend_dbaBase_linktable($linkdbpart);
57         $this->_dbdb = new DbaPartition($dba, 'd');
58     }
59
60     function sortable_columns()
61     {
62         return array('pagename', 'mtime' /*,'author_id','author'*/);
63     }
64
65     function close()
66     {
67         $this->_db->close();
68     }
69
70     function optimize()
71     {
72         $this->_db->optimize();
73     }
74
75     function sync()
76     {
77         $this->_db->sync();
78     }
79
80     function rebuild($args = false)
81     {
82         if (!empty($args['all'])) {
83             parent::rebuild();
84         }
85         // rebuild backlink table
86         $this->_linkdb->rebuild();
87         $this->optimize();
88     }
89
90     function check($args = false)
91     {
92         // cleanup v?Pagename UNKNOWN0x0
93         $errs = array();
94         $pagedb = &$this->_pagedb;
95         for ($page = $pagedb->firstkey();
96              $page !== false;
97              $page = $pagedb->nextkey()) {
98             if (!$page) {
99                 $errs[] = "empty page $page";
100                 trigger_error("empty page $page deleted", E_USER_WARNING);
101                 $this->purge_page($page);
102                 continue;
103             }
104             if (!($data = $pagedb->get($page))) continue;
105             list($version, $flags,) = explode(':', $data, 3);
106             $vdata = $this->_versiondb->get($version . ":" . $page);
107             if ($vdata === false)
108                 continue; // linkrelations
109             // we also had for some internal version vdata is serialized strings,
110             // need to unserialize it twice. We rather purge it.
111             if (!is_string($vdata)
112                 or $vdata == 'UNKNOWN\0'
113                 or !is_array(unserialize($vdata))
114             ) {
115                 $errs[] = "empty revision $version for $page";
116                 trigger_error("empty revision $version for $page deleted", E_USER_WARNING);
117                 $this->delete_versiondata($page, $version);
118             }
119         }
120         // check links per default
121         return array_merge($errs, $this->_linkdb->check());
122     }
123
124     function get_pagedata($pagename)
125     {
126         $result = $this->_pagedb->get($pagename);
127         if (!$result)
128             return false;
129         list(, , $packed) = explode(':', $result, 3);
130         $data = unserialize($packed);
131         return $data;
132     }
133
134     function update_pagedata($pagename, $newdata)
135     {
136         $result = $this->_pagedb->get($pagename);
137         if ($result) {
138             list($latestversion, $flags, $data) = explode(':', $result, 3);
139             $data = unserialize($data);
140         } else {
141             $latestversion = $flags = 0;
142             $data = array();
143         }
144
145         foreach ($newdata as $key => $val) {
146             if (empty($val))
147                 unset($data[$key]);
148             else
149                 $data[$key] = $val;
150         }
151         $this->_pagedb->set($pagename,
152             (int)$latestversion . ':'
153                 . (int)$flags . ':'
154                 . serialize($data));
155     }
156
157     function get_latest_version($pagename)
158     {
159         return (int)$this->_pagedb->get($pagename);
160     }
161
162     function get_previous_version($pagename, $version)
163     {
164         $versdb = &$this->_versiondb;
165
166         while (--$version > 0) {
167             if ($versdb->exists($version . ":$pagename"))
168                 return $version;
169         }
170         return false;
171     }
172
173     //check $want_content
174     function get_versiondata($pagename, $version, $want_content = false)
175     {
176         $data = $this->_versiondb->get((int)$version . ":$pagename");
177         if (empty($data) or $data == 'UNKNOWN\0') return false;
178         else {
179             $vdata = unserialize($data);
180             if (DEBUG and empty($vdata)) { // requires ->check
181                 trigger_error("Delete empty revision: $pagename: " . $data, E_USER_WARNING);
182                 $this->delete_versiondata($pagename, (int)$version);
183             }
184             if (!$want_content)
185                 $vdata['%content'] = !empty($vdata['%content']);
186             return $vdata;
187         }
188     }
189
190     /**
191      * Can be undone and is seen in RecentChanges.
192      * See backend.php
193      */
194     function delete_page($pagename)
195     {
196         $version = $this->get_latest_version($pagename);
197         $data = $this->_versiondb->get((int)$version . ":$pagename");
198         // returns serialized string
199         if (!is_array($data) or empty($data)) {
200             if (is_string($data) and ($vdata = @unserialize($data))) {
201                 $data = $vdata;
202                 unset($vdata);
203             } else // already empty page
204                 $data = array();
205         }
206         assert(is_array($data) and !empty($data)); // mtime
207         $data['%content'] = '';
208         $data['mtime'] = time();
209         $data['summary'] = "removed by " . $GLOBALS["request"]->_deduceUsername();
210         $this->set_versiondata($pagename, $version + 1, $data);
211         $this->set_links($pagename, false);
212     }
213
214     /**
215      * Completely delete all page revisions from the database.
216      */
217     function purge_page($pagename)
218     {
219         $pagedb = &$this->_pagedb;
220         $versdb = &$this->_versiondb;
221
222         $version = $this->get_latest_version($pagename);
223         while ($version > 0) {
224             $versdb->set($version-- . ":$pagename", false);
225         }
226         $pagedb->set($pagename, false);
227
228         $this->set_links($pagename, false);
229     }
230
231     function rename_page($pagename, $to)
232     {
233         $result = $this->_pagedb->get($pagename);
234         if ($result) {
235             list($version, $flags, $data) = explode(':', $result, 3);
236             $data = unserialize($data);
237         } else
238             return false;
239
240         $links = $this->_linkdb->get_links($pagename, false, false);
241         $data['pagename'] = $to;
242         $this->_pagedb->set($to,
243             (int)$version . ':'
244                 . (int)$flags . ':'
245                 . serialize($data));
246         // move over the latest version only
247         $pvdata = $this->get_versiondata($pagename, $version, true);
248         $data['mtime'] = time();
249         $data['summary'] = "renamed from " . $pagename
250             . " by " . $GLOBALS["request"]->_deduceUsername();
251         $this->set_versiondata($to, $version, $pvdata);
252
253         // update links and backlinks
254         $this->_linkdb->set_links($to, $links);
255         // better: update all back-/inlinks for all outlinks.
256
257         $this->_pagedb->delete($pagename);
258         return true;
259     }
260
261     /**
262      * Delete an old revision of a page.
263      */
264     function delete_versiondata($pagename, $version)
265     {
266         $versdb = &$this->_versiondb;
267
268         $latest = $this->get_latest_version($pagename);
269
270         assert($version > 0);
271         assert($version <= $latest);
272
273         $versdb->set((int)$version . ":$pagename", false);
274
275         if ($version == $latest) {
276             $previous = $this->get_previous_version($pagename, $version);
277             if ($previous > 0) {
278                 $pvdata = $this->get_versiondata($pagename, $previous);
279                 $is_empty = empty($pvdata['%content']);
280             } else
281                 $is_empty = true;
282             $this->_update_latest_version($pagename, $previous, $is_empty);
283         }
284     }
285
286     /**
287      * Create a new revision of a page.
288      */
289     function set_versiondata($pagename, $version, $data)
290     {
291         $versdb = &$this->_versiondb;
292         // fix broken pages
293         if (!is_array($data) or empty($data)) {
294             if (is_string($data) and ($vdata = @unserialize($data))) {
295                 trigger_error("broken page version $pagename. Run Check WikiDB",
296                     E_USER_NOTICE);
297                 $data = $vdata;
298             } else
299                 $data = array();
300         }
301         assert(is_array($data) and !empty($data)); // mtime
302         $versdb->set((int)$version . ":$pagename", serialize($data));
303         if ($version > $this->get_latest_version($pagename))
304             $this->_update_latest_version($pagename, $version, empty($data['%content']));
305     }
306
307     function _update_latest_version($pagename, $latest, $flags)
308     {
309         $pagedb = &$this->_pagedb;
310
311         $pdata = $pagedb->get($pagename);
312         if ($pdata)
313             list(, , $pagedata) = explode(':', $pdata, 3);
314         else
315             $pagedata = serialize(array());
316
317         $pagedb->set($pagename, (int)$latest . ':' . (int)$flags . ":$pagedata");
318     }
319
320     function numPages($include_empty = false, $exclude = '')
321     {
322         $pagedb = &$this->_pagedb;
323         $count = 0;
324         for ($page = $pagedb->firstkey(); $page !== false; $page = $pagedb->nextkey()) {
325             if (!$page) {
326                 assert(!empty($page));
327                 continue;
328             }
329             if ($exclude and in_array($page, $exclude)) continue;
330             if (!$include_empty) {
331                 if (!($data = $pagedb->get($page))) continue;
332                 list($latestversion, $flags,) = explode(':', $data, 3);
333                 unset($data);
334                 if ($latestversion == 0 || $flags != 0)
335                     continue; // current content is empty
336             }
337             $count++;
338         }
339         return $count;
340     }
341
342     function get_all_pages($include_empty = false, $sortby = '', $limit = '', $exclude = '')
343     {
344         $pagedb = &$this->_pagedb;
345         $pages = array();
346         $from = 0;
347         $i = 0;
348         $count = 0;
349         if ($limit) { // extract from,count from limit
350             list($from, $count) = $this->limit($limit);
351         }
352         for ($page = $pagedb->firstkey(); $page !== false; $page = $pagedb->nextkey()) {
353             if (!$page) {
354                 assert(!empty($page));
355                 continue;
356             }
357             if ($exclude and in_array($page, $exclude)) continue;
358             if ($limit and $from) {
359                 $i++;
360                 if ($i < $from) continue;
361             }
362             if ($limit and count($pages) >= $count) break;
363             if (!$include_empty) {
364                 if (!($data = $pagedb->get($page))) continue;
365                 list($latestversion, $flags,) = explode(':', $data, 3);
366                 unset($data);
367                 if ($latestversion == 0 || $flags != 0)
368                     continue; // current content is empty
369             }
370             $pages[] = $page;
371         }
372         return new WikiDB_backend_dbaBase_pageiter
373         ($this, $pages,
374             array('sortby' => $sortby)); // already limited
375     }
376
377     function set_links($pagename, $links)
378     {
379         $this->_linkdb->set_links($pagename, $links);
380     }
381
382     function get_links($pagename, $reversed = true, $include_empty = false,
383                        $sortby = '', $limit = '', $exclude = '',
384                        $want_relations = false)
385     {
386         // optimization: if no relation at all is found, mark it in the iterator.
387         $links = $this->_linkdb->get_links($pagename, $reversed, $want_relations);
388
389         return new WikiDB_backend_dbaBase_pageiter
390         ($this, $links,
391             array('sortby' => $sortby,
392                 'limit' => $limit,
393                 'exclude' => $exclude,
394                 'want_relations' => $want_relations,
395                 'found_relations' => $want_relations
396                     ? $this->_linkdb->found_relations : 0
397             ));
398     }
399
400     /**
401      * @access public
402      *
403      * @return array of all linkrelations
404      * Faster than the dumb WikiDB method.
405      */
406     function list_relations($also_attributes = false,
407                             $only_attributes = false,
408                             $sorted = true)
409     {
410         $linkdb = &$this->_linkdb;
411         $relations = array();
412         for ($link = $linkdb->_db->firstkey();
413              $link !== false;
414              $link = $linkdb->_db->nextkey()) {
415             if ($link[0] != 'o') continue;
416             $links = $linkdb->_get_links('o', substr($link, 1));
417             foreach ($links as $link) { // linkto => page, linkrelation => page
418                 if (is_array($link)
419                     and $link['relation']
420                         and !in_array($link['relation'], $relations)
421                 ) {
422                     $is_attribute = empty($link['linkto']); // a relation has both
423                     if ($is_attribute) {
424                         if ($only_attributes or $also_attributes)
425                             $relations[] = $link['relation'];
426                     } elseif (!$only_attributes) {
427                         $relations[] = $link['relation'];
428                     }
429                 }
430             }
431         }
432         if ($sorted) {
433             sort($relations);
434             reset($relations);
435         }
436         return $relations;
437     }
438
439     /**
440      * WikiDB_backend_dumb_LinkSearchIter searches over all
441      * pages and then all its links.  Since there are less
442      * links than pages, and we easily get the pagename from
443      * the link key, we iterate here directly over the
444      * linkdb and check the pagematch there.
445      *
446      * @param $pages     object A TextSearchQuery object for the pagename filter.
447      * @param $query     object A SearchQuery object (Text or Numeric) for the linkvalues,
448      *                          linkto, linkfrom (=backlink), relation or attribute values.
449      * @param $linktype  string One of the 4 linktypes "linkto",
450      *                          "linkfrom" (=backlink), "relation" or "attribute".
451      *                 Maybe also "relation+attribute" for the advanced search.
452      * @param $relation  object A TextSearchQuery for the linkname or false.
453      * @param $options   array Currently ignored. hash of sortby, limit, exclude.
454      * @return object A WikiDB_backend_iterator.
455      * @see WikiDB::linkSearch
456      */
457     function link_search($pages, $query, $linktype,
458                          $relation = false, $options = array())
459     {
460         $linkdb = &$this->_linkdb;
461         $links = array();
462         $reverse = false;
463         $want_relations = false;
464         if ($linktype == 'relation') {
465             $want_relations = true;
466             $field = 'linkrelation';
467         }
468         if ($linktype == 'attribute') {
469             $want_relations = true;
470             $field = 'attribute';
471         }
472         if ($linktype == 'linkfrom') {
473             $reverse = true;
474         }
475
476         for ($link = $linkdb->_db->firstkey();
477              $link !== false;
478              $link = $linkdb->_db->nextkey()) {
479             $type = $reverse ? 'i' : 'o';
480             if ($link[0] != $type) continue;
481             $pagename = substr($link, 1);
482             if (!$pages->match($pagename)) continue;
483             if ($linktype == 'attribute') {
484                 $page = $GLOBALS['request']->_dbi->getPage($pagename);
485                 $attribs = $page->get('attributes');
486                 if ($attribs) {
487                     /* Optimization on expressive searches:
488                        for queries with multiple attributes.
489                        Just take the defined placeholders from the query(ies)
490                        if there are more attributes than query variables.
491                     */
492                     if ($query->getType() != 'text'
493                         and !$relation
494                             and ((count($vars = $query->getVars()) > 1)
495                                 or (count($attribs) > count($vars)))
496                     ) {
497                         // names must strictly match. no * allowed
498                         if (!$query->can_match($attribs)) continue;
499                         if (!($result = $query->match($attribs))) continue;
500                         foreach ($result as $r) {
501                             $r['pagename'] = $pagename;
502                             $links[] = $r;
503                         }
504                     } else {
505                         // textsearch or simple value. no strict bind by name needed
506                         foreach ($attribs as $attribute => $value) {
507                             if ($relation and !$relation->match($attribute)) continue;
508                             if (!$query->match($value)) continue;
509                             $links[] = array('pagename' => $pagename,
510                                 'linkname' => $attribute,
511                                 'linkvalue' => $value);
512                         }
513                     }
514                 }
515             } else {
516                 // TODO: honor limits. this can get large.
517                 if ($want_relations) {
518                     // MAP linkrelation : pagename => thispagename : linkname : linkvalue
519                     $_links = $linkdb->_get_links('o', $pagename);
520                     foreach ($_links as $link) { // linkto => page, linkrelation => page
521                         if (!isset($link['relation']) or !$link['relation']) continue;
522                         if ($relation and !$relation->match($link['relation'])) continue;
523                         if (!$query->match($link['linkto'])) continue;
524                         $links[] = array('pagename' => $pagename,
525                             'linkname' => $link['relation'],
526                             'linkvalue' => $link['linkto']);
527                     }
528                 } else {
529                     $_links = $linkdb->_get_links($reverse ? 'i' : 'o', $pagename);
530                     foreach ($_links as $link) { // linkto => page
531                         if (is_array($link))
532                             $link = $link['linkto'];
533                         if (!$query->match($link)) continue;
534                         $links[] = array('pagename' => $pagename,
535                             'linkname' => '',
536                             'linkvalue' => $link);
537                     }
538                 }
539             }
540         }
541         $options['want_relations'] = true; // Iter hack to force return of the whole hash
542         return new WikiDB_backend_dbaBase_pageiter($this, $links, $options);
543     }
544
545     /**
546      * Handle multi-searches for many relations and attributes in one expression.
547      * Bind all required attributes and relations per page together and pass it
548      * to one query.
549      *   (is_a::city and population < 20000) and (*::city and area > 1000000)
550      *   (is_a::city or linkto::CategoryCountry) and population < 20000 and area > 1000000
551      * Note that the 'linkto' and 'linkfrom' links are relations, containing an array.
552      *
553      * @param $pages     object A TextSearchQuery object for the pagename filter.
554      * @param $query     object A SemanticSearchQuery object for the links.
555      * @param $options   array  Currently ignored. hash of sortby, limit, exclude
556      *                          for the pagelist.
557      * @return object A WikiDB_backend_iterator.
558      * @see WikiDB::linkSearch
559      */
560     function relation_search($pages, $query, $options = array())
561     {
562         $linkdb = &$this->_linkdb;
563         $links = array();
564         // We need to detect which attributes and relation names we should look for. NYI
565         $want_attributes = $query->hasAttributes();
566         $want_relation = $query->hasRelations();
567         $linknames = $query->getLinkNames();
568         // create a hash for faster checks
569         $linkcheck = array();
570         foreach ($linknames as $l) $linkcheck[$l] = 1;
571
572         for ($link = $linkdb->_db->firstkey();
573              $link !== false;
574              $link = $linkdb->_db->nextkey()) {
575             $type = $reverse ? 'i' : 'o';
576             if ($link[0] != $type) continue;
577             $pagename = substr($link, 1);
578             if (!$pages->match($pagename)) continue;
579             $pagelinks = array();
580             if ($want_attributes) {
581                 $page = $GLOBALS['request']->_dbi->getPage($pagename);
582                 $attribs = $page->get('attributes');
583                 $pagelinks = $attribs;
584             }
585             if ($want_relations) {
586                 // all links contain arrays of pagenames, just the attributes
587                 // are guaranteed to be singular
588                 if (isset($linkcheck['linkfrom'])) {
589                     $pagelinks['linkfrom'] = $linkdb->_get_links('i', $pagename);
590                 }
591                 $outlinks = $linkdb->_get_links('o', $pagename);
592                 $want_to = isset($linkcheck['linkto']);
593                 foreach ($outlinks as $link) { // linkto => page, relation => page
594                     // all named links
595                     if ((isset($link['relation'])) and $link['relation']
596                         and isset($linkcheck[$link['relation']])
597                     )
598                         $pagelinks[$link['relation']][] = $link['linkto'];
599                     if ($want_to)
600                         $pagelinks['linkto'][] = is_array($link) ? $link['linkto'] : $link;
601                 }
602             }
603             if ($result = $query->match($pagelinks)) {
604                 $links = array_merge($links, $result);
605             }
606         }
607         $options['want_relations'] = true; // Iter hack to force return of the whole hash
608         return new WikiDB_backend_dbaBase_pageiter($this, $links, $options);
609     }
610 }
611
612 function WikiDB_backend_dbaBase_sortby_pagename_ASC($a, $b)
613 {
614     return strcasecmp($a, $b);
615 }
616
617 function WikiDB_backend_dbaBase_sortby_pagename_DESC($a, $b)
618 {
619     return strcasecmp($b, $a);
620 }
621
622 function WikiDB_backend_dbaBase_sortby_mtime_ASC($a, $b)
623 {
624     return WikiDB_backend_dbaBase_sortby_num($a, $b, 'mtime');
625 }
626
627 function WikiDB_backend_dbaBase_sortby_mtime_DESC($a, $b)
628 {
629     return WikiDB_backend_dbaBase_sortby_num($b, $a, 'mtime');
630 }
631
632 /*
633 function WikiDB_backend_dbaBase_sortby_hits_ASC ($a, $b) {
634     return WikiDB_backend_dbaBase_sortby_num($a, $b, 'hits');
635 }
636 function WikiDB_backend_dbaBase_sortby_hits_DESC ($a, $b) {
637     return WikiDB_backend_dbaBase_sortby_num($b, $a, 'hits');
638 }
639 */
640 function WikiDB_backend_dbaBase_sortby_num($aname, $bname, $field)
641 {
642     global $request;
643     $dbi = $request->getDbh();
644     // fields are stored in versiondata
645     $av = $dbi->_backend->get_latest_version($aname);
646     $bv = $dbi->_backend->get_latest_version($bname);
647     $a = $dbi->_backend->get_versiondata($aname, $av, false);
648     if (!$a) return -1;
649     $b = $dbi->_backend->get_versiondata($bname, $bv, false);
650     if (!$b or !isset($b[$field])) return 0;
651     if (empty($a[$field])) return -1;
652     if ((!isset($a[$field]) and !isset($b[$field])) or ($a[$field] === $b[$field])) {
653         return 0;
654     } else {
655         return ($a[$field] < $b[$field]) ? -1 : 1;
656     }
657 }
658
659 class WikiDB_backend_dbaBase_pageiter
660     extends WikiDB_backend_iterator
661 {
662     // fixed for linkrelations
663     function WikiDB_backend_dbaBase_pageiter(&$backend, &$pages, $options = false)
664     {
665         $this->_backend = $backend;
666         $this->_options = $options;
667         if ($pages) {
668             if (!empty($options['sortby'])) {
669                 $sortby = WikiDB_backend::sortby($options['sortby'], 'db',
670                     array('pagename', 'mtime'));
671                 // check for which column to sortby
672                 if ($sortby and !strstr($sortby, "hits ")) {
673                     usort($pages, 'WikiDB_backend_dbaBase_sortby_'
674                         . str_replace(' ', '_', $sortby));
675                 }
676             }
677             if (!empty($options['limit'])) {
678                 list($offset, $limit) = WikiDB_backend::limit($options['limit']);
679                 $pages = array_slice($pages, $offset, $limit);
680             }
681             $this->_pages = $pages;
682         } else
683             $this->_pages = array();
684     }
685
686     // fixed for relations
687     function next()
688     {
689         if (!($page = array_shift($this->_pages)))
690             return false;
691         if (!empty($this->_options['want_relations'])) {
692             // $linkrelation = $page['linkrelation'];
693             $pagename = $page['pagename'];
694             if (!empty($this->_options['exclude'])
695                 and in_array($pagename, $this->_options['exclude'])
696             )
697                 return $this->next();
698             return $page;
699         }
700         if (!empty($this->_options['exclude'])
701             and in_array($page, $this->_options['exclude'])
702         )
703             return $this->next();
704         return array('pagename' => $page);
705     }
706
707     function reset()
708     {
709         reset($this->_pages);
710     }
711
712     function free()
713     {
714         $this->_pages = array();
715     }
716 }
717
718 class WikiDB_backend_dbaBase_linktable
719 {
720     function WikiDB_backend_dbaBase_linktable(&$dba)
721     {
722         $this->_db = &$dba;
723     }
724
725     //TODO: try storing link lists as hashes rather than arrays.
726     //      backlink deletion would be faster.
727     function get_links($page, $reversed = true, $want_relations = false)
728     {
729         if ($want_relations) {
730             $this->found_relations = 0;
731             $links = $this->_get_links($reversed ? 'i' : 'o', $page);
732             $linksonly = array();
733             foreach ($links as $link) { // linkto => page, linkrelation => page
734                 if (is_array($link) and isset($link['relation'])) {
735                     if ($link['relation'])
736                         $this->found_relations++;
737                     $linksonly[] = array('pagename' => $link['linkto'],
738                         'linkrelation' => $link['relation']);
739                 } else { // empty relations are stripped
740                     $linksonly[] = array('pagename' => $link['linkto']);
741                 }
742             }
743             return $linksonly;
744         } else {
745             $links = $this->_get_links($reversed ? 'i' : 'o', $page);
746             $linksonly = array();
747             foreach ($links as $link) {
748                 if (is_array($link)) {
749                     $linksonly[] = $link['linkto'];
750                 } else
751                     $linksonly[] = $link;
752             }
753             return $linksonly;
754         }
755     }
756
757     // fixed: relations ready
758     function set_links($page, $links)
759     {
760
761         $oldlinks = $this->get_links($page, false, false);
762
763         if (!is_array($links)) {
764             assert(empty($links));
765             $links = array();
766         }
767         $this->_set_links('o', $page, $links);
768
769         /* Now for the backlink update we squash the linkto hashes into a simple array */
770         $newlinks = array();
771         foreach ($links as $hash) {
772             if (!empty($hash['linkto']) and !in_array($hash['linkto'], $newlinks))
773                 // for attributes it's empty
774                 $newlinks[] = $hash['linkto'];
775             elseif (is_string($hash) and !in_array($hash, $newlinks))
776                 $newlinks[] = $hash;
777         }
778         //$newlinks = array_unique($newlinks);
779         sort($oldlinks);
780         sort($newlinks);
781
782         reset($newlinks);
783         reset($oldlinks);
784         $new = current($newlinks);
785         $old = current($oldlinks);
786         while ($new !== false || $old !== false) {
787             if ($old === false || ($new !== false && $new < $old)) {
788                 // $new is a new link (not in $oldlinks).
789                 $this->_add_backlink($new, $page);
790                 $new = next($newlinks);
791             } elseif ($new === false || $old < $new) {
792                 // $old is a obsolete link (not in $newlinks).
793                 $this->_delete_backlink($old, $page);
794                 $old = next($oldlinks);
795             } else {
796                 // Unchanged link (in both $newlist and $oldlinks).
797                 assert($new == $old);
798                 $new = next($newlinks);
799                 $old = next($oldlinks);
800             }
801         }
802     }
803
804     /**
805      * Rebuild the back-link index.
806      *
807      * This should never be needed, but if the database gets hosed for some reason,
808      * this should put it back into a consistent state.
809      *
810      * We assume the forward links in the our table are correct, and recalculate
811      * all the backlinks appropriately.
812      */
813     function rebuild()
814     {
815         $db = &$this->_db;
816
817         // Delete the backlink tables, make a list of lo.page keys.
818         $okeys = array();
819         for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
820             if ($key[0] == 'i')
821                 $db->delete($key);
822             elseif ($key[0] == 'o')
823                 $okeys[] = $key; else {
824                 trigger_error("Bad key in linktable: '$key'", E_USER_WARNING);
825                 $db->delete($key);
826             }
827         }
828         foreach ($okeys as $key) {
829             $page = substr($key, 1);
830             $links = $this->_get_links('o', $page);
831             $db->delete($key);
832             $this->set_links($page, $links);
833         }
834     }
835
836     function check()
837     {
838         $db = &$this->_db;
839
840         // FIXME: check for sortedness and uniqueness in links lists.
841
842         for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
843             if (strlen($key) < 1 || ($key[0] != 'i' && $key[0] != 'o')) {
844                 $errs[] = "Bad key '$key' in table";
845                 continue;
846             }
847             $page = substr($key, 1);
848             if ($key[0] == 'o') {
849                 // Forward links.
850                 foreach ($this->_get_links('o', $page) as $link) {
851                     $link = $link['linkto'];
852                     if (!$this->_has_link('i', $link, $page))
853                         $errs[] = "backlink entry missing for link '$page'->'$link'";
854                 }
855             } else {
856                 assert($key[0] == 'i');
857                 // Backlinks.
858                 foreach ($this->_get_links('i', $page) as $link) {
859                     if (!$this->_has_link('o', $link, $page))
860                         $errs[] = "link entry missing for backlink '$page'<-'$link'";
861                 }
862             }
863         }
864         //if ($errs) $this->rebuild();
865         return isset($errs) ? $errs : false;
866     }
867
868     /* TODO: Add another lrRelationName key for relations.
869      * lrRelationName: frompage => topage
870      */
871
872     function _add_relation($page, $linkedfrom)
873     {
874         $relations = $this->_get_links('r', $page);
875         $backlinks[] = $linkedfrom;
876         sort($backlinks);
877         $this->_set_links('i', $page, $backlinks);
878     }
879
880     function _add_backlink($page, $linkedfrom)
881     {
882         $backlinks = $this->_get_links('i', $page);
883         $backlinks[] = $linkedfrom;
884         sort($backlinks);
885         $this->_set_links('i', $page, $backlinks);
886     }
887
888     function _delete_backlink($page, $linkedfrom)
889     {
890         $backlinks = $this->_get_links('i', $page);
891         foreach ($backlinks as $key => $backlink) {
892             if ($backlink == $linkedfrom)
893                 unset($backlinks[$key]);
894         }
895         $this->_set_links('i', $page, $backlinks);
896     }
897
898     function _has_link($which, $page, $link)
899     {
900         $links = $this->_get_links($which, $page);
901         // since links are always sorted, break if >
902         // TODO: binary search
903         foreach ($links as $l) {
904             if ($l['linkto'] == $link)
905                 return true;
906             if ($l['linkto'] > $link)
907                 return false;
908         }
909         return false;
910     }
911
912     function _get_links($which, $page)
913     {
914         $data = $this->_db->get($which . $page);
915         return $data ? unserialize($data) : array();
916     }
917
918     function _set_links($which, $page, &$links)
919     {
920         $key = $which . $page;
921         if ($links)
922             $this->_db->set($key, serialize($links));
923         else
924             $this->_db->set($key, false);
925     }
926 }
927
928 // Local Variables:
929 // mode: php
930 // tab-width: 8
931 // c-basic-offset: 4
932 // c-hanging-comment-ender-p: nil
933 // indent-tabs-mode: nil
934 // End: