View Issue Details
ID | Project | Category | View Status | Date Submitted | Last Update |
---|---|---|---|---|---|
3254 | Composr | core_notifications | public | 2017-04-26 00:59 | 2021-11-02 03:41 |
Reporter | Chris Graham | Assigned To | Guest | ||
Priority | normal | Severity | feature | ||
Status | new | Resolution | open | ||
Summary | 3254: Unsubscribing from notifications | ||||
Description | Include unsubscribe links in notifications. This would point to a screen that asks if you want to: 1) Unsubscribe from a specific monitor (e.g. a topic) 2) Unsubscribe from the notification type 3) Disable notifications entirely 4) Delete your account It should also integrate with List-Unsubscribe (see attached issue). | ||||
Tags | Has Patch, Roadmap: Over the horizon, Type: Avoiding e-mail spamblocks | ||||
Attach Tags | |||||
Attached Files | notification_unsubscribe.diff (12,943 bytes)
diff --git a/data/notification_unsubscribe.php b/data/notification_unsubscribe.php new file mode 100644 index 0000000..38aaeb2 --- /dev/null +++ b/data/notification_unsubscribe.php @@ -0,0 +1,59 @@ +<?php /* + + Composr + Copyright (c) ocProducts, 2004-2016 + + See text/EN/licence.txt for full licencing information. + + + NOTE TO PROGRAMMERS: + Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder + **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes **** + +*/ + +/** + * @license http://opensource.org/licenses/cpal_1.0 Common Public Attribution License + * @copyright ocProducts Ltd + * @package core_notifications + */ + +// Fixup SCRIPT_FILENAME potentially being missing +$_SERVER['SCRIPT_FILENAME'] = __FILE__; + +// Find Composr base directory, and chdir into it +global $FILE_BASE, $RELATIVE_PATH; +$FILE_BASE = (strpos(__FILE__, './') === false) ? __FILE__ : realpath(__FILE__); +$FILE_BASE = dirname($FILE_BASE); +if (!is_file($FILE_BASE . '/sources/global.php')) { + $RELATIVE_PATH = basename($FILE_BASE); + $FILE_BASE = dirname($FILE_BASE); +} else { + $RELATIVE_PATH = ''; +} +if (!is_file($FILE_BASE . '/sources/global.php')) { + $FILE_BASE = $_SERVER['SCRIPT_FILENAME']; // this is with symlinks-unresolved (__FILE__ has them resolved); we need as we may want to allow zones to be symlinked into the base directory without getting path-resolved + $FILE_BASE = dirname($FILE_BASE); + if (!is_file($FILE_BASE . '/sources/global.php')) { + $RELATIVE_PATH = basename($FILE_BASE); + $FILE_BASE = dirname($FILE_BASE); + } else { + $RELATIVE_PATH = ''; + } +} +@chdir($FILE_BASE); + +global $FORCE_INVISIBLE_GUEST; +$FORCE_INVISIBLE_GUEST = false; +global $EXTERNAL_CALL; +$EXTERNAL_CALL = false; +if (!is_file($FILE_BASE . '/sources/global.php')) { + exit('<!DOCTYPE html>' . "\n" . '<html lang="EN"><head><title>Critical startup error</title></head><body><h1>Composr startup error</h1><p>The second most basic Composr startup file, sources/global.php, could not be located. This is almost always due to an incomplete upload of the Composr system, so please check all files are uploaded correctly.</p><p>Once all Composr files are in place, Composr must actually be installed by running the installer. You must be seeing this message either because your system has become corrupt since installation, or because you have uploaded some but not all files from our manual installer package: the quick installer is easier, so you might consider using that instead.</p><p>ocProducts maintains full documentation for all procedures and tools, especially those for installation. These may be found on the <a href="http://compo.sr">Composr website</a>. If you are unable to easily solve this problem, we may be contacted from our website and can help resolve it for you.</p><hr /><p style="font-size: 0.8em">Composr is a website engine created by ocProducts.</p></body></html>'); +} +require($FILE_BASE . '/sources/global.php'); + +global $BOOTSTRAPPING; +if (!$BOOTSTRAPPING) { + require_code('notifications2'); + notification_unsubscribe_script(); +} // else we intentionally terminated during global2.php and need to not continue diff --git a/sources/notifications2.php b/sources/notifications2.php index 48ccfb0..7ea61b7 100644 --- a/sources/notifications2.php +++ b/sources/notifications2.php @@ -18,6 +18,50 @@ * @package core_notifications */ +/** + * Notification unsubscribe script. + */ +function notification_unsubscribe_script() +{ + header('X-Robots-Tag: noindex'); + + require_lang('notifications'); + require_code('notifications'); + + $notification_code = get_param_string('notification_code'); + $notification_code_label = get_label_for_notification_code($notification_code); + + $member_id = get_param_integer('member_id'); + + $hash = base64_decode(get_param_string('hash')); + require_code('crypt'); + if (!ratchet_hash_verify($GLOBALS['FORUM_DRIVER']->get_member_email_address($member_id), get_site_salt(), $hash)) { + access_denied('I_ERROR'); + } + + list(, $settings_url) = notification_member_management_details($member_id); + + $db = (substr($notification_code, 0, 4) == 'cns_') ? $GLOBALS['FORUM_DB'] : $GLOBALS['SITE_DB']; + $db->query_delete('notifications_enabled', array( + 'l_member_id' => $member_id, + 'l_notification_code' => substr($notification_code, 0, 80), + )); + + $map = array( + 'l_member_id' => $member_id, + 'l_notification_code' => substr($notification_code, 0, 80), + 'l_code_category' => '', + 'l_setting' => A_NA, + ); + $db->query_insert('notifications_enabled', $map); + + $title = get_screen_title('NOTIFICATION_UNSUBSCRIBE', true, array(escape_html($notification_code_label))); + $message = do_lang_tempcode('NOTIFICATION_UNSUBSCRIBED', escape_html($notification_code_label), escape_html($settings_url->evaluate())); + + $tpl = globalise(inform_screen($title, $message), null, '', true, true); + $tpl->evaluate_echo(); +} + /** * Get a map of notification types available to our member. * diff --git a/sources/notifications.php b/sources/notifications.php index c235afe..bc0df21 100644 --- a/sources/notifications.php +++ b/sources/notifications.php @@ -652,7 +652,13 @@ function _dispatch_notification_to_member($to_member_id, $setting, $notification $to_email = $GLOBALS['FORUM_DRIVER']->get_member_email_address($to_member_id); if ($to_email != '') { $wrapped_subject = do_lang('NOTIFICATION_EMAIL_SUBJECT_WRAP', $subject, comcode_escape(get_site_name())); - $wrapped_message = do_lang($use_real_from ? 'NOTIFICATION_EMAIL_MESSAGE_WRAP_DIRECT_REPLY' : 'NOTIFICATION_EMAIL_MESSAGE_WRAP', $message_to_send, comcode_escape(get_site_name())); + + list($hash, $settings_url) = notification_member_management_details($to_member_id); + $notification_code_label = get_label_for_notification_code($notification_code); + $unsubscribe_url = find_script('notification_unsubscribe') . '?notification_code=' . urlencode($notification_code) . '&hash=' . base64_encode($hash) . '&member_id=' . strval($to_member_id); + $unsubscribe_text = do_lang('NOTIFICATION_UNSUBSCRIBE_LINK', $notification_code_label, $unsubscribe_url, array(get_site_name(), $settings_url->evaluate())); + $message_lang_str = $use_real_from ? 'NOTIFICATION_EMAIL_MESSAGE_WRAP_DIRECT_REPLY' : 'NOTIFICATION_EMAIL_MESSAGE_WRAP'; + $wrapped_message = do_lang($message_lang_str, $message_to_send, comcode_escape(get_site_name()), $unsubscribe_text); mail_wrap( $wrapped_subject, @@ -787,6 +793,47 @@ function _dispatch_notification_to_member($to_member_id, $setting, $notification return $no_cc; } +/** + * Get the label for a notification code. + * + * @param string $notification_code Notification code + * @return string Label + */ +function get_label_for_notification_code($notification_code) +{ + static $cache = array(); + if (isset($cache[$notification_code])) { + return $cache[$notification_code]; + } + require_code('notifications'); + $ob = _get_notification_ob_for_code($notification_code); + $codes = $ob->list_handled_codes(); + $cache[$notification_code] = $codes[$notification_code][1]; + return $cache[$notification_code]; +} + +/** + * Get details needed for notification subscription management. + * + * @param MEMBER $member_id Member + * @return array A tuple: Hash to include in URL, Settings URL + * + * @ignore + */ +function notification_member_management_details($member_id) +{ + require_code('crypt'); + $hash = ratchet_hash($GLOBALS['FORUM_DRIVER']->get_member_email_address($member_id), get_site_salt()); + + if (get_forum_type() == 'cns') { + $settings_url = build_url(array('page' => 'members', 'type' => 'view', 'id' => $member_id), get_module_zone('members'), null, false, false, false, 'tab__edit__notifications'); + } else { + $settings_url = build_url(array('page' => 'notifications', 'type' => 'overall'), get_module_zone('notifications')); + } + + return array($hash, $settings_url); +} + /** * Enable notifications for a member on a notification type+category. * @@ -834,10 +881,10 @@ function enable_notifications($notification_code, $notification_category, $membe } $db->query_delete('notifications_enabled', $map); - if ($setting != A_NA) { - $map['l_setting'] = $setting; - $db->query_insert('notifications_enabled', $map); - } + + // Save new setting. Needs to do this even for A_NA, as otherwise Composr would call up the default upon a missing value + $map['l_setting'] = $setting; + $db->query_insert('notifications_enabled', $map); if (($notification_code == 'comment_posted') && (get_forum_type() == 'cns') && (!is_null($notification_category))) { // Sync comment_posted ones to also monitor the forum ones; no need for opposite way as comment ones already trigger forum ones $topic_id = $GLOBALS['FORUM_DRIVER']->find_topic_id_for_topic_identifier(get_option('comments_forum_name'), $notification_category, do_lang('COMMENT')); @@ -854,7 +901,7 @@ function enable_notifications($notification_code, $notification_category, $membe * Disable notifications for a member on a notification type+category. Chances are you don't want to call this, you want to call enable_notifications with $setting = A_NA. That'll stop the default coming back. * * @param ID_TEXT $notification_code The notification code to use - * @param ?SHORT_TEXT $notification_category The category within the notification code (null: none) + * @param SHORT_TEXT $notification_category The category within the notification code * @param ?MEMBER $member_id The member being de-signed up (null: current member) */ function disable_notifications($notification_code, $notification_category, $member_id = null) @@ -871,7 +918,7 @@ function disable_notifications($notification_code, $notification_category, $memb $db->query_delete('notifications_enabled', array( 'l_member_id' => $member_id, 'l_notification_code' => substr($notification_code, 0, 80), - 'l_code_category' => is_null($notification_category) ? '' : $notification_category, + 'l_code_category' => $notification_category, )); if (($notification_code == 'comment_posted') && (get_forum_type() == 'cns')) { // Sync comment_posted ones to the forum ones diff --git a/lang/EN/notifications.ini b/lang/EN/notifications.ini index 5007f37..17ddab1 100644 --- a/lang/EN/notifications.ini +++ b/lang/EN/notifications.ini @@ -10,8 +10,8 @@ NOTIFICATION_PT_SUBJECT_WRAP=Notification: {1} NOTIFICATION_PT_MESSAGE_WRAP=This message was generated automatically and sent to you due to your notification settings. You cannot reply.\n\n---\n\n\n{1} NOTIFICATION_PT_MESSAGE_WRAP_DIRECT_REPLY=This message was generated automatically and sent to you due to your notification settings. You may reply directly if you wish.\n\n---\n\n\n{1} NOTIFICATION_EMAIL_SUBJECT_WRAP=Notification: {1} -NOTIFICATION_EMAIL_MESSAGE_WRAP=This e-mail from {2} was generated automatically and sent to you due to your notification settings. [i]You should not reply directly.[/i]\n\n{1} -NOTIFICATION_EMAIL_MESSAGE_WRAP_DIRECT_REPLY=This e-mail from {2} was generated automatically and sent to you due to your notification settings. [i]You may reply directly if you wish.[/i]\n\n{1} +NOTIFICATION_EMAIL_MESSAGE_WRAP=This e-mail from {2} was generated automatically and sent to you due to your notification settings. [i]You should not reply directly.[/i]\n\n{1}\n\n---\n\n{3} +NOTIFICATION_EMAIL_MESSAGE_WRAP_DIRECT_REPLY=This e-mail from {2} was generated automatically and sent to you due to your notification settings. [i]You may reply directly if you wish.[/i]\n\n{1}\n\n---\n\n{3} NOTIFICATION_SMS_COMPLETE_WRAP={1} PRIVILEGE_may_enable_staff_notifications=May listen to notifications intended for staff DIGEST_EMAIL_INDIVIDUAL_MESSAGE_WRAP=[title="2" sub="{4}"]{1}[/title]\n\n{2}\n @@ -69,3 +69,6 @@ SENT_SIMPLE=Sent {1} FROM_SIMPLE=From {1} BLOCK_TOP_NOTIFICATIONS=Notifications CONFIG_OPTION_block_top_notifications=Show a notifications button (connected to a notifications overlay). +NOTIFICATION_UNSUBSCRIBE_LINK=[size="0.9"]Review your [url="notification settings"]{4}[/url]. [url="Unsubscribe from all '{1}' notifications"]{2}[/url] sent from {3}.[/size] +NOTIFICATION_UNSUBSCRIBE=Unsubscribe from {1} notification +NOTIFICATION_UNSUBSCRIBED=You have been unsubscribed from the “{1}” notification. If you wish you can review all your <a href="{2}">notification settings</a>. | ||||
Time estimation (hours) | 5 | ||||
Sponsorship open | |||||
|
I have a patch that partly implements this issue, which is why I have tagged for v12. It is not worth delaying v11 for. I actually implementing the patch on the back of a "happy birthday" automatic notification being sent out. That to me made the need for one-click unsubscribe to exist, as not everyone is going to want those emails. |
Date Modified | Username | Field | Change |
---|---|---|---|
2017-04-26 00:59 | Chris Graham | New Issue | |
2017-04-26 00:59 | Chris Graham | Tag Attached: Type: Avoiding e-mail spamblocks | |
2017-04-26 00:59 | Chris Graham | Relationship added | related to 3137 |
2019-06-27 18:51 | Chris Graham | Tag Attached: Roadmap: v12 | |
2021-02-24 18:06 | Chris Graham | Note Added: 0006968 | |
2021-02-24 18:06 | Chris Graham | Note Edited: 0006968 | |
2021-11-01 20:07 | Chris Graham | Tag Attached: Has Patch | |
2021-11-02 03:41 | Chris Graham | File Added: notification_unsubscribe.diff | |
2024-03-26 00:58 | PDStig | Tag Renamed | Roadmap: v12 => Roadmap: Over the horizon |