Mereged updates from DokuWiki 38
[sudaraka-org:dokuwiki-mods.git] / inc / subscription.php
1 <?php
2 /**
3  * Utilities for handling (email) subscriptions
4  *
5  * The public interface of this file consists of the functions
6  * - subscription_find
7  * - subscription_send_digest
8  * - subscription_send_list
9  * - subscription_set
10  * - get_info_subscribed
11  * - subscription_addresslist
12  * - subscription_lock
13  * - subscription_unlock
14  *
15  * @author  Adrian Lang <lang@cosmocode.de>
16  * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
17  */
18
19 /**
20  * Get the name of the metafile tracking subscriptions to target page or
21  * namespace
22  *
23  * @author Adrian Lang <lang@cosmocode.de>
24  *
25  * @param string $id The target page or namespace, specified by id; Namespaces
26  *                   are identified by appending a colon.
27  * @return string
28  */
29 function subscription_filename($id) {
30     $meta_fname = '.mlist';
31     if ((substr($id, -1, 1) === ':')) {
32         $meta_froot = getNS($id);
33         $meta_fname = '/' . $meta_fname;
34     } else {
35         $meta_froot = $id;
36     }
37     return metaFN((string) $meta_froot, $meta_fname);
38 }
39
40 /**
41  * Lock subscription info for an ID
42  *
43  * @author Adrian Lang <lang@cosmocode.de>
44  * @param string $id The target page or namespace, specified by id; Namespaces
45  *                   are identified by appending a colon.
46  * @return string
47  */
48 function subscription_lock_filename ($id){
49     global $conf;
50     return $conf['lockdir'].'/_subscr_' . md5($id) . '.lock';
51 }
52
53 /**
54  * Creates a lock file for writing subscription data
55  *
56  * @todo add lock time parameter to io_lock() and use this instead
57  * @param $id
58  * @return bool
59  */
60 function subscription_lock($id) {
61     global $conf;
62     $lock = subscription_lock_filename($id);
63
64     if (is_dir($lock) && time()-@filemtime($lock) > 60*5) {
65         // looks like a stale lock - remove it
66         @rmdir($lock);
67     }
68
69     // try creating the lock directory
70     if (!@mkdir($lock,$conf['dmode'])) {
71         return false;
72     }
73
74     if($conf['dperm']) chmod($lock, $conf['dperm']);
75     return true;
76 }
77
78 /**
79  * Unlock subscription info for an ID
80  *
81  * @author Adrian Lang <lang@cosmocode.de>
82  * @param string $id The target page or namespace, specified by id; Namespaces
83  *                   are identified by appending a colon.
84  * @return bool
85  */
86 function subscription_unlock($id) {
87     $lockf = subscription_lock_filename($id);
88     return @rmdir($lockf);
89 }
90
91 /**
92  * Set subscription information
93  *
94  * Allows to set subscription information for permanent storage in meta files.
95  * Subscriptions consist of a target object, a subscribing user, a subscribe
96  * style and optional data.
97  * A subscription may be deleted by specifying an empty subscribe style.
98  * Only one subscription per target and user is allowed.
99  * The function returns false on error, otherwise true. Note that no error is
100  * returned if a subscription should be deleted but the user is not subscribed
101  * and the subscription meta file exists.
102  *
103  * @author Adrian Lang <lang@cosmocode.de>
104  *
105  * @param string $user      The subscriber or unsubscriber
106  * @param string $page      The target object (page or namespace), specified by
107  *                          id; Namespaces are identified by a trailing colon.
108  * @param string $style     The subscribe style; DokuWiki currently implements
109  *                          “every”, “digest”, and “list”.
110  * @param string $data      An optional data blob
111  * @param bool   $overwrite Whether an existing subscription may be overwritten
112  * @return bool
113  */
114 function subscription_set($user, $page, $style, $data = null,
115                           $overwrite = false) {
116     global $lang;
117     if (is_null($style)) {
118         // Delete subscription.
119         $file = subscription_filename($page);
120         if (!@file_exists($file)) {
121             msg(sprintf($lang['subscr_not_subscribed'], $user,
122                         prettyprint_id($page)), -1);
123             return false;
124         }
125
126         // io_deleteFromFile does not return false if no line matched.
127         return io_deleteFromFile($file,
128                                  subscription_regex(array('user' => auth_nameencode($user))),
129                                  true);
130     }
131
132     // Delete subscription if one exists and $overwrite is true. If $overwrite
133     // is false, fail.
134     $subs = subscription_find($page, array('user' => $user));
135     if (count($subs) > 0 && isset($subs[$page])) {
136         if (!$overwrite) {
137             msg(sprintf($lang['subscr_already_subscribed'], $user,
138                         prettyprint_id($page)), -1);
139             return false;
140         }
141         // Fail if deletion failed, else continue.
142         if (!subscription_set($user, $page, null)) {
143             return false;
144         }
145     }
146
147     $file = subscription_filename($page);
148     $content = auth_nameencode($user) . ' ' . $style;
149     if (!is_null($data)) {
150         $content .= ' ' . $data;
151     }
152     return io_saveFile($file, $content . "\n", true);
153 }
154
155 /**
156  * Recursively search for matching subscriptions
157  *
158  * This function searches all relevant subscription files for a page or
159  * namespace.
160  *
161  * @author Adrian Lang <lang@cosmocode.de>
162  * @see function subscription_regex for $pre documentation
163  *
164  * @param string $page The target object’s (namespace or page) id
165  * @param array  $pre  A hash of predefined values
166  * @return array
167  */
168 function subscription_find($page, $pre) {
169     // Construct list of files which may contain relevant subscriptions.
170     $filenames = array(':' => subscription_filename(':'));
171     do {
172         $filenames[$page] = subscription_filename($page);
173         $page = getNS(rtrim($page, ':')) . ':';
174     } while ($page !== ':');
175
176     // Handle files.
177     $matches = array();
178     foreach ($filenames as $cur_page => $filename) {
179         if (!@file_exists($filename)) {
180             continue;
181         }
182         $subscriptions = file($filename);
183         foreach ($subscriptions as $subscription) {
184             if (strpos($subscription, ' ') === false) {
185                 // This is an old subscription file.
186                 $subscription = trim($subscription) . " every\n";
187             }
188
189             list($user, $rest) = explode(' ', $subscription, 2);
190             $subscription = rawurldecode($user) . " " . $rest;
191
192             if (preg_match(subscription_regex($pre), $subscription,
193                            $line_matches) === 0) {
194                 continue;
195             }
196             $match = array_slice($line_matches, 1);
197             if (!isset($matches[$cur_page])) {
198                 $matches[$cur_page] = array();
199             }
200             $matches[$cur_page][] = $match;
201         }
202     }
203     return array_reverse($matches);
204 }
205
206 /**
207  * Get data for $INFO['subscribed']
208  *
209  * $INFO['subscribed'] is either false if no subscription for the current page
210  * and user is in effect. Else it contains an array of arrays with the fields
211  * “target”, “style”, and optionally “data”.
212  *
213  * @author Adrian Lang <lang@cosmocode.de>
214  */
215 function get_info_subscribed() {
216     global $ID;
217     global $conf;
218     if (!$conf['subscribers']) {
219         return false;
220     }
221
222     $subs = subscription_find($ID, array('user' => $_SERVER['REMOTE_USER']));
223     if (count($subs) === 0) {
224         return false;
225     }
226
227     $_ret = array();
228     foreach ($subs as $target => $subs_data) {
229         $new = array('target' => $target,
230                      'style'  => $subs_data[0][0]);
231         if (count($subs_data[0]) > 1) {
232             $new['data'] = $subs_data[0][1];
233         }
234         $_ret[] = $new;
235     }
236
237     return $_ret;
238 }
239
240 /**
241  * Construct a regular expression parsing a subscription definition line
242  *
243  * @author Adrian Lang <lang@cosmocode.de>
244  *
245  * @param array $pre A hash of predefined values; “user”, “style”, and
246  *                   “data” may be set to limit the results to
247  *                   subscriptions matching these parameters. If
248  *                   “escaped” is true, these fields are inserted into the
249  *                   regular expression without escaping.
250  *
251  * @return string complete regexp including delimiters
252  */
253 function subscription_regex($pre = array()) {
254     if (!isset($pre['escaped']) || $pre['escaped'] === false) {
255         $pre = array_map('preg_quote_cb', $pre);
256     }
257     foreach (array('user', 'style', 'data') as $key) {
258         if (!isset($pre[$key])) {
259             $pre[$key] = '(\S+)';
260         }
261     }
262     return '/^' . $pre['user'] . '(?: ' . $pre['style'] .
263            '(?: ' . $pre['data'] . ')?)?$/';
264 }
265
266 /**
267  * Return a string with the email addresses of all the
268  * users subscribed to a page
269  *
270  * This is the default action for COMMON_NOTIFY_ADDRESSLIST.
271  *
272  * @author Steven Danz <steven-danz@kc.rr.com>
273  * @author Adrian Lang <lang@cosmocode.de>
274  *
275  * @todo this does NOT return a string but uses a reference to write back, either fix function or docs
276  * @param array $data Containing $id (the page id), $self (whether the author
277  *                    should be notified, $addresslist (current email address
278  *                    list)
279  * @return string
280  */
281 function subscription_addresslist(&$data){
282     global $conf;
283     /** @var auth_basic $auth */
284     global $auth;
285
286     $id = $data['id'];
287     $self = $data['self'];
288     $addresslist = $data['addresslist'];
289
290     if (!$conf['subscribers'] || $auth === null) {
291         return '';
292     }
293     $pres = array('style' => 'every', 'escaped' => true);
294     if (!$self && isset($_SERVER['REMOTE_USER'])) {
295         $pres['user'] = '((?!' . preg_quote_cb($_SERVER['REMOTE_USER']) .
296                         '(?: |$))\S+)';
297     }
298     $subs = subscription_find($id, $pres);
299     $emails = array();
300     foreach ($subs as $by_targets) {
301         foreach ($by_targets as $sub) {
302             $info = $auth->getUserData($sub[0]);
303             if ($info === false) continue;
304             $level = auth_aclcheck($id, $sub[0], $info['grps']);
305             if ($level >= AUTH_READ) {
306                 if (strcasecmp($info['mail'], $conf['notify']) != 0) {
307                     $emails[$sub[0]] =  $info['mail'];
308                 }
309             }
310         }
311     }
312     $data['addresslist'] = trim($addresslist . ',' . implode(',', $emails), ',');
313 }
314
315 /**
316  * Send a digest mail
317  *
318  * Sends a digest mail showing a bunch of changes.
319  *
320  * @author Adrian Lang <lang@cosmocode.de>
321  *
322  * @param string $subscriber_mail The target mail address
323  * @param array  $id              The ID
324  * @param int    $lastupdate      Time of the last notification
325  */
326 function subscription_send_digest($subscriber_mail, $id, $lastupdate) {
327     $n = 0;
328     do {
329         $rev = getRevisions($id, $n++, 1);
330         $rev = (count($rev) > 0) ? $rev[0] : null;
331     } while (!is_null($rev) && $rev > $lastupdate);
332
333     $replaces = array('NEWPAGE'   => wl($id, '', true, '&'),
334                       'SUBSCRIBE' => wl($id, array('do' => 'subscribe'), true, '&'));
335     if (!is_null($rev)) {
336         $subject = 'changed';
337         $replaces['OLDPAGE'] = wl($id, "rev=$rev", true, '&');
338         $df = new Diff(explode("\n", rawWiki($id, $rev)),
339                         explode("\n", rawWiki($id)));
340         $dformat = new UnifiedDiffFormatter();
341         $replaces['DIFF'] = $dformat->format($df);
342     } else {
343         $subject = 'newpage';
344         $replaces['OLDPAGE'] = 'none';
345         $replaces['DIFF'] = rawWiki($id);
346     }
347     subscription_send($subscriber_mail, $replaces, $subject, $id,
348                       'subscr_digest');
349 }
350
351 /**
352  * Send a list mail
353  *
354  * Sends a list mail showing a list of changed pages.
355  *
356  * @author Adrian Lang <lang@cosmocode.de>
357  *
358  * @param string $subscriber_mail The target mail address
359  * @param array  $ids             Array of ids
360  * @param string $ns_id           The id of the namespace
361  */
362 function subscription_send_list($subscriber_mail, $ids, $ns_id) {
363     if (count($ids) === 0) return;
364     global $conf;
365     $list = '';
366     foreach ($ids as $id) {
367         $list .= '* ' . wl($id, array(), true) . NL;
368     }
369     subscription_send($subscriber_mail,
370                       array('DIFF'      => rtrim($list),
371                             'SUBSCRIBE' => wl($ns_id . $conf['start'],
372                                               array('do' => 'subscribe'),
373                                               true, '&')),
374                       'subscribe_list',
375                       prettyprint_id($ns_id),
376                       'subscr_list');
377 }
378
379 /**
380  * Helper function for sending a mail
381  *
382  * @author Adrian Lang <lang@cosmocode.de>
383  *
384  * @param string $subscriber_mail The target mail address
385  * @param array  $replaces        Predefined parameters used to parse the
386  *                                template
387  * @param string $subject         The lang id of the mail subject (without the
388  *                                prefix “mail_”)
389  * @param string $id              The page or namespace id
390  * @param string $template        The name of the mail template
391  * @return bool
392  */
393 function subscription_send($subscriber_mail, $replaces, $subject, $id, $template) {
394     global $lang;
395     global $conf;
396
397     $text = rawLocale($template);
398     $trep = array_merge($replaces, array('PAGE' => $id));
399     $hrep = $trep;
400     $hrep['DIFF'] = nl2br(htmlspecialchars($hrep['DIFF']));
401
402     $subject = $lang['mail_' . $subject] . ' ' . $id;
403     $mail = new Mailer();
404     $mail->bcc($subscriber_mail);
405     $mail->subject($subject);
406     $mail->setBody($text,$trep,$hrep);
407     $mail->from($conf['mailfromnobody']);
408     $mail->setHeader(
409         'List-Unsubscribe',
410         '<'.wl($id,array('do'=>'subscribe'),true,'&').'>',
411         false
412     );
413     return $mail->send();
414 }