]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend/dbaBase.php
Reformat code
[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 ;
613
614 function WikiDB_backend_dbaBase_sortby_pagename_ASC($a, $b)
615 {
616     return strcasecmp($a, $b);
617 }
618
619 function WikiDB_backend_dbaBase_sortby_pagename_DESC($a, $b)
620 {
621     return strcasecmp($b, $a);
622 }
623
624 function WikiDB_backend_dbaBase_sortby_mtime_ASC($a, $b)
625 {
626     return WikiDB_backend_dbaBase_sortby_num($a, $b, 'mtime');
627 }
628
629 function WikiDB_backend_dbaBase_sortby_mtime_DESC($a, $b)
630 {
631     return WikiDB_backend_dbaBase_sortby_num($b, $a, 'mtime');
632 }
633
634 /*
635 function WikiDB_backend_dbaBase_sortby_hits_ASC ($a, $b) {
636     return WikiDB_backend_dbaBase_sortby_num($a, $b, 'hits');
637 }
638 function WikiDB_backend_dbaBase_sortby_hits_DESC ($a, $b) {
639     return WikiDB_backend_dbaBase_sortby_num($b, $a, 'hits');
640 }
641 */
642 function WikiDB_backend_dbaBase_sortby_num($aname, $bname, $field)
643 {
644     global $request;
645     $dbi = $request->getDbh();
646     // fields are stored in versiondata
647     $av = $dbi->_backend->get_latest_version($aname);
648     $bv = $dbi->_backend->get_latest_version($bname);
649     $a = $dbi->_backend->get_versiondata($aname, $av, false);
650     if (!$a) return -1;
651     $b = $dbi->_backend->get_versiondata($bname, $bv, false);
652     if (!$b or !isset($b[$field])) return 0;
653     if (empty($a[$field])) return -1;
654     if ((!isset($a[$field]) and !isset($b[$field])) or ($a[$field] === $b[$field])) {
655         return 0;
656     } else {
657         return ($a[$field] < $b[$field]) ? -1 : 1;
658     }
659 }
660
661 class WikiDB_backend_dbaBase_pageiter
662     extends WikiDB_backend_iterator
663 {
664     // fixed for linkrelations
665     function WikiDB_backend_dbaBase_pageiter(&$backend, &$pages, $options = false)
666     {
667         $this->_backend = $backend;
668         $this->_options = $options;
669         if ($pages) {
670             if (!empty($options['sortby'])) {
671                 $sortby = WikiDB_backend::sortby($options['sortby'], 'db',
672                     array('pagename', 'mtime'));
673                 // check for which column to sortby
674                 if ($sortby and !strstr($sortby, "hits ")) {
675                     usort($pages, 'WikiDB_backend_dbaBase_sortby_'
676                         . str_replace(' ', '_', $sortby));
677                 }
678             }
679             if (!empty($options['limit'])) {
680                 list($offset, $limit) = WikiDB_backend::limit($options['limit']);
681                 $pages = array_slice($pages, $offset, $limit);
682             }
683             $this->_pages = $pages;
684         } else
685             $this->_pages = array();
686     }
687
688     // fixed for relations
689     function next()
690     {
691         if (!($page = array_shift($this->_pages)))
692             return false;
693         if (!empty($this->_options['want_relations'])) {
694             // $linkrelation = $page['linkrelation'];
695             $pagename = $page['pagename'];
696             if (!empty($this->_options['exclude'])
697                 and in_array($pagename, $this->_options['exclude'])
698             )
699                 return $this->next();
700             return $page;
701         }
702         if (!empty($this->_options['exclude'])
703             and in_array($page, $this->_options['exclude'])
704         )
705             return $this->next();
706         return array('pagename' => $page);
707     }
708
709     function reset()
710     {
711         reset($this->_pages);
712     }
713
714     function free()
715     {
716         $this->_pages = array();
717     }
718 }
719
720 ;
721
722 class WikiDB_backend_dbaBase_linktable
723 {
724     function WikiDB_backend_dbaBase_linktable(&$dba)
725     {
726         $this->_db = &$dba;
727     }
728
729     //TODO: try storing link lists as hashes rather than arrays.
730     //      backlink deletion would be faster.
731     function get_links($page, $reversed = true, $want_relations = false)
732     {
733         if ($want_relations) {
734             $this->found_relations = 0;
735             $links = $this->_get_links($reversed ? 'i' : 'o', $page);
736             $linksonly = array();
737             foreach ($links as $link) { // linkto => page, linkrelation => page
738                 if (is_array($link) and isset($link['relation'])) {
739                     if ($link['relation'])
740                         $this->found_relations++;
741                     $linksonly[] = array('pagename' => $link['linkto'],
742                         'linkrelation' => $link['relation']);
743                 } else { // empty relations are stripped
744                     $linksonly[] = array('pagename' => $link['linkto']);
745                 }
746             }
747             return $linksonly;
748         } else {
749             $links = $this->_get_links($reversed ? 'i' : 'o', $page);
750             $linksonly = array();
751             foreach ($links as $link) {
752                 if (is_array($link)) {
753                     $linksonly[] = $link['linkto'];
754                 } else
755                     $linksonly[] = $link;
756             }
757             return $linksonly;
758         }
759     }
760
761     // fixed: relations ready
762     function set_links($page, $links)
763     {
764
765         $oldlinks = $this->get_links($page, false, false);
766
767         if (!is_array($links)) {
768             assert(empty($links));
769             $links = array();
770         }
771         $this->_set_links('o', $page, $links);
772
773         /* Now for the backlink update we squash the linkto hashes into a simple array */
774         $newlinks = array();
775         foreach ($links as $hash) {
776             if (!empty($hash['linkto']) and !in_array($hash['linkto'], $newlinks))
777                 // for attributes it's empty
778                 $newlinks[] = $hash['linkto'];
779             elseif (is_string($hash) and !in_array($hash, $newlinks))
780                 $newlinks[] = $hash;
781         }
782         //$newlinks = array_unique($newlinks);
783         sort($oldlinks);
784         sort($newlinks);
785
786         reset($newlinks);
787         reset($oldlinks);
788         $new = current($newlinks);
789         $old = current($oldlinks);
790         while ($new !== false || $old !== false) {
791             if ($old === false || ($new !== false && $new < $old)) {
792                 // $new is a new link (not in $oldlinks).
793                 $this->_add_backlink($new, $page);
794                 $new = next($newlinks);
795             } elseif ($new === false || $old < $new) {
796                 // $old is a obsolete link (not in $newlinks).
797                 $this->_delete_backlink($old, $page);
798                 $old = next($oldlinks);
799             } else {
800                 // Unchanged link (in both $newlist and $oldlinks).
801                 assert($new == $old);
802                 $new = next($newlinks);
803                 $old = next($oldlinks);
804             }
805         }
806     }
807
808     /**
809      * Rebuild the back-link index.
810      *
811      * This should never be needed, but if the database gets hosed for some reason,
812      * this should put it back into a consistent state.
813      *
814      * We assume the forward links in the our table are correct, and recalculate
815      * all the backlinks appropriately.
816      */
817     function rebuild()
818     {
819         $db = &$this->_db;
820
821         // Delete the backlink tables, make a list of lo.page keys.
822         $okeys = array();
823         for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
824             if ($key[0] == 'i')
825                 $db->delete($key);
826             elseif ($key[0] == 'o')
827                 $okeys[] = $key; else {
828                 trigger_error("Bad key in linktable: '$key'", E_USER_WARNING);
829                 $db->delete($key);
830             }
831         }
832         foreach ($okeys as $key) {
833             $page = substr($key, 1);
834             $links = $this->_get_links('o', $page);
835             $db->delete($key);
836             $this->set_links($page, $links);
837         }
838     }
839
840     function check()
841     {
842         $db = &$this->_db;
843
844         // FIXME: check for sortedness and uniqueness in links lists.
845
846         for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
847             if (strlen($key) < 1 || ($key[0] != 'i' && $key[0] != 'o')) {
848                 $errs[] = "Bad key '$key' in table";
849                 continue;
850             }
851             $page = substr($key, 1);
852             if ($key[0] == 'o') {
853                 // Forward links.
854                 foreach ($this->_get_links('o', $page) as $link) {
855                     $link = $link['linkto'];
856                     if (!$this->_has_link('i', $link, $page))
857                         $errs[] = "backlink entry missing for link '$page'->'$link'";
858                 }
859             } else {
860                 assert($key[0] == 'i');
861                 // Backlinks.
862                 foreach ($this->_get_links('i', $page) as $link) {
863                     if (!$this->_has_link('o', $link, $page))
864                         $errs[] = "link entry missing for backlink '$page'<-'$link'";
865                 }
866             }
867         }
868         //if ($errs) $this->rebuild();
869         return isset($errs) ? $errs : false;
870     }
871
872     /* TODO: Add another lrRelationName key for relations.
873      * lrRelationName: frompage => topage
874      */
875
876     function _add_relation($page, $linkedfrom)
877     {
878         $relations = $this->_get_links('r', $page);
879         $backlinks[] = $linkedfrom;
880         sort($backlinks);
881         $this->_set_links('i', $page, $backlinks);
882     }
883
884     function _add_backlink($page, $linkedfrom)
885     {
886         $backlinks = $this->_get_links('i', $page);
887         $backlinks[] = $linkedfrom;
888         sort($backlinks);
889         $this->_set_links('i', $page, $backlinks);
890     }
891
892     function _delete_backlink($page, $linkedfrom)
893     {
894         $backlinks = $this->_get_links('i', $page);
895         foreach ($backlinks as $key => $backlink) {
896             if ($backlink == $linkedfrom)
897                 unset($backlinks[$key]);
898         }
899         $this->_set_links('i', $page, $backlinks);
900     }
901
902     function _has_link($which, $page, $link)
903     {
904         $links = $this->_get_links($which, $page);
905         // since links are always sorted, break if >
906         // TODO: binary search
907         foreach ($links as $l) {
908             if ($l['linkto'] == $link)
909                 return true;
910             if ($l['linkto'] > $link)
911                 return false;
912         }
913         return false;
914     }
915
916     function _get_links($which, $page)
917     {
918         $data = $this->_db->get($which . $page);
919         return $data ? unserialize($data) : array();
920     }
921
922     function _set_links($which, $page, &$links)
923     {
924         $key = $which . $page;
925         if ($links)
926             $this->_db->set($key, serialize($links));
927         else
928             $this->_db->set($key, false);
929     }
930 }
931
932 // Local Variables:
933 // mode: php
934 // tab-width: 8
935 // c-basic-offset: 4
936 // c-hanging-comment-ender-p: nil
937 // indent-tabs-mode: nil
938 // End: