hash ( page => (userid => userhash) ) for pages. * Generate notification emails. * * We add WikiDB handlers and register ourself there: * onChangePage, onDeletePage, onRenamePage * Administrative actions: * [Watch] WatchPage - add a page, or delete watch handlers into the users * pref[notifyPages] slot. * My WatchList - view or edit list/regex of pref[notifyPages]. * EMailConfirm methods: send and verify * * Helper functions: * getPageChangeEmails * MailAdmin * ? handle emailed confirmation links (EmailSignup, ModeratedPage) * * @package MailNotify * @author Reini Urban */ if (!defined("MAILER_LOG")) { if (isWindows()) { define("MAILER_LOG", 'c:/wikimail.log'); } else { define("MAILER_LOG", '/var/log/wikimail.log'); } } class MailNotify { function MailNotify($pagename) { $this->pagename = $pagename; /* which page */ $this->emails = array(); /* to which addresses */ $this->userids = array(); /* corresponding array of displayed names, don't display the email addresses */ /* From: from whom the mail appears to be */ $this->from = $this->fromId(); } function fromId() { global $request; if (defined('FUSIONFORGE') and FUSIONFORGE) { return $request->_user->getId(); } else { return $request->_user->getId() . '@' . $request->get('REMOTE_HOST'); } } function fromEmail() { global $request; return $this->userEmail($request->_user->getId(), false); } function userEmail($userid, $doverify = true) { global $request; // Disable verification of emails for corporate env. if (defined('FUSIONFORGE') and FUSIONFORGE) { $doverify = false; } $u = $request->getUser(); if ($u->UserName() == $userid) { // lucky: current user $prefs = $u->getPreferences(); $email = $prefs->get('email'); // do a dynamic emailVerified check update if ($doverify and !$request->_prefs->get('emailVerified')) { $email = ''; } } else { // not current user if (ENABLE_USER_NEW) { $u = WikiUser($userid); $u->getPreferences(); $prefs = &$u->_prefs; } else { $u = new WikiUser($request, $userid); $prefs = $u->getPreferences(); } $email = $prefs->get('email'); if ($doverify and !$prefs->get('emailVerified')) { $email = ''; } } return $email; } /** * getPageChangeEmails($notify) * @param $notify: hash ( page => (userid => userhash) ) * @return array * unique array of ($emails, $userids) */ function getPageChangeEmails($notify) { global $request; $emails = array(); $userids = array(); foreach ($notify as $page => $users) { if (glob_match($page, $this->pagename)) { $curuser = $request->getUser(); $curuserprefs = $curuser->getPreferences(); $curuserprefsemail = $curuserprefs->get('email'); $ownModifications = $curuserprefs->get('ownModifications'); $majorModificationsOnly = $curuserprefs->get('majorModificationsOnly'); foreach ($users as $userid => $user) { $usermail = $user['email']; if (($usermail == $curuserprefsemail) and ($ownModifications) ) { // It's my own modification // and I do not want to receive it continue; } if ($majorModificationsOnly) { $backend = &$request->_dbi->_backend; $version = $backend->get_latest_version($this->pagename); $versiondata = $backend->get_versiondata($this->pagename, $version, true); if ($versiondata['is_minor_edit']) { // It's a minor modification // and I do not want to receive it continue; } } if (!$user) { // handle the case for ModeratePage: // no prefs, just userid's. $emails[] = $this->userEmail($userid, false); $userids[] = $userid; } else { if (!empty($user['verified']) and !empty($user['email'])) { $emails[] = $user['email']; $userids[] = $userid; } elseif (!empty($user['email'])) { // do a dynamic emailVerified check update $email = $this->userEmail($userid, true); if ($email) { $notify[$page][$userid]['verified'] = 1; $request->_dbi->set('notify', $notify); $emails[] = $email; $userids[] = $userid; } } } } } } $this->emails = array_unique($emails); $this->userids = array_unique($userids); return array($this->emails, $this->userids); } function sendMail($subject, $content, $notice = false, $silent = true) { // Add WIKI_NAME to Subject $subject = "[" . WIKI_NAME . "] " . $subject; // Encode $subject if needed $encoded_subject = $this->subject_encode($subject); $emails = $this->emails; // Do not send if modification is from FusionForge admin if ((defined('FUSIONFORGE') and FUSIONFORGE) and ($this->fromId() == ADMIN_USER)) { return true; } if (!$notice) { $notice = _("PageChange Notification of %s"); } $from = $this->fromEmail(); $headers = "From: $from\r\n" . "Bcc: " . join(',', $emails) . "\r\n" . "MIME-Version: 1.0\r\n" . "Content-Type: text/plain; charset=UTF-8; format=flowed\r\n" . "Content-Transfer-Encoding: 8bit"; $ok = mail(($to = array_shift($emails)), $encoded_subject, $subject . "\n" . $content, $headers ); if (MAILER_LOG and is_writable(MAILER_LOG)) { global $ErrorManager; $f = fopen(MAILER_LOG, "a"); fwrite($f, "\n\nX-MailSentOK: " . $ok ? 'OK' : 'FAILED'); if (!$ok && isset($ErrorManager->_postponed_errors[count($ErrorManager->_postponed_errors) - 1])) { // get last error message $last_err = $ErrorManager->_postponed_errors[count($ErrorManager->_postponed_errors) - 1]; fwrite($f, "\nX-MailFailure: " . "errno: " . $last_err->errno . ", " . "errstr: " . $last_err->errstr . ", " . "errfile: " . $last_err->errfile . ", " . "errline: " . $last_err->errline); } fwrite($f, "\nDate: " . CTime()); fwrite($f, "\nSubject: $encoded_subject"); fwrite($f, "\nFrom: $from"); fwrite($f, "\nTo: $to"); fwrite($f, "\nBcc: " . join(',', $emails)); fwrite($f, "\n\n" . $content); fclose($f); } if ($ok) { if (!$silent) trigger_error(sprintf($notice, $this->pagename) . " " . sprintf(_("sent to %s"), join(',', $this->userids)), E_USER_NOTICE); return true; } else { trigger_error(sprintf($notice, $this->pagename) . " " . sprintf(_("Error: Couldn't send %s to %s"), $subject . "\n" . $content, join(',', $this->userids)), E_USER_WARNING); return false; } } /** * Send udiff for a changed page to multiple users. * See rename and remove methods also */ function sendPageChangeNotification(&$wikitext, $version, &$meta) { global $request; if (isset($request->_deferredPageChangeNotification)) { // collapse multiple changes (loaddir) into one email $request->_deferredPageChangeNotification[] = array($this->pagename, $this->emails, $this->userids); return; } $backend = &$request->_dbi->_backend; $previous = $backend->get_previous_version($this->pagename, $version); if (!isset($meta['mtime'])) { $meta['mtime'] = time(); } if ($previous) { // Page existed, and was modified $subject = _("Page change") . ' ' . ($this->pagename); $difflink = WikiURL($this->pagename, array('action' => 'diff'), true); $cache = &$request->_dbi->_cache; $this_content = explode("\n", $wikitext); $prevdata = $cache->get_versiondata($this->pagename, $previous, true); if (empty($prevdata['%content'])) { $prevdata = $backend->get_versiondata($this->pagename, $previous, true); } $other_content = explode("\n", $prevdata['%content']); include_once 'lib/difflib.php'; $diff2 = new Diff($other_content, $this_content); //$context_lines = max(4, count($other_content) + 1, // count($this_content) + 1); $fmt = new UnifiedDiffFormatter( /*$context_lines*/); $content = $this->pagename . " " . $previous . " " . Iso8601DateTime($prevdata['mtime']) . "\n"; $content .= $this->pagename . " " . $version . " " . Iso8601DateTime($meta['mtime']) . "\n"; $content .= $fmt->format($diff2); $editedby = sprintf(_("Edited by: %s"), $this->fromId()); } else { // Page did not exist, and was created $subject = _("Page creation") . ' ' . ($this->pagename); $difflink = WikiURL($this->pagename, array(), true); $content = $this->pagename . " " . $version . " " . Iso8601DateTime($meta['mtime']) . "\n"; $content .= _("New page"); $content .= "\n\n"; $content .= $wikitext; $editedby = sprintf(_("Created by: %s"), $this->fromId()); } $summary = sprintf(_("Summary: %s"), $meta['summary']); $this->sendMail($subject, $editedby . "\n" . $summary . "\n" . $difflink . "\n\n" . $content); } /** * Support mass rename / remove (TBD) */ function sendPageRenameNotification($to) { $pagename = $this->pagename; $editedby = sprintf(_("Renamed by: %s"), $this->fromId()); $subject = sprintf(_("Page rename %s to %s"), $pagename, $to); $link = WikiURL($to, true); $this->sendMail($subject, $editedby . "\n" . $link . "\n\n" . "Renamed $pagename to $to"); } /** * The handlers: */ function onChangePage(&$wikidb, &$wikitext, $version, &$meta) { if (!isa($GLOBALS['request'], 'MockRequest')) { $notify = $wikidb->get('notify'); /* Generate notification emails? */ if (!empty($notify) and is_array($notify)) { if (empty($this->pagename)) $this->pagename = $meta['pagename']; // TODO: Should be used for ModeratePage and RSS2 Cloud xml-rpc also. $this->getPageChangeEmails($notify); if (!empty($this->emails)) { $this->sendPageChangeNotification($wikitext, $version, $meta); } } } } function onDeletePage(&$wikidb, $pagename) { $result = true; /* Generate notification emails? */ if (!$wikidb->isWikiPage($pagename) and !isa($GLOBALS['request'], 'MockRequest')) { $notify = $wikidb->get('notify'); if (!empty($notify) and is_array($notify)) { //TODO: deferr it (quite a massive load if you remove some pages). $this->getPageChangeEmails($notify); if (!empty($this->emails)) { $subject = sprintf(_("User %s removed page %s"), $this->fromId(), $pagename); $result = $this->sendMail($subject, $subject . "\n\n"); } } } return $result; } function onRenamePage(&$wikidb, $oldpage, $new_pagename) { if (!isa($GLOBALS['request'], 'MockRequest')) { $notify = $wikidb->get('notify'); if (!empty($notify) and is_array($notify)) { $this->getPageChangeEmails($notify); if (!empty($this->emails)) { $this->pagename = $oldpage; $this->sendPageRenameNotification($new_pagename); } } } } /** * Send mail to user and store the cookie in the db * wikiurl?action=ConfirmEmail&id=bla */ function sendEmailConfirmation($email, $userid) { global $request; $id = rand_ascii_readable(16); $wikidb = $request->getDbh(); $data = $wikidb->get('ConfirmEmail'); while (!empty($data[$id])) { // id collision $id = rand_ascii_readable(16); } $subject = _("E-mail address confirmation"); $ip = $request->get('REMOTE_HOST'); $expire_date = time() + 7 * 86400; $content = fmt("Someone, probably you from IP address %s, has registered an account \"%s\" with this e-mail address on %s. To confirm that this account really does belong to you and activate e-mail features on %s, open this link in your browser: %s If this is *not* you, don't follow the link. This confirmation code will expire at %s.", $ip, $userid, WIKI_NAME, WIKI_NAME, WikiURL(HOME_PAGE, array('action' => 'ConfirmEmail', 'id' => $id), true), CTime($expire_date)); $this->sendMail($subject, $content, "", true); $data[$id] = array('email' => $email, 'userid' => $userid, 'expire' => $expire_date); $wikidb->set('ConfirmEmail', $data); return ''; } function checkEmailConfirmation() { global $request; $wikidb = $request->getDbh(); $data = $wikidb->get('ConfirmEmail'); $id = $request->getArg('id'); if (empty($data[$id])) { // id not found return HTML(HTML::h1("Confirm E-mail address"), HTML::h1("Sorry! Wrong URL")); } // upgrade the user $userid = $data['userid']; $u = $request->getUser(); if ($u->UserName() == $userid) { // lucky: current user (session) $request->_user->_level = WIKIAUTH_USER; $request->_prefs->set('emailVerified', true); } else { // not current user if (ENABLE_USER_NEW) { $u = WikiUser($userid); $u->getPreferences(); } else { $u = new WikiUser($request, $userid); } $u->_level = WIKIAUTH_USER; $request->setUser($u); $request->_prefs->set('emailVerified', true); } unset($data[$id]); $wikidb->set('ConfirmEmail', $data); return HTML(HTML::h1("Confirm E-mail address"), HTML::p("Your e-mail address has now been confirmed.")); } function subject_encode($subject) { // We need to encode the subject if it contains non-ASCII characters // The page name may contain non-ASCII characters, as well as // the translation of the messages, e.g. _("PageChange Notification of %s"); // If all characters are ASCII, do nothing if (isAsciiString($subject)) { return $subject; } // Let us try quoted printable first if (function_exists('quoted_printable_encode')) { // PHP 5.3 // quoted_printable_encode inserts "\r\n" if line is too long, use "\n" only return "=?UTF-8?Q?" . str_replace("\r\n", "\n", quoted_printable_encode($subject)) . "?="; } // If not, encode in base64 (less human-readable) return "=?UTF-8?B?" . base64_encode($subject) . "?="; } } // Local Variables: // mode: php // tab-width: 8 // c-basic-offset: 4 // c-hanging-comment-ender-p: nil // indent-tabs-mode: nil // End: