diff options
Diffstat (limited to 'platform/www/inc/Subscriptions')
7 files changed, 882 insertions, 0 deletions
diff --git a/platform/www/inc/Subscriptions/BulkSubscriptionSender.php b/platform/www/inc/Subscriptions/BulkSubscriptionSender.php new file mode 100644 index 0000000..672ef90 --- /dev/null +++ b/platform/www/inc/Subscriptions/BulkSubscriptionSender.php @@ -0,0 +1,261 @@ +<?php + + +namespace dokuwiki\Subscriptions; + + +use dokuwiki\ChangeLog\PageChangeLog; +use dokuwiki\Input\Input; +use DokuWiki_Auth_Plugin; + +class BulkSubscriptionSender extends SubscriptionSender +{ + + /** + * Send digest and list subscriptions + * + * This sends mails to all subscribers that have a subscription for namespaces above + * the given page if the needed $conf['subscribe_time'] has passed already. + * + * This function is called form lib/exe/indexer.php + * + * @param string $page + * + * @return int number of sent mails + */ + public function sendBulk($page) + { + $subscriberManager = new SubscriberManager(); + if (!$subscriberManager->isenabled()) { + return 0; + } + + /** @var DokuWiki_Auth_Plugin $auth */ + global $auth; + global $conf; + global $USERINFO; + /** @var Input $INPUT */ + global $INPUT; + $count = 0; + + $subscriptions = $subscriberManager->subscribers($page, null, ['digest', 'list']); + + // remember current user info + $olduinfo = $USERINFO; + $olduser = $INPUT->server->str('REMOTE_USER'); + + foreach ($subscriptions as $target => $users) { + if (!$this->lock($target)) { + continue; + } + + foreach ($users as $user => $info) { + list($style, $lastupdate) = $info; + + $lastupdate = (int)$lastupdate; + if ($lastupdate + $conf['subscribe_time'] > time()) { + // Less than the configured time period passed since last + // update. + continue; + } + + // Work as the user to make sure ACLs apply correctly + $USERINFO = $auth->getUserData($user); + $INPUT->server->set('REMOTE_USER', $user); + if ($USERINFO === false) { + continue; + } + if (!$USERINFO['mail']) { + continue; + } + + if (substr($target, -1, 1) === ':') { + // subscription target is a namespace, get all changes within + $changes = getRecentsSince($lastupdate, null, getNS($target)); + } else { + // single page subscription, check ACL ourselves + if (auth_quickaclcheck($target) < AUTH_READ) { + continue; + } + $meta = p_get_metadata($target); + $changes = [$meta['last_change']]; + } + + // Filter out pages only changed in small and own edits + $change_ids = []; + foreach ($changes as $rev) { + $n = 0; + while (!is_null($rev) && $rev['date'] >= $lastupdate && + ($INPUT->server->str('REMOTE_USER') === $rev['user'] || + $rev['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT)) { + $pagelog = new PageChangeLog($rev['id']); + $rev = $pagelog->getRevisions($n++, 1); + $rev = (count($rev) > 0) ? $rev[0] : null; + } + + if (!is_null($rev) && $rev['date'] >= $lastupdate) { + // Some change was not a minor one and not by myself + $change_ids[] = $rev['id']; + } + } + + // send it + if ($style === 'digest') { + foreach ($change_ids as $change_id) { + $this->sendDigest( + $USERINFO['mail'], + $change_id, + $lastupdate + ); + $count++; + } + } else { + if ($style === 'list') { + $this->sendList($USERINFO['mail'], $change_ids, $target); + $count++; + } + } + // TODO: Handle duplicate subscriptions. + + // Update notification time. + $subscriberManager->add($target, $user, $style, time()); + } + $this->unlock($target); + } + + // restore current user info + $USERINFO = $olduinfo; + $INPUT->server->set('REMOTE_USER', $olduser); + return $count; + } + + /** + * Lock subscription info + * + * We don't use io_lock() her because we do not wait for the lock and use a larger stale time + * + * @param string $id The target page or namespace, specified by id; Namespaces + * are identified by appending a colon. + * + * @return bool true, if you got a succesful lock + * @author Adrian Lang <lang@cosmocode.de> + */ + protected function lock($id) + { + global $conf; + + $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock'; + + if (is_dir($lock) && time() - @filemtime($lock) > 60 * 5) { + // looks like a stale lock - remove it + @rmdir($lock); + } + + // try creating the lock directory + if (!@mkdir($lock, $conf['dmode'])) { + return false; + } + + if (!empty($conf['dperm'])) { + chmod($lock, $conf['dperm']); + } + return true; + } + + /** + * Unlock subscription info + * + * @param string $id The target page or namespace, specified by id; Namespaces + * are identified by appending a colon. + * + * @return bool + * @author Adrian Lang <lang@cosmocode.de> + */ + protected function unlock($id) + { + global $conf; + $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock'; + return @rmdir($lock); + } + + /** + * Send a digest mail + * + * Sends a digest mail showing a bunch of changes of a single page. Basically the same as sendPageDiff() + * but determines the last known revision first + * + * @param string $subscriber_mail The target mail address + * @param string $id The ID + * @param int $lastupdate Time of the last notification + * + * @return bool + * @author Adrian Lang <lang@cosmocode.de> + * + */ + protected function sendDigest($subscriber_mail, $id, $lastupdate) + { + $pagelog = new PageChangeLog($id); + $n = 0; + do { + $rev = $pagelog->getRevisions($n++, 1); + $rev = (count($rev) > 0) ? $rev[0] : null; + } while (!is_null($rev) && $rev > $lastupdate); + + // TODO I'm not happy with the following line and passing $this->mailer around. Not sure how to solve it better + $pageSubSender = new PageSubscriptionSender($this->mailer); + return $pageSubSender->sendPageDiff( + $subscriber_mail, + 'subscr_digest', + $id, + $rev + ); + } + + /** + * Send a list mail + * + * Sends a list mail showing a list of changed pages. + * + * @param string $subscriber_mail The target mail address + * @param array $ids Array of ids + * @param string $ns_id The id of the namespace + * + * @return bool true if a mail was sent + * @author Adrian Lang <lang@cosmocode.de> + * + */ + protected function sendList($subscriber_mail, $ids, $ns_id) + { + if (count($ids) === 0) { + return false; + } + + $tlist = ''; + $hlist = '<ul>'; + foreach ($ids as $id) { + $link = wl($id, [], true); + $tlist .= '* ' . $link . NL; + $hlist .= '<li><a href="' . $link . '">' . hsc($id) . '</a></li>' . NL; + } + $hlist .= '</ul>'; + + $id = prettyprint_id($ns_id); + $trep = [ + 'DIFF' => rtrim($tlist), + 'PAGE' => $id, + 'SUBSCRIBE' => wl($id, ['do' => 'subscribe'], true, '&'), + ]; + $hrep = [ + 'DIFF' => $hlist, + ]; + + return $this->send( + $subscriber_mail, + 'subscribe_list', + $ns_id, + 'subscr_list', + $trep, + $hrep + ); + } +} diff --git a/platform/www/inc/Subscriptions/MediaSubscriptionSender.php b/platform/www/inc/Subscriptions/MediaSubscriptionSender.php new file mode 100644 index 0000000..1757c2b --- /dev/null +++ b/platform/www/inc/Subscriptions/MediaSubscriptionSender.php @@ -0,0 +1,47 @@ +<?php + + +namespace dokuwiki\Subscriptions; + + +class MediaSubscriptionSender extends SubscriptionSender +{ + + /** + * Send the diff for some media change + * + * @fixme this should embed thumbnails of images in HTML version + * + * @param string $subscriber_mail The target mail address + * @param string $template Mail template ('uploadmail', ...) + * @param string $id Media file for which the notification is + * @param int|bool $rev Old revision if any + * @param int|bool $current_rev New revision if any + */ + public function sendMediaDiff($subscriber_mail, $template, $id, $rev = false, $current_rev = false) + { + global $conf; + + $file = mediaFN($id); + list($mime, /* $ext */) = mimetype($id); + + $trep = [ + 'MIME' => $mime, + 'MEDIA' => ml($id, $current_rev?('rev='.$current_rev):'', true, '&', true), + 'SIZE' => filesize_h(filesize($file)), + ]; + + if ($rev && $conf['mediarevisions']) { + $trep['OLD'] = ml($id, "rev=$rev", true, '&', true); + } else { + $trep['OLD'] = '---'; + } + + $headers = ['Message-Id' => $this->getMessageID($id, @filemtime($file))]; + if ($rev) { + $headers['In-Reply-To'] = $this->getMessageID($id, $rev); + } + + $this->send($subscriber_mail, 'upload', $id, $template, $trep, null, $headers); + } +} diff --git a/platform/www/inc/Subscriptions/PageSubscriptionSender.php b/platform/www/inc/Subscriptions/PageSubscriptionSender.php new file mode 100644 index 0000000..e5577c1 --- /dev/null +++ b/platform/www/inc/Subscriptions/PageSubscriptionSender.php @@ -0,0 +1,88 @@ +<?php + + +namespace dokuwiki\Subscriptions; + + +use Diff; +use InlineDiffFormatter; +use UnifiedDiffFormatter; + +class PageSubscriptionSender extends SubscriptionSender +{ + + /** + * Send the diff for some page change + * + * @param string $subscriber_mail The target mail address + * @param string $template Mail template ('subscr_digest', 'subscr_single', 'mailtext', ...) + * @param string $id Page for which the notification is + * @param int|null $rev Old revision if any + * @param string $summary Change summary if any + * @param int|null $current_rev New revision if any + * + * @return bool true if successfully sent + */ + public function sendPageDiff($subscriber_mail, $template, $id, $rev = null, $summary = '', $current_rev = null) + { + global $DIFF_INLINESTYLES; + + // prepare replacements (keys not set in hrep will be taken from trep) + $trep = [ + 'PAGE' => $id, + 'NEWPAGE' => wl($id, $current_rev?('rev='.$current_rev):'', true, '&'), + 'SUMMARY' => $summary, + 'SUBSCRIBE' => wl($id, ['do' => 'subscribe'], true, '&'), + ]; + $hrep = []; + + if ($rev) { + $subject = 'changed'; + $trep['OLDPAGE'] = wl($id, "rev=$rev", true, '&'); + + $old_content = rawWiki($id, $rev); + $new_content = rawWiki($id); + + $df = new Diff( + explode("\n", $old_content), + explode("\n", $new_content) + ); + $dformat = new UnifiedDiffFormatter(); + $tdiff = $dformat->format($df); + + $DIFF_INLINESTYLES = true; + $df = new Diff( + explode("\n", $old_content), + explode("\n", $new_content) + ); + $dformat = new InlineDiffFormatter(); + $hdiff = $dformat->format($df); + $hdiff = '<table>' . $hdiff . '</table>'; + $DIFF_INLINESTYLES = false; + } else { + $subject = 'newpage'; + $trep['OLDPAGE'] = '---'; + $tdiff = rawWiki($id); + $hdiff = nl2br(hsc($tdiff)); + } + + $trep['DIFF'] = $tdiff; + $hrep['DIFF'] = $hdiff; + + $headers = ['Message-Id' => $this->getMessageID($id)]; + if ($rev) { + $headers['In-Reply-To'] = $this->getMessageID($id, $rev); + } + + return $this->send( + $subscriber_mail, + $subject, + $id, + $template, + $trep, + $hrep, + $headers + ); + } + +} diff --git a/platform/www/inc/Subscriptions/RegistrationSubscriptionSender.php b/platform/www/inc/Subscriptions/RegistrationSubscriptionSender.php new file mode 100644 index 0000000..bd48875 --- /dev/null +++ b/platform/www/inc/Subscriptions/RegistrationSubscriptionSender.php @@ -0,0 +1,40 @@ +<?php + +namespace dokuwiki\Subscriptions; + +class RegistrationSubscriptionSender extends SubscriptionSender +{ + + /** + * Send a notify mail on new registration + * + * @param string $login login name of the new user + * @param string $fullname full name of the new user + * @param string $email email address of the new user + * + * @return bool true if a mail was sent + * @author Andreas Gohr <andi@splitbrain.org> + * + */ + public function sendRegister($login, $fullname, $email) + { + global $conf; + if (empty($conf['registernotify'])) { + return false; + } + + $trep = [ + 'NEWUSER' => $login, + 'NEWNAME' => $fullname, + 'NEWEMAIL' => $email, + ]; + + return $this->send( + $conf['registernotify'], + 'new_user', + $login, + 'registermail', + $trep + ); + } +} diff --git a/platform/www/inc/Subscriptions/SubscriberManager.php b/platform/www/inc/Subscriptions/SubscriberManager.php new file mode 100644 index 0000000..ded1390 --- /dev/null +++ b/platform/www/inc/Subscriptions/SubscriberManager.php @@ -0,0 +1,290 @@ +<?php + +namespace dokuwiki\Subscriptions; + +use dokuwiki\Input\Input; +use DokuWiki_Auth_Plugin; +use Exception; + +class SubscriberManager +{ + + /** + * Check if subscription system is enabled + * + * @return bool + */ + public function isenabled() + { + return actionOK('subscribe'); + } + + /** + * Adds a new subscription for the given page or namespace + * + * This will automatically overwrite any existent subscription for the given user on this + * *exact* page or namespace. It will *not* modify any subscription that may exist in higher namespaces. + * + * @throws Exception when user or style is empty + * + * @param string $id The target page or namespace, specified by id; Namespaces + * are identified by appending a colon. + * @param string $user + * @param string $style + * @param string $data + * + * @return bool + */ + public function add($id, $user, $style, $data = '') + { + if (!$this->isenabled()) { + return false; + } + + // delete any existing subscription + $this->remove($id, $user); + + $user = auth_nameencode(trim($user)); + $style = trim($style); + $data = trim($data); + + if (!$user) { + throw new Exception('no subscription user given'); + } + if (!$style) { + throw new Exception('no subscription style given'); + } + if (!$data) { + $data = time(); + } //always add current time for new subscriptions + + $line = "$user $style $data\n"; + $file = $this->file($id); + return io_saveFile($file, $line, true); + } + + + /** + * Removes a subscription for the given page or namespace + * + * This removes all subscriptions matching the given criteria on the given page or + * namespace. It will *not* modify any subscriptions that may exist in higher + * namespaces. + * + * @param string $id The target object’s (namespace or page) id + * @param string|array $user + * @param string|array $style + * @param string|array $data + * + * @return bool + */ + public function remove($id, $user = null, $style = null, $data = null) + { + if (!$this->isenabled()) { + return false; + } + + $file = $this->file($id); + if (!file_exists($file)) { + return true; + } + + $regexBuilder = new SubscriberRegexBuilder(); + $re = $regexBuilder->buildRegex($user, $style, $data); + return io_deleteFromFile($file, $re, true); + } + + /** + * Get data for $INFO['subscribed'] + * + * $INFO['subscribed'] is either false if no subscription for the current page + * and user is in effect. Else it contains an array of arrays with the fields + * “target”, “style”, and optionally “data”. + * + * @author Adrian Lang <lang@cosmocode.de> + * + * @param string $id Page ID, defaults to global $ID + * @param string $user User, defaults to $_SERVER['REMOTE_USER'] + * + * @return array|false + */ + public function userSubscription($id = '', $user = '') + { + if (!$this->isenabled()) { + return false; + } + + global $ID; + /** @var Input $INPUT */ + global $INPUT; + if (!$id) { + $id = $ID; + } + if (!$user) { + $user = $INPUT->server->str('REMOTE_USER'); + } + + if (empty($user)) { + // not logged in + return false; + } + + $subs = $this->subscribers($id, $user); + if (!count($subs)) { + return false; + } + + $result = []; + foreach ($subs as $target => $info) { + $result[] = [ + 'target' => $target, + 'style' => $info[$user][0], + 'data' => $info[$user][1], + ]; + } + + return $result; + } + + /** + * Recursively search for matching subscriptions + * + * This function searches all relevant subscription files for a page or + * namespace. + * + * @author Adrian Lang <lang@cosmocode.de> + * + * @param string $page The target object’s (namespace or page) id + * @param string|array $user + * @param string|array $style + * @param string|array $data + * + * @return array + */ + public function subscribers($page, $user = null, $style = null, $data = null) + { + if (!$this->isenabled()) { + return []; + } + + // Construct list of files which may contain relevant subscriptions. + $files = [':' => $this->file(':')]; + do { + $files[$page] = $this->file($page); + $page = getNS(rtrim($page, ':')) . ':'; + } while ($page !== ':'); + + $regexBuilder = new SubscriberRegexBuilder(); + $re = $regexBuilder->buildRegex($user, $style, $data); + + // Handle files. + $result = []; + foreach ($files as $target => $file) { + if (!file_exists($file)) { + continue; + } + + $lines = file($file); + foreach ($lines as $line) { + // fix old style subscription files + if (strpos($line, ' ') === false) { + $line = trim($line) . " every\n"; + } + + // check for matching entries + if (!preg_match($re, $line, $m)) { + continue; + } + + $u = rawurldecode($m[1]); // decode the user name + if (!isset($result[$target])) { + $result[$target] = []; + } + $result[$target][$u] = [$m[2], $m[3]]; // add to result + } + } + return array_reverse($result); + } + + /** + * Default callback for COMMON_NOTIFY_ADDRESSLIST + * + * Aggregates all email addresses of user who have subscribed the given page with 'every' style + * + * @author Adrian Lang <lang@cosmocode.de> + * @author Steven Danz <steven-danz@kc.rr.com> + * + * @todo move the whole functionality into this class, trigger SUBSCRIPTION_NOTIFY_ADDRESSLIST instead, + * use an array for the addresses within it + * + * @param array &$data Containing the entries: + * - $id (the page id), + * - $self (whether the author should be notified, + * - $addresslist (current email address list) + * - $replacements (array of additional string substitutions, @KEY@ to be replaced by value) + */ + public function notifyAddresses(&$data) + { + if (!$this->isenabled()) { + return; + } + + /** @var DokuWiki_Auth_Plugin $auth */ + global $auth; + global $conf; + /** @var \Input $INPUT */ + global $INPUT; + + $id = $data['id']; + $self = $data['self']; + $addresslist = $data['addresslist']; + + $subscriptions = $this->subscribers($id, null, 'every'); + + $result = []; + foreach ($subscriptions as $target => $users) { + foreach ($users as $user => $info) { + $userinfo = $auth->getUserData($user); + if ($userinfo === false) { + continue; + } + if (!$userinfo['mail']) { + continue; + } + if (!$self && $user == $INPUT->server->str('REMOTE_USER')) { + continue; + } //skip our own changes + + $level = auth_aclcheck($id, $user, $userinfo['grps']); + if ($level >= AUTH_READ) { + if (strcasecmp($userinfo['mail'], $conf['notify']) != 0) { //skip user who get notified elsewhere + $result[$user] = $userinfo['mail']; + } + } + } + } + $data['addresslist'] = trim($addresslist . ',' . implode(',', $result), ','); + } + + /** + * Return the subscription meta file for the given ID + * + * @author Adrian Lang <lang@cosmocode.de> + * + * @param string $id The target page or namespace, specified by id; Namespaces + * are identified by appending a colon. + * + * @return string + */ + protected function file($id) + { + $meta_fname = '.mlist'; + if ((substr($id, -1, 1) === ':')) { + $meta_froot = getNS($id); + $meta_fname = '/' . $meta_fname; + } else { + $meta_froot = $id; + } + return metaFN((string)$meta_froot, $meta_fname); + } +} diff --git a/platform/www/inc/Subscriptions/SubscriberRegexBuilder.php b/platform/www/inc/Subscriptions/SubscriberRegexBuilder.php new file mode 100644 index 0000000..959702a --- /dev/null +++ b/platform/www/inc/Subscriptions/SubscriberRegexBuilder.php @@ -0,0 +1,70 @@ +<?php + +namespace dokuwiki\Subscriptions; + +use Exception; + +class SubscriberRegexBuilder +{ + + /** + * Construct a regular expression for parsing a subscription definition line + * + * @param string|array $user + * @param string|array $style + * @param string|array $data + * + * @return string complete regexp including delimiters + * @throws Exception when no data is passed + * @author Andreas Gohr <andi@splitbrain.org> + * + */ + public function buildRegex($user = null, $style = null, $data = null) + { + // always work with arrays + $user = (array)$user; + $style = (array)$style; + $data = (array)$data; + + // clean + $user = array_filter(array_map('trim', $user)); + $style = array_filter(array_map('trim', $style)); + $data = array_filter(array_map('trim', $data)); + + // user names are encoded + $user = array_map('auth_nameencode', $user); + + // quote + $user = array_map('preg_quote_cb', $user); + $style = array_map('preg_quote_cb', $style); + $data = array_map('preg_quote_cb', $data); + + // join + $user = join('|', $user); + $style = join('|', $style); + $data = join('|', $data); + + // any data at all? + if ($user . $style . $data === '') { + throw new Exception('no data passed'); + } + + // replace empty values, set which ones are optional + $sopt = ''; + $dopt = ''; + if ($user === '') { + $user = '\S+'; + } + if ($style === '') { + $style = '\S+'; + $sopt = '?'; + } + if ($data === '') { + $data = '\S+'; + $dopt = '?'; + } + + // assemble + return "/^($user)(?:\\s+($style))$sopt(?:\\s+($data))$dopt$/"; + } +} diff --git a/platform/www/inc/Subscriptions/SubscriptionSender.php b/platform/www/inc/Subscriptions/SubscriptionSender.php new file mode 100644 index 0000000..afc05bf --- /dev/null +++ b/platform/www/inc/Subscriptions/SubscriptionSender.php @@ -0,0 +1,86 @@ +<?php + +namespace dokuwiki\Subscriptions; + +use Mailer; + +abstract class SubscriptionSender +{ + protected $mailer; + + public function __construct(Mailer $mailer = null) + { + if ($mailer === null) { + $mailer = new Mailer(); + } + $this->mailer = $mailer; + } + + /** + * Get a valid message id for a certain $id and revision (or the current revision) + * + * @param string $id The id of the page (or media file) the message id should be for + * @param string $rev The revision of the page, set to the current revision of the page $id if not set + * + * @return string + */ + protected function getMessageID($id, $rev = null) + { + static $listid = null; + if (is_null($listid)) { + $server = parse_url(DOKU_URL, PHP_URL_HOST); + $listid = join('.', array_reverse(explode('/', DOKU_BASE))) . $server; + $listid = urlencode($listid); + $listid = strtolower(trim($listid, '.')); + } + + if (is_null($rev)) { + $rev = @filemtime(wikiFN($id)); + } + + return "<$id?rev=$rev@$listid>"; + } + + /** + * Helper function for sending a mail + * + * @param string $subscriber_mail The target mail address + * @param string $subject The lang id of the mail subject (without the + * prefix “mail_”) + * @param string $context The context of this mail, eg. page or namespace id + * @param string $template The name of the mail template + * @param array $trep Predefined parameters used to parse the + * template (in text format) + * @param array $hrep Predefined parameters used to parse the + * template (in HTML format), null to default to $trep + * @param array $headers Additional mail headers in the form 'name' => 'value' + * + * @return bool + * @author Adrian Lang <lang@cosmocode.de> + * + */ + protected function send($subscriber_mail, $subject, $context, $template, $trep, $hrep = null, $headers = []) + { + global $lang; + global $conf; + + $text = rawLocale($template); + $subject = $lang['mail_' . $subject] . ' ' . $context; + $mail = $this->mailer; + $mail->bcc($subscriber_mail); + $mail->subject($subject); + $mail->setBody($text, $trep, $hrep); + if (in_array($template, ['subscr_list', 'subscr_digest'])) { + $mail->from($conf['mailfromnobody']); + } + if (isset($trep['SUBSCRIBE'])) { + $mail->setHeader('List-Unsubscribe', '<' . $trep['SUBSCRIBE'] . '>', false); + } + + foreach ($headers as $header => $value) { + $mail->setHeader($header, $value); + } + + return $mail->send(); + } +} |