]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/MailNotify.php
rcs_id no longer makes sense with Subversion global version number
[SourceForge/phpwiki.git] / lib / MailNotify.php
1 <?php
2 // rcs_id('$Id$');
3 /* Copyright (C) 2006-2007,2009 Reini Urban
4  * Copyright (C) 2009 Marc-Etienne Vargenau, Alcatel-Lucent
5  *
6  * This file is part of PhpWiki.
7  *
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.
12  *
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.
17  *
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
21  */
22
23 /**
24  * Handle the pagelist pref[notifyPages] logic for users
25  * and notify => hash ( page => (userid => userhash) ) for pages.
26  * Generate notification emails.
27  *
28  * We add WikiDB handlers and register ourself there:
29  *   onChangePage, onDeletePage, onRenamePage
30  * Administrative actions:
31  *   [Watch] WatchPage - add a page, or delete watch handlers into the users 
32  *                       pref[notifyPages] slot.
33  *   My WatchList      - view or edit list/regex of pref[notifyPages].
34  *   EMailConfirm methods: send and verify
35  *
36  * Helper functions:
37  *   getPageChangeEmails
38  *   MailAdmin
39  *   ? handle emailed confirmation links (EmailSignup, ModeratedPage)
40  *
41  * @package MailNotify
42  * @author  Reini Urban
43  */
44
45 if (!defined("MAILER_LOG"))
46     if (isWindows())
47         define("MAILER_LOG", 'c:/wikimail.log');
48     else
49         define("MAILER_LOG", '/var/log/wikimail.log');
50
51 class MailNotify {
52
53     function MailNotify($pagename) {
54         $this->pagename = $pagename; /* which page */
55         $this->emails  = array();    /* to which addresses */
56         $this->userids = array();    /* corresponding array of displayed names, 
57                                         don't display the email addresses */
58         /* From: from whom the mail appears to be */
59         $this->from = $this->fromId();
60     }
61
62     function fromId() {
63         global $request;
64         if (defined('GFORGE') and GFORGE) {
65             return $request->_user->getId();
66         } else {
67             return $request->_user->getId() . '@' .  $request->get('REMOTE_HOST');
68         }
69     }
70
71     function userEmail($userid, $doverify = true) {
72         global $request;
73
74         // Disable verification of emails for corporate env.
75         if (defined('GFORGE') and GFORGE) {
76             $doverify = false;
77         }
78
79         $u = $request->getUser();
80         if ($u->UserName() == $userid) { // lucky: current user
81             $prefs = $u->getPreferences();
82             $email = $prefs->get('email');
83             // do a dynamic emailVerified check update
84             if ($doverify and !$request->_prefs->get('emailVerified'))
85                 $email = '';
86         } else {  // not current user
87             if (ENABLE_USER_NEW) {
88                 $u = WikiUser($userid);
89                 $u->getPreferences();
90                 $prefs = &$u->_prefs;
91             } else {
92                 $u = new WikiUser($request, $userid);
93                 $prefs = $u->getPreferences();
94             }
95             $email = $prefs->get('email');
96             if ($doverify and !$prefs->get('emailVerified')) {
97                 $email = '';
98             }
99         }
100         return $email;
101     }
102
103     /**
104      * getPageChangeEmails($notify)
105      * @param  $notify: hash ( page => (userid => userhash) )
106      * @return array
107      *         unique array of ($emails, $userids)
108      */
109     function getPageChangeEmails($notify) {
110         global $request;
111         $emails = array(); $userids = array();
112         foreach ($notify as $page => $users) {
113             if (glob_match($page, $this->pagename)) {
114
115                 global $request;
116                 $curuser = $request->getUser();
117                 $curusername = $curuser->UserName();
118                 $curuserprefs = $curuser->getPreferences();
119                 $curuserprefsemail = $curuserprefs->get('email');
120                 $ownModifications = $curuserprefs->get('ownModifications');
121                 $majorModificationsOnly = $curuserprefs->get('majorModificationsOnly');
122
123                 foreach ($users as $userid => $user) {
124
125                     $usermail = $user['email'];
126
127                     if (($usermail == $curuserprefsemail)
128                         and ($ownModifications)) {
129                         // It's my own modification
130                         // and I do not want to receive it
131                         continue;
132                     }
133
134                     if ($majorModificationsOnly) {
135                         $backend = &$request->_dbi->_backend;
136                         $version = $backend->get_latest_version($this->pagename);
137                         $versiondata = $backend->get_versiondata($this->pagename, $version, true);
138                         if ($versiondata['is_minor_edit']) {
139                             // It's a minor modification
140                             // and I do not want to receive it
141                             continue;
142                         }
143                     }
144
145                     if (!$user) { // handle the case for ModeratePage: 
146                                   // no prefs, just userid's.
147                         $emails[] = $this->userEmail($userid, false);
148                         $userids[] = $userid;
149                     } else {
150                         if (!empty($user['verified']) and !empty($user['email'])) {
151                             $emails[]  = $user['email'];
152                             $userids[] = $userid;
153                         } elseif (!empty($user['email'])) {
154                             // do a dynamic emailVerified check update
155                             $email = $this->userEmail($userid, true);
156                             if ($email) {
157                                 $notify[$page][$userid]['verified'] = 1;
158                                 $request->_dbi->set('notify', $notify);
159                                 $emails[] = $email;
160                                 $userids[] = $userid;
161                             }
162                         }
163                         // ignore verification
164                         /*
165                         if (DEBUG) {
166                             if (!in_array($user['email'], $emails))
167                                 $emails[] = $user['email'];
168                         }
169                         */
170                     }
171                 }
172             }
173         }
174         $this->emails = array_unique($emails);
175         $this->userids = array_unique($userids);
176         return array($this->emails, $this->userids);
177     }
178     
179     function sendMail($subject,
180                       $content,
181                       $notice = false,
182                       $silent = true)
183     {
184         // Add WIKI_NAME to Subject
185         $subject = "[".WIKI_NAME."] ".$subject;
186         // Encode $subject if needed
187         $encoded_subject = $this->subject_encode($subject);
188         $emails = $this->emails;
189         $from = $this->from;
190         // Do not send if modification is from Gforge admin
191         if (defined('GFORGE') and GFORGE) {
192             if ($from == ADMIN_USER) {
193                 return;
194             }
195         }
196         if (!$notice) $notice = _("PageChange Notification of %s");
197         $headers = "From: $from\r\n" .
198                    "Bcc: ".join(',', $emails)."\r\n" .
199                    "MIME-Version: 1.0\r\n" .
200                    "Content-Type: text/plain; charset=".CHARSET."; format=flowed\r\n" .
201                    "Content-Transfer-Encoding: 8bit";
202
203         $ok = mail(($to = array_shift($emails)),
204                    $encoded_subject, 
205                    $subject."\n".$content,
206                    $headers
207                    );
208         if (MAILER_LOG and is_writable(MAILER_LOG)) {
209             $f = fopen(MAILER_LOG, "a");
210             fwrite($f, "\n\nX-MailSentOK: " . $ok ? 'OK' : 'FAILED');
211             if (!$ok) {
212                 global $ErrorManager;
213                 // get last error message
214                 $last_err = 
215                     $ErrorManager->_postponed_errors[count($ErrorHandler->_postponed_errors)-1];
216                 fwrite($f, "\nX-MailFailure: " . $last_err);
217             }
218             fwrite($f, "\nDate: " . CTime());
219             fwrite($f, "\nSubject: $encoded_subject");
220             fwrite($f, "\nFrom: $from");
221             fwrite($f, "\nTo: $to");
222             fwrite($f, "\nBcc: ".join(',', $emails));
223             fwrite($f, "\n\n". $content);
224             fclose($f);
225         }
226         if ($ok) {
227             if (!$silent)
228                 trigger_error(sprintf($notice, $this->pagename)
229                               . " "
230                               . sprintf(_("sent to %s"), join(',',$this->userids)),
231                               E_USER_NOTICE);
232             return true;
233         } else {
234             trigger_error(sprintf($notice, $this->pagename)
235                           . " "
236                           . sprintf(_("Error: Couldn't send %s to %s"), 
237                                    $subject."\n".$content, join(',',$this->userids)), 
238                           E_USER_WARNING);
239             return false;
240         }
241     }
242     
243     /**
244      * Send udiff for a changed page to multiple users.
245      * See rename and remove methods also
246      */
247     function sendPageChangeNotification(&$wikitext, $version, &$meta) {
248
249         global $request;
250
251         if (@is_array($request->_deferredPageChangeNotification)) {
252             // collapse multiple changes (loaddir) into one email
253             $request->_deferredPageChangeNotification[] = 
254                 array($this->pagename, $this->emails, $this->userids);
255             return;
256         }
257         $backend = &$request->_dbi->_backend;
258         $subject = _("Page change").' '.($this->pagename);
259         $previous = $backend->get_previous_version($this->pagename, $version);
260         if (!isset($meta['mtime'])) $meta['mtime'] = time();
261         if ($previous) {
262             $difflink = WikiURL($this->pagename, array('action'=>'diff'), true);
263             $cache = &$request->_dbi->_cache;
264             $this_content = explode("\n", $wikitext);
265             $prevdata = $cache->get_versiondata($this->pagename, $previous, true);
266             if (empty($prevdata['%content']))
267                 $prevdata = $backend->get_versiondata($this->pagename, $previous, true);
268             $other_content = explode("\n", $prevdata['%content']);
269             
270             include_once("lib/difflib.php");
271             $diff2 = new Diff($other_content, $this_content);
272             //$context_lines = max(4, count($other_content) + 1,
273             //                     count($this_content) + 1);
274             $fmt = new UnifiedDiffFormatter(/*$context_lines*/);
275             $content  = $this->pagename . " " . $previous . " " . 
276                 Iso8601DateTime($prevdata['mtime']) . "\n";
277             $content .= $this->pagename . " " . $version . " " .  
278                 Iso8601DateTime($meta['mtime']) . "\n";
279             $content .= $fmt->format($diff2);
280             
281         } else {
282             $difflink = WikiURL($this->pagename,array(),true);
283             $content = $this->pagename . " " . $version . " " .  
284                 Iso8601DateTime($meta['mtime']) . "\n";
285             $content .= _("New page");
286             $content .= "\n\n";
287             $content .= $wikitext;
288         }
289         $editedby = sprintf(_("Edited by: %s"), $this->from);
290         $summary = sprintf(_("Summary: %s"), $meta['summary']);
291         $this->sendMail($subject, 
292                         $editedby."\n".$summary."\n".$difflink."\n\n".$content);
293     }
294
295     /** 
296      * Support mass rename / remove (not yet tested)
297      */
298     function sendPageRenameNotification ($to, &$meta) {
299         global $request;
300
301         if (@is_array($request->_deferredPageRenameNotification)) {
302             $request->_deferredPageRenameNotification[] = 
303                 array($this->pagename, $to, $meta, $this->emails, $this->userids);
304         } else {
305             $pagename = $this->pagename;
306             $editedby = sprintf(_("Edited by: %s"), $this->from);
307             $subject = sprintf(_("Page rename %s to %s"), $pagename, $to);
308             $link = WikiURL($to, true);
309             $this->sendMail($subject, 
310                             $editedby."\n".$link."\n\n"."Renamed $pagename to $to");
311         }
312     }
313
314     /**
315      * The handlers:
316      */
317     function onChangePage (&$wikidb, &$wikitext, $version, &$meta) {
318         $result = true;
319         if (!isa($GLOBALS['request'],'MockRequest')) {
320             $notify = $wikidb->get('notify');
321             /* Generate notification emails? */
322             if (!empty($notify) and is_array($notify)) {
323                 if (empty($this->pagename))
324                     $this->pagename = $meta['pagename'];
325                 // TODO: Should be used for ModeratePage and RSS2 Cloud xml-rpc also.
326                 $this->getPageChangeEmails($notify);
327                 if (!empty($this->emails)) {
328                     $result = $this->sendPageChangeNotification($wikitext, $version, $meta);
329                 }
330             }
331         }
332         return $result;
333     }
334
335     function onDeletePage (&$wikidb, $pagename) {
336         $result = true;
337         /* Generate notification emails? */
338         if (! $wikidb->isWikiPage($pagename) and !isa($GLOBALS['request'],'MockRequest')) {
339             $notify = $wikidb->get('notify');
340             if (!empty($notify) and is_array($notify)) {
341                 //TODO: deferr it (quite a massive load if you remove some pages).
342                 $this->getPageChangeEmails($notify);
343                 if (!empty($this->emails)) {
344                     $subject = sprintf(_("User %s removed page %s"), $this->from, $pagename);
345                     $result = $this->sendMail($subject, $subject."\n\n");
346                 }
347             }
348         }
349         return $result;
350     }
351
352     function onRenamePage (&$wikidb, $oldpage, $new_pagename) {
353         $result = true;
354         if (!isa($GLOBALS['request'], 'MockRequest')) {
355             $notify = $wikidb->get('notify');
356             if (!empty($notify) and is_array($notify)) {
357                 $this->getPageChangeEmails($notify);
358                 if (!empty($this->emails)) {
359                     $newpage = $wikidb->getPage($new_pagename);
360                     $current = $newpage->getCurrentRevision();
361                     $meta = $current->_data;
362                     $this->pagename = $oldpage;
363                     $result = $this->sendPageRenameNotification($new_pagename, $meta);
364                 }
365             }
366         }
367     }
368
369     /**
370      * Send mail to user and store the cookie in the db
371      * wikiurl?action=ConfirmEmail&id=bla
372      */
373     function sendEmailConfirmation ($email, $userid) {
374         $id = rand_ascii_readable(16);
375         $wikidb = $GLOBALS['request']->getDbh();
376         $data = $wikidb->get('ConfirmEmail');
377         while(!empty($data[$id])) { // id collision
378             $id = rand_ascii_readable(16);
379         }
380         $subject = _("E-Mail address confirmation");
381         $ip = $request->get('REMOTE_HOST');
382         $expire_date = time() + 7*86400;
383         $content = fmt("Someone, probably you from IP address %s, has registered an
384 account \"%s\" with this e-mail address on %s.
385
386 To confirm that this account really does belong to you and activate
387 e-mail features on %s, open this link in your browser:
388
389 %s
390
391 If this is *not* you, don't follow the link. This confirmation code
392 will expire at %s.", 
393                        $ip, $userid, WIKI_NAME, WIKI_NAME, 
394                        WikiURL(HOME_PAGE, array('action' => 'ConfirmEmail',
395                                                 'id' => $id), 
396                                true),
397                        CTime($expire_date));
398         $this->sendMail($subject, $content, "", true);
399         $data[$id] = array('email' => $email,
400                            'userid' => $userid,
401                            'expire' => $expire_date);
402         $wikidb->set('ConfirmEmail', $data);
403         return '';
404     }
405
406     function checkEmailConfirmation () {
407         global $request;
408         $wikidb = $request->getDbh();
409         $data = $wikidb->get('ConfirmEmail');
410         $id = $request->getArg('id');
411         if (empty($data[$id])) { // id not found
412             return HTML(HTML::h1("Confirm E-mail address"),
413                         HTML::h1("Sorry! Wrong URL"));
414         }
415         // upgrade the user
416         $userid = $data['userid'];
417         $email = $data['email'];
418         $u = $request->getUser();
419         if ($u->UserName() == $userid) { // lucky: current user (session)
420             $prefs = $u->getPreferences();
421             $request->_user->_level = WIKIAUTH_USER;
422             $request->_prefs->set('emailVerified', true);
423         } else {  // not current user
424             if (ENABLE_USER_NEW) {
425                 $u = WikiUser($userid);
426                 $u->getPreferences();
427                 $prefs = &$u->_prefs;
428             } else {
429                 $u = new WikiUser($request, $userid);
430                 $prefs = $u->getPreferences();
431             }
432             $u->_level = WIKIAUTH_USER;
433             $request->setUser($u);
434             $request->_prefs->set('emailVerified', true);
435         }
436         unset($data[$id]);
437         $wikidb->set('ConfirmEmail', $data);
438         return HTML(HTML::h1("Confirm E-mail address"),
439                     HTML::p("Your e-mail address has now been confirmed."));
440     }
441
442     function subject_encode ($subject) {
443         // We need to encode the subject if it contains non-ASCII characters
444         // The page name may contain non-ASCII characters, as well as
445         // the translation of the messages, e.g. _("PageChange Notification of %s");
446
447         // If all characters are ASCII, do nothing
448         if (isAsciiString($subject)) {
449             return $subject;
450         }
451
452         // Let us try quoted printable first
453         if (function_exists('quoted_printable_encode')) { // PHP 5.3
454             return "=?UTF-8?Q?".quoted_printable_encode($subject)."?=";
455         }
456
457         // If not, encode in base64 (less human-readable)
458         return "=?UTF-8?B?".base64_encode($subject)."?=";
459     }
460 }
461
462 // Local Variables:
463 // mode: php
464 // tab-width: 8
465 // c-basic-offset: 4
466 // c-hanging-comment-ender-p: nil
467 // indent-tabs-mode: nil
468 // End:   
469 ?>