Commit 17131a35 authored by Robert Lyon's avatar Robert Lyon

Bug 1443284: Allow an unsubscribe link for watchlist emails

This patch:
- adds an 'unsubscribetoken' column to usr_watchlist_view table
 (if we want to unsubscribe other messages we'd need to add an
'unsubscribetoken' to the relevant table)
- records the token when one watches the view
- sends email with unsubscribe link with message type and token to
avoid exposing any user data
- link goes to unsubscribe.php page and either unsubscribes user or
gives generic warning

Currently only working for watchlist notifications being sent via
email/email digest

Change-Id: I823249108f521faaefe3435f03b84ddf73e2d360
Signed-off-by: Robert Lyon's avatarRobert Lyon <robertl@catalyst.net.nz>
parent 59bbecac
......@@ -1790,7 +1790,8 @@ function add_feedback_form_submit(Pieform $form, $values) {
if (!$anonymous && !get_field('usr_watchlist_view', 'ctime', 'usr', $author, 'view', $view->get('id')) && ($author != $owner)) {
insert_record('usr_watchlist_view', (object) array('usr' => $author,
'view' => $view->get('id'),
'ctime' => db_format_timestamp(time())));
'ctime' => db_format_timestamp(time()),
'unsubscribetoken' => get_random_key(24)));
$updatelink = ($artefact) ? get_string('removefromwatchlistartefact', 'view', $view->get('title')) : get_string('removefromwatchlist', 'view');
}
......
......@@ -66,6 +66,7 @@ $string['stopmonitoringfailed'] = 'Failed to stop monitoring';
$string['newwatchlistmessage'] = 'New activity on your watchlist';
$string['newwatchlistmessageview1'] = 'The page "%s" belonging to %s has been changed';
$string['blockinstancenotification'] = 'The block "%s" has been added or changed';
$string['newwatchlistmessageunsubscribe'] = 'To stop receiving notifcations about changes to page "%s" please follow the unsubscribe link %s';
$string['nonamegiven'] = 'no name given';
$string['newviewsubject'] = 'New page created';
......
......@@ -1255,7 +1255,7 @@ class ActivityTypeWatchlist extends ActivityType {
if (is_mysql()) {
$casturl = '?';
}
$sql = 'SELECT u.*, p.method, ap.value AS lang, ' . $casturl . ' AS url
$sql = 'SELECT u.*, wv.unsubscribetoken, p.method, ap.value AS lang, ' . $casturl . ' AS url
FROM {usr_watchlist_view} wv
JOIN {usr} u
ON wv.usr = u.id
......@@ -1326,8 +1326,8 @@ class ActivityTypeWatchlistnotification extends ActivityTypeWatchlist{
$this->blocktitles = $data->blocktitles;
$this->usr = $data->usr;
$this->unsubscribelink = get_config('wwwroot') . 'view/unsubscribe.php?a=watchlist&t=';
$this->unsubscribetype = 'watchlist';
$this->viewinfo = new View($this->view);
}
......
......@@ -1020,12 +1020,16 @@
<FIELD NAME="usr" TYPE="int" LENGTH="10" NOTNULL="true" />
<FIELD NAME="view" TYPE="int" LENGTH="10" NOTNULL="true" />
<FIELD NAME="ctime" TYPE="datetime" NOTNULL="true" />
<FIELD NAME="unsubscribetoken" TYPE="char" LENGTH="24" NOTNULL="false" />
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="usr,view" />
<KEY NAME="usrfk" TYPE="foreign" FIELDS="usr" REFTABLE="usr" REFFIELDS="id" />
<KEY NAME="viewfk" TYPE="foreign" FIELDS="view" REFTABLE="view" REFFIELDS="id" />
</KEYS>
<INDEXES>
<INDEX NAME="unsubscribetokenix" UNIQUE="true" FIELDS="unsubscribetoken"/>
</INDEXES>
</TABLE>
<TABLE NAME="import_queue">
<FIELDS>
......
......@@ -5778,5 +5778,17 @@ function xmldb_core_upgrade($oldversion=0) {
set_config('footerlinks', $footerlinkstr);
}
if ($oldversion < 2018022400) {
log_debug('Create a new "unsubscribetoken" field in "usr_watchlist_view" table');
$table = new XMLDBTable('usr_watchlist_view');
$field = new XMLDBField('unsubscribetoken');
$field->setAttributes(XMLDB_TYPE_CHAR, 24);
add_field($table, $field);
$index = new XMLDBIndex('unsubscribetokenix');
$index->setAttributes(XMLDB_INDEX_UNIQUE, array('unsubscribetoken'));
add_index($table, $index);
}
return $status;
}
......@@ -16,7 +16,7 @@ $config = new stdClass();
// See https://wiki.mahara.org/wiki/Developer_Area/Version_Numbering_Policy
// For upgrades on stable branches, increment the version by one. On master, use the date.
$config->version = 2018022300;
$config->version = 2018022400;
$config->series = '18.04';
$config->release = '18.04dev';
$config->minupgradefrom = 2015030409;
......
......@@ -11,9 +11,16 @@
defined('INTERNAL') || die();
$string['name'] = 'Email';
$string['emailsubject'] = '%s';
$string['emailheader'] = 'You have been sent a notification from %s. Message follows:';
$string['emailfooter'] = 'This is an auto-generated notification from %s. To update your notification preferences, visit %s';
$string['referurl'] = 'See %s';
$string['unsubscribe'] = 'To unsubscribe go to %s';
$string['unsubscribetitle'] = 'Unsubscribe';
$string['unsubscribesuccess'] = 'You have successfully unsubscribed.';
$string['unsubscribefailed'] = 'You have failed to unsubscribe. You have either already unsubscribed or you need to sort this manually. Please log in and visit the related section of Mahara.';
$string['name'] = 'Email';
// Watchlist specific strings
$string['unsubscribe_watchlist'] = 'To remove this from your watchlist go to %s';
$string['unsubscribe_watchlist_heading'] = 'Remove your watchlist notification for "%s"';
......@@ -71,7 +71,14 @@ class PluginNotificationEmail extends PluginNotification {
}
$messagebody .= "\n\n$separator";
// Add unsubscribe link
if (isset($user->unsubscribetoken) && !empty($user->unsubscribetoken)) {
$unsubscribemessage = 'unsubscribe';
if (isset($data->unsubscribetype) && !empty($data->unsubscribetype)) {
$unsubscribemessage .= '_' . $data->unsubscribetype;
}
$messagebody .= "\n\n" . get_string_from_language($lang, $unsubscribemessage, 'notification.email', $data->unsubscribelink . $user->unsubscribetoken);
}
$prefurl = get_config('wwwroot') . 'account/activity/preferences/index.php';
$messagebody .= "\n\n" . get_string_from_language($lang, 'emailfooter', 'notification.email', $sitename, $prefurl);
}
......
......@@ -54,10 +54,10 @@ class PluginNotificationEmaildigest extends PluginNotification {
}
$sql = 'SELECT q.id, u.username, u.firstname, u.lastname, u.preferredname, u.email, u.admin, u.staff,
p.value AS lang, q.*,' . db_format_tsfield('q.ctime', 'qctime').'
p.value AS lang, q.*,' . db_format_tsfield('q.ctime', 'qctime').', a.name AS activitytype
FROM {usr} u
JOIN {notification_emaildigest_queue} q
ON q.usr = u.id
JOIN {notification_emaildigest_queue} q ON q.usr = u.id
JOIN {activity_type} a ON a.id = q.type
LEFT OUTER JOIN {usr_account_preference} p ON (p.usr = u.id AND p.field = \'lang\')
ORDER BY usr,type,q.ctime';
......@@ -81,6 +81,11 @@ class PluginNotificationEmaildigest extends PluginNotification {
}
$queue->nicetype = get_string_from_language($users[$queue->usr]->user->lang,
'type' . $types[$queue->type]->name, $types[$queue->type]->section);
if ($queue->activitytype == 'watchlist' && !empty($queue->url)) {
if (preg_match('/[\?\&]id=(\d+)/', $queue->url, $matches)) {
$queue->unsubscribetoken = get_field('usr_watchlist_view', 'unsubscribetoken', 'usr', $queue->usr, 'view', $matches[1]);
}
}
$users[$queue->usr]->entries[$queue->id] = $queue;
}
}
......@@ -103,6 +108,9 @@ class PluginNotificationEmaildigest extends PluginNotification {
}
$body .= "\n" . $entry->url;
}
if (!empty($entry->unsubscribetoken)) {
$body .= "\n" . get_string_from_language($lang, 'unsubscribe', 'notification.email', get_config('wwwroot') . 'view/unsubscribe.php?a=watchlist&t=' . $entry->unsubscribetoken);
}
$body .= "\n\n";
}
$prefurl = get_config('wwwroot') . 'account/activity/preferences/index.php';
......
......@@ -1706,4 +1706,28 @@ JS;
$this->getSession()->getPage()->fillField($element, $date);
}
/**
* Mimic the clicking of the unsubscribe link in the email
* by supplying what user and page it was for
*
* @Then I unsubscribe from :page owned by :user
*
*/
public function i_unsubscribe_via_link($page, $user) {
$tokens = get_records_sql_array("SELECT unsubscribetoken FROM {usr_watchlist_view} wv
JOIN {usr} u ON u.id = wv.usr
JOIN {view} v ON v.id = wv.view
WHERE (u.username = ? OR CONCAT(u.firstname, ' ', u.lastname) = ?)
AND v.title = ?", array($user, $user, $page));
if (!$tokens) {
throw new Exception(sprintf('Invalid token. No unsubscribetoken found for page "%s" owned by "%s".', $page, $user));
}
if (count($tokens) > 1) {
throw new Exception(sprintf('Too many tokens. More than one unsubscribetoken exists for user "%s" for page "%s".', $user, $page));
}
$token = $tokens[0]->unsubscribetoken;
// Go to the unsubscribe page
$this->visitPath("/view/unsubscribe.php?a=watchlist&t={$token}");
}
}
{include file="header.tpl"}
<p>{$heading}</p>
{if $unsubscribed}
<div class="alert alert-success">{str tag="unsubscribesuccess" section="notification.email"}</div>
{else}
<div class="alert alert-danger">{str tag="unsubscribefailed" section="notification.email"}</div>
{/if}
{include file="footer.tpl"}
......@@ -21,6 +21,7 @@ $data = new StdClass;
$data->view = $viewid;
$data->usr = $USER->get('id');
$data->ctime = db_format_timestamp(time());
$data->unsubscribetoken = get_random_key(24);
$result = new StdClass;
require_once(get_config('libroot') . 'view.php');
......
<?php
/**
*
* @package mahara
* @subpackage core
* @author Catalyst IT Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
define('INTERNAL', 1);
define('PUBLIC', 1);
define('SECTION_PLUGINTYPE', 'core');
define('SECTION_PLUGINNAME', 'site');
define('SECTION_PAGE', 'unsubscribe');
require(dirname(dirname(__FILE__)) . '/init.php');
require('view.php');
$unsubscribed = false;
$viewtitle = false;
if (param_exists('a') && param_exists('t')) {
$type = param_alphanum('a');
$token = param_alphanum('t');
// Currently for watchlist
$validtypes = array('watchlist' => 'usr_watchlist_view');
$table = get_config('dbprefix') . $validtypes[$type];
if ($results = get_records_sql_array("SELECT * FROM ". $table . " WHERE unsubscribetoken = ?", array($token))) {
$viewid = $results[0]->view;
$view = new View($viewid);
$viewtitle = $view->get('title');
delete_records_sql("DELETE FROM ". $table . " WHERE unsubscribetoken = ?", array($token));
$unsubscribed = true;
}
else if ($USER->is_logged_in()) {
// redirect to homepage
redirect('/');
}
}
define('TITLE', get_string('unsubscribetitle', 'notification.email'));
$smarty = smarty();
$smarty->assign('unsubscribed', $unsubscribed);
if ($viewtitle) {
$smarty->assign('heading', get_string('unsubscribe_' . $type . '_heading', 'notification.email', $viewtitle));
}
$smarty->display('unsubscribe.tpl');
@javascript @core @core_view
Feature: Unsubscribing from watchlist via link in email
In order to unsubscribe from watchlist for page I am watching
As a user
I follow unsubscription link in email
Background:
Given the following "users" exist:
| username | password | email | firstname | lastname | institution | authname | role |
| UserA | Kupuhipa1 | UserA@example.org | Angela | User | mahara | internal | member |
| UserB | Kupuhipa1 | UserB@example.org | Bob | User | mahara | internal | member |
And the following "pages" exist:
| title | description | ownertype | ownername |
| Page UserA_01 | Page 01 | user | UserA |
| Page UserA_02 | Page 02 | user | UserA |
And the following "permissions" exist:
| title | accesstype |
| Page UserA_01 | loggedin |
Scenario: Viewing a list of pages I watch from the dashboard (Bug 1444784)
Given I log in as "UserB" with password "Kupuhipa1"
And I choose "Notifications" in "Settings" from user menu
And I select "Email" from "Watchlist"
And I press "Save"
And I am on homepage
And I follow "Page UserA_01"
And I press "More..."
And I follow "Add page to watchlist"
And I should see "This page has been added to your watchlist."
And I trigger cron
# Testing the unsubscribe link in the email sent
And I unsubscribe from "Page UserA_01" owned by "UserB"
And I should see "You have successfully unsubscribed"
And I am on homepage
And I follow "Page UserA_01"
And I press "More..."
And I should see "Add page to watchlist"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment