3 * Utilities for handling (email) subscriptions
5 * The public interface of this file consists of the functions
7 * - subscription_send_digest
8 * - subscription_send_list
10 * - get_info_subscribed
11 * - subscription_addresslist
13 * - subscription_unlock
15 * @author Adrian Lang <lang@cosmocode.de>
16 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
20 * Get the name of the metafile tracking subscriptions to target page or
23 * @param string $id The target page or namespace, specified by id; Namespaces
24 * are identified by appending a colon.
26 * @author Adrian Lang <lang@cosmocode.de>
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;
36 return metaFN((string) $meta_froot, $meta_fname);
40 * Lock subscription info for an ID
42 * @param string $id The target page or namespace, specified by id; Namespaces
43 * are identified by appending a colon.
45 * @author Adrian Lang <lang@cosmocode.de>
47 function subscription_lock_filename ($id){
49 return $conf['lockdir'].'/_subscr_' . md5($id) . '.lock';
52 function subscription_lock($id) {
54 $lock = subscription_lock_filename($id);
56 if (is_dir($lock) && time()-@filemtime($lock) > 60*5) {
57 // looks like a stale lock - remove it
61 // try creating the lock directory
62 if (!@mkdir($lock,$conf['dmode'])) {
66 if($conf['dperm']) chmod($lock, $conf['dperm']);
71 * Unlock subscription info for an ID
73 * @param string $id The target page or namespace, specified by id; Namespaces
74 * are identified by appending a colon.
76 * @author Adrian Lang <lang@cosmocode.de>
78 function subscription_unlock($id) {
79 $lockf = subscription_lock_filename($id);
80 return @rmdir($lockf);
84 * Set subscription information
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.
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
103 * @author Adrian Lang <lang@cosmocode.de>
105 function subscription_set($user, $page, $style, $data = null,
106 $overwrite = false) {
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);
117 // io_deleteFromFile does not return false if no line matched.
118 return io_deleteFromFile($file,
119 subscription_regex(array('user' => auth_nameencode($user))),
123 // Delete subscription if one exists and $overwrite is true. If $overwrite
125 $subs = subscription_find($page, array('user' => $user));
126 if (count($subs) > 0 && array_pop(array_keys($subs)) === $page) {
128 msg(sprintf($lang['subscr_already_subscribed'], $user,
129 prettyprint_id($page)), -1);
132 // Fail if deletion failed, else continue.
133 if (!subscription_set($user, $page, null)) {
138 $file = subscription_filename($page);
139 $content = auth_nameencode($user) . ' ' . $style;
140 if (!is_null($data)) {
141 $content .= ' ' . $data;
143 return io_saveFile($file, $content . "\n", true);
147 * Recursively search for matching subscriptions
149 * This function searches all relevant subscription files for a page or
152 * @param string $page The target object’s (namespace or page) id
153 * @param array $pre A hash of predefined values
155 * @see function subscription_regex for $pre documentation
157 * @author Adrian Lang <lang@cosmocode.de>
159 function subscription_find($page, $pre) {
160 // Construct list of files which may contain relevant subscriptions.
161 $filenames = array(':' => subscription_filename(':'));
163 $filenames[$page] = subscription_filename($page);
164 $page = getNS(rtrim($page, ':')) . ':';
165 } while ($page !== ':');
169 foreach ($filenames as $cur_page => $filename) {
170 if (!@file_exists($filename)) {
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";
180 list($user, $rest) = explode(' ', $subscription, 2);
181 $subscription = rawurldecode($user) . " " . $rest;
183 if (preg_match(subscription_regex($pre), $subscription,
184 $line_matches) === 0) {
187 $match = array_slice($line_matches, 1);
188 if (!isset($matches[$cur_page])) {
189 $matches[$cur_page] = array();
191 $matches[$cur_page][] = $match;
194 return array_reverse($matches);
198 * Get data for $INFO['subscribed']
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”.
204 * @author Adrian Lang <lang@cosmocode.de>
206 function get_info_subscribed() {
209 if (!$conf['subscribers']) {
213 $subs = subscription_find($ID, array('user' => $_SERVER['REMOTE_USER']));
214 if (count($subs) === 0) {
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];
232 * Construct a regular expression parsing a subscription definition line
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.
240 * @author Adrian Lang <lang@cosmocode.de>
242 function subscription_regex($pre = array()) {
243 if (!isset($pre['escaped']) || $pre['escaped'] === false) {
244 $pre = array_map('preg_quote_cb', $pre);
246 foreach (array('user', 'style', 'data') as $key) {
247 if (!isset($pre[$key])) {
248 $pre[$key] = '(\S+)';
251 return '/^' . $pre['user'] . '(?: ' . $pre['style'] .
252 '(?: ' . $pre['data'] . ')?)?$/';
256 * Return a string with the email addresses of all the
257 * users subscribed to a page
259 * This is the default action for COMMON_NOTIFY_ADDRESSLIST.
261 * @param array $data Containing $id (the page id), $self (whether the author
262 * should be notified, $addresslist (current email address
265 * @author Steven Danz <steven-danz@kc.rr.com>
266 * @author Adrian Lang <lang@cosmocode.de>
268 function subscription_addresslist(&$data){
273 $self = $data['self'];
274 $addresslist = $data['addresslist'];
276 if (!$conf['subscribers'] || $auth === null) {
279 $pres = array('style' => 'every', 'escaped' => true);
280 if (!$self && isset($_SERVER['REMOTE_USER'])) {
281 $pres['user'] = '((?!' . preg_quote_cb($_SERVER['REMOTE_USER']) .
284 $subs = subscription_find($id, $pres);
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'];
298 $data['addresslist'] = trim($addresslist . ',' . implode(',', $emails), ',');
304 * Sends a digest mail showing a bunch of changes.
306 * @param string $subscriber_mail The target mail address
307 * @param array $id The ID
308 * @param int $lastupdate Time of the last notification
310 * @author Adrian Lang <lang@cosmocode.de>
312 function subscription_send_digest($subscriber_mail, $id, $lastupdate) {
315 $rev = getRevisions($id, $n++, 1);
316 $rev = (count($rev) > 0) ? $rev[0] : null;
317 } while (!is_null($rev) && $rev > $lastupdate);
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);
329 $subject = 'newpage';
330 $replaces['OLDPAGE'] = 'none';
331 $replaces['DIFF'] = rawWiki($id);
333 subscription_send($subscriber_mail, $replaces, $subject, $id,
340 * Sends a list mail showing a list of changed pages.
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
346 * @author Adrian Lang <lang@cosmocode.de>
348 function subscription_send_list($subscriber_mail, $ids, $ns_id) {
349 if (count($ids) === 0) return;
352 foreach ($ids as $id) {
353 $list .= '* ' . wl($id, array(), true) . NL;
355 subscription_send($subscriber_mail,
356 array('DIFF' => rtrim($list),
357 'SUBSCRIBE' => wl($ns_id . $conf['start'],
358 array('do' => 'subscribe'),
361 prettyprint_id($ns_id),
366 * Helper function for sending a mail
368 * @param string $subscriber_mail The target mail address
369 * @param array $replaces Predefined parameters used to parse the
371 * @param string $subject The lang id of the mail subject (without the
373 * @param string $id The page or namespace id
374 * @param string $template The name of the mail template
376 * @author Adrian Lang <lang@cosmocode.de>
378 function subscription_send($subscriber_mail, $replaces, $subject, $id, $template) {
381 $text = rawLocale($template);
382 $replaces = array_merge($replaces, array('TITLE' => $conf['title'],
383 'DOKUWIKIURL' => DOKU_URL,
386 foreach ($replaces as $key => $substitution) {
387 $text = str_replace('@'.strtoupper($key).'@', $substitution, $text);
391 $subject = $lang['mail_' . $subject] . ' ' . $id;
392 mail_send('', '['.$conf['title'].'] '. $subject, $text,
393 $conf['mailfrom'], '', $subscriber_mail);