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