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 * @author Adrian Lang <lang@cosmocode.de>
25 * @param string $id The target page or namespace, specified by id; Namespaces
26 * are identified by appending a colon.
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;
37 return metaFN((string) $meta_froot, $meta_fname);
41 * Lock subscription info for an ID
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.
48 function subscription_lock_filename ($id){
50 return $conf['lockdir'].'/_subscr_' . md5($id) . '.lock';
54 * Creates a lock file for writing subscription data
56 * @todo add lock time parameter to io_lock() and use this instead
60 function subscription_lock($id) {
62 $lock = subscription_lock_filename($id);
64 if (is_dir($lock) && time()-@filemtime($lock) > 60*5) {
65 // looks like a stale lock - remove it
69 // try creating the lock directory
70 if (!@mkdir($lock,$conf['dmode'])) {
74 if($conf['dperm']) chmod($lock, $conf['dperm']);
79 * Unlock subscription info for an ID
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.
86 function subscription_unlock($id) {
87 $lockf = subscription_lock_filename($id);
88 return @rmdir($lockf);
92 * Set subscription information
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.
103 * @author Adrian Lang <lang@cosmocode.de>
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
114 function subscription_set($user, $page, $style, $data = null,
115 $overwrite = false) {
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);
126 // io_deleteFromFile does not return false if no line matched.
127 return io_deleteFromFile($file,
128 subscription_regex(array('user' => auth_nameencode($user))),
132 // Delete subscription if one exists and $overwrite is true. If $overwrite
134 $subs = subscription_find($page, array('user' => $user));
135 if (count($subs) > 0 && isset($subs[$page])) {
137 msg(sprintf($lang['subscr_already_subscribed'], $user,
138 prettyprint_id($page)), -1);
141 // Fail if deletion failed, else continue.
142 if (!subscription_set($user, $page, null)) {
147 $file = subscription_filename($page);
148 $content = auth_nameencode($user) . ' ' . $style;
149 if (!is_null($data)) {
150 $content .= ' ' . $data;
152 return io_saveFile($file, $content . "\n", true);
156 * Recursively search for matching subscriptions
158 * This function searches all relevant subscription files for a page or
161 * @author Adrian Lang <lang@cosmocode.de>
162 * @see function subscription_regex for $pre documentation
164 * @param string $page The target object’s (namespace or page) id
165 * @param array $pre A hash of predefined values
168 function subscription_find($page, $pre) {
169 // Construct list of files which may contain relevant subscriptions.
170 $filenames = array(':' => subscription_filename(':'));
172 $filenames[$page] = subscription_filename($page);
173 $page = getNS(rtrim($page, ':')) . ':';
174 } while ($page !== ':');
178 foreach ($filenames as $cur_page => $filename) {
179 if (!@file_exists($filename)) {
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";
189 list($user, $rest) = explode(' ', $subscription, 2);
190 $subscription = rawurldecode($user) . " " . $rest;
192 if (preg_match(subscription_regex($pre), $subscription,
193 $line_matches) === 0) {
196 $match = array_slice($line_matches, 1);
197 if (!isset($matches[$cur_page])) {
198 $matches[$cur_page] = array();
200 $matches[$cur_page][] = $match;
203 return array_reverse($matches);
207 * Get data for $INFO['subscribed']
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”.
213 * @author Adrian Lang <lang@cosmocode.de>
215 function get_info_subscribed() {
218 if (!$conf['subscribers']) {
222 $subs = subscription_find($ID, array('user' => $_SERVER['REMOTE_USER']));
223 if (count($subs) === 0) {
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];
241 * Construct a regular expression parsing a subscription definition line
243 * @author Adrian Lang <lang@cosmocode.de>
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.
251 * @return string complete regexp including delimiters
253 function subscription_regex($pre = array()) {
254 if (!isset($pre['escaped']) || $pre['escaped'] === false) {
255 $pre = array_map('preg_quote_cb', $pre);
257 foreach (array('user', 'style', 'data') as $key) {
258 if (!isset($pre[$key])) {
259 $pre[$key] = '(\S+)';
262 return '/^' . $pre['user'] . '(?: ' . $pre['style'] .
263 '(?: ' . $pre['data'] . ')?)?$/';
267 * Return a string with the email addresses of all the
268 * users subscribed to a page
270 * This is the default action for COMMON_NOTIFY_ADDRESSLIST.
272 * @author Steven Danz <steven-danz@kc.rr.com>
273 * @author Adrian Lang <lang@cosmocode.de>
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
281 function subscription_addresslist(&$data){
283 /** @var auth_basic $auth */
287 $self = $data['self'];
288 $addresslist = $data['addresslist'];
290 if (!$conf['subscribers'] || $auth === null) {
293 $pres = array('style' => 'every', 'escaped' => true);
294 if (!$self && isset($_SERVER['REMOTE_USER'])) {
295 $pres['user'] = '((?!' . preg_quote_cb($_SERVER['REMOTE_USER']) .
298 $subs = subscription_find($id, $pres);
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'];
312 $data['addresslist'] = trim($addresslist . ',' . implode(',', $emails), ',');
318 * Sends a digest mail showing a bunch of changes.
320 * @author Adrian Lang <lang@cosmocode.de>
322 * @param string $subscriber_mail The target mail address
323 * @param array $id The ID
324 * @param int $lastupdate Time of the last notification
326 function subscription_send_digest($subscriber_mail, $id, $lastupdate) {
329 $rev = getRevisions($id, $n++, 1);
330 $rev = (count($rev) > 0) ? $rev[0] : null;
331 } while (!is_null($rev) && $rev > $lastupdate);
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);
343 $subject = 'newpage';
344 $replaces['OLDPAGE'] = 'none';
345 $replaces['DIFF'] = rawWiki($id);
347 subscription_send($subscriber_mail, $replaces, $subject, $id,
354 * Sends a list mail showing a list of changed pages.
356 * @author Adrian Lang <lang@cosmocode.de>
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
362 function subscription_send_list($subscriber_mail, $ids, $ns_id) {
363 if (count($ids) === 0) return;
366 foreach ($ids as $id) {
367 $list .= '* ' . wl($id, array(), true) . NL;
369 subscription_send($subscriber_mail,
370 array('DIFF' => rtrim($list),
371 'SUBSCRIBE' => wl($ns_id . $conf['start'],
372 array('do' => 'subscribe'),
375 prettyprint_id($ns_id),
380 * Helper function for sending a mail
382 * @author Adrian Lang <lang@cosmocode.de>
384 * @param string $subscriber_mail The target mail address
385 * @param array $replaces Predefined parameters used to parse the
387 * @param string $subject The lang id of the mail subject (without the
389 * @param string $id The page or namespace id
390 * @param string $template The name of the mail template
393 function subscription_send($subscriber_mail, $replaces, $subject, $id, $template) {
397 $text = rawLocale($template);
398 $trep = array_merge($replaces, array('PAGE' => $id));
400 $hrep['DIFF'] = nl2br(htmlspecialchars($hrep['DIFF']));
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']);
410 '<'.wl($id,array('do'=>'subscribe'),true,'&').'>',
413 return $mail->send();