Skip to content
Merged
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
8 changes: 4 additions & 4 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 15 additions & 3 deletions styles/all/template/webpush.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
}
};
}
Expand Down
5 changes: 4 additions & 1 deletion tests/controller/controller_webpush_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)'],
Expand Down Expand Up @@ -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)'],
];
Expand Down
32 changes: 29 additions & 3 deletions tests/javascript/webpush_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
Expand Down Expand Up @@ -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() {},
Expand Down Expand Up @@ -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);
},
},
Expand Down Expand Up @@ -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() {
Expand All @@ -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 => {
Expand Down
16 changes: 13 additions & 3 deletions ucp/controller/webpush.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}

/**
Expand Down