diff options
31 files changed, 928 insertions, 303 deletions
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..1fda0584e 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,102 @@ function act_export($act){ } /** - * Handle page 'subscribe', 'unsubscribe' + * Handle page 'subscribe' * - * @author Steven Danz <steven-danz@kc.rr.com> - * @todo localize + * Throws exception on error. + * + * @author Adrian Lang <lang@cosmocode.de> */ function act_subscription($act){ - global $ID; - global $INFO; global $lang; + global $INFO; + global $ID; - $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); + // get and preprocess data. + $params = array(); + foreach(array('target', 'style', 'action') as $param) { + if (isset($_REQUEST["sub_$param"])) { + $params[$param] = $_REQUEST["sub_$param"]; } } + // any action given? if not just return and show the subscription page + if(!$params['action']) return $act; + + // Handle POST data, may throw exception. + trigger_event('ACTION_HANDLE_SUBSCRIBE', $params, 'subscription_handle_post'); + + $target = $params['target']; + $style = $params['style']; + $data = $params['data']; + $action = $params['action']; + + // Perform action. + require_once DOKU_INC . 'inc/subscription.php'; + if (!subscription_set($_SERVER['REMOTE_USER'], $target, $style, $data)) { + throw new Exception(sprintf($lang["subscr_{$action}_error"], + hsc($INFO['userinfo']['name']), + prettyprint_id($target))); + } + msg(sprintf($lang["subscr_{$action}_success"], hsc($INFO['userinfo']['name']), + prettyprint_id($target)), 1); + act_redirect($ID, $act); + + // Assure that we have valid data if act_redirect somehow fails. + $INFO['subscribed'] = get_info_subscribed(); return 'show'; } /** - * Handle namespace 'subscribe', 'unsubscribe' + * Validate POST data + * + * Validates POST data for a subscribe or unsubscribe request. This is the + * default action for the event ACTION_HANDLE_SUBSCRIBE. * + * @author Adrian Lang <lang@cosmocode.de> */ -function act_subscriptionns($act){ - global $ID; +function subscription_handle_post(&$params) { global $INFO; global $lang; - if(!getNS($ID)) { - $file = metaFN(getNS($ID),'.mlist'); - $ns = "root"; - } else { - $file = metaFN(getNS($ID),'/.mlist'); - $ns = getNS($ID); + // Get and validate parameters. + if (!isset($params['target'])) { + throw new Exception('no subscription target given'); } - - // 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); + $target = $params['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('style', $valid_styles, $params, + 'invalid subscription style given'); + $action = valid_input_set('action', array('subscribe', 'unsubscribe'), + $params, 'invalid subscription action given'); + + // 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'], + $_SERVER['REMOTE_USER'], + prettyprint_id($target))); } + // subscription_set deletes a subscription if style = null. + $style = null; } - return 'show'; + $data = in_array($style, array('list', 'digest')) ? time() : null; + $params = compact('target', 'style', 'data', 'action'); } //Setup VIM: ex: et ts=2 enc=utf-8 : diff --git a/inc/common.php b/inc/common.php index 85187f16d..9cadb56fd 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,13 @@ 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); + $data = array('id' => $id, 'addresslist' => '', 'self' => false); + trigger_event('COMMON_NOTIFY_ADDRESSLIST', $data, + 'subscription_addresslist'); + $bcc = $data['addresslist']; 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 +1100,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{ @@ -1273,97 +1276,6 @@ function obfuscate($email) { } /** - * Let us know if a user is tracking a page or a namespace - * - * @author Andreas Gohr <andi@splitbrain.org> - */ -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 <steven-danz@kc.rr.com> - */ -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 * * @author Andreas Gohr <andi@splitbrain.org> @@ -1545,4 +1457,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 <lang@cosmocode.de> + */ +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 5384593c7..7850682ba 100644 --- a/inc/confutils.php +++ b/inc/confutils.php @@ -249,7 +249,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 <lang@cosmocode.de> + */ + + 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/html.php b/inc/html.php index 29dddbe74..8a215f440 100644 --- a/inc/html.php +++ b/inc/html.php @@ -91,14 +91,18 @@ function html_secedit_button($matches){ global $ID; global $INFO; - $section = $matches[2]; - $name = $matches[1]; + $edittarget = ($matches[1] === 'SECTION') ? 'plain' : + strtolower($matches[1]); + + $section = $matches[3]; + $name = $matches[2]; $secedit = ''; - $secedit .= '<div class="secedit">'; + $secedit .= '<div class="secedit editbutton_' . $edittarget . '">'; $secedit .= html_btn('secedit',$ID,'', array('do' => 'edit', - 'lines' => "$section", + 'lines' => $section, + 'edittarget' => $edittarget, 'rev' => $INFO['lastmod']), 'post', $name); $secedit .= '</div>'; @@ -113,11 +117,13 @@ function html_secedit_button($matches){ function html_secedit($text,$show=true){ global $INFO; + $regexp = '#<!-- ([A-Z]+) (?:"(.*)" )?\[(\d+-\d*)\] -->#'; + if($INFO['writable'] && $show && !$INFO['rev']){ - $text = preg_replace_callback('#<!-- SECTION "(.*?)" \[(\d+-\d*)\] -->#', + $text = preg_replace_callback($regexp, 'html_secedit_button', $text); }else{ - $text = preg_replace('#<!-- SECTION "(.*?)" \[(\d+-\d*)\] -->#','',$text); + $text = preg_replace($regexp,'',$text); } return $text; @@ -181,7 +187,7 @@ function html_btn($name,$id,$akey,$params,$method='get',$tooltip=''){ $tip = htmlspecialchars($label); } - $ret .= '<input type="submit" value="'.htmlspecialchars($label).'" class="button" '; + $ret .= '<input type="submit" value="'.hsc($label).'" class="button" '; if($akey){ $tip .= ' ['.strtoupper($akey).']'; $ret .= 'accesskey="'.$akey.'" '; @@ -1104,11 +1110,9 @@ function html_updateprofile(){ } /** - * This displays the edit form (lots of logic included) + * Preprocess edit form data * - * @fixme this is a huge lump of code and should be modularized * @triggers HTML_PAGE_FROMTEMPLATE - * @triggers HTML_EDITFORM_INJECTION * @author Andreas Gohr <andi@splitbrain.org> */ function html_edit($text=null,$include='edit'){ //FIXME: include needed? @@ -1122,7 +1126,6 @@ function html_edit($text=null,$include='edit'){ //FIXME: include needed? global $SUM; global $lang; global $conf; - global $license; //set summary default if(!$SUM){ @@ -1176,20 +1179,44 @@ function html_edit($text=null,$include='edit'){ //FIXME: include needed? print p_locale_xhtml('read'); } if(!$DATE) $DATE = $INFO['lastmod']; - ?> - <div style="width:99%;"> - <div class="toolbar"> - <div id="draft__status"><?php if(!empty($INFO['draft'])) echo $lang['draftdate'].' '.dformat();?></div> - <div id="tool__bar"><?php if($wr){?><a href="<?php echo DOKU_BASE?>lib/exe/mediamanager.php?ns=<?php echo $INFO['namespace']?>" - target="_blank"><?php echo $lang['mediaselect'] ?></a><?php }?></div> + $data = compact('wr', 'text', 'mod', 'check'); + trigger_event('HTML_EDIT_FORMSELECTION', $data, 'html_edit_form', true); +} +/** + * Display the default edit form + * + * Is the default action for HTML_EDIT_FORMSELECTION. + * + * @triggers HTML_EDITFORM_OUTPUT + */ +function html_edit_form($param) { + extract($param); + global $conf; + global $license; + global $lang; + global $REV; + global $DATE; + global $PRE; + global $SUF; + global $INFO; + global $SUM; + global $ID; + ?> <?php if($wr){?> <script type="text/javascript" charset="utf-8"><!--//--><![CDATA[//><!-- <?php /* sets changed to true when previewed */?> textChanged = <?php ($mod) ? print 'true' : print 'false' ?>; //--><!]]></script> <?php } ?> + <div style="width:99%;"> + + <div class="toolbar"> + <div id="draft__status"><?php if(!empty($INFO['draft'])) echo $lang['draftdate'].' '.dformat();?></div> + <div id="tool__bar"><?php if($wr){?><a href="<?php echo DOKU_BASE?>lib/exe/mediamanager.php?ns=<?php echo $INFO['namespace']?>" + target="_blank"><?php echo $lang['mediaselect'] ?></a><?php }?></div> + </div> <?php $form = new Doku_Form(array('id' => 'dw__editform')); diff --git a/inc/lang/de/lang.php b/inc/lang/de/lang.php index d0c1f8b70..c3fefd926 100644 --- a/inc/lang/de/lang.php +++ b/inc/lang/de/lang.php @@ -203,11 +203,25 @@ $lang['img_copyr'] = 'Copyright'; $lang['img_format'] = 'Format'; $lang['img_camera'] = 'Kamera'; $lang['img_keywords'] = 'Schlagwörter'; -$lang['subscribe_success'] = '%s hat nun Änderungen der Seite %s abonniert'; -$lang['subscribe_error'] = '%s kann die Änderungen der Seite %s nicht abonnieren'; -$lang['subscribe_noaddress'] = 'Weil Ihre E-Mail-Adresse fehlt, können Sie das Thema nicht abonnieren'; -$lang['unsubscribe_success'] = 'Das Abonnement von %s für die Seite %s wurde aufgelöst'; -$lang['unsubscribe_error'] = 'Das Abonnement von %s für die Seite %s konnte nicht aufgelöst werden'; + +$lang['subscr_subscribe_success'] = '%s hat nun Änderungen der Seite %s abonniert'; +$lang['subscr_subscribe_error'] = '%s kann die Änderungen der Seite %s nicht abonnieren'; +$lang['subscr_subscribe_noaddress']= 'Weil Ihre E-Mail-Adresse fehlt, können Sie das Thema nicht abonnieren'; +$lang['subscr_unsubscribe_success']= 'Das Abonnement von %s für die Seite %s wurde aufgelöst'; +$lang['subscr_unsubscribe_error'] = 'Das Abonnement von %s für die Seite %s konnte nicht aufgelöst werden'; +$lang['subscr_already_subscribed'] = '%s hat %s bereits abonniert'; +$lang['subscr_not_subscribed'] = '%s hat %s nicht abonniert'; +// Manage page for subscriptions +$lang['subscr_m_not_subscribed'] = 'Sie haben die aktuelle Seite und ihre Namensräume nicht abonniert.'; +$lang['subscr_m_new_header'] = 'Abonnement hinzufügen'; +$lang['subscr_m_current_header'] = 'Aktuelle Abonnements'; +$lang['subscr_m_unsubscribe'] = 'Löschen'; +$lang['subscr_m_subscribe'] = 'Abonnieren'; + +$lang['subscr_style_every'] = 'Email bei jeder Bearbeitung'; +$lang['subscr_style_digest'] = 'Übersichtsemail für jede veränderte Seite'; +$lang['subscr_style_list'] = 'Email mit Liste der geänderten Seiten'; + $lang['authmodfailed'] = 'Benutzerüberprüfung nicht möglich. Bitte wenden Sie sich an den Systembetreuer.'; $lang['authtempfail'] = 'Benutzerüberprüfung momentan nicht möglich. Falls das Problem andauert, wenden Sie sich an den Systembetreuer.'; $lang['i_chooselang'] = 'Wählen Sie Ihre Sprache'; diff --git a/inc/lang/en/lang.php b/inc/lang/en/lang.php index cf5173d05..98ded12ca 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,24 @@ $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_already_subscribed'] = '%s is already subscribed to %s'; +$lang['subscr_not_subscribed'] = '%s is not subscribed to %s'; +// Manage page for subscriptions +$lang['subscr_m_not_subscribed'] = 'You are currently not subscribed to the current page or namespace.'; +$lang['subscr_m_new_header'] = 'Add subscription'; +$lang['subscr_m_current_header'] = 'Current subscriptions'; +$lang['subscr_m_unsubscribe'] = 'Unsubscribe'; +$lang['subscr_m_subscribe'] = 'Subscribe'; +$lang['subscr_m_receive'] = 'Receive'; +$lang['subscr_style_every'] = 'email on every change'; +$lang['subscr_style_digest'] = 'digest email of changes for each page'; +$lang['subscr_style_list'] = 'list of changed pages since last email'; + /* 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..d606508c6 --- /dev/null +++ b/inc/lang/en/subscr_form.txt @@ -0,0 +1,3 @@ +====== Subscription Management ====== + +This page allows you to manage your subscriptions for the current page and namespace. 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/subscribermail.txt b/inc/lang/en/subscr_single.txt index 673c4c32a..673c4c32a 100644 --- a/inc/lang/en/subscribermail.txt +++ b/inc/lang/en/subscr_single.txt 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 <lang@cosmocode.de> + */ +function prettyprint_id($id) { + if (!$id || $id === ':') { + return '*'; + } + if ((substr($id, -1, 1) === ':')) { + $id .= '*'; + } + return hsc($id); +} diff --git a/inc/parser/handler.php b/inc/parser/handler.php index eaa42f9bf..ea5a69b35 100644 --- a/inc/parser/handler.php +++ b/inc/parser/handler.php @@ -589,10 +589,11 @@ class Doku_Handler { } else { $this->_addCall('tablecell', array(), $pos); } + $this->status['table_begin'] = $pos; break; case DOKU_LEXER_EXIT: - $this->_addCall('table_end', array(), $pos); + $this->_addCall('table_end', array($this->status['table_begin']+1, $pos), $pos); $this->CallWriter->process(); $ReWriter = & $this->CallWriter; $this->CallWriter = & $ReWriter->CallWriter; @@ -1222,7 +1223,7 @@ class Doku_Handler_Table { } function tableEnd($call) { - $this->tableCalls[] = array('table_close',array(),$call[2]); + $this->tableCalls[] = array('table_close',$call[1],$call[2]); $this->finalizeTable(); } diff --git a/inc/parser/renderer.php b/inc/parser/renderer.php index 6082e935d..65dcaf8a1 100644 --- a/inc/parser/renderer.php +++ b/inc/parser/renderer.php @@ -233,7 +233,7 @@ class Doku_Renderer extends DokuWiki_Plugin { function table_open($maxcols = NULL, $numrows = NULL){} - function table_close(){} + function table_close($begin, $end){} function tablerow_open(){} diff --git a/inc/parser/xhtml.php b/inc/parser/xhtml.php index 20acf4281..4e848ec1d 100644 --- a/inc/parser/xhtml.php +++ b/inc/parser/xhtml.php @@ -851,8 +851,9 @@ class Doku_Renderer_xhtml extends Doku_Renderer { $this->doc .= '<table class="inline">'.DOKU_LF; } - function table_close(){ + function table_close($begin, $end){ $this->doc .= '</table>'.DOKU_LF; + $this->doc .= '<!-- TABLE ['. $begin .'-'.$end.'] -->'; } function tablerow_open(){ diff --git a/inc/subscription.php b/inc/subscription.php new file mode 100644 index 000000000..12f0a4b82 --- /dev/null +++ b/inc/subscription.php @@ -0,0 +1,349 @@ +<?php +/** + * Utilities for handling (email) subscriptions + * + * The public interface of this file consists of the functions + * - subscription_find + * - subscription_send_digest + * - subscription_send_list + * - subscription_set + * - get_info_subscribed + * - subscription_addresslist + * + * @author Adrian Lang <lang@cosmocode.de> + * @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 <lang@cosmocode.de> + */ +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 $user The subscriber or unsubscriber + * @param string $page The target object (page or namespace), specified by + * id; Namespaces are identified by a trailing colon. + * @param string $style The subscribe style; DokuWiki currently implements + * “every”, “digest”, and “list”. + * @param string $data An optional data blob + * @param bool $overwrite Whether an existing subscription may be overwritten + * + * @author Adrian Lang <lang@cosmocode.de> + */ +function subscription_set($user, $page, $style, $data = null, + $overwrite = false) { + 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($user, $page, null)) { + return false; + } + } + + $file = subscription_filename($page); + $content = auth_nameencode($user) . ' ' . $style; + if (!is_null($data)) { + $content .= ' ' . $data; + } + 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 <lang@cosmocode.de> + */ +function subscription_find($page, $pre) { + // Construct list of files which may contain relevant subscriptions. + $filenames = array(':' => subscription_filename(':')); + 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 <lang@cosmocode.de> + */ +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 <lang@cosmocode.de> + */ +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 + * + * This is the default action for COMMON_NOTIFY_ADDRESSLIST. + * + * @param array $data Containing $id (the page id), $self (whether the author + * should be notified, $addresslist (current email address + list) + * + * @author Steven Danz <steven-danz@kc.rr.com> + * @author Adrian Lang <lang@cosmocode.de> + */ +function subscription_addresslist($data){ + global $conf; + global $auth; + + $id = $data['id']; + $self = $data['self']; + $addresslist = $data['addresslist']; + + 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']; + } + } + } + } + $data['addresslist'] = trim($addresslist . ',' . 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 <lang@cosmocode.de> + */ +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 <lang@cosmocode.de> + */ +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 <lang@cosmocode.de> + */ +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..84fbda051 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 '<option value="profile">'.$lang['btn_profile'].'</option>'; } - 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 '<option value="unsubscribe">'.$lang['btn_unsubscribe'].'</option>'; - } else { echo '<option value="subscribe">'.$lang['btn_subscribe'].'</option>'; - } - } - } - - if($conf['useacl'] && $auth && $ACT == 'show' && $conf['subscribers'] == 1){ - if($_SERVER['REMOTE_USER']){ - if($INFO['subscribedns']) { - echo '<option value="unsubscribens">'.$lang['btn_unsubscribens'].'</option>'; - } else { - echo '<option value="subscribens">'.$lang['btn_subscribens'].'</option>'; - } } } @@ -1406,5 +1349,70 @@ function tpl_include_page($pageid,$print=true){ echo $html; } +/** + * Display the subscribe form + * + * @author Adrian Lang <lang@cosmocode.de> + */ +function tpl_subscribe() { + global $INFO; + global $ID; + global $lang; + + echo p_locale_xhtml('subscr_form'); + echo '<h2>' . $lang['subscr_m_current_header'] . '</h2>'; + echo '<div class="level2">'; + if ($INFO['subscribed'] === false) { + echo '<p>' . $lang['subscr_m_not_subscribed'] . '</p>'; + } else { + echo '<ul>'; + foreach($INFO['subscribed'] as $sub) { + echo '<li><div class="li">'; + if ($sub['target'] !== $ID) { + echo '<code class="ns">'.hsc(prettyprint_id($sub['target'])).'</code>'; + } else { + echo '<code class="page">'.hsc(prettyprint_id($sub['target'])).'</code>'; + } + $sstl = $lang['subscr_style_'.$sub['style']]; + if(!$sstl) $sstl = hsc($sub['style']); + echo ' ('.$sstl.') '; + + echo '<a href="'.wl($ID,array('do'=>'subscribe','sub_target'=>$sub['target'],'sub_style'=>$sub['style'],'sub_action'=>'unsubscribe')).'" class="unsubscribe">'.$lang['subscr_m_unsubscribe'].'</a>'; + + + echo '</div></li>'; + } + echo '</ul>'; + } + echo '</div>'; + + // Add new subscription form + echo '<h2>' . $lang['subscr_m_new_header'] . '</h2>'; + echo '<div class="level2">'; + $ns = getNS($ID).':'; + $targets = array( + $ID => '<code class="page">'.prettyprint_id($ID).'</code>', + $ns => '<code class="ns">'.prettyprint_id($ns).'</code>', + ); + $styles = array( + 'every' => $lang['subscr_style_every'], + 'digest' => $lang['subscr_style_digest'], + 'list' => $lang['subscr_style_list'], + ); + + $form = new Doku_Form(array('id' => 'subscribe__form')); + $form->startFieldset($lang['subscr_m_subscribe']); + $form->addRadioSet('sub_target', $targets); + $form->startFieldset($lang['subscr_m_receive']); + $form->addRadioSet('sub_style', $styles); + $form->addHidden('sub_action', 'subscribe'); + $form->addHidden('do', 'subscribe'); + $form->addHidden('id', $ID); + $form->endFieldset(); + $form->addElement(form_makeButton('submit', 'subscribe', $lang['subscr_m_subscribe'])); + html_form('SUBSCRIBE', $form); + echo '</div>'; +} + //Setup VIM: ex: et ts=4 enc=utf-8 : diff --git a/lib/exe/indexer.php b/lib/exe/indexer.php index 1c4128eb7..84eb9d482 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(); @@ -335,6 +336,84 @@ function runSitemapper(){ } /** + * Send digest and list mails for all subscriptions which are in effect for the + * current page + * + * @author Adrian Lang <lang@cosmocode.de> + */ +function sendDigest() { + echo 'sendDigest(): start'.NL; + global $ID; + global $conf; + if (!$conf['subscribers']) { + return; + } + require_once DOKU_INC . 'inc/subscription.php'; + $subscriptions = subscription_find($ID, array('style' => '(digest|list)', + 'escaped' => true)); + global $auth; + global $lang; + global $conf; + global $USERINFO; + + // remember current user info + $olduinfo = $USERINFO; + $olduser = $_SERVER['REMOTE_USER']; + + 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; + } + + // Work as the user to make sure ACLs apply correctly + $USERINFO = $auth->getUserData($user); + $_SERVER['REMOTE_USER'] = $user; + if ($USERINFO === false) { + 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($USERINFO['mail'], $change, + $lastupdate); + } + } elseif ($style === 'list') { + subscription_send_list($USERINFO['mail'], $changes, $id); + } + // TODO: Handle duplicate subscriptions. + } else { + if(auth_quickaclcheck($id) < AUTH_READ) continue; + + $meta = p_get_metadata($id); + $rev = $meta['last_change']['date']; + if ($rev < $lastupdate) { + // There is no new revision. + continue; + } + subscription_send_digest($USERINFO['mail'], $meta['last_change'], + $lastupdate); + } + // Update notification time. + subscription_set($user, $id, $style, time(), true); + } + } + + // restore current user info + $USERINFO = $olduinfo; + $_SERVER['REMOTE_USER'] = $olduser; +} + +/** * Formats a timestamp as ISO 8601 date * * @author <ungu at terong dot com> 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/plugins/config/lang/en/lang.php b/lib/plugins/config/lang/en/lang.php index 25103278c..45b653680 100644 --- a/lib/plugins/config/lang/en/lang.php +++ b/lib/plugins/config/lang/en/lang.php @@ -130,6 +130,7 @@ $lang['gdlib'] = 'GD Lib version'; $lang['im_convert'] = 'Path to ImageMagick\'s convert tool'; $lang['jpg_quality'] = 'JPG compression quality (0-100)'; $lang['subscribers'] = 'Enable page subscription support'; +$lang['subscribe_time'] = 'Time after which subscription lists and digests are sent (sec); This should be larger than the time specified in recent_days.'; $lang['compress'] = 'Compact CSS and javascript output'; $lang['hidepages'] = 'Hide matching pages (regular expressions)'; $lang['send404'] = 'Send "HTTP 404/Page Not Found" for non existing pages'; diff --git a/lib/plugins/config/settings/config.metadata.php b/lib/plugins/config/settings/config.metadata.php index 742faf387..da2ba4aef 100644 --- a/lib/plugins/config/settings/config.metadata.php +++ b/lib/plugins/config/settings/config.metadata.php @@ -148,6 +148,7 @@ $meta['htmlok'] = array('onoff'); $meta['phpok'] = array('onoff'); $meta['notify'] = array('email', '_multiple' => true); $meta['subscribers'] = array('onoff'); +$meta['subscribe_time'] = array('numeric'); $meta['locktime'] = array('numeric'); $meta['cachetime'] = array('numeric'); diff --git a/lib/scripts/drag.js b/lib/scripts/drag.js index 4726b77de..fa249a996 100644 --- a/lib/scripts/drag.js +++ b/lib/scripts/drag.js @@ -1,8 +1,9 @@ /** * Makes a DOM object draggable * - * This is currently for movable DOM dialogs only. If needed it could be - * extended to execute callbacks on special events... + * If you just want to move objects around, use drag.attach. For full + * customization, drag can be used as a javascript prototype, it is + * inheritance-aware. * * @link http://nofunc.org/Drag_Drop/ */ @@ -26,33 +27,36 @@ var drag = { attach: function (obj,handle) { if(handle){ handle.dragobject = obj; - addEvent($(handle),'mousedown',drag.start); }else{ - addEvent($(obj),'mousedown',drag.start); + handle = obj; } + var _this = this; + addEvent($(handle),'mousedown',function (e) {return _this.start(e); }); }, /** * Starts the dragging operation */ start: function (e){ - drag.handle = e.target; - if(drag.handle.dragobject){ - drag.obj = drag.handle.dragobject; + this.handle = e.target; + if(this.handle.dragobject){ + this.obj = this.handle.dragobject; }else{ - drag.obj = drag.handle; + this.obj = this.handle; } - drag.handle.className += ' ondrag'; - drag.obj.className += ' ondrag'; + this.handle.className += ' ondrag'; + this.obj.className += ' ondrag'; - drag.oX = parseInt(drag.obj.style.left); - drag.oY = parseInt(drag.obj.style.top); - drag.eX = drag.evX(e); - drag.eY = drag.evY(e); + this.oX = parseInt(this.obj.style.left); + this.oY = parseInt(this.obj.style.top); + this.eX = drag.evX(e); + this.eY = drag.evY(e); - addEvent(document,'mousemove',drag.drag); - addEvent(document,'mouseup',drag.stop); + var _this = this; + this.mousehandlers = [function (e) {return _this.drag(e);}, function (e) {return _this.stop(e);}]; + addEvent(document,'mousemove', this.mousehandlers[0]); + addEvent(document,'mouseup', this.mousehandlers[1]); e.preventDefault(); e.stopPropagation(); @@ -63,21 +67,21 @@ var drag = { * Ends the dragging operation */ stop: function(){ - drag.handle.className = drag.handle.className.replace(/ ?ondrag/,''); - drag.obj.className = drag.obj.className.replace(/ ?ondrag/,''); - removeEvent(document,'mousemove',drag.drag); - removeEvent(document,'mouseup',drag.stop); - drag.obj = null; - drag.handle = null; + this.handle.className = this.handle.className.replace(/ ?ondrag/,''); + this.obj.className = this.obj.className.replace(/ ?ondrag/,''); + removeEvent(document,'mousemove', this.mousehandlers[0]); + removeEvent(document,'mouseup', this.mousehandlers[1]); + this.obj = null; + this.handle = null; }, /** * Moves the object during the dragging operation */ drag: function(e) { - if(drag.obj) { - drag.obj.style.top = (drag.evY(e)+drag.oY-drag.eY+'px'); - drag.obj.style.left = (drag.evX(e)+drag.oX-drag.eX+'px'); + if(this.obj) { + this.obj.style.top = (this.evY(e)+this.oY-this.eY+'px'); + this.obj.style.left = (this.evX(e)+this.oX-this.eX+'px'); } }, @@ -96,4 +100,3 @@ var drag = { } }; - diff --git a/lib/scripts/edit.js b/lib/scripts/edit.js index d7391b7e7..4d8ead858 100644 --- a/lib/scripts/edit.js +++ b/lib/scripts/edit.js @@ -12,12 +12,15 @@ * * @author Andreas Gohr <andi@splitbrain.org> */ -function createToolButton(icon,label,key,id){ +function createToolButton(icon,label,key,id,classname){ var btn = document.createElement('button'); var ico = document.createElement('img'); // preapare the basic button stuff btn.className = 'toolbutton'; + if(classname){ + btn.className += ' '+classname; + } btn.title = label; if(key){ btn.title += ' ['+key.toUpperCase()+']'; diff --git a/lib/scripts/script.js b/lib/scripts/script.js index ccba82144..b611f980a 100644 --- a/lib/scripts/script.js +++ b/lib/scripts/script.js @@ -538,24 +538,41 @@ addInitEvent(function(){ var break_classes = new RegExp('secedit|toc|page'); var btns = getElementsByClass('btn_secedit',document,'form'); for(var i=0; i<btns.length; i++){ - addEvent(btns[i],'mouseover',function(e){ - var tgt = e.target; - if(tgt.form) tgt = tgt.form; - tgt = tgt.parentNode.previousSibling; - if(tgt.nodeName != "DIV") tgt = tgt.previousSibling; - while(!break_classes.test(tgt.className)) { - tgt.className += ' section_highlight'; - if (tgt.tagName == 'H1') break; - tgt = (tgt.previousSibling != null) ? tgt.previousSibling : tgt.parentNode; - } - }); + switch(btns[i].parentNode.className.match(/editbutton_(\w+)/)[1]) { + case 'plain': + addEvent(btns[i],'mouseover',function(e){ + var tgt = e.target.form.parentNode; + do { + tgt = tgt.previousSibling; + } while (tgt && !tgt.tagName); + if (!tgt) return; + if(tgt.nodeName != "DIV") tgt = tgt.previousSibling; + while(!break_classes.test(tgt.className)) { + tgt.className += ' section_highlight'; + if (tgt.tagName == 'H1') break; + tgt = (tgt.previousSibling != null) ? tgt.previousSibling : tgt.parentNode; + } + }); + break; + + case 'table': + addEvent(btns[i],'mouseover',function(e){ + var tgt = e.target.form.parentNode; + do { + tgt = tgt.previousSibling; + } while (tgt && !tgt.tagName); + if (tgt && tgt.tagName === 'TABLE') { + tgt.className += ' section_highlight'; + } + }); + break; + } addEvent(btns[i],'mouseout',function(e){ var secs = getElementsByClass('section_highlight'); for(var j=0; j<secs.length; j++){ secs[j].className = secs[j].className.replace(/section_highlight/,''); } - var secs = getElementsByClass('section_highlight'); }); } }); diff --git a/lib/scripts/subscriptions.js b/lib/scripts/subscriptions.js new file mode 100644 index 000000000..d701f258f --- /dev/null +++ b/lib/scripts/subscriptions.js @@ -0,0 +1,46 @@ +/** + * Hide list subscription style if target is a page + * + * @author Adrian Lang <lang@cosmocode.de> + */ + +addInitEvent(function () { + var form = $('subscribe__form'); + 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 'sub_target': + addEvent(inputs[i], 'click', update_state); + if (inputs[i].checked) { + cur_sel = inputs[i]; + } + break; + case 'sub_style': + styleradios[inputs[i].value] = inputs[i]; + break; + } + } + update_state.call(cur_sel); +}); diff --git a/lib/scripts/toolbar.js b/lib/scripts/toolbar.js index 1e4a91864..99ad1bb9c 100644 --- a/lib/scripts/toolbar.js +++ b/lib/scripts/toolbar.js @@ -27,7 +27,9 @@ function initToolbar(tbid,edid,tb){ // create new button var btn = createToolButton(tb[i]['icon'], tb[i]['title'], - tb[i]['key']); + tb[i]['key'], + tb[i]['id'], + tb[i]['class']); // type is a tb function -> assign it as onclick diff --git a/lib/tpl/default/_subscription.css b/lib/tpl/default/_subscription.css new file mode 100644 index 000000000..0792c8c21 --- /dev/null +++ b/lib/tpl/default/_subscription.css @@ -0,0 +1,21 @@ +/** + * Styles for the subscription page + */ + +form#subscribe__form { + display: block; + width: 300px; + text-align: center; +} + +form#subscribe__form fieldset { + text-align: left; + margin: 0.5em 0; +} + +form#subscribe__form label { + display:block; + margin: 0 0.5em 0.5em; +} + + diff --git a/lib/tpl/default/design.css b/lib/tpl/default/design.css index 4830a9e2c..cd4d03e43 100644 --- a/lib/tpl/default/design.css +++ b/lib/tpl/default/design.css @@ -741,8 +741,8 @@ div.dokuwiki ul.search_quickhits li { width: 30%; } -div.dokuwiki div.section_highlight { - background-color: __background_alt__; +div.dokuwiki .section_highlight { + background-color: __background_alt__ !important; } /* ------------------ Additional ---------------------- */ @@ -833,3 +833,4 @@ div.dokuwiki div.imagemeta img.thumb { float: left; margin-right: 0.1em; } + 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(); </div> <div class="bar-right" id="bar__bottomright"> <?php tpl_button('subscribe')?> - <?php tpl_button('subscribens')?> <?php tpl_button('admin')?> <?php tpl_button('profile')?> <?php tpl_button('login')?> diff --git a/lib/tpl/default/style.ini b/lib/tpl/default/style.ini index dfd5500fa..84d04e743 100644 --- a/lib/tpl/default/style.ini +++ b/lib/tpl/default/style.ini @@ -10,9 +10,10 @@ layout.css = screen design.css = screen style.css = screen -media.css = screen -_admin.css = screen -_linkwiz.css = screen +media.css = screen +_admin.css = screen +_linkwiz.css = screen +_subscription.css = screen rtl.css = rtl print.css = print |