]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/plugin/ModeratedPage.php
protect _HomePagehandle
[SourceForge/phpwiki.git] / lib / plugin / ModeratedPage.php
1 <?php // -*-php-*-
2 rcs_id('$Id: ModeratedPage.php,v 1.7 2007-08-25 18:52:34 rurban Exp $');
3 /*
4  Copyright 2004,2005 $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  * This plugin requires an action page (default: ModeratedPage)
25  * and provides delayed execution of restricted actions, 
26  * after a special moderators request. Usually by email.
27  *   http://mywiki/SomeModeratedPage?action=ModeratedPage&id=kdclcr78431zr43uhrn&pass=approve
28  *
29  * Not yet ready! part 3/3 is missing: The moderator approve/reject methods.
30  *
31  * See http://phpwiki.org/PageModeration
32  * Author: ReiniUrban
33  */
34
35 require_once("lib/WikiPlugin.php");
36
37 class WikiPlugin_ModeratedPage
38 extends WikiPlugin
39 {
40     function getName () {
41         return _("ModeratedPage");
42     }
43     function getDescription () {
44         return _("Support moderated pages");
45     }
46     function getVersion() {
47         return preg_replace("/[Revision: $]/", '',
48                             "\$Revision: 1.7 $");
49     }
50     function getDefaultArguments() {
51         return array('page'          => '[pagename]',
52                      'moderators'    => false,
53                      'require_level' => false,   // 1=bogo
54                      'require_access' => 'edit,remove,change',
55                      'id'   => '',
56                      'pass' => '',
57                     );
58     }
59
60     function run($dbi, $argstr, &$request, $basepage) {
61         $args = $this->getArgs($argstr, $request);
62
63         // Handle moderation request from urls sent by email
64         if (!empty($args['id']) and !empty($args['pass'])) {
65             if (!$args['page'])
66                 return $this->error("No page specified");
67             $page = $dbi->getPage($args['page']);
68             if ($moderated = $page->get("moderated")) {
69                 if (array_key_exists($args['id'], $moderated['data'])) {
70                     $moderation = $moderated['data'][$args['id']];
71                     // handle defaults:
72                     //   approve or reject
73                     if ($request->isPost()) {
74                         $button = $request->getArg('ModeratedPage');
75                         if (isset($button['reject']))
76                             return $this->reject($request, $args, $moderation);
77                         elseif (isset($button['approve']))
78                             return $this->approve($request, $args, $moderation);
79                         else  
80                             return $this->error("Wrong button pressed");   
81                     }
82                     if ($args['pass'] == 'approve')
83                         return $this->approve($request, $args, $moderation);
84                     elseif ($args['pass'] == 'reject')
85                         return $this->reject($request, $args, $moderation);
86                     else
87                         return $this->error("Wrong pass ".$args['pass']);
88                 } else {
89                     return $this->error("Wrong id ".htmlentities($args['id']));
90                 }
91             }
92         }
93         return '';
94     }
95
96     /**
97      * resolve moderators and require_access (not yet) from actionpage plugin argstr
98      */
99     function resolve_argstr(&$request, $argstr) {
100         $args = $this->getArgs($argstr);
101         $group = $request->getGroup();
102         if (empty($args['moderators'])) {
103             $admins = $group->getSpecialMembersOf(GROUP_ADMIN);
104             // email or usernames?
105             $args['moderators'] = array_merge($admins, array(ADMIN_USER));
106         } else { 
107             // resolve possible group names
108             $moderators = explode(',', $args['moderators']); 
109             for ($i=0; $i < count($moderators); $i++) {
110                 $members = $group->getMembersOf($moderators[$i]);
111                 if (!empty($members)) {
112                     array_splice($moderators, $i, 1, $members);
113                 }
114             }
115             if (!$moderators) $moderators = array(ADMIN_USER);
116             $args['moderators'] = $moderators;
117         }
118         //resolve email for $args['moderators']
119         $page = $request->getPage();
120         $users = array();
121         foreach ($args['moderators'] as $userid) {
122             $users[$userid] = 0;
123         }
124         require_once("lib/MailNotify.php");
125         $mail = new MailNotify($page->getName());
126         
127         list($args['emails'], $args['moderators']) = 
128             $mail->getPageChangeEmails(array($page->getName() => $users));
129
130         if (!empty($args['require_access'])) {
131             $args['require_access'] = preg_split("/\s*,\s*/", $args['require_access']);
132             if (empty($args['require_access']))
133                 unset($args['require_access']);
134         }
135         if ($args['require_level'] !== false) {
136             $args['require_level'] = (integer) $args['require_level'];
137         }
138         unset($args['id']);
139         unset($args['page']);
140         unset($args['pass']);
141         return $args;
142     }
143     
144     /**
145      * Handle client-side moderation change request.
146      * Hook called on the lock action, if moderation metadata already exists.
147      */
148     function lock_check(&$request, &$page, $moderated) {
149         $action_page = $request->getPage(_("ModeratedPage"));
150         $status = $this->getSiteStatus($request, $action_page);
151         if (is_array($status)) {
152             if (empty($status['emails'])) {
153                 trigger_error(_("ModeratedPage: No emails for the moderators defined"), 
154                               E_USER_WARNING);
155                 return false;
156             }
157             $page->set('moderation', array('status' => $status));
158             return $this->notice(
159                        fmt("ModeratedPage status update:\n  Moderators: '%s'\n  require_access: '%s'", 
160                        join(',', $status['moderators']), $status['require_access']));
161         } else {
162             $page->set('moderation', false);
163             return $this->notice(HTML($status,
164                         fmt("'%s' is no ModeratedPage anymore.", $page->getName()))); 
165         }
166     }
167
168     /**
169      * Handle client-side moderation change request by the user.
170      * Hook called on the lock action, if moderation metadata should be added.
171      * Need to store the the plugin args (who, when) in the page meta-data
172      */
173     function lock_add(&$request, &$page, &$action_page) {
174         $status = $this->getSiteStatus($request, $action_page);
175         if (is_array($status)) {
176             if (empty($status['emails'])) {
177                 // We really should present such warnings prominently.
178                 trigger_error(_("ModeratedPage: No emails for the moderators defined"), 
179                               E_USER_WARNING);
180                 return false;
181             }
182             $page->set('moderation', array('status' => $status));
183             return $this->notice(
184                        fmt("ModeratedPage status update: '%s' is now a ModeratedPage.\n  Moderators: '%s'\n  require_access: '%s'", 
185                        $page->getName(), join(',', $status['moderators']), $status['require_access']));
186         }
187         else { // error
188             return $status;
189         }
190     }
191     
192     function notice($msg) {
193         return HTML::div(array('class' => 'wiki-edithelp'), $msg);
194     }
195
196     function generateId() {
197         better_srand();
198         $s = "";
199         for ($i = 1; $i <= 25; $i++) {
200             $r = function_exists('mt_rand') ? mt_rand(55, 90) : rand(55, 90);
201             $s .= chr(($r < 65) ? ($r-17) : $r);
202         }
203         $len = $r = function_exists('mt_rand') ? mt_rand(15, 25) : rand(15,25);
204         return substr(base64_encode($s),3,$len);
205     }
206
207     /** 
208      * Handle client-side POST moderation request on any moderated page.
209      *   if ($page->get('moderation')) WikiPlugin_ModeratedPage::handler(...);
210      * return false if not handled (pass through), true if handled and displayed.
211      */
212     function handler(&$request, &$page) {
213         $action = $request->getArg('action');
214         $moderated = $page->get('moderated');
215         // cached version, need re-lock of each page to update moderators
216         if (!empty($moderated['status'])) 
217             $status = $moderated['status'];
218         else {
219             $action_page = $request->getPage(_("ModeratedPage"));
220             $status = $this->getSiteStatus($request, $action_page);
221             $moderated['status'] = $status;
222         }
223         if (empty($status['emails'])) {
224             trigger_error(_("ModeratedPage: No emails for the moderators defined"),
225                           E_USER_WARNING);
226             return true;
227         }
228         // which action?
229         if (!empty($status['require_access']) 
230             and !in_array(action2access($action), $status['require_access']))
231             return false; // allow and fall through, not moderated
232         if (!empty($status['require_level']) 
233             and $request->_user->_level >= $status['require_level'])
234             return false; // allow and fall through, not moderated
235         // else all post actions are moderated by default
236         if (1) /* or in_array($action, array('edit','remove','rename')*/ {
237             $id = $this->generateId();
238             while (!empty($moderated['data'][$id])) $id = $this->generateId(); // avoid duplicates
239             $moderated['id'] = $id;             // overwrite current id
240             $tempuser = $request->_user;
241             if (isset($tempuser->_HomePagehandle))
242                 unset($tempuser->_HomePagehandle);
243             $moderated['data'][$id] = array(    // add current request
244                                             'timestamp' => time(),
245                                             'userid' => $request->_user->getId(),
246                                             'args'   => $request->getArgs(),
247                                             'user'   => serialize( $tempuser ),
248                                             );
249             $this->_tokens['CONTENT'] = 
250                 HTML::div(array('class' => 'wikitext'),
251                           fmt("%s: action forwarded to moderator %s", 
252                               $action, 
253                               join(", ", $status['moderators'])
254                               ));
255             // Send email
256             require_once("lib/MailNotify.php");
257             $pagename = $page->getName();
258             $mailer = new MailNotify($pagename);
259             $subject = "[".WIKI_NAME.'] '.$action.': '._("ModeratedPage").' '.$pagename;
260             $content =  "You are approved as Moderator of the ".WIKI_NAME. " wiki.\n".
261                      "Someone wanted to edit a moderated page, which you have to approve or reject.\n\n".
262                      $action.': '._("ModeratedPage").' '.$pagename."\n"
263                      //. serialize($moderated['data'][$id])
264                      ."\n<".WikiURL($pagename, array('action' => _("ModeratedPage"), 
265                                                      'id' => $id, 'pass' => 'approve'), 1).">"
266                      ."\n<".WikiURL($pagename, array('action' => _("ModeratedPage"), 
267                                                      'id' => $id, 'pass' => 'reject'), 1).">\n";
268             $mailer->emails = $mailer->userids = $status['emails'];
269             $mailer->from = $request->_user->_userid;
270             if ($mailer->sendMail($subject, $content, "Moderation notice")) {
271                 $page->set('moderated', $moderated);
272                 return false; // pass thru
273             } else {
274                 //DELETEME!
275                 $page->set('moderated', $moderated);
276                 //FIXME: This msg gets lost on the edit redirect
277                 trigger_error(_("ModeratedPage Notification Error: Couldn't send email"), 
278                               E_USER_ERROR);
279                 return true;
280             }
281         }
282         return false;
283     }
284
285     /** 
286      * Handle admin-side moderation resolve.
287      * We might have to convert the GET to a POST request to continue 
288      * with the left-over stored request.
289      * Better we display a post form for verification.
290      */
291     function approve(&$request, $args, &$moderation) {
292         if ($request->isPost()) {
293             // this is unsafe because we dont know if it will succeed. but we tried.
294             $this->cleanup_and_notify($request, $args, $moderation);
295             // start from scratch, dispatch the action as in lib/main to the action handler
296             $request->discardOutput();
297             $oldargs = $request->args;  
298             $olduser = $request->_user; 
299             $request->args = $moderation['args'];
300             $request->_user->_userid = $moderation['userid']; // keep current perms but fake the id.
301             // TODO: fake author ip also
302             extract($request->args);
303             $method = "action_$action";
304             if (method_exists($request, $method)) {
305                 $request->{$method}();
306             }
307             elseif ($page = $this->findActionPage($action)) {
308                 $this->actionpage($page);
309             }
310             else {
311                 $this->finish(fmt("%s: Bad action", $action));
312             }
313             // now we are gone and nobody brings us back here.
314
315             //$moderated['data'][$id]->args->action+edit(array)+...
316             //                              timestamp,user(obj)+userid
317             // handle $moderated['data'][$id]['args']['action']
318         } else {
319             return $this->_approval_form($request, $args, $moderation, 'approve');
320         }
321     }
322
323     /** 
324      * Handle admin-side moderation resolve.
325      */
326     function reject(&$request, $args, &$moderation) {
327         // check id, delete action
328         if ($request->isPost()) {
329             // clean up and notify the requestor. Mabye: store and revert to have a diff later on?
330             $this->cleanup_and_notify($request, $args, $moderation);
331         } else {
332             return $this->_approval_form($request, $args, $moderation, 'reject');
333         }
334     }
335
336     function cleanup_and_notify (&$request, $args, &$moderation) {
337         $pagename = $moderation['args']['pagename'];
338         $page = $request->_dbi->getPage($pagename);
339         $pass = $args['pass'];     // accept or reject
340         $reason = $args['reason']; // summary why
341         $user = $moderation['args']['user'];
342         $action = $moderation['args']['action'];
343         $id = $args['id'];
344         unset($moderation['data'][$id]);
345         unset($moderation['id']);
346         $page->set('moderation', $moderation);
347
348         // TODO: Notify the user, only if the user has an email:
349         if ($email = $user->getPref('email')) {
350             $action_page = $request->getPage(_("ModeratedPage"));
351             $status = $this->getSiteStatus($request, $action_page);
352             require_once("lib/MailNotify.php");
353             $mailer = new MailNotify($pagename);
354             $subject = "[".WIKI_NAME."] $pass $action "._("ModeratedPage").': '.$pagename;
355             $mailer->from = $request->_user->UserFrom();
356             $content = sprintf(_("%s approved your wiki action from %s"),
357                                  $mailer->from,CTime($moderation['timestamp']))
358                 ."\n\n"
359                 ."Decision: ".$pass
360                 ."Reason: ".$reason
361                 ."\n<".WikiURL($pagename).">\n";
362             $mailer->emails = $mailer->userids = $email;
363             $mailer->sendMail($subject, $content, "Approval notice");
364         }
365     }
366
367     function _approval_form(&$request, $args, $moderation, $pass='approve') {
368         $header = HTML::h3(_("Please approve or reject this request:"));
369         
370         $loader = new WikiPluginLoader();
371         $BackendInfo = $loader->getPlugin("_BackendInfo");
372         $table = HTML::table(array('border' => 1,
373                                      'cellpadding' => 2,
374                                      'cellspacing' => 0));
375         $content = $table;
376         $diff = '';
377         if ($moderation['args']['action'] == 'edit') {
378             $pagename = $moderation['args']['pagename'];
379             $p = $request->_dbi->getPage($pagename);
380             $rev = $p->getCurrentRevision(true);
381             $curr_content = $rev->getPackedContent();
382             $new_content = $moderation['args']['edit']['content'];
383             include_once("lib/difflib.php");
384             $diff2 = new Diff($curr_content, $new_content);
385             $fmt = new UnifiedDiffFormatter(/*$context_lines*/);
386             $diff  = $pagename . " Current Version " . 
387                 Iso8601DateTime($p->get('mtime')) . "\n";
388             $diff .= $pagename . " Edited Version " .  
389                 Iso8601DateTime($moderation['timestamp']) . "\n";
390             $diff .= $fmt->format($diff2);
391         }
392         $content->pushContent($BackendInfo->_showhash("Request", 
393                         array('User'      => $moderation['userid'],
394                               'When'      => CTime($moderation['timestamp']),
395                               'Pagename'  => $pagename,
396                               'Action'    => $moderation['args']['action'],
397                               'Diff'      => HTML::pre($diff))));                            
398         $content_dbg = $table;
399         $myargs  = $args;
400         $BackendInfo->_fixupData($myargs);
401         $content_dbg->pushContent($BackendInfo->_showhash("raw request args", $myargs));
402         $BackendInfo->_fixupData($moderation);
403         $content_dbg->pushContent($BackendInfo->_showhash("raw moderation data", $moderation));
404         $reason = HTML::div(_("Reason: "), HTML::textarea(array('name' => 'reason')));
405         $approve = Button('submit:ModeratedPage[approve]', _("Approve"), 
406                           $pass == 'approve' ? 'wikiadmin' : 'button');
407         $reject  = Button('submit:ModeratedPage[reject]', _("Reject"),
408                           $pass == 'reject' ? 'wikiadmin' : 'button');
409         $args['action'] = _("ModeratedPage");
410         return HTML::form(array('action' => $request->getPostURL(),
411                                 'method' => 'post'),
412                           $header,
413                           $content, HTML::p(""), $content_dbg,
414                           $reason,
415                           ENABLE_PAGEPERM 
416                             ? ''
417                             : HiddenInputs(array('require_authority_for_post' => WIKIAUTH_ADMIN)),
418                           HiddenInputs($args),
419                           $pass == 'approve' ? HTML::p($approve, $reject) 
420                                              : HTML::p($reject, $approve));
421     }
422     
423     /**
424      * Get the side-wide ModeratedPage status, reading the action-page args.
425      * Who are the moderators? What actions should be moderated?
426      */
427     function getSiteStatus(&$request, &$action_page) {
428         $loader = new WikiPluginLoader();
429         $rev = $action_page->getCurrentRevision();
430         $content = $rev->getPackedContent();
431         list($pi) = explode("\n", $content, 2); // plugin ModeratedPage must be first line!
432         if ($parsed = $loader->parsePI($pi)) {
433             $plugin =& $parsed[1];
434             if ($plugin->getName() != _("ModeratedPage"))
435                 return $this->error(sprintf(_("<?plugin ModeratedPage ... ?> not found in first line of %s"),
436                                             $action_page->getName()));
437             if (!$action_page->get('locked'))
438                 return $this->error(sprintf(_("%s is not locked!"),
439                                             $action_page->getName()));
440             return $plugin->resolve_argstr($request, $parsed[2]);
441         } else {
442             return $this->error(sprintf(_("<?plugin ModeratedPage ... ?> not found in first line of %s"),
443                                         $action_page->getName()));
444         }
445     }
446     
447 };
448
449 // $Log: not supported by cvs2svn $
450 // Revision 1.6  2007/01/07 18:45:46  rurban
451 // Finish 3/3 of the functionality, the Moderators approve and reject, Fix some logical flaws with !empty($status[emails]). Generate a better ID
452 //
453 // Revision 1.5  2006/08/15 13:41:08  rurban
454 // just aesthetics
455 //
456 // Revision 1.4  2005/01/29 19:52:09  rurban
457 // more work on the last part
458 //
459 // Revision 1.3  2004/12/06 19:50:05  rurban
460 // enable action=remove which is undoable and seeable in RecentChanges: ADODB ony for now.
461 // renamed delete_page to purge_page.
462 // enable action=edit&version=-1 to force creation of a new version.
463 // added BABYCART_PATH config
464 // fixed magiqc in adodb.inc.php
465 // and some more docs
466 //
467 // Revision 1.2  2004/11/30 17:46:49  rurban
468 // added ModeratedPage POST action hook (part 2/3)
469 //
470 // Revision 1.1  2004/11/19 19:22:35  rurban
471 // ModeratePage part1: change status
472 //
473
474 // For emacs users
475 // Local Variables:
476 // mode: php
477 // tab-width: 8
478 // c-basic-offset: 4
479 // c-hanging-comment-ender-p: nil
480 // indent-tabs-mode: nil
481 // End:
482 ?>