]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiDB/backend/dbaBase.php
Use __construct
[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 separate 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 __construct(&$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      * @return array of all linkrelations
402      * Faster than the dumb WikiDB method.
403      */
404     public function list_relations($also_attributes = false,
405                             $only_attributes = false,
406                             $sorted = true)
407     {
408         $linkdb = &$this->_linkdb;
409         $relations = array();
410         for ($link = $linkdb->_db->firstkey();
411              $link !== false;
412              $link = $linkdb->_db->nextkey()) {
413             if ($link[0] != 'o') continue;
414             $links = $linkdb->_get_links('o', substr($link, 1));
415             foreach ($links as $link) { // linkto => page, linkrelation => page
416                 if (is_array($link)
417                     and $link['relation']
418                         and !in_array($link['relation'], $relations)
419                 ) {
420                     $is_attribute = empty($link['linkto']); // a relation has both
421                     if ($is_attribute) {
422                         if ($only_attributes or $also_attributes)
423                             $relations[] = $link['relation'];
424                     } elseif (!$only_attributes) {
425                         $relations[] = $link['relation'];
426                     }
427                 }
428             }
429         }
430         if ($sorted) {
431             sort($relations);
432             reset($relations);
433         }
434         return $relations;
435     }
436
437     /**
438      * WikiDB_backend_dumb_LinkSearchIter searches over all
439      * pages and then all its links.  Since there are less
440      * links than pages, and we easily get the pagename from
441      * the link key, we iterate here directly over the
442      * linkdb and check the pagematch there.
443      *
444      * @param $pages     object A TextSearchQuery object for the pagename filter.
445      * @param $query     object A SearchQuery object (Text or Numeric) for the linkvalues,
446      *                          linkto, linkfrom (=backlink), relation or attribute values.
447      * @param $linktype  string One of the 4 linktypes "linkto",
448      *                          "linkfrom" (=backlink), "relation" or "attribute".
449      *                 Maybe also "relation+attribute" for the advanced search.
450      * @param $relation  object A TextSearchQuery for the linkname or false.
451      * @param $options   array Currently ignored. hash of sortby, limit, exclude.
452      * @return object A WikiDB_backend_iterator.
453      * @see WikiDB::linkSearch
454      */
455     function link_search($pages, $query, $linktype,
456                          $relation = false, $options = array())
457     {
458         $linkdb = &$this->_linkdb;
459         $links = array();
460         $reverse = false;
461         $want_relations = false;
462         if ($linktype == 'relation') {
463             $want_relations = true;
464             $field = 'linkrelation';
465         }
466         if ($linktype == 'attribute') {
467             $want_relations = true;
468             $field = 'attribute';
469         }
470         if ($linktype == 'linkfrom') {
471             $reverse = true;
472         }
473
474         for ($link = $linkdb->_db->firstkey();
475              $link !== false;
476              $link = $linkdb->_db->nextkey()) {
477             $type = $reverse ? 'i' : 'o';
478             if ($link[0] != $type) continue;
479             $pagename = substr($link, 1);
480             if (!$pages->match($pagename)) continue;
481             if ($linktype == 'attribute') {
482                 $page = $GLOBALS['request']->_dbi->getPage($pagename);
483                 $attribs = $page->get('attributes');
484                 if ($attribs) {
485                     /* Optimization on expressive searches:
486                        for queries with multiple attributes.
487                        Just take the defined placeholders from the query(ies)
488                        if there are more attributes than query variables.
489                     */
490                     if ($query->getType() != 'text'
491                         and !$relation
492                             and ((count($vars = $query->getVars()) > 1)
493                                 or (count($attribs) > count($vars)))
494                     ) {
495                         // names must strictly match. no * allowed
496                         if (!$query->can_match($attribs)) continue;
497                         if (!($result = $query->match($attribs))) continue;
498                         foreach ($result as $r) {
499                             $r['pagename'] = $pagename;
500                             $links[] = $r;
501                         }
502                     } else {
503                         // textsearch or simple value. no strict bind by name needed
504                         foreach ($attribs as $attribute => $value) {
505                             if ($relation and !$relation->match($attribute)) continue;
506                             if (!$query->match($value)) continue;
507                             $links[] = array('pagename' => $pagename,
508                                 'linkname' => $attribute,
509                                 'linkvalue' => $value);
510                         }
511                     }
512                 }
513             } else {
514                 // TODO: honor limits. this can get large.
515                 if ($want_relations) {
516                     // MAP linkrelation : pagename => thispagename : linkname : linkvalue
517                     $_links = $linkdb->_get_links('o', $pagename);
518                     foreach ($_links as $link) { // linkto => page, linkrelation => page
519                         if (!isset($link['relation']) or !$link['relation']) continue;
520                         if ($relation and !$relation->match($link['relation'])) continue;
521                         if (!$query->match($link['linkto'])) continue;
522                         $links[] = array('pagename' => $pagename,
523                             'linkname' => $link['relation'],
524                             'linkvalue' => $link['linkto']);
525                     }
526                 } else {
527                     $_links = $linkdb->_get_links($reverse ? 'i' : 'o', $pagename);
528                     foreach ($_links as $link) { // linkto => page
529                         if (is_array($link))
530                             $link = $link['linkto'];
531                         if (!$query->match($link)) continue;
532                         $links[] = array('pagename' => $pagename,
533                             'linkname' => '',
534                             'linkvalue' => $link);
535                     }
536                 }
537             }
538         }
539         $options['want_relations'] = true; // Iter hack to force return of the whole hash
540         return new WikiDB_backend_dbaBase_pageiter($this, $links, $options);
541     }
542
543     /**
544      * Handle multi-searches for many relations and attributes in one expression.
545      * Bind all required attributes and relations per page together and pass it
546      * to one query.
547      *   (is_a::city and population < 20000) and (*::city and area > 1000000)
548      *   (is_a::city or linkto::CategoryCountry) and population < 20000 and area > 1000000
549      * Note that the 'linkto' and 'linkfrom' links are relations, containing an array.
550      *
551      * @param $pages     object A TextSearchQuery object for the pagename filter.
552      * @param $query     object A SemanticSearchQuery object for the links.
553      * @param $options   array  Currently ignored. hash of sortby, limit, exclude
554      *                          for the pagelist.
555      * @return object A WikiDB_backend_iterator.
556      * @see WikiDB::linkSearch
557      */
558     function relation_search($pages, $query, $options = array())
559     {
560         $linkdb = &$this->_linkdb;
561         $links = array();
562         // We need to detect which attributes and relation names we should look for. NYI
563         $want_attributes = $query->hasAttributes();
564         $want_relation = $query->hasRelations();
565         $linknames = $query->getLinkNames();
566         // create a hash for faster checks
567         $linkcheck = array();
568         foreach ($linknames as $l) $linkcheck[$l] = 1;
569
570         for ($link = $linkdb->_db->firstkey();
571              $link !== false;
572              $link = $linkdb->_db->nextkey()) {
573             $type = $reverse ? 'i' : 'o';
574             if ($link[0] != $type) continue;
575             $pagename = substr($link, 1);
576             if (!$pages->match($pagename)) continue;
577             $pagelinks = array();
578             if ($want_attributes) {
579                 $page = $GLOBALS['request']->_dbi->getPage($pagename);
580                 $attribs = $page->get('attributes');
581                 $pagelinks = $attribs;
582             }
583             if ($want_relations) {
584                 // all links contain arrays of pagenames, just the attributes
585                 // are guaranteed to be singular
586                 if (isset($linkcheck['linkfrom'])) {
587                     $pagelinks['linkfrom'] = $linkdb->_get_links('i', $pagename);
588                 }
589                 $outlinks = $linkdb->_get_links('o', $pagename);
590                 $want_to = isset($linkcheck['linkto']);
591                 foreach ($outlinks as $link) { // linkto => page, relation => page
592                     // all named links
593                     if ((isset($link['relation'])) and $link['relation']
594                         and isset($linkcheck[$link['relation']])
595                     )
596                         $pagelinks[$link['relation']][] = $link['linkto'];
597                     if ($want_to)
598                         $pagelinks['linkto'][] = is_array($link) ? $link['linkto'] : $link;
599                 }
600             }
601             if ($result = $query->match($pagelinks)) {
602                 $links = array_merge($links, $result);
603             }
604         }
605         $options['want_relations'] = true; // Iter hack to force return of the whole hash
606         return new WikiDB_backend_dbaBase_pageiter($this, $links, $options);
607     }
608 }
609
610 function WikiDB_backend_dbaBase_sortby_pagename_ASC($a, $b)
611 {
612     return strcasecmp($a, $b);
613 }
614
615 function WikiDB_backend_dbaBase_sortby_pagename_DESC($a, $b)
616 {
617     return strcasecmp($b, $a);
618 }
619
620 function WikiDB_backend_dbaBase_sortby_mtime_ASC($a, $b)
621 {
622     return WikiDB_backend_dbaBase_sortby_num($a, $b, 'mtime');
623 }
624
625 function WikiDB_backend_dbaBase_sortby_mtime_DESC($a, $b)
626 {
627     return WikiDB_backend_dbaBase_sortby_num($b, $a, 'mtime');
628 }
629
630 /*
631 function WikiDB_backend_dbaBase_sortby_hits_ASC ($a, $b) {
632     return WikiDB_backend_dbaBase_sortby_num($a, $b, 'hits');
633 }
634 function WikiDB_backend_dbaBase_sortby_hits_DESC ($a, $b) {
635     return WikiDB_backend_dbaBase_sortby_num($b, $a, 'hits');
636 }
637 */
638 function WikiDB_backend_dbaBase_sortby_num($aname, $bname, $field)
639 {
640     global $request;
641     $dbi = $request->getDbh();
642     // fields are stored in versiondata
643     $av = $dbi->_backend->get_latest_version($aname);
644     $bv = $dbi->_backend->get_latest_version($bname);
645     $a = $dbi->_backend->get_versiondata($aname, $av, false);
646     if (!$a) return -1;
647     $b = $dbi->_backend->get_versiondata($bname, $bv, false);
648     if (!$b or !isset($b[$field])) return 0;
649     if (empty($a[$field])) return -1;
650     if ((!isset($a[$field]) and !isset($b[$field])) or ($a[$field] === $b[$field])) {
651         return 0;
652     } else {
653         return ($a[$field] < $b[$field]) ? -1 : 1;
654     }
655 }
656
657 class WikiDB_backend_dbaBase_pageiter
658     extends WikiDB_backend_iterator
659 {
660     // fixed for linkrelations
661     function __construct(&$backend, &$pages, $options = false)
662     {
663         $this->_backend = $backend;
664         $this->_options = $options;
665         if ($pages) {
666             if (!empty($options['sortby'])) {
667                 $sortby = WikiDB_backend::sortby($options['sortby'], 'db',
668                     array('pagename', 'mtime'));
669                 // check for which column to sortby
670                 if ($sortby and !strstr($sortby, "hits ")) {
671                     usort($pages, 'WikiDB_backend_dbaBase_sortby_'
672                         . str_replace(' ', '_', $sortby));
673                 }
674             }
675             if (!empty($options['limit'])) {
676                 list($offset, $limit) = WikiDB_backend::limit($options['limit']);
677                 $pages = array_slice($pages, $offset, $limit);
678             }
679             $this->_pages = $pages;
680         } else
681             $this->_pages = array();
682     }
683
684     // fixed for relations
685     function next()
686     {
687         if (!($page = array_shift($this->_pages)))
688             return false;
689         if (!empty($this->_options['want_relations'])) {
690             // $linkrelation = $page['linkrelation'];
691             $pagename = $page['pagename'];
692             if (!empty($this->_options['exclude'])
693                 and in_array($pagename, $this->_options['exclude'])
694             )
695                 return $this->next();
696             return $page;
697         }
698         if (!empty($this->_options['exclude'])
699             and in_array($page, $this->_options['exclude'])
700         )
701             return $this->next();
702         return array('pagename' => $page);
703     }
704
705     function reset()
706     {
707         reset($this->_pages);
708     }
709
710     function free()
711     {
712         $this->_pages = array();
713     }
714 }
715
716 class WikiDB_backend_dbaBase_linktable
717 {
718     function WikiDB_backend_dbaBase_linktable(&$dba)
719     {
720         $this->_db = &$dba;
721     }
722
723     //TODO: try storing link lists as hashes rather than arrays.
724     //      backlink deletion would be faster.
725     function get_links($page, $reversed = true, $want_relations = false)
726     {
727         if ($want_relations) {
728             $this->found_relations = 0;
729             $links = $this->_get_links($reversed ? 'i' : 'o', $page);
730             $linksonly = array();
731             foreach ($links as $link) { // linkto => page, linkrelation => page
732                 if (is_array($link) and isset($link['relation'])) {
733                     if ($link['relation'])
734                         $this->found_relations++;
735                     $linksonly[] = array('pagename' => $link['linkto'],
736                         'linkrelation' => $link['relation']);
737                 } else { // empty relations are stripped
738                     $linksonly[] = array('pagename' => $link['linkto']);
739                 }
740             }
741             return $linksonly;
742         } else {
743             $links = $this->_get_links($reversed ? 'i' : 'o', $page);
744             $linksonly = array();
745             foreach ($links as $link) {
746                 if (is_array($link)) {
747                     $linksonly[] = $link['linkto'];
748                 } else
749                     $linksonly[] = $link;
750             }
751             return $linksonly;
752         }
753     }
754
755     // fixed: relations ready
756     function set_links($page, $links)
757     {
758
759         $oldlinks = $this->get_links($page, false, false);
760
761         if (!is_array($links)) {
762             assert(empty($links));
763             $links = array();
764         }
765         $this->_set_links('o', $page, $links);
766
767         /* Now for the backlink update we squash the linkto hashes into a simple array */
768         $newlinks = array();
769         foreach ($links as $hash) {
770             if (!empty($hash['linkto']) and !in_array($hash['linkto'], $newlinks))
771                 // for attributes it's empty
772                 $newlinks[] = $hash['linkto'];
773             elseif (is_string($hash) and !in_array($hash, $newlinks))
774                 $newlinks[] = $hash;
775         }
776         //$newlinks = array_unique($newlinks);
777         sort($oldlinks);
778         sort($newlinks);
779
780         reset($newlinks);
781         reset($oldlinks);
782         $new = current($newlinks);
783         $old = current($oldlinks);
784         while ($new !== false || $old !== false) {
785             if ($old === false || ($new !== false && $new < $old)) {
786                 // $new is a new link (not in $oldlinks).
787                 $this->_add_backlink($new, $page);
788                 $new = next($newlinks);
789             } elseif ($new === false || $old < $new) {
790                 // $old is a obsolete link (not in $newlinks).
791                 $this->_delete_backlink($old, $page);
792                 $old = next($oldlinks);
793             } else {
794                 // Unchanged link (in both $newlist and $oldlinks).
795                 assert($new == $old);
796                 $new = next($newlinks);
797                 $old = next($oldlinks);
798             }
799         }
800     }
801
802     /**
803      * Rebuild the back-link index.
804      *
805      * This should never be needed, but if the database gets hosed for some reason,
806      * this should put it back into a consistent state.
807      *
808      * We assume the forward links in the our table are correct, and recalculate
809      * all the backlinks appropriately.
810      */
811     function rebuild()
812     {
813         $db = &$this->_db;
814
815         // Delete the backlink tables, make a list of lo.page keys.
816         $okeys = array();
817         for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
818             if ($key[0] == 'i')
819                 $db->delete($key);
820             elseif ($key[0] == 'o')
821                 $okeys[] = $key; else {
822                 trigger_error("Bad key in linktable: '$key'", E_USER_WARNING);
823                 $db->delete($key);
824             }
825         }
826         foreach ($okeys as $key) {
827             $page = substr($key, 1);
828             $links = $this->_get_links('o', $page);
829             $db->delete($key);
830             $this->set_links($page, $links);
831         }
832     }
833
834     function check()
835     {
836         $db = &$this->_db;
837
838         // FIXME: check for sortedness and uniqueness in links lists.
839
840         for ($key = $db->firstkey(); $key; $key = $db->nextkey()) {
841             if (strlen($key) < 1 || ($key[0] != 'i' && $key[0] != 'o')) {
842                 $errs[] = "Bad key '$key' in table";
843                 continue;
844             }
845             $page = substr($key, 1);
846             if ($key[0] == 'o') {
847                 // Forward links.
848                 foreach ($this->_get_links('o', $page) as $link) {
849                     $link = $link['linkto'];
850                     if (!$this->_has_link('i', $link, $page))
851                         $errs[] = "backlink entry missing for link '$page'->'$link'";
852                 }
853             } else {
854                 assert($key[0] == 'i');
855                 // Backlinks.
856                 foreach ($this->_get_links('i', $page) as $link) {
857                     if (!$this->_has_link('o', $link, $page))
858                         $errs[] = "link entry missing for backlink '$page'<-'$link'";
859                 }
860             }
861         }
862         //if ($errs) $this->rebuild();
863         return isset($errs) ? $errs : false;
864     }
865
866     /* TODO: Add another lrRelationName key for relations.
867      * lrRelationName: frompage => topage
868      */
869
870     function _add_relation($page, $linkedfrom)
871     {
872         $relations = $this->_get_links('r', $page);
873         $backlinks[] = $linkedfrom;
874         sort($backlinks);
875         $this->_set_links('i', $page, $backlinks);
876     }
877
878     function _add_backlink($page, $linkedfrom)
879     {
880         $backlinks = $this->_get_links('i', $page);
881         $backlinks[] = $linkedfrom;
882         sort($backlinks);
883         $this->_set_links('i', $page, $backlinks);
884     }
885
886     function _delete_backlink($page, $linkedfrom)
887     {
888         $backlinks = $this->_get_links('i', $page);
889         foreach ($backlinks as $key => $backlink) {
890             if ($backlink == $linkedfrom)
891                 unset($backlinks[$key]);
892         }
893         $this->_set_links('i', $page, $backlinks);
894     }
895
896     function _has_link($which, $page, $link)
897     {
898         $links = $this->_get_links($which, $page);
899         // since links are always sorted, break if >
900         // TODO: binary search
901         foreach ($links as $l) {
902             if ($l['linkto'] == $link)
903                 return true;
904             if ($l['linkto'] > $link)
905                 return false;
906         }
907         return false;
908     }
909
910     function _get_links($which, $page)
911     {
912         $data = $this->_db->get($which . $page);
913         return $data ? unserialize($data) : array();
914     }
915
916     function _set_links($which, $page, &$links)
917     {
918         $key = $which . $page;
919         if ($links)
920             $this->_db->set($key, serialize($links));
921         else
922             $this->_db->set($key, false);
923     }
924 }
925
926 // Local Variables:
927 // mode: php
928 // tab-width: 8
929 // c-basic-offset: 4
930 // c-hanging-comment-ender-p: nil
931 // indent-tabs-mode: nil
932 // End: