From aa6d176929b82431218b4f2f5d53d1bd95a49d59 Mon Sep 17 00:00:00 2001
From: Kevin Cristiano <kcristiano@kcristiano.com>
Date: Fri, 26 Jan 2024 08:23:36 -0500
Subject: [PATCH] civicrm release-5.69.3

---
 civicrm.php                                   |   4 +-
 civicrm/CRM/Admin/Form/Setting/Mail.php       |   1 +
 civicrm/CRM/Contact/Form/Contact.php          |   2 +-
 .../CRM/Contribute/Form/Contribution/Main.php |   4 +-
 civicrm/CRM/Mailing/BAO/Mailing.php           |  26 +-
 civicrm/CRM/Mailing/Form/Unsubscribe.php      |   2 +-
 civicrm/CRM/Mailing/Page/Unsubscribe.php      |  42 ++-
 .../CRM/Mailing/Service/ListUnsubscribe.php   |  85 ++++++
 civicrm/CRM/Mailing/xml/Menu/Mailing.xml      |   2 +-
 civicrm/CRM/Utils/Check/Component/Mailing.php |  46 +++
 civicrm/CRM/Utils/Check/Message.php           |   3 +
 civicrm/CRM/Utils/System/Base.php             |   2 +-
 civicrm/Civi/Test/LocalHttpClient.php         | 276 ++++++++++++++++++
 .../Civi/Test/LocalHttpClient/ClassProps.php  |  40 +++
 .../Civi/Test/LocalHttpClient/SuperGlobal.php |  37 +++
 civicrm/ang/crmStatusPage/StatusPageCtrl.js   |   2 +-
 civicrm/civicrm-version.php                   |   2 +-
 civicrm/ext/afform/admin/info.xml             |   4 +-
 civicrm/ext/afform/core/info.xml              |   4 +-
 civicrm/ext/afform/html/info.xml              |   4 +-
 civicrm/ext/afform/mock/info.xml              |   4 +-
 civicrm/ext/authx/info.xml                    |   4 +-
 civicrm/ext/civi_campaign/info.xml            |   4 +-
 civicrm/ext/civi_case/info.xml                |   4 +-
 civicrm/ext/civi_contribute/info.xml          |   4 +-
 civicrm/ext/civi_event/info.xml               |   4 +-
 civicrm/ext/civi_mail/info.xml                |   4 +-
 civicrm/ext/civi_member/info.xml              |   4 +-
 civicrm/ext/civi_pledge/info.xml              |   4 +-
 civicrm/ext/civi_report/info.xml              |   4 +-
 civicrm/ext/civicrm_admin_ui/info.xml         |   4 +-
 civicrm/ext/civicrm_search_ui/info.xml        |   4 +-
 civicrm/ext/civigrant/info.xml                |   4 +-
 civicrm/ext/civiimport/info.xml               |   4 +-
 civicrm/ext/ckeditor4/info.xml                |   4 +-
 .../ext/contributioncancelactions/info.xml    |   4 +-
 civicrm/ext/elavon/info.xml                   |   4 +-
 civicrm/ext/eventcart/info.xml                |   4 +-
 civicrm/ext/ewaysingle/info.xml               |   4 +-
 civicrm/ext/financialacls/info.xml            |   4 +-
 civicrm/ext/flexmailer/info.xml               |   4 +-
 .../Civi/FlexMailer/FlexMailerSystemTest.php  |   6 +-
 civicrm/ext/greenwich/info.xml                |   4 +-
 civicrm/ext/legacycustomsearches/info.xml     |   4 +-
 civicrm/ext/message_admin/info.xml            |   4 +-
 civicrm/ext/oauth-client/info.xml             |   4 +-
 civicrm/ext/payflowpro/info.xml               |   4 +-
 civicrm/ext/recaptcha/info.xml                |   4 +-
 civicrm/ext/scheduled_communications/info.xml |   4 +-
 civicrm/ext/search_kit/info.xml               |   4 +-
 civicrm/ext/sequentialcreditnotes/info.xml    |   4 +-
 civicrm/ext/standaloneusers/info.xml          |   4 +-
 civicrm/ext/user_dashboard/info.xml           |   4 +-
 civicrm/js/version.json                       |   2 +-
 civicrm/release-notes.md                      |   9 +
 civicrm/release-notes/5.69.3.md               |  49 ++++
 civicrm/settings/Mailing.setting.php          |  24 ++
 civicrm/sql/civicrm_data.mysql                |   2 +-
 civicrm/sql/civicrm_generated.mysql           |   2 +-
 .../CRM/Contribute/Form/Contribution/Main.tpl |   2 +-
 civicrm/templates/CRM/Price/Page/LineItem.tpl |   5 +-
 civicrm/vendor/autoload.php                   |   2 +-
 civicrm/vendor/composer/autoload_real.php     |  14 +-
 civicrm/vendor/composer/autoload_static.php   |  12 +-
 civicrm/vendor/composer/installed.php         |   4 +-
 civicrm/xml/version.xml                       |   2 +-
 66 files changed, 729 insertions(+), 122 deletions(-)
 create mode 100644 civicrm/CRM/Mailing/Service/ListUnsubscribe.php
 create mode 100644 civicrm/CRM/Utils/Check/Component/Mailing.php
 create mode 100644 civicrm/Civi/Test/LocalHttpClient.php
 create mode 100644 civicrm/Civi/Test/LocalHttpClient/ClassProps.php
 create mode 100644 civicrm/Civi/Test/LocalHttpClient/SuperGlobal.php
 create mode 100644 civicrm/release-notes/5.69.3.md

diff --git a/civicrm.php b/civicrm.php
index cd768d1bde..493058e427 100644
--- a/civicrm.php
+++ b/civicrm.php
@@ -2,7 +2,7 @@
 /**
  * Plugin Name: CiviCRM
  * Description: CiviCRM - Growing and Sustaining Relationships
- * Version: 5.69.2
+ * Version: 5.69.3
  * Requires at least: 4.9
  * Requires PHP:      7.3
  * Author: CiviCRM LLC
@@ -36,7 +36,7 @@ if (!defined('ABSPATH')) {
 }
 
 // Set version here: changing it forces Javascript and CSS to reload.
-define('CIVICRM_PLUGIN_VERSION', '5.69.2');
+define('CIVICRM_PLUGIN_VERSION', '5.69.3');
 
 // Store reference to this file.
 if (!defined('CIVICRM_PLUGIN_FILE')) {
diff --git a/civicrm/CRM/Admin/Form/Setting/Mail.php b/civicrm/CRM/Admin/Form/Setting/Mail.php
index 4b49514c34..358c87f81f 100644
--- a/civicrm/CRM/Admin/Form/Setting/Mail.php
+++ b/civicrm/CRM/Admin/Form/Setting/Mail.php
@@ -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,
   ];
 
   /**
diff --git a/civicrm/CRM/Contact/Form/Contact.php b/civicrm/CRM/Contact/Form/Contact.php
index 99d0761d00..d66616d09a 100644
--- a/civicrm/CRM/Contact/Form/Contact.php
+++ b/civicrm/CRM/Contact/Form/Contact.php
@@ -365,7 +365,7 @@ class CRM_Contact_Form_Contact extends CRM_Core_Form {
         // the buildForm adds the contact_sub_type to the form we need to look in _submitValues for it - submitValues
         // is a un-sanitised version of what is in the form submission (_POST) whereas `getSubmittedValues()` retrieves
         // 'allowed' POSTED values - ie values which match available fields, with some localization handling.
-        CRM_Custom_Form_CustomData::preProcess($this, NULL, $this->isSubmitted() ? ($this->_submitValues['contact_sub_type'] ?? []) : $this->getContactValue('contact_sub_type'),
+        CRM_Custom_Form_CustomData::preProcess($this, NULL, $this->isSubmitted() ? ($this->_submitValues['contact_sub_type'] ?? []) : $this->getContactValue('contact_sub_type') ?? $this->_contactSubType,
           1, $this->_contactType, $this->getContactID()
         );
         $this->assign('customValueCount', $this->_customValueCount);
diff --git a/civicrm/CRM/Contribute/Form/Contribution/Main.php b/civicrm/CRM/Contribute/Form/Contribution/Main.php
index 8b5544e590..a39d1b0665 100644
--- a/civicrm/CRM/Contribute/Form/Contribution/Main.php
+++ b/civicrm/CRM/Contribute/Form/Contribution/Main.php
@@ -1852,9 +1852,9 @@ class CRM_Contribute_Form_Contribution_Main extends CRM_Contribute_Form_Contribu
       return 0;
     }
     if (!$this->isQuickConfig()) {
-      return CRM_Member_BAO_MembershipType::getMembershipType($membershipTypeID)['auto_renew'];
+      return CRM_Member_BAO_MembershipType::getMembershipType($membershipTypeID)['auto_renew'] ?? 0;
     }
-    $membershipTypeAutoRenewOption = CRM_Member_BAO_MembershipType::getMembershipType($membershipTypeID)['auto_renew'];
+    $membershipTypeAutoRenewOption = CRM_Member_BAO_MembershipType::getMembershipType($membershipTypeID)['auto_renew'] ?? 0;
     if ($membershipTypeAutoRenewOption === 2 || $membershipTypeAutoRenewOption === 0) {
       // It is not possible to override never or always at the membership block leve.
       return $membershipTypeAutoRenewOption;
diff --git a/civicrm/CRM/Mailing/BAO/Mailing.php b/civicrm/CRM/Mailing/BAO/Mailing.php
index c476818874..ad551aeb11 100644
--- a/civicrm/CRM/Mailing/BAO/Mailing.php
+++ b/civicrm/CRM/Mailing/BAO/Mailing.php
@@ -2439,19 +2439,19 @@ LEFT JOIN civicrm_mailing_group g ON g.mailing_id   = m.id
   public static function self_hook_civicrm_pre(\Civi\Core\Event\PreEvent $event) {
     if ($event->action === 'create') {
       $params = &$event->params;
-      $params['created_id'] ??= CRM_Core_Session::singleton()->getLoggedInContactID();
-      $params['override_verp'] ??= !Civi::settings()->get('track_civimail_replies');
-      $params['visibility'] ??= 'Public Pages';
-      $params['dedupe_email'] ??= Civi::settings()->get('dedupe_email_default');
-      $params['open_tracking'] ??= Civi::settings()->get('open_tracking_default');
-      $params['url_tracking'] ??= Civi::settings()->get('url_tracking_default');
-      $params['header_id'] ??= CRM_Mailing_PseudoConstant::defaultComponent('Header', '');
-      $params['footer_id'] ??= CRM_Mailing_PseudoConstant::defaultComponent('Footer', '');
-      $params['optout_id'] ??= CRM_Mailing_PseudoConstant::defaultComponent('OptOut', '');
-      $params['reply_id'] ??= CRM_Mailing_PseudoConstant::defaultComponent('Reply', '');
-      $params['resubscribe_id'] ??= CRM_Mailing_PseudoConstant::defaultComponent('Resubscribe', '');
-      $params['unsubscribe_id'] ??= CRM_Mailing_PseudoConstant::defaultComponent('Unsubscribe', '');
-      $params['mailing_type'] ??= 'standalone';
+      $params['created_id'] = $params['created_id'] ?? CRM_Core_Session::singleton()->getLoggedInContactID();
+      $params['override_verp'] = $params['override_verp'] ?? !Civi::settings()->get('track_civimail_replies');
+      $params['visibility'] = $params['visibility'] ?? 'Public Pages';
+      $params['dedupe_email'] = $params['dedupe_email'] ?? Civi::settings()->get('dedupe_email_default');
+      $params['open_tracking'] = $params['open_tracking'] ?? Civi::settings()->get('open_tracking_default');
+      $params['url_tracking'] = $params['url_tracking'] ?? Civi::settings()->get('url_tracking_default');
+      $params['header_id'] = $params['header_id'] ?? CRM_Mailing_PseudoConstant::defaultComponent('Header', '');
+      $params['footer_id'] = $params['footer_id'] ?? CRM_Mailing_PseudoConstant::defaultComponent('Footer', '');
+      $params['optout_id'] = $params['optout_id'] ?? CRM_Mailing_PseudoConstant::defaultComponent('OptOut', '');
+      $params['reply_id'] = $params['reply_id'] ?? CRM_Mailing_PseudoConstant::defaultComponent('Reply', '');
+      $params['resubscribe_id'] = $params['resubscribe_id'] ?? CRM_Mailing_PseudoConstant::defaultComponent('Resubscribe', '');
+      $params['unsubscribe_id'] = $params['unsubscribe_id'] ?? CRM_Mailing_PseudoConstant::defaultComponent('Unsubscribe', '');
+      $params['mailing_type'] = $params['mailing_type'] ?? 'standalone';
     }
     if ($event->action === 'delete' && $event->id) {
       // Delete all file attachments
diff --git a/civicrm/CRM/Mailing/Form/Unsubscribe.php b/civicrm/CRM/Mailing/Form/Unsubscribe.php
index f0a8e0d1f9..6abca91f81 100644
--- a/civicrm/CRM/Mailing/Form/Unsubscribe.php
+++ b/civicrm/CRM/Mailing/Form/Unsubscribe.php
@@ -64,7 +64,7 @@ class CRM_Mailing_Form_Unsubscribe extends CRM_Core_Form {
       );
     }
 
-    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);
     $nameMasked = '';
     $names = explode(' ', $displayName);
diff --git a/civicrm/CRM/Mailing/Page/Unsubscribe.php b/civicrm/CRM/Mailing/Page/Unsubscribe.php
index 349ee59d2a..218fe4c828 100644
--- a/civicrm/CRM/Mailing/Page/Unsubscribe.php
+++ b/civicrm/CRM/Mailing/Page/Unsubscribe.php
@@ -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.
@@ -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')
+    );
   }
 
 }
diff --git a/civicrm/CRM/Mailing/Service/ListUnsubscribe.php b/civicrm/CRM/Mailing/Service/ListUnsubscribe.php
new file mode 100644
index 0000000000..f7f8910563
--- /dev/null
+++ b/civicrm/CRM/Mailing/Service/ListUnsubscribe.php
@@ -0,0 +1,85 @@
+<?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 {
+
+  /**
+   * @var string|null
+   */
+  private $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']);
+  }
+
+}
diff --git a/civicrm/CRM/Mailing/xml/Menu/Mailing.xml b/civicrm/CRM/Mailing/xml/Menu/Mailing.xml
index f3267235d3..7942323fad 100644
--- a/civicrm/CRM/Mailing/xml/Menu/Mailing.xml
+++ b/civicrm/CRM/Mailing/xml/Menu/Mailing.xml
@@ -115,7 +115,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>
diff --git a/civicrm/CRM/Utils/Check/Component/Mailing.php b/civicrm/CRM/Utils/Check/Component/Mailing.php
new file mode 100644
index 0000000000..b7288fb8f0
--- /dev/null
+++ b/civicrm/CRM/Utils/Check/Component/Mailing.php
@@ -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];
+  }
+
+}
diff --git a/civicrm/CRM/Utils/Check/Message.php b/civicrm/CRM/Utils/Check/Message.php
index 8aae42516f..deb4a4b6cb 100644
--- a/civicrm/CRM/Utils/Check/Message.php
+++ b/civicrm/CRM/Utils/Check/Message.php
@@ -155,6 +155,9 @@ class CRM_Utils_Check_Message {
    *   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
    */
diff --git a/civicrm/CRM/Utils/System/Base.php b/civicrm/CRM/Utils/System/Base.php
index 8559f220b2..a5cbcaf780 100644
--- a/civicrm/CRM/Utils/System/Base.php
+++ b/civicrm/CRM/Utils/System/Base.php
@@ -977,7 +977,7 @@ abstract class CRM_Utils_System_Base {
       CRM_Utils_System::setHttpHeader($name, implode(', ', (array) $values));
     }
     echo $response->getBody();
-    CRM_Utils_System::civiExit();
+    CRM_Utils_System::civiExit(0, ['response' => $response]);
   }
 
   /**
diff --git a/civicrm/Civi/Test/LocalHttpClient.php b/civicrm/Civi/Test/LocalHttpClient.php
new file mode 100644
index 0000000000..f7e9d01da8
--- /dev/null
+++ b/civicrm/Civi/Test/LocalHttpClient.php
@@ -0,0 +1,276 @@
+<?php
+
+namespace Civi\Test;
+
+use Civi\Test\LocalHttpClient\ClassProps;
+use Civi\Test\LocalHttpClient\SuperGlobal;
+use GuzzleHttp\Psr7\Response;
+use Psr\Http\Client\ClientInterface;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * (Experimental) Send HTTP-style requests directly to CRM_Core_Invoke (PSR-18 ClientInterface).
+ * This allows you to process many requests within the same PHP process, which can be useful for
+ * headless unit-testing.
+ *
+ * $c = new LocalHttpClient(['reboot' => FALSE]);
+ * $response = $c->sendRequest(new Request('GET', '/civicrm/foo?reset=1&bar=100'));
+ * $response = $c->sendRequest(new Request('GET', '/civicrm/whiz?reset=1&bang=200'));
+ *
+ * In theory, this could be the basis for headless HTTP testing with client-libraries like Guzzle, Mink, or BrowserKit.
+ *
+ * WHY: CiviCRM predates the PSR HTTP OOP conventions -- many things are built with $_GET, $_REQUEST, etc.
+ * To simulate an HTTP request to these, we swap-in and swap-out values for $_GET, $_REQUEST, etc.
+ * Consequently, there is some limited isolation between the parent/requester and child/requestee.
+ *
+ * NOTE: You can improve the isolation more with `reboot=>TRUE`. This will swap (and reinitialize)
+ * the CiviCRM runtime-config and service-container. However, there is no comprehensive option to
+ * swap all static properties (other classes), so some data may still leak between requester+requestee.
+ *
+ * NOTE: This is primarily intended for use in headless testing (CIVICRM_UF=UnitTests). It may
+ * or may not be quirky with real UFs.
+ *
+ * @link https://www.php-fig.org/psr/psr-18/
+ */
+class LocalHttpClient implements ClientInterface {
+
+  /**
+   * List of scopes which should be backed-up, (re)populated, (re)set for the duration of the subrequest.
+   *
+   * @var array
+   *   Ex: ['_GET' => new SuperGlobal('_GET')]
+   */
+  protected $scopes;
+
+  /**
+   * List of scopes which should be inherited/extended within the subrequest.
+   *
+   * @var array
+   *   Ex: ['_COOKIE', '_SERVER']
+   */
+  protected $inherit;
+
+  /**
+   * Whether to generate the HTML <HEAD>er
+   *
+   * @var bool
+   */
+  protected $htmlHeader;
+
+  /**
+   * @param array $options
+   *   - reboot (bool): TRUE if you want to re-bootstrap CiviCRM (config/container) on each request
+   *     Default: FALSE
+   *   - htmlHeader (bool): TRUE if you want the generated page to include the full HTML header
+   *     This may become standard (non-optional). It's opt-out to help debug/work-around some early
+   *     quirks when first using LocalHttpClient in CI.
+   *   - globals (string[]): List of (super)globals that should be backed-up, populated, used, and restored.
+   *     Default: ['_GET', '_POST', '_COOKIE', '_FILES', '_SERVER', '_REQUEST']
+   *   - inherit (string[]): When populating these (super)globals, build on top of the existing values.
+   *     Default: ['_COOKIE', '_SERVER']
+   */
+  public function __construct(array $options = []) {
+    $defaultOptions = [
+      'reboot' => FALSE,
+      'htmlHeader' => TRUE,
+      'globals' => ['_GET', '_POST', '_COOKIE', '_FILES', '_SERVER', '_REQUEST'],
+      'inherit' => ['_COOKIE', '_SERVER'],
+    ];
+    $options = array_merge($defaultOptions, $options);
+
+    $this->inherit = $options['inherit'];
+    $this->htmlHeader = $options['htmlHeader'];
+    $this->scopes = [];
+
+    foreach ($options['globals'] as $scopeName) {
+      $this->scopes[$scopeName] = new SuperGlobal($scopeName);
+    }
+
+    if ($options['reboot']) {
+      $classes = [
+        \Civi::class,
+        \CRM_Core_Config::class,
+        \CRM_Utils_Hook::class,
+        \Civi\Core\Resolver::class,
+        \CRM_Queue_Service::class,
+        \CRM_Utils_System::class,
+        \CRM_Utils_Cache::class,
+      ];
+      foreach ($classes as $class) {
+        $this->scopes[$class] = new ClassProps($class);
+      }
+    }
+  }
+
+  public function sendRequest(RequestInterface $request): ResponseInterface {
+    $backup = $this->getAllValues();
+    try {
+      $this->initScopes($request);
+
+      $var = \CRM_Core_Config::singleton()->userFrameworkURLVar;
+      if (!isset($_GET[$var])) {
+        $_GET[$var] = ltrim($request->getUri()->getPath(), '/');
+      }
+      $body = $this->invoke($_GET[$var]);
+      // FIXME: There's probably a way to instrument CRM_Utils_System_UnitTests to do this better.
+      return new Response(200, [], $body);
+    }
+    catch (\CRM_Core_Exception_PrematureExitException $e) {
+      if (isset($e->errorData['response'])) {
+        return $e->errorData['response'];
+      }
+      // FIXME: There are some things which emit PrematureExitException but don't provide the $response object.
+      // We should probably revise \CRM_Utils_System::redirect() and returnJsonResponse()
+      else {
+        throw $e;
+      }
+    }
+    finally {
+      $this->restoreAllValues($backup);
+    }
+  }
+
+  protected function initScopes(RequestInterface $request) {
+    foreach ($this->scopes as $scopeName => $scope) {
+      if (!in_array($scopeName, $this->inherit)) {
+        $scope->unsetKeys(array_keys($scope->getValues()));
+      }
+
+      $method = 'initValues' . $scopeName;
+      $initValues = is_callable([$this, $method]) ? $this->$method($request) : [];
+      $scope->setValues($initValues);
+    }
+    if (in_array('CRM_Core_Config', $this->scopes)) {
+      \CRM_Core_Config::singleton();
+    }
+  }
+
+  /**
+   * Map data from the request to $_GET.
+   *
+   * @param \Psr\Http\Message\RequestInterface $request
+   * @return array
+   */
+  protected function initValues_GET(RequestInterface $request): array {
+    $result = [];
+    parse_str($request->getUri()->getQuery() ?: '', $result);
+    return $result;
+  }
+
+  /**
+   * Map data from the request to $_POST.
+   *
+   * @param \Psr\Http\Message\RequestInterface $request
+   * @return array
+   */
+  protected function initValues_POST(RequestInterface $request): array {
+    $result = [];
+    if ($request->getMethod() === 'POST') {
+      $contentTypes = $request->getHeader('Content-Type');
+      if (in_array('application/x-www-form-urlencoded', $contentTypes) || empty($contentTypes)) {
+        $body = (string) $request->getBody();
+        parse_str($body, $result);
+      }
+    }
+    return $result;
+  }
+
+  /**
+   * Map data from the request to $_REQUEST.
+   *
+   * @param \Psr\Http\Message\RequestInterface $request
+   * @return array
+   */
+  protected function initValues_REQUEST(RequestInterface $request): array {
+    $sources = ['g' => '_GET', 'p' => '_POST', 'c' => '_COOKIE'];
+
+    if (ini_get('request_order')) {
+      $order = strtolower(ini_get('request_order'));
+    }
+    elseif (ini_get('variables_order')) {
+      $order = strtolower(ini_get('variables_order'));
+    }
+    else {
+      $order = 'gpc';
+    }
+
+    $result = [];
+    for ($i = 0; $i < strlen($order); $i++) {
+      if (isset($sources[$order[$i]])) {
+        $scope = $this->scopes[$sources[$order[$i]]];
+        $result = array_merge($result, $scope->getValues());
+      }
+    }
+    return $result;
+  }
+
+  protected function initValues_SERVER(RequestInterface $request): array {
+    $uri = $request->getUri();
+
+    $result = [];
+    $result['REQUEST_METHOD'] = $request->getMethod();
+    $result['REQUEST_URI'] = $uri->getPath();
+    if ($uri->getQuery()) {
+      $result['REQUEST_URI'] .= '?' . $uri->getQuery();
+    }
+    if ($uri->getHost()) {
+      $result['HTTP_HOST'] = $uri->getHost();
+      if ($uri->getPort()) {
+        $result['HTTP_HOST'] .= ':' . $uri->getPort();
+        $result['SERVER_PORT'] = $uri->getPort();
+      }
+      $result['SERVER_NAME'] = $uri->getHost();
+    }
+    $result['HTTP_USER_AGENT'] = __CLASS__;
+    return $result;
+  }
+
+  protected function invoke(string $route): ?string {
+    if ($this->htmlHeader) {
+      \CRM_Core_Resources::singleton()->addCoreResources('html-header');
+    }
+
+    ob_start();
+    try {
+      $pageContent = \CRM_Core_Invoke::_invoke(explode('/', $route));
+    }
+    finally {
+      $printedContent = ob_get_clean();
+    }
+
+    if (empty($pageContent) && !empty($printedContent)) {
+      $pageContent = $printedContent;
+    }
+
+    $locale = \CRM_Core_I18n::getLocale();
+    $lang = substr($locale, 0, 2);
+    $dir = \CRM_Core_I18n::isLanguageRTL($locale) ? 'rtl' : 'ltr';
+    $head = $this->htmlHeader ? \CRM_Core_Region::instance('html-header')->render('') : '';
+
+    return <<<PAGETPL
+<!DOCTYPE html>
+<html lang="$lang" dir="$dir">
+<head>$head</head>
+<body class="civicrm-unittest-body">$pageContent</body>
+</html>
+PAGETPL;
+  }
+
+  public function getAllValues(): array {
+    $backup = [];
+    foreach ($this->scopes as $scopeName => $scope) {
+      $backup[$scopeName] = $scope->getValues();
+    }
+    return $backup;
+  }
+
+  protected function restoreAllValues(array &$backup): void {
+    foreach ($this->scopes as $scopeName => $scope) {
+      $extraKeys = array_diff(array_keys($scope->getValues()), array_keys($backup[$scopeName]));
+      $scope->unsetKeys($extraKeys);
+      $scope->setValues($backup[$scopeName]);
+    }
+  }
+
+}
diff --git a/civicrm/Civi/Test/LocalHttpClient/ClassProps.php b/civicrm/Civi/Test/LocalHttpClient/ClassProps.php
new file mode 100644
index 0000000000..736c18ffdd
--- /dev/null
+++ b/civicrm/Civi/Test/LocalHttpClient/ClassProps.php
@@ -0,0 +1,40 @@
+<?php
+namespace Civi\Test\LocalHttpClient;
+
+use Civi\Test\Invasive;
+
+/**
+ * @internal
+ */
+class ClassProps {
+
+  /**
+   * @var \ReflectionClass
+   */
+  protected $class;
+
+  public function __construct(string $class) {
+    $this->class = new \ReflectionClass($class);
+  }
+
+  public function getValues() {
+    return $this->class->getStaticProperties() ?: [];
+  }
+
+  public function setValues(iterable $values): void {
+    foreach ($values as $key => $value) {
+      // In PHP 7.3, setStaticPropertyValue() fails for private properties.
+      // $this->class->setStaticPropertyValue($key, $value);
+      Invasive::set([$this->class->getName(), $key], $value);
+    }
+  }
+
+  public function unsetKeys(iterable $keys): void {
+    foreach ($keys as $key) {
+      // In PHP 7.3, setStaticPropertyValue() fails for private properties.
+      // $this->class->setStaticPropertyValue($key, NULL);
+      Invasive::set([$this->class->getName(), $key], NULL);
+    }
+  }
+
+}
diff --git a/civicrm/Civi/Test/LocalHttpClient/SuperGlobal.php b/civicrm/Civi/Test/LocalHttpClient/SuperGlobal.php
new file mode 100644
index 0000000000..437f10af1e
--- /dev/null
+++ b/civicrm/Civi/Test/LocalHttpClient/SuperGlobal.php
@@ -0,0 +1,37 @@
+<?php
+namespace Civi\Test\LocalHttpClient;
+
+/**
+ * @internal
+ */
+class SuperGlobal {
+
+  /**
+   * @var string
+   */
+  protected $name;
+
+  /**
+   * @param string $name
+   */
+  public function __construct(string $name) {
+    $this->name = $name;
+  }
+
+  public function getValues() {
+    return $GLOBALS[$this->name];
+  }
+
+  public function setValues(iterable $values): void {
+    foreach ($values as $key => $value) {
+      $GLOBALS[$this->name][$key] = $value;
+    }
+  }
+
+  public function unsetKeys(iterable $keys): void {
+    foreach ($keys as $key) {
+      unset($GLOBALS[$this->name][$key]);
+    }
+  }
+
+}
diff --git a/civicrm/ang/crmStatusPage/StatusPageCtrl.js b/civicrm/ang/crmStatusPage/StatusPageCtrl.js
index 5dad04fc59..2d6d75f61f 100644
--- a/civicrm/ang/crmStatusPage/StatusPageCtrl.js
+++ b/civicrm/ang/crmStatusPage/StatusPageCtrl.js
@@ -51,7 +51,7 @@
         function run() {
           switch (action.type) {
             case 'href':
-              window.location = CRM.url(action.params.path, action.params.query, action.params.mode);
+              window.location = action.params.url ? action.params.url : CRM.url(action.params.path, action.params.query, action.params.mode);
               break;
 
             case 'api3':
diff --git a/civicrm/civicrm-version.php b/civicrm/civicrm-version.php
index 803b154382..362820b041 100644
--- a/civicrm/civicrm-version.php
+++ b/civicrm/civicrm-version.php
@@ -1,7 +1,7 @@
 <?php
 /** @deprecated */
 function civicrmVersion( ) {
-  return array( 'version'  => '5.69.2',
+  return array( 'version'  => '5.69.3',
                 'cms'      => 'Wordpress',
                 'revision' => '' );
 }
diff --git a/civicrm/ext/afform/admin/info.xml b/civicrm/ext/afform/admin/info.xml
index fd44926b3d..3762b1ff97 100644
--- a/civicrm/ext/afform/admin/info.xml
+++ b/civicrm/ext/afform/admin/info.xml
@@ -12,8 +12,8 @@
     <url desc="Chat">https://chat.civicrm.org/civicrm/channels/dev-afform</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>beta</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/afform/core/info.xml b/civicrm/ext/afform/core/info.xml
index 5a8c0e747b..fcf0b28f5e 100644
--- a/civicrm/ext/afform/core/info.xml
+++ b/civicrm/ext/afform/core/info.xml
@@ -12,8 +12,8 @@
     <url desc="Chat">https://chat.civicrm.org/civicrm/channels/dev-afform</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <compatibility>
     <ver>5.69</ver>
   </compatibility>
diff --git a/civicrm/ext/afform/html/info.xml b/civicrm/ext/afform/html/info.xml
index 3c4f105ab2..dada7a31a7 100644
--- a/civicrm/ext/afform/html/info.xml
+++ b/civicrm/ext/afform/html/info.xml
@@ -12,8 +12,8 @@
     <url desc="Chat">https://chat.civicrm.org/civicrm/channels/dev-afform</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>alpha</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/afform/mock/info.xml b/civicrm/ext/afform/mock/info.xml
index ac698c4727..795b178102 100644
--- a/civicrm/ext/afform/mock/info.xml
+++ b/civicrm/ext/afform/mock/info.xml
@@ -11,8 +11,8 @@
   <urls>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <tags>
     <tag>mgmt:hidden</tag>
   </tags>
diff --git a/civicrm/ext/authx/info.xml b/civicrm/ext/authx/info.xml
index 921c9e6f18..5f0ef54f69 100644
--- a/civicrm/ext/authx/info.xml
+++ b/civicrm/ext/authx/info.xml
@@ -14,8 +14,8 @@
     <url desc="Issues">https://lab.civicrm.org/dev/core/-/issues</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <tags>
     <tag>mgmt:required</tag>
diff --git a/civicrm/ext/civi_campaign/info.xml b/civicrm/ext/civi_campaign/info.xml
index a8a9372e66..355cf41c72 100644
--- a/civicrm/ext/civi_campaign/info.xml
+++ b/civicrm/ext/civi_campaign/info.xml
@@ -12,8 +12,8 @@
     <url desc="Documentation">https://docs.civicrm.org/user/en/latest/campaign/what-is-civicampaign/</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <tags>
     <tag>component</tag>
diff --git a/civicrm/ext/civi_case/info.xml b/civicrm/ext/civi_case/info.xml
index e0fd52eb2d..f2a0fff4c2 100644
--- a/civicrm/ext/civi_case/info.xml
+++ b/civicrm/ext/civi_case/info.xml
@@ -12,8 +12,8 @@
     <url desc="Documentation">https://docs.civicrm.org/user/en/latest/case-management/what-is-civicase/</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <tags>
     <tag>component</tag>
diff --git a/civicrm/ext/civi_contribute/info.xml b/civicrm/ext/civi_contribute/info.xml
index 3f201436df..f01beb6924 100644
--- a/civicrm/ext/civi_contribute/info.xml
+++ b/civicrm/ext/civi_contribute/info.xml
@@ -12,8 +12,8 @@
     <url desc="Documentation">https://docs.civicrm.org/user/en/latest/contributions/what-is-civicontribute/</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <tags>
     <tag>component</tag>
diff --git a/civicrm/ext/civi_event/info.xml b/civicrm/ext/civi_event/info.xml
index 7185d36e42..ffcd3900c5 100644
--- a/civicrm/ext/civi_event/info.xml
+++ b/civicrm/ext/civi_event/info.xml
@@ -12,8 +12,8 @@
     <url desc="Documentation">https://docs.civicrm.org/user/en/latest/events/what-is-civievent</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <tags>
     <tag>component</tag>
diff --git a/civicrm/ext/civi_mail/info.xml b/civicrm/ext/civi_mail/info.xml
index e03fb4c40a..aa0114e164 100644
--- a/civicrm/ext/civi_mail/info.xml
+++ b/civicrm/ext/civi_mail/info.xml
@@ -12,8 +12,8 @@
     <url desc="Documentation">https://docs.civicrm.org/user/en/latest/email/what-is-civimail/</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <tags>
     <tag>component</tag>
diff --git a/civicrm/ext/civi_member/info.xml b/civicrm/ext/civi_member/info.xml
index 1f78fd8167..4499c7671b 100644
--- a/civicrm/ext/civi_member/info.xml
+++ b/civicrm/ext/civi_member/info.xml
@@ -12,8 +12,8 @@
     <url desc="Documentation">https://docs.civicrm.org/user/en/latest/membership/what-is-civimember/</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <tags>
     <tag>component</tag>
diff --git a/civicrm/ext/civi_pledge/info.xml b/civicrm/ext/civi_pledge/info.xml
index a8ab9b1279..bb400f8b5f 100644
--- a/civicrm/ext/civi_pledge/info.xml
+++ b/civicrm/ext/civi_pledge/info.xml
@@ -12,8 +12,8 @@
     <url desc="Documentation">https://docs.civicrm.org/user/en/latest/pledges/what-is-civipledge/</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <tags>
     <tag>component</tag>
diff --git a/civicrm/ext/civi_report/info.xml b/civicrm/ext/civi_report/info.xml
index e42793d098..b9fa72fcf4 100644
--- a/civicrm/ext/civi_report/info.xml
+++ b/civicrm/ext/civi_report/info.xml
@@ -12,8 +12,8 @@
     <url desc="Documentation">https://docs.civicrm.org/user/en/latest/reporting/what-is-civireport/</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <tags>
     <tag>component</tag>
diff --git a/civicrm/ext/civicrm_admin_ui/info.xml b/civicrm/ext/civicrm_admin_ui/info.xml
index 42da1f2468..df1c2c867c 100644
--- a/civicrm/ext/civicrm_admin_ui/info.xml
+++ b/civicrm/ext/civicrm_admin_ui/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">https://lab.civicrm.org/dev/core/-/issues</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>beta</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/civicrm_search_ui/info.xml b/civicrm/ext/civicrm_search_ui/info.xml
index 55908928e7..f025e92455 100644
--- a/civicrm/ext/civicrm_search_ui/info.xml
+++ b/civicrm/ext/civicrm_search_ui/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">https://lab.civicrm.org/dev/core/-/issues</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>alpha</develStage>
   <requires>
     <ext>org.civicrm.search_kit</ext>
diff --git a/civicrm/ext/civigrant/info.xml b/civicrm/ext/civigrant/info.xml
index a8a22d1c37..f66d8f9941 100644
--- a/civicrm/ext/civigrant/info.xml
+++ b/civicrm/ext/civigrant/info.xml
@@ -12,8 +12,8 @@
     <url desc="Documentation">https://docs.civicrm.org/user/en/latest/grants/what-is-civigrant/</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/civiimport/info.xml b/civicrm/ext/civiimport/info.xml
index af06112910..968163f372 100644
--- a/civicrm/ext/civiimport/info.xml
+++ b/civicrm/ext/civiimport/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">http://civicrm.org</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>alpha</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/ckeditor4/info.xml b/civicrm/ext/ckeditor4/info.xml
index e619949d1c..b1a57717b6 100644
--- a/civicrm/ext/ckeditor4/info.xml
+++ b/civicrm/ext/ckeditor4/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">https://github.com/civicrm/civicrm-core/</url>
     <url desc="Licensing">https://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/contributioncancelactions/info.xml b/civicrm/ext/contributioncancelactions/info.xml
index 675f8d2c67..25113898e1 100644
--- a/civicrm/ext/contributioncancelactions/info.xml
+++ b/civicrm/ext/contributioncancelactions/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">http://civicrm.org</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/elavon/info.xml b/civicrm/ext/elavon/info.xml
index bd9b0fdddc..b1c63a816a 100644
--- a/civicrm/ext/elavon/info.xml
+++ b/civicrm/ext/elavon/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">https://lab.civicrm.org</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/eventcart/info.xml b/civicrm/ext/eventcart/info.xml
index 381bb5b05d..7df0d6a3b4 100644
--- a/civicrm/ext/eventcart/info.xml
+++ b/civicrm/ext/eventcart/info.xml
@@ -12,8 +12,8 @@
     <url desc="Main Extension Page">https://github.com/civicrm/civicrm-core/tree/master/ext/eventcart</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <tags>
     <tag>mgmt:hidden</tag>
   </tags>
diff --git a/civicrm/ext/ewaysingle/info.xml b/civicrm/ext/ewaysingle/info.xml
index 2d2bf431b6..99447186b4 100644
--- a/civicrm/ext/ewaysingle/info.xml
+++ b/civicrm/ext/ewaysingle/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">https://github.com/civicrm/civicrm-core/blob/master/ext/ewaysingle</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <tags>
     <tag>mgmt:hidden</tag>
   </tags>
diff --git a/civicrm/ext/financialacls/info.xml b/civicrm/ext/financialacls/info.xml
index 3dc506da21..e2d4b8dfab 100644
--- a/civicrm/ext/financialacls/info.xml
+++ b/civicrm/ext/financialacls/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">http://FIXME</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/flexmailer/info.xml b/civicrm/ext/flexmailer/info.xml
index 1fc9314c69..036f9bb28d 100644
--- a/civicrm/ext/flexmailer/info.xml
+++ b/civicrm/ext/flexmailer/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">http://civicrm.stackexchange.com/</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <comments>
     FlexMailer is an email delivery engine which replaces the internal guts
diff --git a/civicrm/ext/flexmailer/tests/phpunit/Civi/FlexMailer/FlexMailerSystemTest.php b/civicrm/ext/flexmailer/tests/phpunit/Civi/FlexMailer/FlexMailerSystemTest.php
index d91e6ec490..679b4b6426 100644
--- a/civicrm/ext/flexmailer/tests/phpunit/Civi/FlexMailer/FlexMailerSystemTest.php
+++ b/civicrm/ext/flexmailer/tests/phpunit/Civi/FlexMailer/FlexMailerSystemTest.php
@@ -75,13 +75,13 @@ class FlexMailerSystemTest extends \CRM_Mailing_BaseMailingSystemTest {
    * @see CRM_Utils_Hook::alterMailParams
    */
   public function hook_alterMailParams(&$params, $context = NULL) {
-    $this->counts['hook_alterMailParams'] = 1;
-    $this->assertEquals('flexmailer', $context);
+    $this->counts["hook_alterMailParams::$context"] = 1;
   }
 
   public function tearDown(): void {
     parent::tearDown();
-    $this->assertNotEmpty($this->counts['hook_alterMailParams']);
+    $this->assertNotEmpty($this->counts['hook_alterMailParams::flexmailer']);
+    $this->assertEmpty($this->counts['hook_alterMailParams::civimail'] ?? NULL);
     foreach (FlexMailer::getEventTypes() as $event => $class) {
       $this->assertTrue(
         $this->counts[$class] > 0,
diff --git a/civicrm/ext/greenwich/info.xml b/civicrm/ext/greenwich/info.xml
index 475525e8ec..15b35f61e5 100644
--- a/civicrm/ext/greenwich/info.xml
+++ b/civicrm/ext/greenwich/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">http://civicrm.org</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <tags>
     <tag>mgmt:hidden</tag>
   </tags>
diff --git a/civicrm/ext/legacycustomsearches/info.xml b/civicrm/ext/legacycustomsearches/info.xml
index 7ff4a1d888..3948283af2 100644
--- a/civicrm/ext/legacycustomsearches/info.xml
+++ b/civicrm/ext/legacycustomsearches/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">http://FIXME</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/message_admin/info.xml b/civicrm/ext/message_admin/info.xml
index f72d3766cf..b8bd023d17 100644
--- a/civicrm/ext/message_admin/info.xml
+++ b/civicrm/ext/message_admin/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">http://civicrm.org</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>alpha</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/oauth-client/info.xml b/civicrm/ext/oauth-client/info.xml
index 22c8170196..e8e73931d5 100644
--- a/civicrm/ext/oauth-client/info.xml
+++ b/civicrm/ext/oauth-client/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">https://lab.civicrm.org/dev/core/-/issues</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/payflowpro/info.xml b/civicrm/ext/payflowpro/info.xml
index d49749d23b..5d9b36d5cb 100644
--- a/civicrm/ext/payflowpro/info.xml
+++ b/civicrm/ext/payflowpro/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">https://lab.civicrm.org</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/recaptcha/info.xml b/civicrm/ext/recaptcha/info.xml
index 9d82903a1e..548b52d5f0 100644
--- a/civicrm/ext/recaptcha/info.xml
+++ b/civicrm/ext/recaptcha/info.xml
@@ -12,8 +12,8 @@
     <url desc="Main Extension Page">https://github.com/civicrm/civicrm-core/tree/master/ext/recaptcha</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <tags>
     <tag>mgmt:hidden</tag>
   </tags>
diff --git a/civicrm/ext/scheduled_communications/info.xml b/civicrm/ext/scheduled_communications/info.xml
index 7750124795..4659c4d5f7 100644
--- a/civicrm/ext/scheduled_communications/info.xml
+++ b/civicrm/ext/scheduled_communications/info.xml
@@ -12,8 +12,8 @@
     <url desc="Chat">https://chat.civicrm.org/civicrm/channels/search-improvements</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>beta</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/search_kit/info.xml b/civicrm/ext/search_kit/info.xml
index f9071bcb40..b64fb79767 100644
--- a/civicrm/ext/search_kit/info.xml
+++ b/civicrm/ext/search_kit/info.xml
@@ -14,8 +14,8 @@
     <url desc="Issues">https://lab.civicrm.org/dev/report/-/issues</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>stable</develStage>
   <tags>
     <tag>mgmt:required</tag>
diff --git a/civicrm/ext/sequentialcreditnotes/info.xml b/civicrm/ext/sequentialcreditnotes/info.xml
index c1abec7e72..b64cbf4cfe 100644
--- a/civicrm/ext/sequentialcreditnotes/info.xml
+++ b/civicrm/ext/sequentialcreditnotes/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">https://lab.civicrm.org/extensions/sequentialcreditnotes</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <tags>
     <tag>mgmt:hidden</tag>
   </tags>
diff --git a/civicrm/ext/standaloneusers/info.xml b/civicrm/ext/standaloneusers/info.xml
index 03439363c6..083bf03bab 100644
--- a/civicrm/ext/standaloneusers/info.xml
+++ b/civicrm/ext/standaloneusers/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">http://FIXME</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>alpha</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/ext/user_dashboard/info.xml b/civicrm/ext/user_dashboard/info.xml
index c587ec6056..20b53aa576 100644
--- a/civicrm/ext/user_dashboard/info.xml
+++ b/civicrm/ext/user_dashboard/info.xml
@@ -14,8 +14,8 @@
     <url desc="Support">http://FIXME</url>
     <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
   </urls>
-  <releaseDate>2024-01-11</releaseDate>
-  <version>5.69.2</version>
+  <releaseDate>2024-01-25</releaseDate>
+  <version>5.69.3</version>
   <develStage>alpha</develStage>
   <compatibility>
     <ver>5.69</ver>
diff --git a/civicrm/js/version.json b/civicrm/js/version.json
index 01750314f6..ff623e264c 100644
--- a/civicrm/js/version.json
+++ b/civicrm/js/version.json
@@ -1 +1 @@
-"5.69.2"
+"5.69.3"
diff --git a/civicrm/release-notes.md b/civicrm/release-notes.md
index 7729a3d403..75fc12be41 100644
--- a/civicrm/release-notes.md
+++ b/civicrm/release-notes.md
@@ -15,6 +15,15 @@ Other resources for identifying changes are:
     * https://github.com/civicrm/civicrm-joomla
     * https://github.com/civicrm/civicrm-wordpress
 
+## CiviCRM 5.69.3
+
+Released January 26, 2024
+
+- **[Synopsis](release-notes/5.69.3.md#synopsis)**
+- **[Bugs resolved](release-notes/5.69.3.md#bugs)**
+- **[Credits](release-notes/5.69.3.md#credits)**
+- **[Feedback](release-notes/5.69.3.md#feedback)**
+
 ## CiviCRM 5.69.2
 
 Released January 11, 2024
diff --git a/civicrm/release-notes/5.69.3.md b/civicrm/release-notes/5.69.3.md
new file mode 100644
index 0000000000..ac4014acd1
--- /dev/null
+++ b/civicrm/release-notes/5.69.3.md
@@ -0,0 +1,49 @@
+# CiviCRM 5.69.3
+
+Released January 26, 2024
+
+- **[Synopsis](#synopsis)**
+- **[Bugs resolved](#bugs)**
+- **[Credits](#credits)**
+- **[Feedback](#feedback)**
+
+## <a name="synopsis"></a>Synopsis
+
+| *Does this version...?*                                         |          |
+| --------------------------------------------------------------- | -------- |
+| Change the database schema?                                     | no       |
+| Alter the API?                                                  | no       |
+| **Require attention to configuration options?**                 | **yes**  |
+| Fix problems installing or upgrading to a previous version?     | no       |
+| **Introduce features?**                                         | **yes**  |
+| **Fix bugs?**                                                   | **yes**  |
+| Fix security vulnerabilities?                                   | no       |
+
+## <a name="bugs"></a>Bugs resolved
+
+* **_CiviMail_: Add support for "HTTP One-Click" unsubscribes ([dev/core#4641](https://lab.civicrm.org/dev/core/-/issues/4641): [#28964](https://github.com/civicrm/civicrm-core/pull/28964), [#29086](https://github.com/civicrm/civicrm-core/pull/29086))**
+
+  CiviMail users with large mailing lists should consider enabling this new option. [More info](https://civicrm.org/redirect/unsubscribe-one-click)
+
+* **_CiviContribute_: Fix error viewing contributions when "Tax and Invoicing" is enabled ([#28994](https://github.com/civicrm/civicrm-core/pull/28994), [#29000](https://github.com/civicrm/civicrm-core/pull/29000))**
+* **_CiviContribute_: Fix focus-handling for "Other Amount"  ([dev/core#4912](https://lab.civicrm.org/dev/core/-/issues/4912): [#29016](https://github.com/civicrm/civicrm-core/pull/29016))**
+* **_CiviMember_: Fix handling of "Auto Renew" checkbox in certain configurations ([#29069](https://github.com/civicrm/civicrm-core/pull/29069))**
+* **_Custom Fields_: Fix for custom fields when creating new (subtyped) contacts ([dev/core#4910](https://lab.civicrm.org/dev/core/-/issues/4910): [#29003](https://github.com/civicrm/civicrm-core/pull/29003))**
+* **_PHP 7.3_: Restore compatibility with PHP 7.3 ([#29074](https://github.com/civicrm/civicrm-core/pull/29074))**
+
+## <a name="credits"></a>Credits
+
+This release was developed by the following authors and reviewers:
+
+Wikimedia Foundation - Eileen McNaughton; tresero; Tadpole Collective - Kevin Cristiano;
+Skvare - Mark Hanna; pbarmak; Megaphone Technology Consulting - Jon Goldberg; Makoa - Usha
+F. Matisson; lkuttner; JMA Consulting - Seamus Lee; iank; Humanists UK - Andrew West;;
+Fuzion - Peter Davis; Dave D; Coop SymbioTIC - Shane Bill; CiviDesk - Yashodha Chaku;
+CiviCRM - Tim Otten, Coleman Watts, Josh Gowans; Christian Wach; Chabadrichmond;
+Australian Greens - John Twyman; Artful Robot - Rich Lott; Agileware - Justin Freeman
+
+## <a name="feedback"></a>Feedback
+
+These release notes are edited by Tim Otten and Andie Hunt.  If you'd like to
+provide feedback on them, please login to https://chat.civicrm.org/civicrm and
+contact `@agh1`.
diff --git a/civicrm/settings/Mailing.setting.php b/civicrm/settings/Mailing.setting.php
index d000dc47fd..15e127bce9 100644
--- a/civicrm/settings/Mailing.setting.php
+++ b/civicrm/settings/Mailing.setting.php
@@ -18,6 +18,8 @@
  * Settings metadata file
  */
 
+$unsubLearnMore = '<br/>' . ts('(<a %1">Learn more</a>)', [1 => 'href="https://civicrm.org/redirect/unsubscribe-one-click" target="_blank"']);
+
 return [
   'profile_double_optin' => [
     'group_name' => 'Mailing Preferences',
@@ -91,6 +93,28 @@ return [
     'is_contact' => 0,
     'help_text' => NULL,
   ],
+  'civimail_unsubscribe_methods' => [
+    'group_name' => 'Mailing Preferences',
+    'group' => 'mailing',
+    'name' => 'civimail_unsubscribe_methods',
+    'type' => 'Array',
+    'quick_form_type' => 'Select',
+    'html_type' => 'Select',
+    'html_attributes' => [
+      'multiple' => 1,
+      'class' => 'crm-select2',
+    ],
+    'default' => version_compare(CRM_Utils_System::version(), '5.72', '<=') ? ['mailto'] : ['mailto', 'http', 'oneclick'],
+    'add' => '5.69',
+    'title' => ts('Unsubscribe Methods'),
+    'is_domain' => 1,
+    'is_contact' => 0,
+    'description' => ts("These methods will be offered to email clients for semi-automated unsubscribes. Support for each depends on the recipient's email client.") . $unsubLearnMore,
+    'help_text' => NULL,
+    'pseudoconstant' => [
+      'callback' => 'CRM_Mailing_Service_ListUnsubscribe::getMethods',
+    ],
+  ],
   'replyTo' => [
     'group_name' => 'Mailing Preferences',
     'group' => 'mailing',
diff --git a/civicrm/sql/civicrm_data.mysql b/civicrm/sql/civicrm_data.mysql
index 4c73ac3040..d77cb76bf3 100644
--- a/civicrm/sql/civicrm_data.mysql
+++ b/civicrm/sql/civicrm_data.mysql
@@ -21703,4 +21703,4 @@ INSERT INTO `civicrm_report_instance`
     ( `domain_id`, `title`, `report_id`, `description`, `permission`, `form_values`)
 VALUES
     (  @domainID, 'Survey Details', 'survey/detail', 'Detailed report for canvassing, phone-banking, walk lists or other surveys.', 'access CiviReport', 'a:39:{s:6:"fields";a:2:{s:9:"sort_name";s:1:"1";s:6:"result";s:1:"1";}s:22:"assignee_contact_id_op";s:2:"eq";s:25:"assignee_contact_id_value";s:0:"";s:12:"sort_name_op";s:3:"has";s:15:"sort_name_value";s:0:"";s:17:"street_number_min";s:0:"";s:17:"street_number_max";s:0:"";s:16:"street_number_op";s:3:"lte";s:19:"street_number_value";s:0:"";s:14:"street_name_op";s:3:"has";s:17:"street_name_value";s:0:"";s:15:"postal_code_min";s:0:"";s:15:"postal_code_max";s:0:"";s:14:"postal_code_op";s:3:"lte";s:17:"postal_code_value";s:0:"";s:7:"city_op";s:3:"has";s:10:"city_value";s:0:"";s:20:"state_province_id_op";s:2:"in";s:23:"state_province_id_value";a:0:{}s:13:"country_id_op";s:2:"in";s:16:"country_id_value";a:0:{}s:12:"survey_id_op";s:2:"in";s:15:"survey_id_value";a:0:{}s:12:"status_id_op";s:2:"eq";s:15:"status_id_value";s:1:"1";s:11:"custom_1_op";s:2:"in";s:14:"custom_1_value";a:0:{}s:11:"custom_2_op";s:2:"in";s:14:"custom_2_value";a:0:{}s:17:"custom_3_relative";s:1:"0";s:13:"custom_3_from";s:0:"";s:11:"custom_3_to";s:0:"";s:11:"description";s:75:"Detailed report for canvassing, phone-banking, walk lists or other surveys.";s:13:"email_subject";s:0:"";s:8:"email_to";s:0:"";s:8:"email_cc";s:0:"";s:10:"permission";s:17:"access CiviReport";s:6:"groups";s:0:"";s:9:"domain_id";i:1;}');
-UPDATE civicrm_domain SET version = '5.69.2';
+UPDATE civicrm_domain SET version = '5.69.3';
diff --git a/civicrm/sql/civicrm_generated.mysql b/civicrm/sql/civicrm_generated.mysql
index a70d269fee..0246c63aed 100644
--- a/civicrm/sql/civicrm_generated.mysql
+++ b/civicrm/sql/civicrm_generated.mysql
@@ -2976,7 +2976,7 @@ UNLOCK TABLES;
 LOCK TABLES `civicrm_domain` WRITE;
 /*!40000 ALTER TABLE `civicrm_domain` DISABLE KEYS */;
 INSERT INTO `civicrm_domain` (`id`, `name`, `description`, `version`, `contact_id`, `locales`, `locale_custom_strings`) VALUES
- (1,'Default Domain Name',NULL,'5.69.2',1,NULL,'a:1:{s:5:\"en_US\";a:0:{}}');
+ (1,'Default Domain Name',NULL,'5.69.3',1,NULL,'a:1:{s:5:\"en_US\";a:0:{}}');
 /*!40000 ALTER TABLE `civicrm_domain` ENABLE KEYS */;
 UNLOCK TABLES;
 
diff --git a/civicrm/templates/CRM/Contribute/Form/Contribution/Main.tpl b/civicrm/templates/CRM/Contribute/Form/Contribution/Main.tpl
index 84f8490e06..d44bd2b600 100644
--- a/civicrm/templates/CRM/Contribute/Form/Contribution/Main.tpl
+++ b/civicrm/templates/CRM/Contribute/Form/Contribution/Main.tpl
@@ -22,7 +22,7 @@
         element = document.Main.elements[i];
         if ( element.type == 'radio' && element.name === mainPriceFieldName ) {
           if (element.value == '0' ) {
-            element.click();
+            element.checked = true;
           }
           else {
             element.checked = false;
diff --git a/civicrm/templates/CRM/Price/Page/LineItem.tpl b/civicrm/templates/CRM/Price/Page/LineItem.tpl
index 8635cf9028..e714491211 100644
--- a/civicrm/templates/CRM/Price/Page/LineItem.tpl
+++ b/civicrm/templates/CRM/Price/Page/LineItem.tpl
@@ -72,7 +72,7 @@
         <td></td>
         <td></td>
       {/if}
-      <td class="right">{$line.line_total+$line.tax_amount|crmMoney:$currency}</td>
+      <td class="right">{assign var=totalWithTax value=$line.line_total+$line.tax_amount}{$totalWithTax|crmMoney:$currency}</td>
     {/if}
           {if $pricesetFieldsCount}
             <td class="right">{$line.participant_count}</td>
@@ -92,7 +92,8 @@
       {ts}Contribution Total{/ts}:
     {elseif $context EQ "Event"}
       {if $totalTaxAmount}
-        {ts}Event SubTotal: {$totalAmount-$totalTaxAmount|crmMoney:$currency}{/ts}<br />
+        {assign var=eventSubTotal value=$totalAmount-$totalTaxAmount}
+        {ts 1=$eventSubTotal|crmMoney:$currency}Event SubTotal: %1{/ts}<br />
       {/if}
       {ts}Total Amount{/ts}:
     {elseif $context EQ "Membership"}
diff --git a/civicrm/vendor/autoload.php b/civicrm/vendor/autoload.php
index 7d254f595f..4d99ded32f 100644
--- a/civicrm/vendor/autoload.php
+++ b/civicrm/vendor/autoload.php
@@ -9,4 +9,4 @@ if (PHP_VERSION_ID < 50600) {
 
 require_once __DIR__ . '/composer/autoload_real.php';
 
-return ComposerAutoloaderInit0fcbe1ec640ae9fe693b02538241f085::getLoader();
+return ComposerAutoloaderInite36d44b412c906747bbbe03020d7d072::getLoader();
diff --git a/civicrm/vendor/composer/autoload_real.php b/civicrm/vendor/composer/autoload_real.php
index 9109da8966..9ba5ef25b5 100644
--- a/civicrm/vendor/composer/autoload_real.php
+++ b/civicrm/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@
 
 // autoload_real.php @generated by Composer
 
-class ComposerAutoloaderInit0fcbe1ec640ae9fe693b02538241f085
+class ComposerAutoloaderInite36d44b412c906747bbbe03020d7d072
 {
     private static $loader;
 
@@ -24,22 +24,22 @@ class ComposerAutoloaderInit0fcbe1ec640ae9fe693b02538241f085
 
         require __DIR__ . '/platform_check.php';
 
-        spl_autoload_register(array('ComposerAutoloaderInit0fcbe1ec640ae9fe693b02538241f085', 'loadClassLoader'), true, true);
+        spl_autoload_register(array('ComposerAutoloaderInite36d44b412c906747bbbe03020d7d072', 'loadClassLoader'), true, true);
         self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
-        spl_autoload_unregister(array('ComposerAutoloaderInit0fcbe1ec640ae9fe693b02538241f085', 'loadClassLoader'));
+        spl_autoload_unregister(array('ComposerAutoloaderInite36d44b412c906747bbbe03020d7d072', 'loadClassLoader'));
 
         $includePaths = require __DIR__ . '/include_paths.php';
         $includePaths[] = get_include_path();
         set_include_path(implode(PATH_SEPARATOR, $includePaths));
 
         require __DIR__ . '/autoload_static.php';
-        call_user_func(\Composer\Autoload\ComposerStaticInit0fcbe1ec640ae9fe693b02538241f085::getInitializer($loader));
+        call_user_func(\Composer\Autoload\ComposerStaticInite36d44b412c906747bbbe03020d7d072::getInitializer($loader));
 
         $loader->register(true);
 
-        $includeFiles = \Composer\Autoload\ComposerStaticInit0fcbe1ec640ae9fe693b02538241f085::$files;
+        $includeFiles = \Composer\Autoload\ComposerStaticInite36d44b412c906747bbbe03020d7d072::$files;
         foreach ($includeFiles as $fileIdentifier => $file) {
-            composerRequire0fcbe1ec640ae9fe693b02538241f085($fileIdentifier, $file);
+            composerRequiree36d44b412c906747bbbe03020d7d072($fileIdentifier, $file);
         }
 
         return $loader;
@@ -51,7 +51,7 @@ class ComposerAutoloaderInit0fcbe1ec640ae9fe693b02538241f085
  * @param string $file
  * @return void
  */
-function composerRequire0fcbe1ec640ae9fe693b02538241f085($fileIdentifier, $file)
+function composerRequiree36d44b412c906747bbbe03020d7d072($fileIdentifier, $file)
 {
     if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
         $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
diff --git a/civicrm/vendor/composer/autoload_static.php b/civicrm/vendor/composer/autoload_static.php
index 6c7aa78c9f..e75a9e1d8e 100644
--- a/civicrm/vendor/composer/autoload_static.php
+++ b/civicrm/vendor/composer/autoload_static.php
@@ -4,7 +4,7 @@
 
 namespace Composer\Autoload;
 
-class ComposerStaticInit0fcbe1ec640ae9fe693b02538241f085
+class ComposerStaticInite36d44b412c906747bbbe03020d7d072
 {
     public static $files = array (
         'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
@@ -713,11 +713,11 @@ class ComposerStaticInit0fcbe1ec640ae9fe693b02538241f085
     public static function getInitializer(ClassLoader $loader)
     {
         return \Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = ComposerStaticInit0fcbe1ec640ae9fe693b02538241f085::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = ComposerStaticInit0fcbe1ec640ae9fe693b02538241f085::$prefixDirsPsr4;
-            $loader->prefixesPsr0 = ComposerStaticInit0fcbe1ec640ae9fe693b02538241f085::$prefixesPsr0;
-            $loader->fallbackDirsPsr0 = ComposerStaticInit0fcbe1ec640ae9fe693b02538241f085::$fallbackDirsPsr0;
-            $loader->classMap = ComposerStaticInit0fcbe1ec640ae9fe693b02538241f085::$classMap;
+            $loader->prefixLengthsPsr4 = ComposerStaticInite36d44b412c906747bbbe03020d7d072::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInite36d44b412c906747bbbe03020d7d072::$prefixDirsPsr4;
+            $loader->prefixesPsr0 = ComposerStaticInite36d44b412c906747bbbe03020d7d072::$prefixesPsr0;
+            $loader->fallbackDirsPsr0 = ComposerStaticInite36d44b412c906747bbbe03020d7d072::$fallbackDirsPsr0;
+            $loader->classMap = ComposerStaticInite36d44b412c906747bbbe03020d7d072::$classMap;
 
         }, null, ClassLoader::class);
     }
diff --git a/civicrm/vendor/composer/installed.php b/civicrm/vendor/composer/installed.php
index b25cdac267..52232eb15b 100644
--- a/civicrm/vendor/composer/installed.php
+++ b/civicrm/vendor/composer/installed.php
@@ -3,7 +3,7 @@
         'name' => 'civicrm/civicrm-core',
         'pretty_version' => '5.69.x-dev',
         'version' => '5.69.9999999.9999999-dev',
-        'reference' => 'b74e1246ea7d980ff89773c5f22186b95fc4d3bb',
+        'reference' => 'c3f742eff3a4248f26ed499e5ea61b2deee461e9',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -40,7 +40,7 @@
         'civicrm/civicrm-core' => array(
             'pretty_version' => '5.69.x-dev',
             'version' => '5.69.9999999.9999999-dev',
-            'reference' => 'b74e1246ea7d980ff89773c5f22186b95fc4d3bb',
+            'reference' => 'c3f742eff3a4248f26ed499e5ea61b2deee461e9',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
diff --git a/civicrm/xml/version.xml b/civicrm/xml/version.xml
index e0fb9ccfbc..36efda8d5e 100644
--- a/civicrm/xml/version.xml
+++ b/civicrm/xml/version.xml
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="iso-8859-1" ?>
 <version>
-  <version_no>5.69.2</version_no>
+  <version_no>5.69.3</version_no>
 </version>
-- 
GitLab