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