2 rcs_id('$Id: ModeratedPage.php,v 1.7 2007-08-25 18:52:34 rurban Exp $');
4 Copyright 2004,2005 $ThePhpWikiProgrammingTeam
6 This file is part of PhpWiki.
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.
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.
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
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
29 * Not yet ready! part 3/3 is missing: The moderator approve/reject methods.
31 * See http://phpwiki.org/PageModeration
35 require_once("lib/WikiPlugin.php");
37 class WikiPlugin_ModeratedPage
41 return _("ModeratedPage");
43 function getDescription () {
44 return _("Support moderated pages");
46 function getVersion() {
47 return preg_replace("/[Revision: $]/", '',
50 function getDefaultArguments() {
51 return array('page' => '[pagename]',
52 'moderators' => false,
53 'require_level' => false, // 1=bogo
54 'require_access' => 'edit,remove,change',
60 function run($dbi, $argstr, &$request, $basepage) {
61 $args = $this->getArgs($argstr, $request);
63 // Handle moderation request from urls sent by email
64 if (!empty($args['id']) and !empty($args['pass'])) {
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']];
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);
80 return $this->error("Wrong button pressed");
82 if ($args['pass'] == 'approve')
83 return $this->approve($request, $args, $moderation);
84 elseif ($args['pass'] == 'reject')
85 return $this->reject($request, $args, $moderation);
87 return $this->error("Wrong pass ".$args['pass']);
89 return $this->error("Wrong id ".htmlentities($args['id']));
97 * resolve moderators and require_access (not yet) from actionpage plugin argstr
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));
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);
115 if (!$moderators) $moderators = array(ADMIN_USER);
116 $args['moderators'] = $moderators;
118 //resolve email for $args['moderators']
119 $page = $request->getPage();
121 foreach ($args['moderators'] as $userid) {
124 require_once("lib/MailNotify.php");
125 $mail = new MailNotify($page->getName());
127 list($args['emails'], $args['moderators']) =
128 $mail->getPageChangeEmails(array($page->getName() => $users));
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']);
135 if ($args['require_level'] !== false) {
136 $args['require_level'] = (integer) $args['require_level'];
139 unset($args['page']);
140 unset($args['pass']);
145 * Handle client-side moderation change request.
146 * Hook called on the lock action, if moderation metadata already exists.
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"),
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']));
162 $page->set('moderation', false);
163 return $this->notice(HTML($status,
164 fmt("'%s' is no ModeratedPage anymore.", $page->getName())));
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
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"),
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']));
192 function notice($msg) {
193 return HTML::div(array('class' => 'wiki-edithelp'), $msg);
196 function generateId() {
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);
203 $len = $r = function_exists('mt_rand') ? mt_rand(15, 25) : rand(15,25);
204 return substr(base64_encode($s),3,$len);
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.
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'];
219 $action_page = $request->getPage(_("ModeratedPage"));
220 $status = $this->getSiteStatus($request, $action_page);
221 $moderated['status'] = $status;
223 if (empty($status['emails'])) {
224 trigger_error(_("ModeratedPage: No emails for the moderators defined"),
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 ),
249 $this->_tokens['CONTENT'] =
250 HTML::div(array('class' => 'wikitext'),
251 fmt("%s: action forwarded to moderator %s",
253 join(", ", $status['moderators'])
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
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"),
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.
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}();
307 elseif ($page = $this->findActionPage($action)) {
308 $this->actionpage($page);
311 $this->finish(fmt("%s: Bad action", $action));
313 // now we are gone and nobody brings us back here.
315 //$moderated['data'][$id]->args->action+edit(array)+...
316 // timestamp,user(obj)+userid
317 // handle $moderated['data'][$id]['args']['action']
319 return $this->_approval_form($request, $args, $moderation, 'approve');
324 * Handle admin-side moderation resolve.
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);
332 return $this->_approval_form($request, $args, $moderation, 'reject');
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'];
344 unset($moderation['data'][$id]);
345 unset($moderation['id']);
346 $page->set('moderation', $moderation);
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']))
361 ."\n<".WikiURL($pagename).">\n";
362 $mailer->emails = $mailer->userids = $email;
363 $mailer->sendMail($subject, $content, "Approval notice");
367 function _approval_form(&$request, $args, $moderation, $pass='approve') {
368 $header = HTML::h3(_("Please approve or reject this request:"));
370 $loader = new WikiPluginLoader();
371 $BackendInfo = $loader->getPlugin("_BackendInfo");
372 $table = HTML::table(array('border' => 1,
374 'cellspacing' => 0));
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);
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;
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(),
413 $content, HTML::p(""), $content_dbg,
417 : HiddenInputs(array('require_authority_for_post' => WIKIAUTH_ADMIN)),
419 $pass == 'approve' ? HTML::p($approve, $reject)
420 : HTML::p($reject, $approve));
424 * Get the side-wide ModeratedPage status, reading the action-page args.
425 * Who are the moderators? What actions should be moderated?
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]);
442 return $this->error(sprintf(_("<?plugin ModeratedPage ... ?> not found in first line of %s"),
443 $action_page->getName()));
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
453 // Revision 1.5 2006/08/15 13:41:08 rurban
456 // Revision 1.4 2005/01/29 19:52:09 rurban
457 // more work on the last part
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
467 // Revision 1.2 2004/11/30 17:46:49 rurban
468 // added ModeratedPage POST action hook (part 2/3)
470 // Revision 1.1 2004/11/19 19:22:35 rurban
471 // ModeratePage part1: change status
479 // c-hanging-comment-ender-p: nil
480 // indent-tabs-mode: nil