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