View Issue Details

IDProjectCategoryView StatusLast Update
3254Composrcore_notificationspublic2021-11-02 03:41
ReporterChris Graham Assigned ToGuest  
PrioritynormalSeverityfeature 
Status newResolutionopen 
Summary3254: Unsubscribing from notifications
DescriptionInclude 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).
TagsHas 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 &ldquo;{1}&rdquo; notification. If you wish you can review all your <a href="{2}">notification settings</a>.
notification_unsubscribe.diff (12,943 bytes)   
Time estimation (hours)5
Sponsorship open

Sponsor

Date Added Member Amount Sponsored

Relationships

related to 3137 Not AssignedGuest Support List-Unsubscribe header 

Activities

Chris Graham

2021-02-24 18:06

administrator   ~6968

Last edited: 2021-02-24 18:06

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.

Add Note

View Status
Note
Upload Files
Maximum size: 32,768 KiB

Attach files by dragging & dropping, selecting or pasting them.
You are not logged in You are not logged in. This means you will not get any e-mail notifications. And if you reply, we will not know for sure you are the original poster of the issue.

Issue History

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