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