Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

5.64 one click #7

Open
wants to merge 12 commits into
base: 5.64
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CRM/Admin/Form/Setting/Mail.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class CRM_Admin_Form_Setting_Mail extends CRM_Admin_Form_Setting {
// dev/core#1768 Make this interval configurable.
'civimail_sync_interval' => CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME,
'replyTo' => CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME,
'civimail_unsubscribe_methods' => CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME,
];

/**
Expand Down
2 changes: 1 addition & 1 deletion CRM/Mailing/Form/Unsubscribe.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function preProcess() {
throw new CRM_Core_Exception(ts("There was an error in your request"));
}

list($displayName, $email) = CRM_Mailing_Event_BAO_MailingEventQueue::getContactInfo($queue_id);
[$displayName, $email] = CRM_Mailing_Event_BAO_MailingEventQueue::getContactInfo($queue_id);
$this->assign('display_name', $displayName);
$emailMasked = CRM_Utils_String::maskEmail($email);
$this->assign('email_masked', $emailMasked);
Expand Down
42 changes: 39 additions & 3 deletions CRM/Mailing/Page/Unsubscribe.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
class CRM_Mailing_Page_Unsubscribe extends CRM_Mailing_Page_Common {
class CRM_Mailing_Page_Unsubscribe extends CRM_Core_Page {

/**
* Run page.
Expand All @@ -25,8 +25,44 @@ class CRM_Mailing_Page_Unsubscribe extends CRM_Mailing_Page_Common {
* @throws Exception
*/
public function run() {
$this->_type = 'unsubscribe';
return parent::run();
$isOneClick = ($_SERVER['REQUEST_METHOD'] === 'POST' && CRM_Utils_Request::retrieve('List-Unsubscribe', 'String') === 'One-Click');
if ($isOneClick) {
$this->handleOneClick();
return NULL;
}

$wrapper = new CRM_Utils_Wrapper();
return $wrapper->run('CRM_Mailing_Form_Unsubscribe', $this->_title);
}

/**
*
* Pre-condition: Validated the _job_id, _queue_id, _hash.
* Post-condition: Unsubscribed
*
* @link https://datatracker.ietf.org/doc/html/rfc8058
* @return void
*/
public function handleOneClick(): void {
$jobId = CRM_Utils_Request::retrieve('jid', 'Integer');
$queueId = CRM_Utils_Request::retrieve('qid', 'Integer');
$hash = CRM_Utils_Request::retrieve('h', 'String');

$q = CRM_Mailing_Event_BAO_MailingEventQueue::verify(NULL, $queueId, $hash);
if (!$q) {
CRM_Utils_System::sendResponse(
new \GuzzleHttp\Psr7\Response(400, [], ts("Invalid request: bad parameters"))
);
}

$groups = CRM_Mailing_Event_BAO_MailingEventUnsubscribe::unsub_from_mailing($jobId, $queueId, $hash);
if (!empty($groups)) {
CRM_Mailing_Event_BAO_MailingEventUnsubscribe::send_unsub_response($queueId, $groups, FALSE, $jobId);
}

CRM_Utils_System::sendResponse(
new \GuzzleHttp\Psr7\Response(200, [], 'OK')
);
}

}
82 changes: 82 additions & 0 deletions CRM/Mailing/Service/ListUnsubscribe.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

/**
* Apply a full range of `List-Unsubscribe` header options.
*
* @service civi.mailing.listUnsubscribe
* @link https://datatracker.ietf.org/doc/html/rfc8058
*/
class CRM_Mailing_Service_ListUnsubscribe extends \Civi\Core\Service\AutoService implements \Symfony\Component\EventDispatcher\EventSubscriberInterface {

private ?string $urlFlags = NULL;

public static function getMethods(): array {
return [
'mailto' => ts('Mailto'),
'http' => ts('HTTP(S) Web-Form'),
'oneclick' => ts('HTTP(S) One-Click'),
];
}

public static function getSubscribedEvents() {
return [
'&hook_civicrm_alterMailParams' => ['alterMailParams', 1000],
];
}

/**
* @see \CRM_Utils_Hook::alterMailParams()
*/
public function alterMailParams(&$params, $context = NULL): void {
// FIXME: Flexmailer (BasicHeaders) and BAO (getVerpAndUrlsAndHeaders) separately define
// `List-Unsubscribe: <mailto:....>`. And they have separate invocations of alterMailParams.
//
// This code is a little ugly because it anticipates serving both code-paths.
// But the BAO path should be properly killed. Doing so will allow you cleanup this code more.

if (!in_array($context, ['civimail', 'flexmailer'])) {
return;
}

$methods = Civi::settings()->get('civimail_unsubscribe_methods');
if ($methods === ['mailto']) {
return;
}

$sep = preg_quote(Civi::settings()->get('verpSeparator'), ';');
$regex = ";^<mailto:[^>]*u{$sep}(\d+){$sep}(\d+){$sep}(\w*)@(.+)>$;";
if (!preg_match($regex, $params['List-Unsubscribe'], $m)) {
\Civi::log()->warning('Failed to set final value of List-Unsubscribe');
return;
}

if ($this->urlFlags === NULL) {
$this->urlFlags = 'a';
if (in_array('oneclick', $methods) && empty(parse_url(CIVICRM_UF_BASEURL, PHP_URL_PORT))) {
// Yahoo etal require HTTPS for one-click URLs. Cron-runs can be a bit inconsistent wrt HTTP(S),
// so we force-SSL for most production-style sites.
$this->urlFlags .= 's';
}
}

$listUnsubscribe = [];
if (in_array('mailto', $methods)) {
$listUnsubscribe[] = $params['List-Unsubscribe'];
}
if (array_intersect(['http', 'oneclick'], $methods)) {
$listUnsubscribe[] = '<' . Civi::url('frontend://civicrm/mailing/unsubscribe', $this->urlFlags)->addQuery([
'reset' => 1,
'jid' => $m[1],
'qid' => $m[2],
'h' => $m[3],
]) . '>';
}

if (in_array('oneclick', $methods)) {
$params['headers']['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click';
}
$params['headers']['List-Unsubscribe'] = implode(', ', $listUnsubscribe);
unset($params['List-Unsubscribe']);
}

}
2 changes: 1 addition & 1 deletion CRM/Mailing/xml/Menu/Mailing.xml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
<item>
<path>civicrm/mailing/unsubscribe</path>
<title>Unsubscribe</title>
<page_callback>CRM_Mailing_Form_Unsubscribe</page_callback>
<page_callback>CRM_Mailing_Page_Unsubscribe</page_callback>
<access_arguments>access CiviMail subscribe/unsubscribe pages</access_arguments>
<is_public>true</is_public>
<weight>640</weight>
Expand Down
46 changes: 46 additions & 0 deletions CRM/Utils/Check/Component/Mailing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/

/**
*
* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
class CRM_Utils_Check_Component_Mailing extends CRM_Utils_Check_Component {

/**
* @return CRM_Utils_Check_Message[]
*/
public function checkUnsubscribeMethods() {
if (!\CRM_Core_Component::isEnabled('CiviMail')) {
return [];
}

$methods = Civi::settings()->get('civimail_unsubscribe_methods');
if (in_array('oneclick', $methods)) {
return [];
}

// OK, all guards passed. Show message.
$message = new CRM_Utils_Check_Message(
__FUNCTION__,
'<p>' . ts('Beginning in 2024, some web-mail services (Google and Yahoo) will require that large mailing-lists support another unsubscribe method: "HTTP One-Click" (RFC 8058). Please review the documentation and update the settings.') . '</p>',
ts('CiviMail: Enable One-Click Unsubscribe'),
\Psr\Log\LogLevel::NOTICE,
'fa-server'
);
$message->addAction(ts('Learn more'), FALSE, 'href', ['url' => 'https://civicrm.org/redirect/unsubscribe-one-click'], 'fa-info-circle');
$message->addAction(ts('Update settings'), FALSE, 'href', ['path' => 'civicrm/admin/mail', 'query' => 'reset=1'], 'fa-wrench');

return [$message];
}

}
3 changes: 3 additions & 0 deletions CRM/Utils/Check/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ public function addHelp($help) {
* Currently supports: api3 or href
* @param array $params
* Params to be passed to CRM.api3 or CRM.url depending on type
* Ex: ['MyApiEntity', 'MyApiAction', [...params...]]
* Ex: ['path' => 'civicrm/admin/foo', 'query' => 'reset=1']
* Ex: ['url' => 'https://example.com/more/info']
* @param string $icon
* Fa-icon class for the button
*/
Expand Down
2 changes: 1 addition & 1 deletion CRM/Utils/System/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,7 @@ public function sendResponse(\Psr\Http\Message\ResponseInterface $response) {
CRM_Utils_System::setHttpHeader($name, implode(', ', (array) $values));
}
echo $response->getBody();
CRM_Utils_System::civiExit();
CRM_Utils_System::civiExit(0, ['response' => $response]);
}

/**
Expand Down
Loading