From 5b75cd1f5c479ada468fbf62a733c54edad152f1 Mon Sep 17 00:00:00 2001 From: Adrian Lang Date: Tue, 5 Jan 2010 14:14:00 +0100 Subject: New mail subscription with digest --- conf/dokuwiki.php | 2 + inc/actions.php | 124 +++++++-------- inc/common.php | 129 ++++------------ inc/confutils.php | 1 - inc/form.php | 21 +++ inc/lang/en/lang.php | 31 ++-- inc/lang/en/subscr_digest.txt | 20 +++ inc/lang/en/subscr_form.txt | 3 + inc/lang/en/subscr_list.txt | 17 ++ inc/lang/en/subscr_single.txt | 23 +++ inc/lang/en/subscribermail.txt | 23 --- inc/pageutils.php | 17 ++ inc/subscription.php | 342 +++++++++++++++++++++++++++++++++++++++++ inc/template.php | 138 +++++++++-------- lib/exe/indexer.php | 70 +++++++++ lib/exe/js.php | 1 + lib/scripts/subscriptions.js | 46 ++++++ lib/tpl/default/design.css | 14 ++ lib/tpl/default/main.php | 1 - 19 files changed, 758 insertions(+), 265 deletions(-) create mode 100644 inc/lang/en/subscr_digest.txt create mode 100644 inc/lang/en/subscr_form.txt create mode 100644 inc/lang/en/subscr_list.txt create mode 100644 inc/lang/en/subscr_single.txt delete mode 100644 inc/lang/en/subscribermail.txt create mode 100644 inc/subscription.php create mode 100644 lib/scripts/subscriptions.js diff --git a/conf/dokuwiki.php b/conf/dokuwiki.php index 74d95147e..e6a19e60b 100644 --- a/conf/dokuwiki.php +++ b/conf/dokuwiki.php @@ -103,6 +103,8 @@ $conf['gdlib'] = 2; //the GDlib version (0, 1 or 2) 2 tries $conf['im_convert'] = ''; //path to ImageMagicks convert (will be used instead of GD) $conf['jpg_quality'] = '70'; //quality of compression when scaling jpg images (0-100) $conf['subscribers'] = 0; //enable change notice subscription support +$conf['subscribe_time'] = 24 * 60 * 60; //Time after which digests / lists are sent (in sec, default 1 day) + //Should be larger than the time specified in recent_days $conf['compress'] = 1; //Strip whitespaces and comments from Styles and JavaScript? 1|0 $conf['hidepages'] = ''; //Regexp for pages to be skipped from RSS, Search and Recent Changes $conf['send404'] = 0; //Send a HTTP 404 status for non existing pages? diff --git a/inc/actions.php b/inc/actions.php index 92f817133..a856b7919 100644 --- a/inc/actions.php +++ b/inc/actions.php @@ -47,12 +47,13 @@ function act_dispatch(){ } //check if user is asking to (un)subscribe a page - if($ACT == 'subscribe' || $ACT == 'unsubscribe') - $ACT = act_subscription($ACT); - - //check if user is asking to (un)subscribe a namespace - if($ACT == 'subscribens' || $ACT == 'unsubscribens') - $ACT = act_subscriptionns($ACT); + if($ACT == 'subscribe') { + try { + $ACT = act_subscription($ACT); + } catch (Exception $e) { + msg($e->getMessage(), -1); + } + } //check permissions $ACT = act_permcheck($ACT); @@ -550,81 +551,68 @@ function act_export($act){ } /** - * Handle page 'subscribe', 'unsubscribe' + * Handle page 'subscribe' + * + * Throws exception on error. * - * @author Steven Danz - * @todo localize + * @author Adrian Lang */ function act_subscription($act){ - global $ID; - global $INFO; global $lang; - - $file=metaFN($ID,'.mlist'); - if ($act=='subscribe' && !$INFO['subscribed']){ - if ($INFO['userinfo']['mail']){ - if (io_saveFile($file,$_SERVER['REMOTE_USER']."\n",true)) { - $INFO['subscribed'] = true; - msg(sprintf($lang[$act.'_success'], $INFO['userinfo']['name'], $ID),1); - } else { - msg(sprintf($lang[$act.'_error'], $INFO['userinfo']['name'], $ID),1); - } - } else { - msg($lang['subscribe_noaddress']); - } - } elseif ($act=='unsubscribe' && $INFO['subscribed']){ - if (io_deleteFromFile($file,$_SERVER['REMOTE_USER']."\n")) { - $INFO['subscribed'] = false; - msg(sprintf($lang[$act.'_success'], $INFO['userinfo']['name'], $ID),1); - } else { - msg(sprintf($lang[$act.'_error'], $INFO['userinfo']['name'], $ID),1); - } - } - - return 'show'; -} - -/** - * Handle namespace 'subscribe', 'unsubscribe' - * - */ -function act_subscriptionns($act){ - global $ID; global $INFO; - global $lang; - if(!getNS($ID)) { - $file = metaFN(getNS($ID),'.mlist'); - $ns = "root"; - } else { - $file = metaFN(getNS($ID),'/.mlist'); - $ns = getNS($ID); + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + // No post to handle, let tpl_subscribe manage the request. + return $act; } - // reuse strings used to display the status of the subscribe action - $act_msg = rtrim($act, 'ns'); - - if ($act=='subscribens' && !$INFO['subscribedns']){ - if ($INFO['userinfo']['mail']){ - if (io_saveFile($file,$_SERVER['REMOTE_USER']."\n",true)) { - $INFO['subscribedns'] = true; - msg(sprintf($lang[$act_msg.'_success'], $INFO['userinfo']['name'], $ns),1); - } else { - msg(sprintf($lang[$act_msg.'_error'], $INFO['userinfo']['name'], $ns),1); + // Get and validate parameters. + if (!isset($_POST['subscribe_target'])) { + throw new Exception($lang['subscr_no_target']); + } + $target = $_POST['subscribe_target']; + $valid_styles = array('every', 'digest'); + if (substr($target, -1, 1) === ':') { + // Allow “list” subscribe style since the target is a namespace. + $valid_styles[] = 'list'; + } + $style = valid_input_set('subscribe_style', $valid_styles, $_POST, + $lang['subscr_invalid_style']); + $action = valid_input_set('subscribe_action', array('subscribe', + 'unsubscribe'), + $_POST, $lang['subscr_invalid_action']); + + // Check other conditions. + if ($action === 'subscribe') { + if ($INFO['userinfo']['mail'] === '') { + throw new Exception($lang['subscr_subscribe_noaddress']); + } + } elseif ($action === 'unsubscribe') { + $is = false; + foreach($INFO['subscribed'] as $subscr) { + if ($subscr['target'] === $target) { + $is = true; } - } else { - msg($lang['subscribe_noaddress']); } - } elseif ($act=='unsubscribens' && $INFO['subscribedns']){ - if (io_deleteFromFile($file,$_SERVER['REMOTE_USER']."\n")) { - $INFO['subscribedns'] = false; - msg(sprintf($lang[$act_msg.'_success'], $INFO['userinfo']['name'], $ns),1); - } else { - msg(sprintf($lang[$act_msg.'_error'], $INFO['userinfo']['name'], $ns),1); + if ($is === false) { + throw new Exception(sprintf($lang['subscr_not_subscribed_you'], + prettyprint_id($target))); } + // subscription_set deletes a subscription if style = null. + $style = null; } - return 'show'; + // Perform action. + require_once DOKU_INC . 'inc/subscription.php'; + if (!subscription_set($target, $_SERVER['REMOTE_USER'], $style)) { + throw new Exception(sprintf($lang["subscr_{$action}_error"], + hsc($INFO['userinfo']['name']), + prettyprint_id($target))); + } + $INFO['subscribed'] = get_info_subscribed(); + msg(sprintf($lang["subscr_{$action}_success"], hsc($INFO['userinfo']['name']), + prettyprint_id($target)), 1); + return $act; } //Setup VIM: ex: et ts=2 enc=utf-8 : diff --git a/inc/common.php b/inc/common.php index 85187f16d..2cc279844 100644 --- a/inc/common.php +++ b/inc/common.php @@ -13,6 +13,7 @@ require_once(DOKU_INC.'inc/utf8.php'); require_once(DOKU_INC.'inc/mail.php'); require_once(DOKU_INC.'inc/parserutils.php'); require_once(DOKU_INC.'inc/infoutils.php'); +require_once DOKU_INC.'inc/subscription.php'; /** * These constants are used with the recents function @@ -117,8 +118,7 @@ function pageinfo(){ if(isset($_SERVER['REMOTE_USER'])){ $info['userinfo'] = $USERINFO; $info['perm'] = auth_quickaclcheck($ID); - $info['subscribed'] = is_subscribed($ID,$_SERVER['REMOTE_USER'],false); - $info['subscribedns'] = is_subscribed($ID,$_SERVER['REMOTE_USER'],true); + $info['subscribed'] = get_info_subscribed(); $info['client'] = $_SERVER['REMOTE_USER']; if($info['perm'] == AUTH_ADMIN){ @@ -1061,10 +1061,10 @@ function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){ }elseif($who == 'subscribers'){ if(!$conf['subscribers']) return; //subscribers enabled? if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors - $bcc = subscriber_addresslist($id,false); + $bcc = subscription_addresslist($id,false); if(empty($bcc)) return; $to = ''; - $text = rawLocale('subscribermail'); + $text = rawLocale('subscr_single'); }elseif($who == 'register'){ if(empty($conf['registernotify'])) return; $text = rawLocale('registermail'); @@ -1097,7 +1097,7 @@ function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){ $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text); require_once(DOKU_INC.'inc/DifferenceEngine.php'); $df = new Diff(explode("\n",rawWiki($id,$rev)), - explode("\n",rawWiki($id))); + explode("\n",rawWiki($id))); $dformat = new UnifiedDiffFormatter(); $diff = $dformat->format($df); }else{ @@ -1272,97 +1272,6 @@ function obfuscate($email) { } } -/** - * Let us know if a user is tracking a page or a namespace - * - * @author Andreas Gohr - */ -function is_subscribed($id,$uid,$ns=false){ - if(!$ns) { - $file=metaFN($id,'.mlist'); - } else { - if(!getNS($id)) { - $file = metaFN(getNS($id),'.mlist'); - } else { - $file = metaFN(getNS($id),'/.mlist'); - } - } - if (@file_exists($file)) { - $mlist = file($file); - $pos = array_search($uid."\n",$mlist); - return is_int($pos); - } - - return false; -} - -/** - * Return a string with the email addresses of all the - * users subscribed to a page - * - * @author Steven Danz - */ -function subscriber_addresslist($id,$self=true){ - global $conf; - global $auth; - - if (!$conf['subscribers']) return ''; - - $users = array(); - $emails = array(); - - // load the page mlist file content - $mlist = array(); - $file=metaFN($id,'.mlist'); - if (@file_exists($file)) { - $mlist = file($file); - foreach ($mlist as $who) { - $who = rtrim($who); - if(!$self && $who == $_SERVER['REMOTE_USER']) continue; - $users[$who] = true; - } - } - - // load also the namespace mlist file content - $ns = getNS($id); - while ($ns) { - $nsfile = metaFN($ns,'/.mlist'); - if (@file_exists($nsfile)) { - $mlist = file($nsfile); - foreach ($mlist as $who) { - $who = rtrim($who); - if(!$self && $who == $_SERVER['REMOTE_USER']) continue; - $users[$who] = true; - } - } - $ns = getNS($ns); - } - // root namespace - $nsfile = metaFN('','.mlist'); - if (@file_exists($nsfile)) { - $mlist = file($nsfile); - foreach ($mlist as $who) { - $who = rtrim($who); - if(!$self && $who == $_SERVER['REMOTE_USER']) continue; - $users[$who] = true; - } - } - if(!empty($users)) { - foreach (array_keys($users) as $who) { - $info = $auth->getUserData($who); - if($info === false) continue; - $level = auth_aclcheck($id,$who,$info['grps']); - if ($level >= AUTH_READ) { - if (strcasecmp($info['mail'],$conf['notify']) != 0) { - $emails[] = $info['mail']; - } - } - } - } - - return implode(',',$emails); -} - /** * Removes quoting backslashes * @@ -1545,4 +1454,30 @@ function send_redirect($url){ exit; } -//Setup VIM: ex: et ts=4 enc=utf-8 : +/** + * Validate a value using a set of valid values + * + * This function checks whether a specified value is set and in the array + * $valid_values. If not, the function returns a default value or, if no + * default is specified, throws an exception. + * + * @param string $param The name of the parameter + * @param array $valid_values A set of valid values; Optionally a default may + * be marked by the key “default”. + * @param array $array The array containing the value (typically $_POST + * or $_GET) + * @param string $exc The text of the raised exception + * + * @author Adrian Lang + */ +function valid_input_set($param, $valid_values, $array, $exc = '') { + if (isset($array[$param]) && in_array($array[$param], $valid_values)) { + return $array[$param]; + } elseif (isset($valid_values['default'])) { + return $valid_values['default']; + } else { + throw new Exception($exc); + } +} + +//Setup VIM: ex: et ts=2 enc=utf-8 : diff --git a/inc/confutils.php b/inc/confutils.php index abfde8a80..de63846de 100644 --- a/inc/confutils.php +++ b/inc/confutils.php @@ -248,7 +248,6 @@ function actionOK($action){ if(isset($conf['resendpasswd']) && !$conf['resendpasswd']) $disabled[] = 'resendpwd'; if(isset($conf['subscribers']) && !$conf['subscribers']) { $disabled[] = 'subscribe'; - $disabled[] = 'subscribens'; } $disabled = array_unique($disabled); } diff --git a/inc/form.php b/inc/form.php index 6d496f414..0a6bc2bba 100644 --- a/inc/form.php +++ b/inc/form.php @@ -283,6 +283,27 @@ class Doku_Form { echo $this->getForm(); } + /** + * Add a radio set + * + * This function adds a set of radio buttons to the form. If $_POST[$name] + * is set, this radio is preselected, else the first radio button. + * + * @param string $name The HTML field name + * @param array $entries An array of entries $value => $caption + * + * @author Adrian Lang + */ + + function addRadioSet($name, $entries) { + $value = (isset($_POST[$name]) && isset($entries[$_POST[$name]])) ? + $_POST[$name] : key($entries); + foreach($entries as $val => $cap) { + $data = ($value === $val) ? array('checked' => 'checked') : array(); + $this->addElement(form_makeRadioField($name, $val, $cap, '', '', $data)); + } + } + } /** diff --git a/inc/lang/en/lang.php b/inc/lang/en/lang.php index cf5173d05..728d7823b 100644 --- a/inc/lang/en/lang.php +++ b/inc/lang/en/lang.php @@ -39,10 +39,7 @@ $lang['btn_delete'] = 'Delete'; $lang['btn_back'] = 'Back'; $lang['btn_backlink'] = "Backlinks"; $lang['btn_backtomedia'] = 'Back to Mediafile Selection'; -$lang['btn_subscribe'] = 'Subscribe Page Changes'; -$lang['btn_unsubscribe'] = 'Unsubscribe Page Changes'; -$lang['btn_subscribens'] = 'Subscribe Namespace Changes'; -$lang['btn_unsubscribens'] = 'Unsubscribe Namespace Changes'; +$lang['btn_subscribe'] = 'Manage Subscriptions'; $lang['btn_profile'] = 'Update Profile'; $lang['btn_reset'] = 'Reset'; $lang['btn_resendpwd'] = 'Send new password'; @@ -158,6 +155,7 @@ $lang['download'] = 'Download Snippet'; $lang['mail_newpage'] = 'page added:'; $lang['mail_changed'] = 'page changed:'; +$lang['mail_subscribe_list'] = 'pages changed in namespace:'; $lang['mail_new_user'] = 'new user:'; $lang['mail_upload'] = 'file uploaded:'; @@ -212,11 +210,26 @@ $lang['img_format'] = 'Format'; $lang['img_camera'] = 'Camera'; $lang['img_keywords']= 'Keywords'; -$lang['subscribe_success'] = 'Added %s to subscription list for %s'; -$lang['subscribe_error'] = 'Error adding %s to subscription list for %s'; -$lang['subscribe_noaddress']= 'There is no address associated with your login, you cannot be added to the subscription list'; -$lang['unsubscribe_success']= 'Removed %s from subscription list for %s'; -$lang['unsubscribe_error'] = 'Error removing %s from subscription list for %s'; +$lang['subscr_subscribe_success'] = 'Added %s to subscription list for %s'; +$lang['subscr_subscribe_error'] = 'Error adding %s to subscription list for %s'; +$lang['subscr_subscribe_noaddress']= 'There is no address associated with your login, you cannot be added to the subscription list'; +$lang['subscr_unsubscribe_success']= 'Removed %s from subscription list for %s'; +$lang['subscr_unsubscribe_error'] = 'Error removing %s from subscription list for %s'; +$lang['subscr_no_target'] = 'No subscription target'; +$lang['subscr_invalid_style'] = 'Invalid subscription style'; +$lang['subscr_invalid_action'] = 'Invalid subscription action'; +$lang['subscr_already_subscribed'] = '%s is already subscribed to %s'; +$lang['subscr_not_subscribed'] = '%s is not subscribed to %s'; +$lang['subscr_not_subscribed_you'] = 'You are not subscribed to %s'; +// Manage page for subscriptions +$lang['subscr_m_current_header'] = 'Current subscriptions'; +$lang['subscr_m_current'] = 'Your current subscriptions:'; +$lang['subscr_m_entry'] = 'Subscribed to %s receiving %s.'; +$lang['subscr_m_delete'] = 'Delete'; +$lang['subscr_m_not_subscribed'] = 'You are currently not subscribed to this page.'; +$lang['subscr_m_new_header'] = 'Add subscription'; +$lang['subscr_m_noemail'] = 'You did not set an email address.'; +$lang['subscr_m_subscribe'] = 'Subscribe'; /* auth.class language support */ $lang['authmodfailed'] = 'Bad user authentication configuration. Please inform your Wiki Admin.'; diff --git a/inc/lang/en/subscr_digest.txt b/inc/lang/en/subscr_digest.txt new file mode 100644 index 000000000..35011b6e6 --- /dev/null +++ b/inc/lang/en/subscr_digest.txt @@ -0,0 +1,20 @@ +Hello! + +The page @PAGE@ in the @TITLE@ wiki changed. +Here are the changes: + +-------------------------------------------------------- +@DIFF@ +-------------------------------------------------------- + +Old Revision: @OLDPAGE@ +New Revision: @NEWPAGE@ + +To cancel the page notifications, log into the wiki at +@DOKUWIKIURL@ then visit +@SUBSCRIBE@ +and unsubscribe page and/or namespace changes. + +-- +This mail was generated by DokuWiki at +@DOKUWIKIURL@ diff --git a/inc/lang/en/subscr_form.txt b/inc/lang/en/subscr_form.txt new file mode 100644 index 000000000..94b75258c --- /dev/null +++ b/inc/lang/en/subscr_form.txt @@ -0,0 +1,3 @@ +====== Manage subscriptions ====== + +This form allows you to manage your subscriptions for the current page. diff --git a/inc/lang/en/subscr_list.txt b/inc/lang/en/subscr_list.txt new file mode 100644 index 000000000..efe27d866 --- /dev/null +++ b/inc/lang/en/subscr_list.txt @@ -0,0 +1,17 @@ +Hello! + +Pages in the namespace @PAGE@ of the @TITLE@ wiki changed. +Here are the changed pages: + +-------------------------------------------------------- +@DIFF@ +-------------------------------------------------------- + +To cancel the page notifications, log into the wiki at +@DOKUWIKIURL@ then visit +@SUBSCRIBE@ +and unsubscribe page and/or namespace changes. + +-- +This mail was generated by DokuWiki at +@DOKUWIKIURL@ diff --git a/inc/lang/en/subscr_single.txt b/inc/lang/en/subscr_single.txt new file mode 100644 index 000000000..673c4c32a --- /dev/null +++ b/inc/lang/en/subscr_single.txt @@ -0,0 +1,23 @@ +Hello! + +The page @PAGE@ in the @TITLE@ wiki changed. +Here are the changes: + +-------------------------------------------------------- +@DIFF@ +-------------------------------------------------------- + +Date : @DATE@ +User : @USER@ +Edit Summary: @SUMMARY@ +Old Revision: @OLDPAGE@ +New Revision: @NEWPAGE@ + +To cancel the page notifications, log into the wiki at +@DOKUWIKIURL@ then visit +@NEWPAGE@ +and unsubscribe page and/or namespace changes. + +-- +This mail was generated by DokuWiki at +@DOKUWIKIURL@ diff --git a/inc/lang/en/subscribermail.txt b/inc/lang/en/subscribermail.txt deleted file mode 100644 index 673c4c32a..000000000 --- a/inc/lang/en/subscribermail.txt +++ /dev/null @@ -1,23 +0,0 @@ -Hello! - -The page @PAGE@ in the @TITLE@ wiki changed. -Here are the changes: - --------------------------------------------------------- -@DIFF@ --------------------------------------------------------- - -Date : @DATE@ -User : @USER@ -Edit Summary: @SUMMARY@ -Old Revision: @OLDPAGE@ -New Revision: @NEWPAGE@ - -To cancel the page notifications, log into the wiki at -@DOKUWIKIURL@ then visit -@NEWPAGE@ -and unsubscribe page and/or namespace changes. - --- -This mail was generated by DokuWiki at -@DOKUWIKIURL@ diff --git a/inc/pageutils.php b/inc/pageutils.php index 9c192e5e6..239ff41c5 100644 --- a/inc/pageutils.php +++ b/inc/pageutils.php @@ -534,4 +534,21 @@ function isVisiblePage($id){ return !isHiddenPage($id); } +/** + * Format an id for output to a user + * + * Namespaces are denoted by a trailing “:*”. The root namespace is + * “*”. Output is escaped. + * + * @author Adrian Lang + */ +function prettyprint_id($id) { + if (!$id || $id === ':') { + return '*'; + } + if ((substr($id, -1, 1) === ':')) { + $id .= '*'; + } + return hsc($id); +} diff --git a/inc/subscription.php b/inc/subscription.php new file mode 100644 index 000000000..1dcecf6f5 --- /dev/null +++ b/inc/subscription.php @@ -0,0 +1,342 @@ + + * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) + */ + +require_once DOKU_INC.'/inc/pageutils.php'; + +/** + * Get the name of the metafile tracking subscriptions to target page or + * namespace + * + * @param string $id The target page or namespace, specified by id; Namespaces + * are identified by appending a colon. + * + * @author Adrian Lang + */ +function subscription_filename($id) { + $meta_fname = '.mlist'; + if ((substr($id, -1, 1) === ':')) { + $meta_froot = getNS($id); + if ($meta_froot === false) { + $meta_fname = '/' . $meta_fname; + } + } else { + $meta_froot = $id; + } + return metaFN($meta_froot, $meta_fname); +} + +/** + * Set subscription information + * + * Allows to set subscription informations for permanent storage in meta files. + * Subscriptions consist of a target object, a subscribing user, a subscribe + * style and optional data. + * A subscription may be deleted by specifying an empty subscribe style. + * Only one subscription per target and user is allowed. + * The function returns false on error, otherwise true. Note that no error is + * returned if a subscription should be deleted but the user is not subscribed + * and the subscription meta file exists. + * + * @param string $page The target object (page or namespace), specified by + * id; Namespaces are identified by a trailing colon. + * @param string $user The user + * @param string $style The subscribe style; DokuWiki currently implements + * “every”, “digest”, and “list”. + * @param bool $overwrite Whether an existing subscription may be overwritten + * @param string $data An optional data blob; For list and digest, this + * defaults to time(). + * + * @author Adrian Lang + */ +function subscription_set($page, $user, $style, $overwrite = false, $data = null) { + global $lang; + if (is_null($style)) { + // Delete subscription. + $file = subscription_filename($page); + if (!@file_exists($file)) { + msg(sprintf($lang['subscr_not_subscribed'], $user, + prettyprint_id($page)), -1); + return false; + } + + // io_deleteFromFile does not return false if no line matched. + return io_deleteFromFile($file, + subscription_regex(array('user' => $user)), + true); + } + + // Delete subscription if one exists and $overwrite is true. If $overwrite + // is false, fail. + $subs = subscription_find($page, array('user' => $user)); + if (count($subs) > 0 && array_pop(array_keys($subs)) === $page) { + if (!$overwrite) { + msg(sprintf($lang['subscr_already_subscribed'], $user, + prettyprint_id($page)), -1); + return false; + } + // Fail if deletion failed, else continue. + if (!subscription_set($page, $user, null)) { + return false; + } + } + + $file = subscription_filename($page); + $content = auth_nameencode($user) . ' ' . $style; + if (in_array($style, array('list', 'digest'))) { + $content .= ' ' . (!is_null($data) ? $data : time()); + } + return io_saveFile($file, $content . "\n", true); +} + +/** + * Recursively search for matching subscriptions + * + * This function searches all relevant subscription files for a page or + * namespace. + * + * @param string $page The target object’s (namespace or page) id + * @param array $pre A hash of predefined values + * + * @see function subscription_regex for $pre documentation + * + * @author Adrian Lang + */ +function subscription_find($page, $pre) { + // Construct list of files which may contain relevant subscriptions. + $filenames = array(); + do { + $filenames[$page] = subscription_filename($page); + $page = getNS(rtrim($page, ':')) . ':'; + } while ($page !== ':'); + + // Handle files. + $matches = array(); + foreach ($filenames as $cur_page => $filename) { + if (!@file_exists($filename)) { + continue; + } + $subscriptions = file($filename); + foreach ($subscriptions as $subscription) { + if (strpos($subscription, ' ') === false) { + // This is an old subscription file. + $subscription = trim($subscription) . " every\n"; + } + if (preg_match(subscription_regex($pre), $subscription, + &$line_matches) === 0) { + continue; + } + $match = array_slice($line_matches, 1); + if (!isset($matches[$cur_page])) { + $matches[$cur_page] = array(); + } + $matches[$cur_page][] = $match; + } + } + return array_reverse($matches); +} + +/** + * 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 + */ +function get_info_subscribed() { + global $ID; + global $conf; + if (!$conf['subscribers']) { + return false; + } + + $subs = subscription_find($ID, array('user' => $_SERVER['REMOTE_USER'])); + if (count($subs) === 0) { + return false; + } + + $_ret = array(); + foreach ($subs as $target => $subs_data) { + $new = array('target' => $target, + 'style' => $subs_data[0][0]); + if (count($subs_data[0]) > 1) { + $new['data'] = $subs_data[0][1]; + } + $_ret[] = $new; + } + + return $_ret; +} + +/** + * Construct a regular expression parsing a subscription definition line + * + * @param array $pre A hash of predefined values; “user”, “style”, and + * “data” may be set to limit the results to + * subscriptions matching these parameters. If + * “escaped” is true, these fields are inserted into the + * regular expression without escaping. + * + * @author Adrian Lang + */ +function subscription_regex($pre = array()) { + if (!isset($pre['escaped']) || $pre['escaped'] === false) { + $pre = array_map('preg_quote_cb', $pre); + } + foreach (array('user', 'style', 'data') as $key) { + if (!isset($pre[$key])) { + $pre[$key] = '(\S+)'; + } + } + return '/^' . $pre['user'] . '(?: ' . $pre['style'] . + '(?: ' . $pre['data'] . ')?)?$/'; +} + +/** + * Return a string with the email addresses of all the + * users subscribed to a page + * + * @param string $id The id of the changed page + * @param bool $self Whether a notice should be sent to the editor if he is + * subscribed + * + * @author Steven Danz + */ +function subscription_addresslist($id, $self=true){ + global $conf; + global $auth; + + if (!$conf['subscribers']) { + return ''; + } + $pres = array('style' => 'every', 'escaped' => true); + if (!$self && isset($_SERVER['REMOTE_USER'])) { + $pres['user'] = '((?:(?!' . preg_quote_cb($_SERVER['REMOTE_USER']) . + ')\S?)+)'; + } + $subs = subscription_find($id, $pres); + $emails = array(); + foreach ($subs as $by_targets) { + foreach ($by_targets as $sub) { + $info = $auth->getUserData($sub[0]); + if ($info === false) continue; + $level = auth_aclcheck($id, $sub[0], $info['grps']); + if ($level >= AUTH_READ) { + if (strcasecmp($info['mail'], $conf['notify']) != 0) { + $emails[$sub[0]] = $info['mail']; + } + } + } + } + return implode(',', $emails); +} + +/** + * Send a digest mail + * + * Sends a digest mail showing a bunch of changes. + * + * @param string $subscriber_mail The target mail address + * @param array $change The newest change + * @param int $lastupdate Time of the last notification + * + * @author Adrian Lang + */ +function subscription_send_digest($subscriber_mail, $change, $lastupdate) { + $id = $change['id']; + $n = 0; + do { + $rev = getRevisions($id, $n++, 1); + $rev = (count($rev) > 0) ? $rev[0] : null; + } while (!is_null($rev) && $rev > $lastupdate); + + $ip = $change['ip']; + $replaces = array('NEWPAGE' => wl($id, '', true, '&'), + 'SUBSCRIBE' => wl($id, array('do' => 'subscribe'), true, '&')); + if (!is_null($rev)) { + $subject = 'changed'; + $replaces['OLDPAGE'] = wl($id, "rev=$rev", true, '&'); + require_once DOKU_INC.'inc/DifferenceEngine.php'; + $df = new Diff(explode("\n", rawWiki($id, $rev)), + explode("\n", rawWiki($id))); + $dformat = new UnifiedDiffFormatter(); + $replaces['DIFF'] = $dformat->format($df); + } else { + $subject = 'newpage'; + $replaces['OLDPAGE'] = 'none'; + $replaces['DIFF'] = rawWiki($id); + } + subscription_send($subscriber_mail, $replaces, $subject, $id, + 'subscr_digest'); +} + +/** + * Send a list mail + * + * Sends a list mail showing a list of changed pages. + * + * @param string $subscriber_mail The target mail address + * @param array $changes Array of changes + * @param string $id The id of the namespace + * + * @author Adrian Lang + */ +function subscription_send_list($subscriber_mail, $changes, $id) { + $list = ''; + foreach ($changes as $change) { + $list .= '* ' . $change['id'] . NL; + } + subscription_send($subscriber_mail, + array('DIFF' => rtrim($list), + 'SUBSCRIBE' => wl($changes[0]['id'], + array('do' => 'subscribe'), + true, '&')), + 'subscribe_list', + prettyprint_id($id), + 'subscr_list'); +} + +/** + * Helper function for sending a mail + * + * @param string $subscriber_mail The target mail address + * @param array $replaces Predefined parameters used to parse the + * template + * @param string $subject The lang id of the mail subject (without the + * prefix “mail_”) + * @param string $id The page or namespace id + * @param string $template The name of the mail template + * + * @author Adrian Lang + */ +function subscription_send($subscriber_mail, $replaces, $subject, $id, $template) { + global $conf; + + $text = rawLocale($template); + $replaces = array_merge($replaces, array('TITLE' => $conf['title'], + 'DOKUWIKIURL' => DOKU_URL, + 'PAGE' => $id)); + + foreach ($replaces as $key => $substitution) { + $text = str_replace('@'.strtoupper($key).'@', $substitution, $text); + } + + global $lang; + $subject = $lang['mail_' . $subject] . ' ' . $id; + mail_send('', '['.$conf['title'].'] '. $subject, $text, + $conf['mailfrom'], '', $subscriber_mail); +} diff --git a/inc/template.php b/inc/template.php index 4681300eb..9b738bf8f 100644 --- a/inc/template.php +++ b/inc/template.php @@ -126,6 +126,9 @@ function tpl_content_core(){ case 'admin': tpl_admin(); break; + case 'subscribe': + tpl_subscribe(); + break; default: $evt = new Doku_Event('TPL_ACT_UNKNOWN',$ACT); if ($evt->advise_before()) @@ -540,31 +543,10 @@ function tpl_button($type,$return=false){ } break; case 'subscribe': - case 'subscription': - if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers'] == 1){ - if($_SERVER['REMOTE_USER']){ - if($INFO['subscribed']){ - if(actionOK('unsubscribe')) - $out .= html_btn('unsubscribe',$ID,'',array('do' => 'unsubscribe',)); - } else { - if(actionOK('subscribe')) - $out .= html_btn('subscribe',$ID,'',array('do' => 'subscribe',)); - } - } - } - if($type == 'subscribe') break; - // else: fall through for backward compatibility - case 'subscribens': - if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers'] == 1){ - if($_SERVER['REMOTE_USER']){ - if($INFO['subscribedns']){ - if(actionOK('unsubscribens')) - $out .= html_btn('unsubscribens',$ID,'',array('do' => 'unsubscribens',)); - } else { - if(actionOK('subscribens')) - $out .= html_btn('subscribens',$ID,'',array('do' => 'subscribens',)); - } - } + if ($conf['useacl'] && $auth && $ACT == 'show' && + $conf['subscribers'] && isset($_SERVER['REMOTE_USER']) && + actionOK('subscribe')) { + $out .= html_btn('subscribe',$ID,'',array('do' => 'subscribe',)); } break; case 'backlink': @@ -712,37 +694,12 @@ function tpl_actionlink($type,$pre='',$suf='',$inner='',$return=false){ break; case 'subscribe': case 'subscription': - if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers'] == 1){ + if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers']) { if($_SERVER['REMOTE_USER']){ - if($INFO['subscribed']) { - if(actionOK('unsubscribe')) - $out .= tpl_link(wl($ID,'do=unsubscribe'), - $pre.(($inner)?$inner:$lang['btn_unsubscribe']).$suf, - 'class="action unsubscribe" rel="nofollow"',1); - } else { if(actionOK('subscribe')) $out .= tpl_link(wl($ID,'do=subscribe'), $pre.(($inner)?$inner:$lang['btn_subscribe']).$suf, 'class="action subscribe" rel="nofollow"',1); - } - } - } - if($type == 'subscribe') break; - // else: fall through for backward compatibility - case 'subscribens': - if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers'] == 1){ - if($_SERVER['REMOTE_USER']){ - if($INFO['subscribedns']) { - if(actionOK('unsubscribens')) - $out .= tpl_link(wl($ID,'do=unsubscribens'), - $pre.(($inner)?$inner:$lang['btn_unsubscribens']).$suf, - 'class="action unsubscribens" rel="nofollow"',1); - } else { - if(actionOK('subscribens')) - $out .= tpl_link(wl($ID,'do=subscribens'), - $pre.(($inner)?$inner:$lang['btn_subscribens']).$suf, - 'class="action subscribens" rel="nofollow"',1); - } } } break; @@ -1323,23 +1280,9 @@ function tpl_actiondropdown($empty='',$button='>'){ echo ''; } - if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers'] == 1){ + if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers']){ if($_SERVER['REMOTE_USER']){ - if($INFO['subscribed']) { - echo ''; - } else { echo ''; - } - } - } - - if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers'] == 1){ - if($_SERVER['REMOTE_USER']){ - if($INFO['subscribedns']) { - echo ''; - } else { - echo ''; - } } } @@ -1406,5 +1349,68 @@ function tpl_include_page($pageid,$print=true){ echo $html; } +/** + * Display the subscribe form + * + * @author Adrian Lang + */ + +function tpl_subscribe() { + global $INFO; + global $ID; + global $lang; + $targets = array($ID => 'the current page', + 'namespace' => 'the namespace “%s”'); + $styles = array('every' => 'a notice on every change', + 'digest' => 'a digest for each changed page*', + 'list' => 'a list of changed pages*'); + + echo p_locale_xhtml('subscr_form'); + + echo '

' . $lang['subscr_m_current_header'] . '

'; + if ($INFO['subscribed'] === false) { + echo '

' . $lang['subscr_m_not_subscribed'] . '

'; + } else { + echo '

' . $lang['subscr_m_current'] . '

'; + echo '
    '; + foreach($INFO['subscribed'] as $sub) { + $form = new Doku_Form(array('class' => 'unsubscribe')); + if ($sub['target'] !== $ID) { + $str = sprintf($targets['namespace'], prettyprint_id($sub['target'])); + } else { + $str = $targets[$ID]; + } + $form->addElement('
  • ' . + sprintf($lang['subscr_m_entry'], $str, + $styles[$sub['style']])); + $form->addElement(form_makeButton('submit', 'subscribe', $lang['subscr_m_delete'])); + $form->addHidden('subscribe_target', $sub['target']); + $form->addHidden('subscribe_style', $sub['style']); + $form->addHidden('subscribe_action', 'unsubscribe'); + $form->addElement('
  • '); + html_form('UNSUBSCRIBE', $form); + } + echo '
'; + } + + echo '

' . $lang['subscr_m_new_header'] . '

'; + if ($INFO['userinfo']['mail'] === '') { + echo $lang['subscr_m_noemail']; + return; + } + $styles['list'] = $styles['list'] . ' (Not allowed for single pages)'; + $form = new Doku_Form(array('id' => 'subscribe')); + $ns = getNS($ID). ':'; + $targets[$ns] = sprintf($targets['namespace'], prettyprint_id($ns)); + unset($targets['namespace']); + $form->addElement('

' . 'Subscribe to' . '

'); + $form->addRadioSet('subscribe_target', $targets); + $form->addElement('

' . 'Receive' . '

'); + $form->addRadioSet('subscribe_style', $styles); + $form->addHidden('subscribe_action', 'subscribe'); + $form->addElement(form_makeButton('submit', 'subscribe', $lang['subscr_m_subscribe'])); + html_form('SUBSCRIBE', $form); +} + //Setup VIM: ex: et ts=4 enc=utf-8 : diff --git a/lib/exe/indexer.php b/lib/exe/indexer.php index 1c4128eb7..eb1556e1d 100644 --- a/lib/exe/indexer.php +++ b/lib/exe/indexer.php @@ -37,6 +37,7 @@ if ($evt->advise_before()) { runIndexer() or metaUpdate() or runSitemapper() or + sendDigest() or runTrimRecentChanges() or runTrimRecentChanges(true) or $evt->advise_after(); @@ -334,6 +335,75 @@ function runSitemapper(){ return true; } +/** + * Send digest and list mails for all subscriptions which are in effect for the + * current page + * + * @author Adrian Lang + */ +function sendDigest() { + require_once DOKU_INC . 'inc/subscription.php'; + echo 'sendDigest(): start'.NL; + global $ID; + global $conf; + if (!$conf['subscribers']) { + return; + } + + $subscriptions = subscription_find($ID, array('style' => '(digest|list)', + 'escaped' => true)); + global $auth; + global $lang; + global $conf; + foreach($subscriptions as $id => $users) { + foreach($users as $data) { + list($user, $style, $lastupdate) = $data; + $lastupdate = (int) $lastupdate; + if ($lastupdate + $conf['subscribe_interval'] > time()) { + // Less than a day passed since last update. + continue; + } + // TODO: Does that suffice for namespaces? + $info = $auth->getUserData($user); + if ($info === false) { + continue; + } + $level = auth_aclcheck($id, $user, $info['grps']); + if ($level < AUTH_READ) { + continue; + } + + if (substr($id, -1, 1) === ':') { + // The subscription target is a namespace + $changes = getRecentsSince($lastupdate, null, getNS($id)); + if (count($changes) === 0) { + continue; + } + if ($style === 'digest') { + foreach($changes as $change) { + subscription_send_digest($info['mail'], $change, + $lastupdate); + } + } elseif ($style === 'list') { + subscription_send_list($info['mail'], $changes, $id); + } + // TODO: Handle duplicate subscriptions. + } else { + $meta = p_get_metadata($id); + $rev = $meta['last_change']['date']; + if ($rev < $lastupdate) { + // There is no new revision. + continue; + } + subscription_send_digest($info['mail'], $meta['last_change'], + $lastupdate); + } + // Update notification time. + subscription_set($id, $user, $style, true); + } + } +} + /** * Formats a timestamp as ISO 8601 date * diff --git a/lib/exe/js.php b/lib/exe/js.php index 38fda1789..8648bf18f 100644 --- a/lib/exe/js.php +++ b/lib/exe/js.php @@ -53,6 +53,7 @@ function js_out(){ DOKU_INC.'lib/scripts/edit.js', DOKU_INC.'lib/scripts/linkwiz.js', DOKU_INC.'lib/scripts/media.js', + DOKU_INC.'lib/scripts/subscriptions.js', DOKU_TPLINC.'script.js', ); diff --git a/lib/scripts/subscriptions.js b/lib/scripts/subscriptions.js new file mode 100644 index 000000000..9f602dde8 --- /dev/null +++ b/lib/scripts/subscriptions.js @@ -0,0 +1,46 @@ +/** + * Hide list subscription style if target is a page + * + * @author Adrian Lang + */ + +addInitEvent(function () { + var form = $('subscribe'); + if (!form) { + return; + } + + var styleradios = {}; + + function update_state() { + if (!this.checked) { + return; + } + if (this.value.match(/:$/)) { + styleradios.list.parentNode.style.display = ''; + } else { + styleradios.list.parentNode.style.display = 'none'; + if (styleradios.list.checked) { + styleradios.digest.checked = 'checked'; + } + } + } + + var cur_sel = null; + + var inputs = form.getElementsByTagName('input'); + for (var i = 0; i < inputs.length ; ++i) { + switch (inputs[i].name) { + case 'subscribe_target': + inputs[i].addEventListener('change', update_state, false); + if (inputs[i].checked) { + cur_sel = inputs[i]; + } + break; + case 'subscribe_style': + styleradios[inputs[i].value] = inputs[i]; + break; + } + } + update_state.call(cur_sel); +}); diff --git a/lib/tpl/default/design.css b/lib/tpl/default/design.css index 4830a9e2c..02804256c 100644 --- a/lib/tpl/default/design.css +++ b/lib/tpl/default/design.css @@ -833,3 +833,17 @@ div.dokuwiki div.imagemeta img.thumb { float: left; margin-right: 0.1em; } + +form#subscribe p { + margin: 0.5em 0; +} + +form#subscribe label { + display:block; + margin: 0 0.5em 0.5em; +} + +form#subscribe fieldset, form#unsubscribe fieldset { + text-align:inherit; + margin: 0; +} diff --git a/lib/tpl/default/main.php b/lib/tpl/default/main.php index 67c3a00ba..b5717c009 100644 --- a/lib/tpl/default/main.php +++ b/lib/tpl/default/main.php @@ -117,7 +117,6 @@ if (!defined('DOKU_INC')) die();
- -- cgit v1.2.3