From b18b02e4bf4dbb0fc03f9e3ebff56a4c08b649af Mon Sep 17 00:00:00 2001 From: Matt Friedman Date: Fri, 13 Nov 2026 15:15:39 -0800 Subject: [PATCH 1/3] Prevent localstorage causing falures --- styles/all/template/webpush.js | 18 +++++++++++++++--- tests/javascript/webpush_test.js | 32 +++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/styles/all/template/webpush.js b/styles/all/template/webpush.js index 6b5f32f..5fc557b 100644 --- a/styles/all/template/webpush.js +++ b/styles/all/template/webpush.js @@ -636,15 +636,27 @@ function PhpbbWebpush() { key: 'wpn_popup_denied', set() { - localStorage.setItem(this.key, 'true'); + try { + localStorage.setItem(this.key, 'true'); + } catch (e) { + // Browser storage may be disabled or restricted. + } }, get() { - return localStorage.getItem(this.key); + try { + return localStorage.getItem(this.key); + } catch (e) { + return null; + } }, remove() { - localStorage.removeItem(this.key); + try { + localStorage.removeItem(this.key); + } catch (e) { + // Browser storage may be disabled or restricted. + } } }; } diff --git a/tests/javascript/webpush_test.js b/tests/javascript/webpush_test.js index 378853d..16959ef 100644 --- a/tests/javascript/webpush_test.js +++ b/tests/javascript/webpush_test.js @@ -43,12 +43,14 @@ async function settle() { await new Promise(resolve => setImmediate(resolve)); } -async function runScenario({stored = [], expirationTime = 0, applicationServerKey = [1, 2, 3], click = false}) { +async function runScenario({stored = [], expirationTime = 0, applicationServerKey = [1, 2, 3], click = false, storageThrows = false}) { const calls = {fetch: 0, subscribe: 0, unsubscribe: 0}; const subscribeButton = createButton('subscribe_webpush'); const unsubscribeButton = createButton('unsubscribe_webpush'); + const denyButton = createButton('wpn_popup_deny'); subscribeButton.classList.owner = subscribeButton; unsubscribeButton.classList.owner = unsubscribeButton; + denyButton.classList.owner = denyButton; const popup = { style: {display: 'none'}, @@ -102,7 +104,10 @@ async function runScenario({stored = [], expirationTime = 0, applicationServerKe return null; }, getElementById(id) { - return id === 'wpn_popup_prompt' ? popup : null; + if (id === 'wpn_popup_prompt') { + return popup; + } + return id === 'wpn_popup_deny' ? denyButton : null; }, addEventListener() {}, removeEventListener() {}, @@ -140,12 +145,21 @@ async function runScenario({stored = [], expirationTime = 0, applicationServerKe }, localStorage: { getItem(key) { + if (storageThrows) { + throw new Error('Browser storage is restricted'); + } return storage.get(key) || null; }, setItem(key, value) { + if (storageThrows) { + throw new Error('Browser storage is restricted'); + } storage.set(key, value); }, removeItem(key) { + if (storageThrows) { + throw new Error('Browser storage is restricted'); + } storage.delete(key); }, }, @@ -196,7 +210,7 @@ async function runScenario({stored = [], expirationTime = 0, applicationServerKe await settle(); } - return {calls, popup, subscribeButton, unsubscribeButton}; + return {calls, popup, subscribeButton, unsubscribeButton, denyButton}; } async function run() { @@ -219,6 +233,18 @@ async function run() { const explicit = await runScenario({stored: [], click: true}); assert.deepEqual(explicit.calls, {fetch: 1, subscribe: 1, unsubscribe: 1}, 'explicit opt-in may replace foreign subscription'); + + const restrictedStorage = await runScenario({stored: [], storageThrows: true}); + assert.equal(restrictedStorage.popup.style.display, 'flex', 'restricted storage must not interrupt popup initialization'); + assert.doesNotThrow(() => restrictedStorage.denyButton.listeners.click({ + stopPropagation() {}, + }), 'restricted storage must not interrupt popup dismissal'); + assert.equal(restrictedStorage.popup.style.display, 'none', 'popup must close when denial cannot be stored'); + + const restrictedStorageOptIn = await runScenario({stored: [], click: true, storageThrows: true}); + assert.deepEqual(restrictedStorageOptIn.calls, {fetch: 1, subscribe: 1, unsubscribe: 1}, 'restricted storage must not make successful opt-in fail'); + assert.equal(restrictedStorageOptIn.subscribeButton.hidden, true, 'successful opt-in must show subscribed state with restricted storage'); + assert.equal(restrictedStorageOptIn.unsubscribeButton.hidden, false, 'successful opt-in must allow unsubscribe with restricted storage'); } run().catch(error => { From 6c2f575c23d8ef878e5f7777b2b7a896a13a0923 Mon Sep 17 00:00:00 2001 From: Matt Friedman Date: Fri, 13 Nov 2026 15:16:04 -0800 Subject: [PATCH 2/3] reserved URL semantics --- tests/controller/controller_webpush_test.php | 5 ++++- ucp/controller/webpush.php | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/controller/controller_webpush_test.php b/tests/controller/controller_webpush_test.php index 5d0b7a7..1bc2835 100644 --- a/tests/controller/controller_webpush_test.php +++ b/tests/controller/controller_webpush_test.php @@ -330,7 +330,8 @@ public function legacy_notification_url_data(): array { return [ 'board relative' => ['./viewtopic.php?p=1', 'http://localhost/phpBB/viewtopic.php?p=1'], - 'leading slash' => ['/viewtopic.php?p=1', 'http://localhost/phpBB/viewtopic.php?p=1'], + 'origin relative' => ['/viewtopic.php?p=1', 'http://localhost/viewtopic.php?p=1'], + 'protocol relative' => ['//example.com/viewtopic.php?p=1', 'http://example.com/viewtopic.php?p=1'], 'absolute HTTPS' => ['https://example.com/viewtopic.php?p=1', 'https://example.com/viewtopic.php?p=1'], 'absolute HTTP' => ['http://example.com/viewtopic.php?p=1', 'http://example.com/viewtopic.php?p=1'], 'unsafe scheme unchanged for worker rejection' => ['javascript:alert(1)', 'javascript:alert(1)'], @@ -729,6 +730,8 @@ public function current_notification_url_data(): array return [ 'board relative' => ['viewtopic.php?p=1&x=2', 'http://localhost/phpBB/viewtopic.php?p=1&x=2'], 'route relative' => ['./../../../ucp.php?i=pm&p=4', 'http://localhost/phpBB/ucp.php?i=pm&p=4'], + 'origin relative' => ['/viewtopic.php?p=1', 'http://localhost/viewtopic.php?p=1'], + 'protocol relative' => ['//example.com/topic/1', 'http://example.com/topic/1'], 'absolute HTTPS' => ['https://example.com/topic/1', 'https://example.com/topic/1'], 'unsafe scheme unchanged for worker rejection' => ['javascript:alert(1)', 'javascript:alert(1)'], ]; diff --git a/ucp/controller/webpush.php b/ucp/controller/webpush.php index ebb7bc8..81156b5 100644 --- a/ucp/controller/webpush.php +++ b/ucp/controller/webpush.php @@ -275,7 +275,7 @@ private function get_notification_data(string $notification_data): string } /** - * Convert a board-relative notification URL to an absolute URL + * Convert a relative notification URL to an absolute URL * * @param string $url Notification URL * @return string @@ -287,8 +287,18 @@ protected function normalize_notification_url(string $url): string return $url; } - $relative_url = ltrim($url, '/'); - return $this->get_board_url() . preg_replace('#^(?:(?:\.\.?)/)+#', '', $relative_url); + if (strpos($url, '//') === 0) + { + $scheme = parse_url($this->get_board_url(), PHP_URL_SCHEME); + return (is_string($scheme) ? $scheme . ':' : '') . $url; + } + + if (strpos($url, '/') === 0) + { + return generate_board_url(true) . $url; + } + + return $this->get_board_url() . preg_replace('#^(?:(?:\.\.?)/)+#', '', $url); } /** From efa512c7e42e066c5745bf400dad2c12504fe517 Mon Sep 17 00:00:00 2001 From: Matt Friedman Date: Fri, 13 Nov 2026 15:16:15 -0800 Subject: [PATCH 3/3] Update composer lock --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index fc5fc63..2f1564d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e5c2801135f7590ea563271378cbaff2", + "content-hash": "21dab9620cc5a48eb206804120651042", "packages": [ { "name": "brick/math", @@ -1600,7 +1600,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -1610,9 +1610,9 @@ "ext-mbstring": "*", "ext-openssl": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" }