]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/wikilens/RatingsDb.php
Update PHP Doc; type compatibility
[SourceForge/phpwiki.git] / lib / wikilens / RatingsDb.php
1 <?php
2
3 /*
4  * @author:  Dan Frankowski (wikilens group manager), Reini Urban (as plugin)
5  *
6  * TODO:
7  * - fix RATING_STORAGE = WIKIPAGE (dba, file)
8  * - fix smart caching
9  * - finish mysuggest.c (external engine with data from mysql)
10  * - add the various show modes (esp. TopN queries in PHP)
11  */
12 /*
13  CREATE TABLE rating (
14         dimension INT(4) NOT NULL,
15         raterpage INT(11) NOT NULL,
16         rateepage INT(11) NOT NULL,
17         ratingvalue FLOAT NOT NULL,
18         rateeversion INT(11) NOT NULL,
19         isPrivate ENUM('yes','no'),
20         tstamp TIMESTAMP(14) NOT NULL,
21         PRIMARY KEY (dimension, raterpage, rateepage)
22  );
23 */
24
25 // For other than SQL backends. dba + adodb SQL ratings are allowed but deprecated.
26 // We will probably drop this hack.
27 if (!defined('RATING_STORAGE'))
28     // for DATABASE_TYPE=dba and forced RATING_STORAGE=SQL we must use ADODB,
29     // but this is problematic.
30     define('RATING_STORAGE', $GLOBALS['request']->_dbi->_backend->isSQL() ? 'SQL' : 'WIKIPAGE');
31 //define('RATING_STORAGE','WIKIPAGE');   // not fully supported yet
32
33 // leave undefined for internal, slow php engine.
34 //if (!defined('RATING_EXTERNAL'))
35 //    define('RATING_EXTERNAL',PHPWIKI_DIR . 'suggest.exe');
36
37 // Dimensions
38 if (!defined('EXPLICIT_RATINGS_DIMENSION'))
39     define('EXPLICIT_RATINGS_DIMENSION', 0);
40 if (!defined('LIST_ITEMS_DIMENSION'))
41     define('LIST_ITEMS_DIMENSION', 1);
42 if (!defined('LIST_OWNER_DIMENSION'))
43     define('LIST_OWNER_DIMENSION', 2);
44 if (!defined('LIST_TYPE_DIMENSION'))
45     define('LIST_TYPE_DIMENSION', 3);
46
47 //TODO: split class into SQL and metadata backends
48 class RatingsDb extends WikiDB
49 {
50
51     function RatingsDb()
52     {
53         global $request;
54         $this->_dbi = &$request->_dbi;
55         $this->_backend = &$this->_dbi->_backend;
56         $this->dimension = null;
57         if (RATING_STORAGE == 'SQL') {
58             if (isa($this->_backend, 'WikiDB_backend_PearDB')) {
59                 $this->_sqlbackend = &$this->_backend;
60                 $this->dbtype = "PearDB";
61             } elseif (isa($this->_backend, 'WikiDB_backend_ADODOB')) {
62                 $this->_sqlbackend = &$this->_backend;
63                 $this->dbtype = "ADODB";
64             } else {
65                 include_once 'lib/WikiDB/backend/ADODB.php';
66                 // It is not possible to decouple a ref from the source again. (4.3.11)
67                 // It replaced the main request backend. So we don't initialize _sqlbackend before.
68                 //$this->_sqlbackend = clone($this->_backend);
69                 $this->_sqlbackend = new WikiDB_backend_ADODB($GLOBALS['DBParams']);
70                 $this->dbtype = "ADODB";
71             }
72             $this->iter_class = "WikiDB_backend_" . $this->dbtype . "_generic_iter";
73
74             extract($this->_sqlbackend->_table_names);
75             if (empty($rating_tbl)) {
76                 $rating_tbl = (!empty($GLOBALS['DBParams']['prefix'])
77                     ? $GLOBALS['DBParams']['prefix'] : '') . 'rating';
78                 $this->_sqlbackend->_table_names['rating_tbl'] = $rating_tbl;
79             }
80         } else {
81             $this->iter_class = "WikiDB_Array_PageIterator";
82         }
83     }
84
85     // this is a singleton.  It ensures there is only 1 ratingsDB.
86     function & getTheRatingsDb()
87     {
88         static $_theRatingsDb;
89
90         if (!isset($_theRatingsDb)) {
91             $_theRatingsDb = new RatingsDb();
92         }
93         //echo "rating db is $_theRatingsDb";
94         return $_theRatingsDb;
95     }
96
97
98 /// *************************************************************************************
99 // FIXME
100 // from Reini Urban's RateIt plugin
101     function addRating($rating, $userid, $pagename, $dimension)
102     {
103         if (RATING_STORAGE == 'SQL') {
104             $page = $this->_dbi->getPage($pagename);
105             $current = $page->getCurrentRevision();
106             $rateeversion = $current->getVersion();
107             $this->sql_rate($userid, $pagename, $rateeversion, $dimension, $rating);
108         } else {
109             $this->metadata_set_rating($userid, $pagename, $dimension, $rating);
110         }
111     }
112
113     function deleteRating($userid = null, $pagename = null, $dimension = null)
114     {
115         if (is_null($dimension)) $dimension = $this->dimension;
116         if (is_null($userid)) $userid = $this->userid;
117         if (is_null($pagename)) $pagename = $this->pagename;
118         if (RATING_STORAGE == 'SQL') {
119             $this->sql_delete_rating($userid, $pagename, $dimension);
120         } else {
121             $this->metadata_set_rating($userid, $pagename, $dimension, -1);
122         }
123     }
124
125     function getRating($userid = null, $pagename = null, $dimension = null)
126     {
127         if (RATING_STORAGE == 'SQL') {
128             $ratings_iter = $this->sql_get_rating($dimension, $userid, $pagename);
129             if ($rating = $ratings_iter->next() and isset($rating['ratingvalue'])) {
130                 return $rating['ratingvalue'];
131             } else
132                 return false;
133         } else {
134             return $this->metadata_get_rating($userid, $pagename, $dimension);
135         }
136     }
137
138     function getUsersRated($dimension = null, $orderby = null)
139     {
140         if (is_null($dimension)) $dimension = $this->dimension;
141         //if (is_null($userid))    $userid = $this->userid;
142         //if (is_null($pagename))  $pagename = $this->pagename;
143         if (RATING_STORAGE == 'SQL') {
144             $ratings_iter = $this->sql_get_users_rated($dimension, $orderby);
145             if ($rating = $ratings_iter->next() and isset($rating['ratingvalue'])) {
146                 return $rating['ratingvalue'];
147             } else
148                 return false;
149         } else {
150             return $this->metadata_get_users_rated($dimension, $orderby);
151         }
152     }
153
154     /**
155      * Get ratings.
156      *
157      * @param dimension  The rating dimension id.
158      *                   Example: 0
159      *                   [optional]
160      *                   If this is null (or left off), the search for ratings
161      *                   is not restricted by dimension.
162      *
163      * @param rater  The page id of the rater, i.e. page doing the rating.
164      *               This is a Wiki page id, often of a user page.
165      *               Example: "DanFr"
166      *               [optional]
167      *               If this is null (or left off), the search for ratings
168      *               is not restricted by rater.
169      *               TODO: Support an array
170      *
171      * @param ratee  The page id of the ratee, i.e. page being rated.
172      *               Example: "DudeWheresMyCar"
173      *               [optional]
174      *               If this is null (or left off), the search for ratings
175      *               is not restricted by ratee.
176      *
177      * @param orderby An order-by clause with fields and (optionally) ASC
178      *                or DESC.
179      *               Example: "ratingvalue DESC"
180      *               [optional]
181      *               If this is null (or left off), the search for ratings
182      *               has no guaranteed order
183      *
184      * @param pageinfo The type of page that has its info returned (i.e.,
185      *               'pagename', 'hits', and 'pagedata') in the rows.
186      *               Example: "rater"
187      *               [optional]
188      *               If this is null (or left off), the info returned
189      *               is for the 'ratee' page (i.e., thing being rated).
190      *
191      * @return DB iterator with results
192      */
193     function get_rating($dimension = null, $rater = null, $ratee = null,
194                         $orderby = null, $pageinfo = "ratee")
195     {
196         if (RATING_STORAGE == 'SQL') {
197             $ratings_iter = $this->sql_get_rating($dimension, $rater, $pagename);
198             if ($rating = $ratings_iter->next() and isset($rating['ratingvalue'])) {
199                 return $rating['ratingvalue'];
200             } else
201                 return false;
202         } else {
203             return $this->metadata_get_rating($rater, $pagename, $dimension);
204         }
205     }
206
207     /* UR: What is this for? NOT USED!
208        Maybe the list of users (ratees) who rated on this page.
209      */
210     function get_users_rated($dimension = null, $pagename = null, $orderby = null)
211     {
212         if (RATING_STORAGE == 'SQL') {
213             $ratings_iter = $this->sql_get_users_rated($dimension, $pagename, $orderby);
214             // iter as userid
215             $users = array();
216             while ($rating = $ratings_iter->next()) {
217                 $users[] = $rating['userid'];
218             }
219             return $users;
220         } else {
221             return $this->metadata_get_users_rated($dimension, $pagename, $orderby);
222         }
223     }
224
225     /**
226      * Like get_rating(), but return a WikiDB_PageIterator
227      * FIXME!
228      */
229     function get_rating_page($dimension = null, $rater = null, $ratee = null,
230                              $orderby = null, $pageinfo = "ratee")
231     {
232         if (RATING_STORAGE == 'SQL') {
233             return $this->sql_get_rating($dimension, $rater, $ratee, $orderby, $pageinfo);
234         } else {
235             // empty dummy iterator
236             $pages = array();
237             return new WikiDB_Array_PageIterator($pages);
238         }
239     }
240
241     /**
242      * Delete a rating.
243      *
244      * @param rater  The page id of the rater, i.e. page doing the rating.
245      *               This is a Wiki page id, often of a user page.
246      * @param ratee  The page id of the ratee, i.e. page being rated.
247      * @param dimension  The rating dimension id.
248      *
249      * @return true upon success
250      */
251     public function delete_rating($rater, $ratee, $dimension)
252     {
253         if (RATING_STORAGE == 'SQL') {
254             $this->sql_delete_rating($rater, $ratee, $dimension);
255         } else {
256             $this->metadata_set_rating($rater, $ratee, $dimension, -1);
257         }
258     }
259
260     /**
261      * Rate a page.
262      *
263      * @param rater  The page id of the rater, i.e. page doing the rating.
264      *               This is a Wiki page id, often of a user page.
265      * @param ratee  The page id of the ratee, i.e. page being rated.
266      * @param rateeversion  The version of the ratee page.
267      * @param dimension  The rating dimension id.
268      * @param rating The rating value (a float).
269      *
270      * @return true upon success
271      */
272     public function rate($rater, $ratee, $rateeversion, $dimension, $rating)
273     {
274         if (RATING_STORAGE == 'SQL') {
275             $page = $this->_dbi->getPage($pagename);
276             $current = $page->getCurrentRevision();
277             $rateeversion = $current->getVersion();
278             $this->sql_rate($userid, $pagename, $rateeversion, $dimension, $rating);
279         } else {
280             $this->metadata_set_rating($userid, $pagename, $dimension, $rating);
281         }
282     }
283
284     //function getUsersRated(){}
285
286 //*******************************************************************************
287     // TODO:
288     // Use wikilens/RatingsUser.php for the php methods.
289     //
290     // Old:
291     // Currently we have to call the "suggest" CGI
292     //   http://www-users.cs.umn.edu/~karypis/suggest/
293     // until we implement a simple recommendation engine.
294     // Note that "suggest" is only free for non-profit organizations.
295     // I am currently writing a binary CGI mysuggest using suggest, which loads
296     // data from mysql.
297     function getPrediction($userid = null, $pagename = null, $dimension = null)
298     {
299         if (is_null($dimension)) $dimension = $this->dimension;
300         if (is_null($userid)) $userid = $this->userid;
301         if (is_null($pagename)) $pagename = $this->pagename;
302
303         if (RATING_STORAGE == 'SQL') {
304             $dbh = &$this->_sqlbackend;
305             if (isset($pagename))
306                 $page = $dbh->_get_pageid($pagename);
307             else
308                 return 0;
309             if (isset($userid))
310                 $user = $dbh->_get_pageid($userid);
311             else
312                 return 0;
313         }
314         if (defined('RATING_EXTERNAL') and RATING_EXTERNAL) {
315             // how call mysuggest.exe? as CGI or natively
316             //$rating = HTML::Raw("<!--#include virtual=".RATING_ENGINE." -->");
317             $args = "-u$user -p$page -malpha"; // --top 10
318             if (isset($dimension))
319                 $args .= " -d$dimension";
320             $rating = passthru(RATING_EXTERNAL . " $args");
321         } else {
322             $rating = $this->php_prediction($userid, $pagename, $dimension);
323         }
324         return $rating;
325     }
326
327     /**
328      * Slow item-based recommendation engine, similar to suggest RType=2.
329      * Only the SUGGEST_EstimateAlpha part
330      * Take wikilens/RatingsUser.php for the php methods.
331      */
332     function php_prediction($userid = null, $pagename = null, $dimension = null)
333     {
334         if (is_null($dimension)) $dimension = $this->dimension;
335         if (is_null($userid)) $userid = $this->userid;
336         if (is_null($pagename)) $pagename = $this->pagename;
337         if (empty($this->buddies)) {
338             require_once 'lib/wikilens/RatingsUser.php';
339             require_once 'lib/wikilens/Buddy.php';
340             $user = RatingsUserFactory::getUser($userid);
341             $this->buddies = getBuddies($user, $GLOBALS['request']->_dbi);
342         }
343         return $user->knn_uu_predict($pagename, $this->buddies, $dimension);
344     }
345
346     function getNumUsers($pagename = null, $dimension = null)
347     {
348         if (is_null($dimension)) $dimension = $this->dimension;
349         if (is_null($pagename)) $pagename = $this->pagename;
350         if (RATING_STORAGE == 'SQL') {
351             $ratings_iter = $this->sql_get_rating($dimension, null, $pagename,
352                 null, "ratee");
353             return $ratings_iter->count();
354         } else {
355             if (!$pagename) return 0;
356             $page = $this->_dbi->getPage($pagename);
357             $data = $page->get('rating');
358             if (!empty($data[$dimension]))
359                 return count($data[$dimension]);
360             else
361                 return 0;
362         }
363     }
364
365     function getAvg($pagename = null, $dimension = null)
366     {
367         if (is_null($dimension)) $dimension = $this->dimension;
368         if (is_null($pagename)) $pagename = $this->pagename;
369         if (RATING_STORAGE == 'SQL') {
370             $dbi = &$this->_sqlbackend;
371             if (isset($pagename) || isset($dimension)) {
372                 $where = "WHERE";
373             }
374             if (isset($pagename)) {
375                 if (defined('FUSIONFORGE') and FUSIONFORGE) {
376                     $rateeid = $this->_sqlbackend->_get_pageid($pagename, true);
377                     $where .= " rateepage=$rateeid";
378                 } else {
379                     $raterid = $this->_sqlbackend->_get_pageid($pagename, true);
380                     $where .= " raterpage=$raterid";
381                 }
382             }
383             if (isset($dimension)) {
384                 if (isset($pagename)) $where .= " AND";
385                 $where .= " dimension=$dimension";
386             }
387             extract($dbi->_table_names);
388             if (defined('FUSIONFORGE') and FUSIONFORGE) {
389                 $query = "SELECT AVG(ratingvalue) as avg FROM $rating_tbl " . $where;
390             } else {
391                 $query = "SELECT AVG(ratingvalue) as avg FROM $rating_tbl r, $page_tbl p " . $where . " GROUP BY raterpage";
392             }
393             $result = $dbi->_dbh->query($query);
394             $iter = new $this->iter_class($this, $result);
395             $row = $iter->next();
396             return $row['avg'];
397         } else {
398             if (!$pagename) return 0;
399             $page = $this->_dbi->getPage($pagename);
400             $data = $page->get('rating');
401             if (!empty($data[$dimension]))
402                 // hash of userid => rating
403                 return array_sum(array_values($data[$dimension])) / count($data[$dimension]);
404             else
405                 return 0;
406         }
407     }
408
409 //*******************************************************************************
410
411     /**
412      * Get ratings.
413      *
414      * @param dimension  The rating dimension id.
415      *                   Example: 0
416      *                   [optional]
417      *                   If this is null (or left off), the search for ratings
418      *                   is not restricted by dimension.
419      *
420      * @param int $rater  The page id of the rater, i.e. page doing the rating.
421      *               This is a Wiki page id, often of a user page.
422      *               Example: "DanFr"
423      *               [optional]
424      *               If this is null (or left off), the search for ratings
425      *               is not restricted by rater.
426      *               TODO: Support an array
427      *
428      * @param int $ratee  The page id of the ratee, i.e. page being rated.
429      *               Example: "DudeWheresMyCar"
430      *               [optional]
431      *               If this is null (or left off), the search for ratings
432      *               is not restricted by ratee.
433      *               TODO: Support an array
434      *
435      * @param string $orderby An order-by clause with fields and (optionally) ASC
436      *                or DESC.
437      *               Example: "ratingvalue DESC"
438      *               [optional]
439      *               If this is null (or left off), the search for ratings
440      *               has no guaranteed order
441      *
442      * @param string $pageinfo The type of page that has its info returned (i.e.,
443      *               'pagename', 'hits', and 'pagedata') in the rows.
444      *               Example: "rater"
445      *               [optional]
446      *               If this is null (or left off), the info returned
447      *               is for the 'ratee' page (i.e., thing being rated).
448      *
449      * @return DB iterator with results
450      */
451
452     function sql_get_rating($dimension = null, $rater = null, $ratee = null,
453                             $orderby = null, $pageinfo = "ratee")
454     {
455         if (is_null($dimension)) $dimension = $this->dimension;
456         $result = $this->_sql_get_rating_result($dimension, $rater, $ratee, $orderby, $pageinfo);
457         return new $this->iter_class($this, $result);
458     }
459
460     function sql_get_users_rated($dimension = null, $pagename = null, $orderby = null)
461     {
462         if (is_null($dimension)) $dimension = $this->dimension;
463         $result = $this->_sql_get_rating_result($dimension, null, $pagename, $orderby, "rater");
464         return new $this->iter_class($this, $result);
465     }
466
467     // all users who rated this page resp if null all pages.. needed?
468     function metadata_get_users_rated($dimension = null, $pagename = null, $orderby = null)
469     {
470         if (is_null($dimension)) $dimension = $this->dimension;
471         $users = array();
472         if (!$pagename) {
473             // TODO: all pages?
474             return new WikiDB_Array_PageIterator($users);
475         }
476         $page = $this->_dbi->getPage($pagename);
477         $data = $page->get('rating');
478         if (!empty($data[$dimension])) {
479             //array($userid => (float)$rating);
480             return new WikiDB_Array_PageIterator(array_keys($data[$dimension]));
481         }
482         return new WikiDB_Array_PageIterator($users);
483     }
484
485     /**
486      * @return result ressource, suitable to the iterator
487      */
488     private function _sql_get_rating_result($dimension = null, $rater = null, $ratee = null,
489                                     $orderby = null, $pageinfo = "ratee")
490     {
491         // pageinfo must be 'rater' or 'ratee'
492         if (($pageinfo != "ratee") && ($pageinfo != "rater"))
493             return;
494         $dbi = &$this->_sqlbackend;
495         if (is_null($dbi))
496             return;
497         //$dbh = &$this->_dbi;
498         extract($dbi->_table_names);
499         $where = "WHERE r." . $pageinfo . "page = p.id";
500         if (isset($dimension)) {
501             $where .= " AND dimension=$dimension";
502         }
503         if (isset($rater)) {
504             $raterid = $dbi->_get_pageid($rater, true);
505             $where .= " AND raterpage=$raterid";
506         }
507         if (isset($ratee)) {
508             if (is_array($ratee)) {
509                 $where .= " AND (";
510                 for ($i = 0; $i < count($ratee); $i++) {
511                     $rateeid = $dbi->_get_pageid($ratee[$i], true);
512                     $where .= "rateepage=$rateeid";
513                     if ($i != (count($ratee) - 1)) {
514                         $where .= " OR ";
515                     }
516                 }
517                 $where .= ")";
518             } else {
519                 $rateeid = $dbi->_get_pageid($ratee, true);
520                 $where .= " AND rateepage=$rateeid";
521             }
522         }
523         $orderbyStr = "";
524         if (isset($orderby)) {
525             $orderbyStr = " ORDER BY " . $orderby;
526         }
527         if (isset($rater) or isset($ratee)) $what = '*';
528         // same as _get_users_rated_result()
529         else {
530             $what = 'DISTINCT p.pagename';
531             if ($pageinfo == 'rater')
532                 $what = 'DISTINCT p.pagename as userid';
533         }
534
535         $query = "SELECT $what"
536             . " FROM $rating_tbl r, $page_tbl p "
537             . $where
538             . $orderbyStr;
539         $result = $dbi->_dbh->query($query);
540         return $result;
541     }
542
543     /**
544      * Delete a rating.
545      *
546      * @param rater  The page id of the rater, i.e. page doing the rating.
547      *               This is a Wiki page id, often of a user page.
548      * @param ratee  The page id of the ratee, i.e. page being rated.
549      * @param dimension  The rating dimension id.
550      *
551      * @return true upon success
552      */
553     public function sql_delete_rating($rater, $ratee, $dimension)
554     {
555         //$dbh = &$this->_dbi;
556         $dbi = &$this->_sqlbackend;
557         extract($dbi->_table_names);
558
559         $dbi->lock();
560         $raterid = $dbi->_get_pageid($rater, true);
561         $rateeid = $dbi->_get_pageid($ratee, true);
562         $where = "WHERE raterpage=$raterid and rateepage=$rateeid";
563         if (isset($dimension)) {
564             $where .= " AND dimension=$dimension";
565         }
566         $dbi->_dbh->query("DELETE FROM $rating_tbl $where");
567         $dbi->unlock();
568         return true;
569     }
570
571     /**
572      * Rate a page.
573      *
574      * @param rater  The page id of the rater, i.e. page doing the rating.
575      *               This is a Wiki page id, often of a user page.
576      * @param ratee  The page id of the ratee, i.e. page being rated.
577      * @param rateeversion  The version of the ratee page.
578      * @param dimension  The rating dimension id.
579      * @param rating The rating value (a float).
580      *
581      * @return true upon success
582      */
583     public function sql_rate($rater, $ratee, $rateeversion, $dimension, $rating)
584     {
585         $dbi = &$this->_sqlbackend;
586         extract($dbi->_table_names);
587         if (empty($rating_tbl))
588             $rating_tbl = $this->_dbi->getParam('prefix') . 'rating';
589
590         $dbi->lock();
591         $raterid = $dbi->_get_pageid($rater, true);
592         $rateeid = $dbi->_get_pageid($ratee, true);
593         assert($raterid);
594         assert($rateeid);
595         //mysql optimize: REPLACE if raterpage and rateepage are keys
596         $dbi->_dbh->query("DELETE from $rating_tbl WHERE dimension=$dimension AND raterpage=$raterid AND rateepage=$rateeid");
597         $where = "WHERE raterpage='$raterid' AND rateepage='$rateeid'";
598         $insert = "INSERT INTO $rating_tbl (dimension, raterpage, rateepage, ratingvalue, rateeversion)"
599             . " VALUES ('$dimension', $raterid, $rateeid, '$rating', '$rateeversion')";
600         $dbi->_dbh->query($insert);
601
602         $dbi->unlock();
603         return true;
604     }
605
606     function metadata_get_rating($userid, $pagename, $dimension)
607     {
608         if (!$pagename) return false;
609         $page = $this->_dbi->getPage($pagename);
610         $data = $page->get('rating');
611         if (!empty($data[$dimension][$userid]))
612             return (float)$data[$dimension][$userid];
613         else
614             return false;
615     }
616
617     function metadata_set_rating($userid, $pagename, $dimension, $rating = -1)
618     {
619         if (!$pagename) return false;
620         $page = $this->_dbi->getPage($pagename);
621         $data = $page->get('rating');
622         if ($rating == -1)
623             unset($data[$dimension][$userid]);
624         else {
625             if (empty($data[$dimension]))
626                 $data[$dimension] = array($userid => (float)$rating);
627             else
628                 $data[$dimension][$userid] = (float)$rating;
629         }
630         $page->set('rating', $data);
631     }
632
633 }
634
635 // Local Variables:
636 // mode: php
637 // tab-width: 8
638 // c-basic-offset: 4
639 // c-hanging-comment-ender-p: nil
640 // indent-tabs-mode: nil
641 // End: