4 * Copyright 2004,2005 $ThePhpWikiProgrammingTeam
5 * Copyright 2009 Marc-Etienne Vargenau, Alcatel-Lucent
7 * This file is part of PhpWiki.
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.
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.
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.
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
30 * Not yet ready! part 3/3 is missing: The moderator approve/reject methods.
32 * See http://phpwiki.org/PageModeration
36 require_once 'lib/WikiPlugin.php';
38 class WikiPlugin_ModeratedPage
43 return _("ModeratedPage");
46 function getDescription()
48 return _("Support moderated pages.");
51 function getDefaultArguments()
53 return array('page' => '[pagename]',
54 'moderators' => false,
55 'require_level' => false, // 1=bogo
56 'require_access' => 'edit,remove,change',
62 function run($dbi, $argstr, &$request, $basepage)
64 $args = $this->getArgs($argstr, $request);
66 // Handle moderation request from URLs sent by e-mail
67 if (!empty($args['id']) and !empty($args['pass'])) {
69 return $this->error(sprintf(_("A required argument “%s” is missing."), 'page'));
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']];
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");
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']);
91 return $this->error("Wrong id " . htmlentities($args['id']));
99 * resolve moderators and require_access (not yet) from actionpage plugin argstr
101 function resolve_argstr(&$request, $argstr)
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));
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);
118 if (!$moderators) $moderators = array(ADMIN_USER);
119 $args['moderators'] = $moderators;
121 //resolve email for $args['moderators']
122 $page = $request->getPage();
124 foreach ($args['moderators'] as $userid) {
127 require_once 'lib/MailNotify.php';
128 $mail = new MailNotify($page->getName());
130 list($args['emails'], $args['moderators']) =
131 $mail->getPageChangeEmails(array($page->getName() => $users));
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']);
138 if ($args['require_level'] !== false) {
139 $args['require_level'] = (integer)$args['require_level'];
142 unset($args['page']);
143 unset($args['pass']);
148 * Handle client-side moderation change request.
149 * Hook called on the lock action, if moderation metadata already exists.
151 function lock_check(&$request, &$page, $moderated)
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(_("No e-mails for the moderators defined"), E_USER_WARNING);
160 $page->set('moderation', array('status' => $status));
161 return $this->notice(
162 fmt("ModeratedPage status update:\n Moderators: “%s”\n require_access: “%s”",
163 join(',', $status['moderators']), $status['require_access']));
165 $page->set('moderation', false);
166 return $this->notice(HTML($status,
167 fmt("“%s” is no ModeratedPage anymore.", $page->getName())));
172 * Handle client-side moderation change request by the user.
173 * Hook called on the lock action, if moderation metadata should be added.
174 * Need to store the the plugin args (who, when) in the page meta-data
176 function lock_add(&$request, &$page, &$action_page)
178 $status = $this->getSiteStatus($request, $action_page);
179 if (is_array($status)) {
180 if (empty($status['emails'])) {
181 // We really should present such warnings prominently.
182 trigger_error(_("No e-mails for the moderators defined"), E_USER_WARNING);
185 $page->set('moderation', array('status' => $status));
186 return $this->notice(
187 fmt("ModeratedPage status update: “%s” is now a ModeratedPage.\n Moderators: “%s”\n require_access: “%s”",
188 $page->getName(), join(',', $status['moderators']), $status['require_access']));
194 function notice($msg)
196 return HTML::div(array('class' => 'wiki-edithelp'), $msg);
199 function generateId()
203 for ($i = 1; $i <= 25; $i++) {
204 $r = function_exists('mt_rand') ? mt_rand(55, 90) : rand(55, 90);
205 $s .= chr(($r < 65) ? ($r - 17) : $r);
207 $len = $r = function_exists('mt_rand') ? mt_rand(15, 25) : rand(15, 25);
208 return substr(base64_encode($s), 3, $len);
212 * Handle client-side POST moderation request on any moderated page.
213 * if ($page->get('moderation')) WikiPlugin_ModeratedPage::handler(...);
214 * return false if not handled (pass through), true if handled and displayed.
216 function handler(&$request, &$page)
218 $action = $request->getArg('action');
219 $moderated = $page->get('moderated');
220 // cached version, need re-lock of each page to update moderators
221 if (!empty($moderated['status']))
222 $status = $moderated['status'];
224 $action_page = $request->getPage(_("ModeratedPage"));
225 $status = $this->getSiteStatus($request, $action_page);
226 $moderated['status'] = $status;
228 if (empty($status['emails'])) {
229 trigger_error(_("No e-mails for the moderators defined"), E_USER_WARNING);
233 if (!empty($status['require_access'])
234 and !in_array(action2access($action), $status['require_access'])
236 return false; // allow and fall through, not moderated
237 if (!empty($status['require_level'])
238 and $request->_user->_level >= $status['require_level']
240 return false; // allow and fall through, not moderated
241 // else all post actions are moderated by default
242 if (1) /* or in_array($action, array('edit','remove','rename')*/ {
243 $id = $this->generateId();
244 while (!empty($moderated['data'][$id])) $id = $this->generateId(); // avoid duplicates
245 $moderated['id'] = $id; // overwrite current id
246 $tempuser = $request->_user;
247 if (isset($tempuser->_HomePagehandle))
248 unset($tempuser->_HomePagehandle);
249 $moderated['data'][$id] = array( // add current request
250 'timestamp' => time(),
251 'userid' => $request->_user->getId(),
252 'args' => $request->getArgs(),
253 'user' => serialize($tempuser),
255 $this->_tokens['CONTENT'] =
256 HTML::div(array('class' => 'wikitext'),
257 fmt("%s: action forwarded to moderator %s",
259 join(", ", $status['moderators'])
262 require_once 'lib/MailNotify.php';
263 $pagename = $page->getName();
264 $mailer = new MailNotify($pagename);
265 $subject = "[" . WIKI_NAME . '] ' . $action . _(": ") . _("ModeratedPage") . ' ' . $pagename;
266 $content = "You are approved as Moderator of the " . WIKI_NAME . " wiki.\n" .
267 "Someone wanted to edit a moderated page, which you have to approve or reject.\n\n" .
268 $action . _(": ") . _("ModeratedPage") . ' ' . $pagename . "\n"
269 //. serialize($moderated['data'][$id])
270 . "\n<" . WikiURL($pagename, array('action' => _("ModeratedPage"),
271 'id' => $id, 'pass' => 'approve'), 1) . ">"
272 . "\n<" . WikiURL($pagename, array('action' => _("ModeratedPage"),
273 'id' => $id, 'pass' => 'reject'), 1) . ">\n";
274 $mailer->emails = $mailer->userids = $status['emails'];
275 $mailer->from = $request->_user->_userid;
276 if ($mailer->sendMail($subject, $content, "Moderation notice")) {
277 $page->set('moderated', $moderated);
278 return false; // pass thru
281 $page->set('moderated', $moderated);
282 //FIXME: This msg gets lost on the edit redirect
283 trigger_error(_("ModeratedPage Notification Error: Couldn't send e-mail"),
292 * Handle admin-side moderation resolve.
293 * We might have to convert the GET to a POST request to continue
294 * with the left-over stored request.
295 * Better we display a post form for verification.
297 function approve(&$request, $args, &$moderation)
299 if ($request->isPost()) {
300 // this is unsafe because we dont know if it will succeed. but we tried.
301 $this->cleanup_and_notify($request, $args, $moderation);
302 // start from scratch, dispatch the action as in lib/main to the action handler
303 $request->discardOutput();
304 $oldargs = $request->args;
305 $olduser = $request->_user;
306 $request->args = $moderation['args'];
307 $request->_user->_userid = $moderation['userid']; // keep current perms but fake the id.
308 // TODO: fake author ip also
309 extract($request->args);
310 $method = "action_$action";
311 if (method_exists($request, $method)) {
312 $request->{$method}();
313 } elseif ($page = $this->findActionPage($action)) {
314 $this->actionpage($page);
316 $this->finish(fmt("%s: Bad action", $action));
318 // now we are gone and nobody brings us back here.
320 //$moderated['data'][$id]->args->action+edit(array)+...
321 // timestamp,user(obj)+userid
322 // handle $moderated['data'][$id]['args']['action']
324 return $this->_approval_form($request, $args, $moderation, 'approve');
329 * Handle admin-side moderation resolve.
331 function reject(&$request, $args, &$moderation)
333 // check id, delete action
334 if ($request->isPost()) {
335 // clean up and notify the requestor. Mabye: store and revert to have a diff later on?
336 $this->cleanup_and_notify($request, $args, $moderation);
338 return $this->_approval_form($request, $args, $moderation, 'reject');
342 function cleanup_and_notify(&$request, $args, &$moderation)
344 $pagename = $moderation['args']['pagename'];
345 $page = $request->_dbi->getPage($pagename);
346 $pass = $args['pass']; // accept or reject
347 $reason = $args['reason']; // summary why
348 $user = $moderation['args']['user'];
349 $action = $moderation['args']['action'];
351 unset($moderation['data'][$id]);
352 unset($moderation['id']);
353 $page->set('moderation', $moderation);
355 // TODO: Notify the user, only if the user has an email:
356 if ($email = $user->getPref('email')) {
357 $action_page = $request->getPage(_("ModeratedPage"));
358 $status = $this->getSiteStatus($request, $action_page);
359 require_once 'lib/MailNotify.php';
360 $mailer = new MailNotify($pagename);
361 $subject = "[" . WIKI_NAME . "] $pass $action " . _("ModeratedPage") . _(": ") . $pagename;
362 $mailer->from = $request->_user->UserFrom();
363 $content = sprintf(_("%s approved your wiki action from %s"),
364 $mailer->from, CTime($moderation['timestamp']))
366 . "Decision: " . $pass
367 . "Reason: " . $reason
368 . "\n<" . WikiURL($pagename) . ">\n";
369 $mailer->emails = $mailer->userids = $email;
370 $mailer->sendMail($subject, $content, "Approval notice");
374 function _approval_form(&$request, $args, $moderation, $pass = 'approve')
376 $header = HTML::h3(_("Please approve or reject this request:"));
378 $loader = new WikiPluginLoader();
379 $BackendInfo = $loader->getPlugin("_BackendInfo");
380 $table = HTML::table(array('border' => 1,
382 'cellspacing' => 0));
385 if ($moderation['args']['action'] == 'edit') {
386 $pagename = $moderation['args']['pagename'];
387 $p = $request->_dbi->getPage($pagename);
388 $rev = $p->getCurrentRevision(true);
389 $curr_content = $rev->getPackedContent();
390 $new_content = $moderation['args']['edit']['content'];
391 include_once 'lib/difflib.php';
392 $diff2 = new Diff($curr_content, $new_content);
393 $fmt = new UnifiedDiffFormatter( /*$context_lines*/);
394 $diff = $pagename . " Current Version " .
395 Iso8601DateTime($p->get('mtime')) . "\n";
396 $diff .= $pagename . " Edited Version " .
397 Iso8601DateTime($moderation['timestamp']) . "\n";
398 $diff .= $fmt->format($diff2);
400 $content->pushContent($BackendInfo->_showhash("Request",
401 array('User' => $moderation['userid'],
402 'When' => CTime($moderation['timestamp']),
403 'Pagename' => $pagename,
404 'Action' => $moderation['args']['action'],
405 'Diff' => HTML::pre($diff))));
406 $content_dbg = $table;
408 $BackendInfo->_fixupData($myargs);
409 $content_dbg->pushContent($BackendInfo->_showhash("raw request args", $myargs));
410 $BackendInfo->_fixupData($moderation);
411 $content_dbg->pushContent($BackendInfo->_showhash("raw moderation data", $moderation));
412 $reason = HTML::div(_("Reason: "), HTML::textarea(array('name' => 'reason')));
413 $approve = Button('submit:ModeratedPage[approve]', _("Approve"),
414 $pass == 'approve' ? 'wikiadmin' : 'button');
415 $reject = Button('submit:ModeratedPage[reject]', _("Reject"),
416 $pass == 'reject' ? 'wikiadmin' : 'button');
417 $args['action'] = _("ModeratedPage");
418 return HTML::form(array('action' => $request->getPostURL(),
421 $content, HTML::p(""), $content_dbg,
425 : HiddenInputs(array('require_authority_for_post' => WIKIAUTH_ADMIN)),
427 $pass == 'approve' ? HTML::p($approve, $reject)
428 : HTML::p($reject, $approve));
432 * Get the side-wide ModeratedPage status, reading the action-page args.
433 * Who are the moderators? What actions should be moderated?
435 function getSiteStatus(&$request, &$action_page)
437 $loader = new WikiPluginLoader();
438 $rev = $action_page->getCurrentRevision();
439 $content = $rev->getPackedContent();
440 list($pi) = explode("\n", $content, 2); // plugin ModeratedPage must be first line!
441 if ($parsed = $loader->parsePI($pi)) {
442 $plugin =& $parsed[1];
443 if ($plugin->getName() != _("ModeratedPage"))
444 return $this->error(sprintf(_("<<ModeratedPage ... >> not found in first line of %s"),
445 $action_page->getName()));
446 if (!$action_page->get('locked'))
447 return $this->error(sprintf(_("%s is not locked!"),
448 $action_page->getName()));
449 return $plugin->resolve_argstr($request, $parsed[2]);
451 return $this->error(sprintf(_("<<ModeratedPage ... >> not found in first line of %s"),
452 $action_page->getName()));
462 // c-hanging-comment-ender-p: nil
463 // indent-tabs-mode: nil