]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/plugin/RateIt.php
Use base64 to make file UTF-8 clean
[SourceForge/phpwiki.git] / lib / plugin / RateIt.php
1 <?php // -*-php-*-
2 rcs_id('$Id$');
3 /*
4  * Copyright 2004,2007,2009 $ThePhpWikiProgrammingTeam
5  *
6  * This file is part of PhpWiki.
7  *
8  * PhpWiki is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * PhpWiki is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with PhpWiki; if not, write to the Free Software
20  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21  */
22
23 /**
24  * RateIt: A recommender system, based on MovieLens and "suggest".
25  * Store user ratings per pagename. The wikilens theme displays a navbar image bar
26  * with some nice javascript magic and this plugin shows various recommendations.
27  *
28  * There should be two methods to store ratings:
29  * In a SQL database as in wikilens http://dickens.cs.umn.edu/dfrankow/wikilens
30  *
31  * The most important fact: A page has more than one rating. There can
32  * be (and will be!) many ratings per page (ratee): different raters
33  * (users), in different dimensions. Are those stored per page
34  * (ratee)? Then what if I wish to access the ratings per rater
35  * (user)?
36  * wikilens plans several user-centered applications like:
37  * a) show my ratings
38  * b) show my buddies' ratings
39  * c) show how my ratings are like my buddies'
40  * d) show where I agree/disagree with my buddy
41  * e) show what this group of people agree/disagree on
42  *
43  * If the ratings are stored in a real DB in a table, we can index the
44  * ratings by rater and ratee, and be confident in
45  * performance. Currently MovieLens has 80,000 users, 7,000 items,
46  * 10,000,000 ratings. This is an average of 1400 ratings/page if each
47  * page were rated equally. However, they're not: the most popular
48  * things have tens of thousands of ratings (e.g., "Pulp Fiction" has
49  * 42,000 ratings). If ratings are stored per page, you would have to
50  * save/read huge page metadata every time someone submits a
51  * rating. Finally, the movie domain has an unusually small number of
52  * items-- I'd expect a lot more in music, for example.
53  *
54  * For a simple rating system one can also store the rating in the page
55  * metadata (default).
56  *
57  * Recommender Engines:
58  * Recommendation/Prediction is a special field of "Data Mining"
59  * For a list of (also free) software see
60  *  http://www.the-data-mine.com/bin/view/Software/WebIndex
61  * - movielens: (Java Server) will be gpl'd in summer 2004 (weighted)
62  * - suggest: is free for non-commercial use, available as compiled library
63  *     (non-weighted)
64  * - Autoclass: simple public domain C library
65  * - MLC++: C++ library http://www.sgi.com/tech/mlc/
66  *
67  * Usage:    <?plugin RateIt ?>          just the widget without text
68  *   Note: The wikilens theme or any derivate must be enabled, to enable this plugin!
69  *           <?plugin RateIt show=top ?> text plus widget below
70  *           <?plugin RateIt show=ratings ?> to show my ratings
71  *   TODO:   <?plugin RateIt show=buddies ?> to show my buddies
72  *           <?plugin RateIt show=ratings dimension=1 ?>
73  *   TODO:   <?plugin RateIt show=text ?> just text, no widget, for dumps
74  *
75  * @author:  Dan Frankowski (wikilens author), Reini Urban (as plugin)
76  *
77  * TODO:
78  * - finish mysuggest.c (external engine with data from mysql)
79  */
80
81 require_once("lib/WikiPlugin.php");
82 require_once("lib/wikilens/RatingsDb.php");
83
84 class WikiPlugin_RateIt
85 extends WikiPlugin
86 {
87     function getName() {
88         return _("RateIt");
89     }
90     function getDescription() {
91         return _("Rating system. Store user ratings per page");
92     }
93     function getVersion() {
94         return preg_replace("/[Revision: $]/", '',
95                             "\$Revision$");
96     }
97
98     function RatingWidgetJavascript() {
99         global $WikiTheme;
100         if (!empty($this->imgPrefix))
101             $imgPrefix = $this->imgPrefix;
102         elseif (defined("RATEIT_IMGPREFIX"))
103             $imgPrefix = RATEIT_IMGPREFIX;
104         else $imgPrefix = '';
105         if ($imgPrefix and !$WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png",1))
106             $imgPrefix = '';
107         $img   = substr($WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png"),0,-7);
108         $urlprefix = WikiURL("",0,1); // TODO: check actions USE_PATH_INFO=false
109         $js_globals = "var rateit_imgsrc = '".$img."';
110 var rateit_action = '".urlencode("RateIt")."';
111 ";
112         $WikiTheme->addMoreHeaders
113                 (JavaScript('',
114                             array('src' => $WikiTheme->_findData('themes/wikilens/wikilens.js'))));
115         return JavaScript($js_globals);
116     }
117
118     function actionImgPath() {
119         global $WikiTheme;
120         return $WikiTheme->_findFile("images/RateItAction.png", 1);
121     }
122
123     /**
124      * Take a string and quote it sufficiently to be passed as a Javascript
125      * string between ''s
126      */
127     function _javascript_quote_string($s) {
128         return str_replace("'", "\'", $s);
129     }
130
131     function getDefaultArguments() {
132         return array( 'pagename'  => '[pagename]',
133                       'version'   => false,
134                       'id'        => 'rateit',
135                       'imgPrefix' => '',      // '' or BStar or Star
136                       'dimension' => false,
137                       'small'     => false,
138                       'show'      => false,
139                       'mode'      => false,
140                       );
141     }
142
143     function head() { // early side-effects (before body)
144         global $WikiTheme;
145         $WikiTheme->addMoreHeaders(JavaScript(
146 "var prediction = new Array; var rating = new Array;
147 var avg = new Array; var numusers = new Array;
148 var msg_rating_votes = '"._("Rating: %.1f (%d votes)")."';
149 var msg_curr_rating = '"._("Your current rating: ")."';
150 var msg_curr_prediction = '"._("Your current prediction: ")."';
151 var msg_chg_rating = '"._("Change your rating from ")."';
152 var msg_to = '"._(" to ")."';
153 var msg_add_rating = '"._("Add your rating: ")."';
154 var msg_thanks = '"._("Thanks!")."';
155 var msg_rating_deleted = '"._("Rating deleted!")."';
156 "));
157         $WikiTheme->addMoreHeaders($this->RatingWidgetJavascript());
158     }
159
160     function displayActionImg ($mode) {
161         global $WikiTheme, $request;
162         if (!empty($request->_is_buffering_output))
163             ob_end_clean();  // discard any previous output
164         // delete the cache
165         $page = $request->getPage();
166         //$page->set('_cached_html', false);
167         $request->cacheControl('MUST-REVALIDATE');
168         $dbi = $request->getDbh();
169         $dbi->touch();
170         //fake validators without args
171         $request->appendValidators(array('wikiname' => WIKI_NAME,
172                                          'args'     => wikihash('')));
173         $request->discardOutput();
174         $actionImg = $WikiTheme->_path . $this->actionImgPath();
175         if (file_exists($actionImg)) {
176             header('Content-type: image/png');
177             readfile($actionImg);
178         } else {
179             header('Content-type: image/png');
180             echo base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAQMAAABIeJ9nAAAAA1BMVEX///'
181                                  .'+nxBvIAAAAAXRSTlMAQObYZgAAABNJREFUeF4NwAEBAAAAgJD+r5YGAAQAAXHhfPAAAAAASUVORK5CYII=');
182         }
183         exit;
184     }
185
186     // Only for signed users done in template only yet.
187     function run($dbi, $argstr, &$request, $basepage) {
188         global $WikiTheme;
189         //$this->_request = & $request;
190         //$this->_dbi = & $dbi;
191         $user = $request->getUser();
192         //FIXME: fails on test with DumpHtml:RateIt
193         if (!is_object($user)) {
194             return HTML::raw('');
195         }
196         $this->userid = $user->getId();
197         if (!$this->userid) {
198             return HTML::raw('');
199         }
200         $args = $this->getArgs($argstr, $request);
201         $this->dimension = $args['dimension'];
202         $this->imgPrefix = $args['imgPrefix'];
203         if ($this->dimension == '') {
204             $this->dimension = 0;
205             $args['dimension'] = 0;
206         }
207         if ($args['pagename']) {
208             // Expand relative page names.
209             $page = new WikiPageName($args['pagename'], $basepage);
210             $args['pagename'] = $page->name;
211         }
212         if (empty($args['pagename'])) {
213             return $this->error(_("no page specified"));
214         }
215         $this->pagename = $args['pagename'];
216
217         $rdbi = RatingsDb::getTheRatingsDb();
218         $this->_rdbi =& $rdbi;
219
220         if ($args['mode'] === 'add') {
221             //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
222             $this->rating = $request->getArg('rating');
223             $rdbi->addRating($this->rating, $this->userid, $this->pagename, $this->dimension);
224             $this->displayActionImg('add');
225
226         } elseif ($args['mode'] === 'delete') {
227             //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
228             $rdbi->deleteRating($this->userid, $this->pagename, $this->dimension);
229             unset($this->rating);
230             $this->displayActionImg('delete');
231         } elseif (! $args['show'] ) {
232             return $this->RatingWidgetHtml($args['pagename'], $args['version'], $args['imgPrefix'],
233                                            $args['dimension'], $args['small']);
234         } else {
235             //if (!$user->isSignedIn()) return $this->error(_("You must sign in"));
236             //extract($args);
237             $this->rating   = $rdbi->getRating($this->userid, $this->pagename, $this->dimension);
238             $this->avg      = $rdbi->getAvg($this->pagename, $this->dimension);
239             $this->numusers = $rdbi->getNumUsers($this->pagename, $this->dimension);
240             // Update this text on rateit in javascript. needed: NumUsers, Avg
241             $html = HTML::div
242                 (
243                  HTML::span(array('class' => 'rateit'),
244                             sprintf(_("Rating: %.1f (%d votes)"),
245                                     $this->avg, $this->numusers)));
246             if ($args['show'] == 'top') {
247                 if (ENABLE_PAGE_PUBLIC) {
248                     $page = $dbi->getPage($this->pagename);
249                     if ($page->get('public'))
250                         $html->setAttr('class', "public");
251                 }
252                 $html->setAttr('id', "rateit-widget-top");
253                 $html->pushContent(HTML::br(),
254                                    $this->RatingWidgetHtml($args['pagename'], $args['version'],
255                                                            $args['imgPrefix'],
256                                                            $args['dimension'], $args['small']));
257             } elseif ($args['show'] == 'text') {
258                 if (!$WikiTheme->DUMP_MODE)
259                     $html->pushContent(HTML::br(),
260                                        sprintf(_("Your rating was %.1f"),
261                                                $this->rating));
262             } elseif ($this->rating) {
263                 $html->pushContent(HTML::br(),
264                                    sprintf(_("Your rating was %.1f"),
265                                            $this->rating));
266             } else {
267                     $this->pred = $rdbi->getPrediction($this->userid, $this->pagename, $this->dimension);
268                     if (is_string($this->pred))
269                     $html->pushContent(HTML::br(),
270                                        sprintf(_("Prediction: %s"),
271                                                $this->pred));
272                 elseif ($this->pred)
273                     $html->pushContent(HTML::br(),
274                                        sprintf(_("Prediction: %.1f"),
275                                                $this->pred));
276             }
277             //$html->pushContent(HTML::p());
278             //$html->pushContent(HTML::em("(Experimental: This might be entirely bogus data)"));
279             return $html;
280         }
281     }
282
283     // box is used to display a fixed-width, narrow version with common header
284     function box($args=false, $request=false, $basepage=false) {
285         if (!$request) $request =& $GLOBALS['request'];
286         if (!$request->_user->isSignedIn()) return;
287         if (!isset($args)) $args = array();
288         $args['small'] = 1;
289         $argstr = '';
290         foreach ($args as $key => $value)
291             $argstr .= $key."=".$value;
292         $widget = $this->run($request->_dbi, $argstr, $request, $basepage);
293
294         return $this->makeBox(WikiLink(_("RateIt"),'',_("Rate It")),
295                               $widget);
296     }
297
298     /**
299      * HTML widget display
300      *
301      * This needs to be put in the <body> section of the page.
302      *
303      * @param pagename    Name of the page to rate
304      * @param version     Version of the page to rate (may be "" for current)
305      * @param imgPrefix   Prefix of the names of the images that display the rating
306      *                    You can have two widgets for the same page displayed at
307      *                    once iff the imgPrefix-s are different.
308      * @param dimension   Id of the dimension to rate
309      * @param small       Makes a smaller ratings widget if non-false
310      *
311      * Limitations: Currently this can only print the current users ratings.
312      *              And only the widget, but no value (for buddies) also.
313      */
314     function RatingWidgetHtml($pagename, $version, $imgPrefix, $dimension, $small = false) {
315         global $WikiTheme, $request;
316
317         $dbi =& $request->_dbi;
318         $version = $dbi->_backend->get_latest_version($pagename);
319         $pageid = sprintf("%u",crc32($pagename)); // MangleXmlIdentifier($pagename)
320         $imgId = 'RateIt' . $pageid;
321         $actionImgName = 'RateIt'.$pageid.'Action';
322
323         //$rdbi =& $this->_rdbi;
324         $rdbi = RatingsDb::getTheRatingsDb();
325
326         // check if the imgPrefix icons exist.
327         if (! $WikiTheme->_findData("images/RateIt".$imgPrefix."Nk0.png", true))
328             $imgPrefix = '';
329
330         // Protect against \'s, though not \r or \n
331         $reImgPrefix = $this->_javascript_quote_string($imgPrefix);
332         $reImgId     = $this->_javascript_quote_string($imgId);
333         $reActionImgName = $this->_javascript_quote_string($actionImgName);
334         $rePagename      = $this->_javascript_quote_string($pagename);
335         //$dimension = $args['pagename'] . "rat";
336
337         $html = HTML::span(array("class" => "rateit-widget", "id" => $imgId));
338         for ($i=0; $i < 2; $i++) {
339             $ok[$i] = $WikiTheme->_findData("images/RateIt".$imgPrefix."Ok".$i.".png"); // empty
340             $nk[$i] = $WikiTheme->_findData("images/RateIt".$imgPrefix."Nk".$i.".png"); // rated
341             $rk[$i] = $WikiTheme->_findData("images/RateIt".$imgPrefix."Rk".$i.".png"); // pred
342         }
343
344         if (empty($this->userid)) {
345             $user = $request->getUser();
346             $this->userid = $user->getId();
347         }
348         if (empty($this->rating)) {
349             $this->rating = $rdbi->getRating($this->userid, $pagename, $dimension);
350             if (!$this->rating and empty($this->pred)) {
351                 $this->pred = $rdbi->getPrediction($this->userid, $pagename, $dimension);
352             }
353         }
354
355         for ($i = 1; $i <= 10; $i++) {
356             $j = $i / 2;
357             $a1 = HTML::a(array('href' => "javascript:clickRating('$reImgPrefix','$rePagename','$version',"
358                                 ."'$reImgId','$dimension',$j)"));
359             $img_attr = array();
360             $img_attr['src'] = $nk[$i%2];
361             if ($this->rating) {
362                 $img_attr['src'] = $ok[$i%2];
363                 $img_attr['onmouseover'] = "displayRating('$reImgId','$reImgPrefix',$j,0,1)";
364                 $img_attr['onmouseout']  = "displayRating('$reImgId','$reImgPrefix',$this->rating,0,1)";
365             }
366             else if (!$this->rating and $this->pred) {
367                 $img_attr['src'] = $rk[$i%2];
368                 $img_attr['onmouseover'] = "displayRating('$reImgId','$reImgPrefix',$j,1,1)";
369                 $img_attr['onmouseout']  = "displayRating('$reImgId','$reImgPrefix',$this->pred,1,1)";
370             }
371             else {
372                 $img_attr['onmouseover'] = "displayRating('$reImgId','$reImgPrefix',$j,0,1)";
373                 $img_attr['onmouseout']  = "displayRating('$reImgId','$reImgPrefix',0,0,1)";
374             }
375             //$imgName = 'RateIt'.$reImgId.$i;
376             $img_attr['name'] = $imgId . $i;
377             $img_attr['alt'] = $img_attr['name'];
378             $img_attr['border'] = 0;
379             $a1->pushContent(HTML::img($img_attr));
380             //$a1->addToolTip(_("Rate the topic of this page"));
381             $html->pushContent($a1);
382
383             //This adds a space between the rating smilies:
384             //if (($i%2) == 0) $html->pushContent("\n");
385         }
386         $html->pushContent(HTML::Raw("&nbsp;"));
387
388         $a0 = HTML::a(array('href' => "javascript:clickRating('$reImgPrefix','$rePagename','$version',"
389                             ."'$reImgId','$dimension','X')"));
390         $msg = _("Cancel your rating");
391         $imgprops = array('src'   => $WikiTheme->getImageUrl("RateIt".$imgPrefix."Cancel"),
392                           'name'  => $imgId.$imgPrefix.'Cancel',
393                           'border'=> 0,
394                           'alt'   => $msg,
395                           'title' => $msg);
396         if (!$this->rating)
397             $imgprops['style'] = 'display:none';
398         $a0->pushContent(HTML::img($imgprops));
399         $a0->addToolTip($msg);
400         $html->pushContent($a0);
401
402         /*} elseif ($pred) {
403             $msg = _("No opinion");
404             $html->pushContent(HTML::img(array('src' => $WikiTheme->getImageUrl("RateItCancelN"),
405                                                'name'=> $imgPrefix.'Cancel',
406                                                'alt' => $msg)));
407             //$a0->addToolTip($msg);
408             //$html->pushContent($a0);
409         }*/
410         $img_attr = array();
411         $img_attr['src'] = $WikiTheme->_findData("images/spacer.png");
412         $img_attr['name'] = $actionImgName;
413         $img_attr['alt'] = $img_attr['name'];
414         $img_attr['border'] = 0;
415         $img_attr['height'] = 15;
416         $img_attr['width'] = 20;
417         $html->pushContent(HTML::img($img_attr));
418
419         // Display your current rating if there is one, or the current prediction
420         // or the empty widget.
421         $pred = empty($this->pred) ? 0 : $this->pred;
422         $js = '';
423         if (!empty($this->avg))
424             $js .= "avg['$reImgId']=$this->avg; numusers['$reImgId']=$this->numusers;\n";
425         if ($this->rating) {
426             $js .= "rating['$reImgId']=$this->rating; prediction['$reImgId']=$pred;\n";
427             $html->pushContent(JavaScript($js
428                     ."displayRating('$reImgId','$reImgPrefix',$this->rating,0,1);"));
429         } elseif (!empty($this->pred)) {
430             $js .= "rating['$reImgId']=0; prediction['$reImgId']=$this->pred;\n";
431             $html->pushContent(JavaScript($js
432                     ."displayRating('$reImgId','$reImgPrefix',$this->pred,1,1);"));
433         } else {
434             $js .= "rating['$reImgId']=0; prediction['$reImgId']=0;\n";
435             $html->pushContent(JavaScript($js
436                     ."displayRating('$reImgId','$reImgPrefix',0,0,1);"));
437         }
438         return $html;
439     }
440
441 };
442
443 // For emacs users
444 // Local Variables:
445 // mode: php
446 // tab-width: 8
447 // c-basic-offset: 4
448 // c-hanging-comment-ender-p: nil
449 // indent-tabs-mode: nil
450 // End:
451 ?>