]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/plugin/ModeratedPage.php
Use CSS
[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 getDescription()
42     {
43         return _("Support moderated pages.");
44     }
45
46     function getDefaultArguments()
47     {
48         return array('page' => '[pagename]',
49             'moderators' => false,
50             'require_level' => false, // 1=bogo
51             'require_access' => 'edit,remove,change',
52             'id' => '',
53             'pass' => '',
54         );
55     }
56
57     function run($dbi, $argstr, &$request, $basepage)
58     {
59         $args = $this->getArgs($argstr, $request);
60
61         // Handle moderation request from URLs sent by e-mail
62         if (!empty($args['id']) and !empty($args['pass'])) {
63             if (!$args['page']) {
64                 return $this->error(sprintf(_("A required argument “%s” is missing."), 'page'));
65             }
66             $page = $dbi->getPage($args['page']);
67             if ($moderated = $page->get("moderated")) {
68                 if (array_key_exists($args['id'], $moderated['data'])) {
69                     $moderation = $moderated['data'][$args['id']];
70                     // handle defaults:
71                     //   approve or reject
72                     if ($request->isPost()) {
73                         $button = $request->getArg('ModeratedPage');
74                         if (isset($button['reject']))
75                             return $this->reject($request, $args, $moderation);
76                         elseif (isset($button['approve']))
77                             return $this->approve($request, $args, $moderation); else
78                             return $this->error("Wrong button pressed");
79                     }
80                     if ($args['pass'] == 'approve')
81                         return $this->approve($request, $args, $moderation);
82                     elseif ($args['pass'] == 'reject')
83                         return $this->reject($request, $args, $moderation); else
84                         return $this->error("Wrong pass " . $args['pass']);
85                 } else {
86                     return $this->error("Wrong id " . htmlentities($args['id']));
87                 }
88             }
89         }
90         return HTML::raw('');
91     }
92
93     /**
94      * resolve moderators and require_access (not yet) from actionpage plugin argstr
95      */
96     function resolve_argstr(&$request, $argstr)
97     {
98         $args = $this->getArgs($argstr);
99         $group = $request->getGroup();
100         if (empty($args['moderators'])) {
101             $admins = $group->getSpecialMembersOf(GROUP_ADMIN);
102             // email or usernames?
103             $args['moderators'] = array_merge($admins, array(ADMIN_USER));
104         } else {
105             // resolve possible group names
106             $moderators = explode(',', $args['moderators']);
107             for ($i = 0; $i < count($moderators); $i++) {
108                 $members = $group->getMembersOf($moderators[$i]);
109                 if (!empty($members)) {
110                     array_splice($moderators, $i, 1, $members);
111                 }
112             }
113             if (!$moderators) $moderators = array(ADMIN_USER);
114             $args['moderators'] = $moderators;
115         }
116         //resolve email for $args['moderators']
117         $page = $request->getPage();
118         $users = array();
119         foreach ($args['moderators'] as $userid) {
120             $users[$userid] = 0;
121         }
122         require_once 'lib/MailNotify.php';
123         $mail = new MailNotify($page->getName());
124
125         list($args['emails'], $args['moderators']) =
126             $mail->getPageChangeEmails(array($page->getName() => $users));
127
128         if (!empty($args['require_access'])) {
129             $args['require_access'] = preg_split("/\s*,\s*/", $args['require_access']);
130             if (empty($args['require_access']))
131                 unset($args['require_access']);
132         }
133         if ($args['require_level'] !== false) {
134             $args['require_level'] = (integer)$args['require_level'];
135         }
136         unset($args['id']);
137         unset($args['page']);
138         unset($args['pass']);
139         return $args;
140     }
141
142     /**
143      * Handle client-side moderation change request.
144      * Hook called on the lock action, if moderation metadata already exists.
145      */
146     function lock_check(&$request, &$page, $moderated)
147     {
148         $action_page = $request->getPage(_("ModeratedPage"));
149         $status = $this->getSiteStatus($request, $action_page);
150         if (is_array($status)) {
151             if (empty($status['emails'])) {
152                 trigger_error(_("No e-mails for the moderators defined"), E_USER_WARNING);
153                 return false;
154             }
155             $page->set('moderation', array('status' => $status));
156             return $this->notice(
157                 fmt("ModeratedPage status update:\n  Moderators: “%s”\n  require_access: “%s”",
158                     join(',', $status['moderators']), $status['require_access']));
159         } else {
160             $page->set('moderation', false);
161             return $this->notice(HTML($status,
162                 fmt("“%s” is no ModeratedPage anymore.", $page->getName())));
163         }
164     }
165
166     /**
167      * Handle client-side moderation change request by the user.
168      * Hook called on the lock action, if moderation metadata should be added.
169      * Need to store the the plugin args (who, when) in the page meta-data
170      */
171     function lock_add(&$request, &$page, &$action_page)
172     {
173         $status = $this->getSiteStatus($request, $action_page);
174         if (is_array($status)) {
175             if (empty($status['emails'])) {
176                 // We really should present such warnings prominently.
177                 trigger_error(_("No e-mails for the moderators defined"), E_USER_WARNING);
178                 return false;
179             }
180             $page->set('moderation', array('status' => $status));
181             return $this->notice(
182                 fmt("ModeratedPage status update: “%s” is now a ModeratedPage.\n  Moderators: “%s”\n  require_access: “%s”",
183                     $page->getName(), join(',', $status['moderators']), $status['require_access']));
184         } else { // error
185             return $status;
186         }
187     }
188
189     function notice($msg)
190     {
191         return HTML::div(array('class' => 'wiki-edithelp'), $msg);
192     }
193
194     function generateId()
195     {
196         better_srand();
197         $s = "";
198         for ($i = 1; $i <= 25; $i++) {
199             $r = function_exists('mt_rand') ? mt_rand(55, 90) : rand(55, 90);
200             $s .= chr(($r < 65) ? ($r - 17) : $r);
201         }
202         $len = $r = function_exists('mt_rand') ? mt_rand(15, 25) : rand(15, 25);
203         return substr(base64_encode($s), 3, $len);
204     }
205
206     /**
207      * Handle client-side POST moderation request on any moderated page.
208      *   if ($page->get('moderation')) WikiPlugin_ModeratedPage::handler(...);
209      * return false if not handled (pass through), true if handled and displayed.
210      */
211     function handler(&$request, &$page)
212     {
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(_("No e-mails for the moderators defined"), E_USER_WARNING);
225             return true;
226         }
227         // which action?
228         if (!empty($status['require_access'])
229             and !in_array(action2access($action), $status['require_access'])
230         )
231             return false; // allow and fall through, not moderated
232         if (!empty($status['require_level'])
233             and $request->_user->_level >= $status['require_level']
234         )
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 e-mail
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 e-mail"),
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     {
294         if ($request->isPost()) {
295             // this is unsafe because we dont know if it will succeed. but we tried.
296             $this->cleanup_and_notify($request, $args, $moderation);
297             // start from scratch, dispatch the action as in lib/main to the action handler
298             $request->discardOutput();
299             $oldargs = $request->args;
300             $olduser = $request->_user;
301             $request->args = $moderation['args'];
302             $request->_user->_userid = $moderation['userid']; // keep current perms but fake the id.
303             // TODO: fake author ip also
304             extract($request->args);
305             $method = "action_$action";
306             if (method_exists($request, $method)) {
307                 $request->{$method}();
308             } elseif ($page = $this->findActionPage($action)) {
309                 $this->actionpage($page);
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         return '';
322     }
323
324     /**
325      * Handle admin-side moderation resolve.
326      */
327     function reject(&$request, $args, &$moderation)
328     {
329         // check id, delete action
330         if ($request->isPost()) {
331             // clean up and notify the requestor. Mabye: store and revert to have a diff later on?
332             $this->cleanup_and_notify($request, $args, $moderation);
333         } else {
334             return $this->approval_form($request, $args, $moderation, 'reject');
335         }
336         return '';
337     }
338
339     function cleanup_and_notify(&$request, $args, &$moderation)
340     {
341         $pagename = $moderation['args']['pagename'];
342         $page = $request->_dbi->getPage($pagename);
343         $pass = $args['pass']; // accept or reject
344         $reason = $args['reason']; // summary why
345         $user = $moderation['args']['user'];
346         $action = $moderation['args']['action'];
347         $id = $args['id'];
348         unset($moderation['data'][$id]);
349         unset($moderation['id']);
350         $page->set('moderation', $moderation);
351
352         // TODO: Notify the user, only if the user has an email:
353         if ($email = $user->getPref('email')) {
354             $action_page = $request->getPage(_("ModeratedPage"));
355             $status = $this->getSiteStatus($request, $action_page);
356             require_once 'lib/MailNotify.php';
357             $mailer = new MailNotify($pagename);
358             $subject = "[" . WIKI_NAME . "] $pass $action " . _("ModeratedPage") . _(": ") . $pagename;
359             $mailer->from = $request->_user->UserFrom();
360             $content = sprintf(_("%s approved your wiki action from %s"),
361                 $mailer->from, CTime($moderation['timestamp']))
362                 . "\n\n"
363                 . "Decision: " . $pass
364                 . "Reason: " . $reason
365                 . "\n<" . WikiURL($pagename) . ">\n";
366             $mailer->emails = $mailer->userids = $email;
367             $mailer->sendMail($subject, $content, "Approval notice");
368         }
369     }
370
371     private function approval_form(&$request, $args, $moderation, $pass = 'approve')
372     {
373         $header = HTML::h3(_("Please approve or reject this request:"));
374
375         $loader = new WikiPluginLoader();
376         $BackendInfo = $loader->getPlugin("DebugBackendInfo");
377         $table = HTML::table(array('border' => 1));
378         $content = $table;
379         $diff = '';
380         if ($moderation['args']['action'] == 'edit') {
381             $pagename = $moderation['args']['pagename'];
382             $p = $request->_dbi->getPage($pagename);
383             $rev = $p->getCurrentRevision(true);
384             $curr_content = $rev->getPackedContent();
385             $new_content = $moderation['args']['edit']['content'];
386             include_once 'lib/difflib.php';
387             $diff2 = new Diff($curr_content, $new_content);
388             $fmt = new UnifiedDiffFormatter( /*$context_lines*/);
389             $diff = $pagename . " Current Version " .
390                 Iso8601DateTime($p->get('mtime')) . "\n";
391             $diff .= $pagename . " Edited Version " .
392                 Iso8601DateTime($moderation['timestamp']) . "\n";
393             $diff .= $fmt->format($diff2);
394         }
395         $content->pushContent($BackendInfo->_showhash("Request",
396             array('User' => $moderation['userid'],
397                 'When' => CTime($moderation['timestamp']),
398                 'Pagename' => $pagename,
399                 'Action' => $moderation['args']['action'],
400                 'Diff' => HTML::pre($diff))));
401         $content_dbg = $table;
402         $myargs = $args;
403         $BackendInfo->_fixupData($myargs);
404         $content_dbg->pushContent($BackendInfo->_showhash("raw request args", $myargs));
405         $BackendInfo->_fixupData($moderation);
406         $content_dbg->pushContent($BackendInfo->_showhash("raw moderation data", $moderation));
407         $reason = HTML::div(_("Reason: "), HTML::textarea(array('name' => 'reason')));
408         $approve = Button('submit:ModeratedPage[approve]', _("Approve"),
409             $pass == 'approve' ? 'wikiadmin' : 'button');
410         $reject = Button('submit:ModeratedPage[reject]', _("Reject"),
411             $pass == 'reject' ? 'wikiadmin' : 'button');
412         $args['action'] = _("ModeratedPage");
413         return HTML::form(array('action' => $request->getPostURL(),
414                 'method' => 'post'),
415             $header,
416             $content, HTML::p(""), $content_dbg,
417             $reason,
418             ENABLE_PAGEPERM
419                 ? ''
420                 : HiddenInputs(array('require_authority_for_post' => WIKIAUTH_ADMIN)),
421             HiddenInputs($args),
422             $pass == 'approve' ? HTML::p($approve, $reject)
423                 : HTML::p($reject, $approve));
424     }
425
426     /**
427      * Get the side-wide ModeratedPage status, reading the action-page args.
428      * Who are the moderators? What actions should be moderated?
429      */
430     function getSiteStatus(&$request, &$action_page)
431     {
432         $loader = new WikiPluginLoader();
433         $rev = $action_page->getCurrentRevision();
434         $content = $rev->getPackedContent();
435         list($pi) = explode("\n", $content, 2); // plugin ModeratedPage must be first line!
436         if ($parsed = $loader->parsePI($pi)) {
437             $plugin =& $parsed[1];
438             if ($plugin->getName() != _("ModeratedPage"))
439                 return $this->error(sprintf(_("<<ModeratedPage ... >> not found in first line of %s"),
440                     $action_page->getName()));
441             if (!$action_page->get('locked'))
442                 return $this->error(sprintf(_("%s is not locked!"),
443                     $action_page->getName()));
444             return $plugin->resolve_argstr($request, $parsed[2]);
445         } else {
446             return $this->error(sprintf(_("<<ModeratedPage ... >> not found in first line of %s"),
447                 $action_page->getName()));
448         }
449     }
450
451 }
452
453 // Local Variables:
454 // mode: php
455 // tab-width: 8
456 // c-basic-offset: 4
457 // c-hanging-comment-ender-p: nil
458 // indent-tabs-mode: nil
459 // End: