From 1007cafb75215f65477262f3bae2b26213b76117 Mon Sep 17 00:00:00 2001 From: rofe Date: Wed, 5 Apr 2023 10:45:32 +0200 Subject: [PATCH 01/28] feat: add auth token to every admin api request --- src/extension/background.js | 59 +++++++++++++++++++++++++++++++++++-- src/extension/manifest.json | 3 +- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/extension/background.js b/src/extension/background.js index 9dce25304..6e3200b68 100644 --- a/src/extension/background.js +++ b/src/extension/background.js @@ -117,8 +117,10 @@ async function guessIfFranklinSite({ id }) { }); // listen for response message from tab const listener = ({ isFranklinSite }) => { - chrome.runtime.onMessage.removeListener(listener); - resolve(isFranklinSite); + if (typeof isFranklinSite === 'boolean') { + chrome.runtime.onMessage.removeListener(listener); + resolve(isFranklinSite); + } }; chrome.runtime.onMessage.addListener(listener); }); @@ -428,6 +430,59 @@ function checkViewDocSource(id) { actions[actionFromTab](tab); } }); + + // listen for request to admin api and add auth to headers where needed + chrome.webRequest.onBeforeSendHeaders.addListener( + async (request) => { + const { url, method, requestHeaders } = request; + const { hostname, pathname } = new URL(url); + if (hostname !== 'admin.hlx.page' || !['GET', 'POST', 'DELETE'].includes(method)) { + log.debug(`onBeforeSendheaders: ignore request ${method} ${url}`); + return request; + } + // extract owner and repo from path + const match = /\/[a-z]+\/([A-Za-z0-9-_]+)\/([A-Za-z0-9-_]+)\//.exec(pathname); + if (!match || match.length < 3) { + log.debug(`onBeforeSendheaders: ignore, no owner/repo found in pathname ${pathname}`); + return request; + } + const [, owner, repo] = match; + const project = await getProject({ owner, repo }); + if (!project) { + log.warn(`onBeforeSendheaders: ignore, no project found for ${owner}/${repo}`); + return request; + } + log.info('onBeforeSendHeaders: checking', url); + + const authHeader = requestHeaders.find((h) => h.name === 'x-auth-token'); + const { authToken } = project; + if (authToken) { + log.info(`onBeforeSendHeaders: setting x-auth-token header for ${owner}/${repo}`); + if (!authHeader) { + requestHeaders.push({ + name: 'x-auth-token', + value: authToken, + }); + log.debug(`onBeforeSendHeaders: x-auth-token header added for ${owner}/${repo}`); + } else { + authHeader.value = authToken; + log.debug(`onBeforeSendHeaders: x-auth-token header updated for ${owner}/${repo}`); + } + } else if (authHeader) { + log.info(`onBeforeSendHeaders: deleting x-auth-token header for ${owner}/${repo}`); + requestHeaders.splice(requestHeaders.findIndex(authHeader), 1); + } else { + log.info(`onBeforeSendHeaders: no x-auth-token header needed ${owner}/${repo}`); + } + console.log(request); + return request; + }, + { + urls: ['https://admin.hlx.page/*'], + types: ['xmlhttprequest'], + }, + ['requestHeaders'], + ); })(); // announce sidekick display state diff --git a/src/extension/manifest.json b/src/extension/manifest.json index 42bbec5c1..2e02913f5 100644 --- a/src/extension/manifest.json +++ b/src/extension/manifest.json @@ -8,10 +8,11 @@ "options_page": "options.html", "description": "__MSG_description__", "permissions": [ + "activeTab", "contextMenus", "scripting", "storage", - "activeTab" + "webRequest" ], "host_permissions": [ "http://localhost:3000/*", From e11b61df713111484f7f0e684048d0391ca36020 Mon Sep 17 00:00:00 2001 From: rofe Date: Wed, 5 Apr 2023 17:15:51 +0200 Subject: [PATCH 02/28] feat: add auth token to every admin api request --- src/extension/background.js | 60 +++++-------------------------------- src/extension/manifest.json | 4 +-- src/extension/utils.js | 43 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 54 deletions(-) diff --git a/src/extension/background.js b/src/extension/background.js index 6e3200b68..5b796977b 100644 --- a/src/extension/background.js +++ b/src/extension/background.js @@ -31,6 +31,7 @@ import { getConfig, storeAuthToken, updateProjectConfigs, + setAdminAuthHeaderRule, } from './utils.js'; /** @@ -431,58 +432,13 @@ function checkViewDocSource(id) { } }); - // listen for request to admin api and add auth to headers where needed - chrome.webRequest.onBeforeSendHeaders.addListener( - async (request) => { - const { url, method, requestHeaders } = request; - const { hostname, pathname } = new URL(url); - if (hostname !== 'admin.hlx.page' || !['GET', 'POST', 'DELETE'].includes(method)) { - log.debug(`onBeforeSendheaders: ignore request ${method} ${url}`); - return request; - } - // extract owner and repo from path - const match = /\/[a-z]+\/([A-Za-z0-9-_]+)\/([A-Za-z0-9-_]+)\//.exec(pathname); - if (!match || match.length < 3) { - log.debug(`onBeforeSendheaders: ignore, no owner/repo found in pathname ${pathname}`); - return request; - } - const [, owner, repo] = match; - const project = await getProject({ owner, repo }); - if (!project) { - log.warn(`onBeforeSendheaders: ignore, no project found for ${owner}/${repo}`); - return request; - } - log.info('onBeforeSendHeaders: checking', url); - - const authHeader = requestHeaders.find((h) => h.name === 'x-auth-token'); - const { authToken } = project; - if (authToken) { - log.info(`onBeforeSendHeaders: setting x-auth-token header for ${owner}/${repo}`); - if (!authHeader) { - requestHeaders.push({ - name: 'x-auth-token', - value: authToken, - }); - log.debug(`onBeforeSendHeaders: x-auth-token header added for ${owner}/${repo}`); - } else { - authHeader.value = authToken; - log.debug(`onBeforeSendHeaders: x-auth-token header updated for ${owner}/${repo}`); - } - } else if (authHeader) { - log.info(`onBeforeSendHeaders: deleting x-auth-token header for ${owner}/${repo}`); - requestHeaders.splice(requestHeaders.findIndex(authHeader), 1); - } else { - log.info(`onBeforeSendHeaders: no x-auth-token header needed ${owner}/${repo}`); - } - console.log(request); - return request; - }, - { - urls: ['https://admin.hlx.page/*'], - types: ['xmlhttprequest'], - }, - ['requestHeaders'], - ); + // for each project, listen for admin api requests and add auth to headers where needed + setAdminAuthHeaderRule(); + // for local debugging, add "declarativeNetRequestFeedback" to permissions in manifest.json + // and uncomment the following lines: + // chrome.declarativeNetRequest.onRuleMatchedDebug.addListener(({ request, rule }) => { + // console.log('rule matched', request.method, request.url, rule.ruleId); + // }); })(); // announce sidekick display state diff --git a/src/extension/manifest.json b/src/extension/manifest.json index 2e02913f5..65ccf456c 100644 --- a/src/extension/manifest.json +++ b/src/extension/manifest.json @@ -10,9 +10,9 @@ "permissions": [ "activeTab", "contextMenus", + "declarativeNetRequest", "scripting", - "storage", - "webRequest" + "storage" ], "host_permissions": [ "http://localhost:3000/*", diff --git a/src/extension/utils.js b/src/extension/utils.js index 99f78f1e6..27b8db283 100644 --- a/src/extension/utils.js +++ b/src/extension/utils.js @@ -243,6 +243,44 @@ export function isValidShareURL(shareurl) { return Object.keys(getShareSettings(shareurl)).length > 1; } +export async function setAdminAuthHeaderRule(project, index) { + if (!project) { + const projects = await getConfig('sync', 'hlxSidekickProjects'); + // loop through all projects + projects.forEach(async (handle, i) => { + await setAdminAuthHeaderRule(await getConfig('sync', handle), i); + }); + return; + } + const { owner, repo, authToken } = project; + const rules = await chrome.declarativeNetRequest.getSessionRules(); + const removeRuleIds = rules.filter((r) => r.id === index).map((r) => r.id); + const options = { + removeRuleIds, + }; + if (authToken) { + options.addRules = [{ + id: index, + priority: 1, + action: { + type: 'modifyHeaders', + requestHeaders: [{ + operation: 'set', + header: 'x-auth-token', + value: authToken, + }], + }, + condition: { + regexFilter: `^https://admin.hlx.page/[a-z]+/${owner}/${repo}/.*`, + requestDomains: ['admin.hlx.page'], + requestMethods: ['get', 'post', 'delete'], + resourceTypes: ['xmlhttprequest'], + }, + }]; + } + await chrome.declarativeNetRequest.updateSessionRules(options); +} + async function getProjectConfig(owner, repo, ref = 'main') { const cfg = {}; let res; @@ -320,6 +358,9 @@ export async function setProject(project, cb) { await setConfig('sync', { hlxSidekickProjects: projects }); } log.info('updated project', project); + // set admin auth header rule + await setAdminAuthHeaderRule(projects, project); + if (typeof cb === 'function') { cb(project); } @@ -346,6 +387,8 @@ export async function deleteProject(handle, cb) { const i = projects.indexOf(handle); if (i >= 0) { if (confirm(i18n('config_delete_confirm'))) { + // delete admin auth header rule + await setAdminAuthHeaderRule(projects, await getConfig('sync', handle)); // delete the project entry await removeConfig('sync', handle); // remove project entry from index From 2ad4c5241b60c7649c473965ad0514d7924932ba Mon Sep 17 00:00:00 2001 From: rofe Date: Wed, 5 Apr 2023 22:54:33 +0200 Subject: [PATCH 03/28] chore: only remove existing rules --- src/extension/utils.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/extension/utils.js b/src/extension/utils.js index 27b8db283..d9c507a1b 100644 --- a/src/extension/utils.js +++ b/src/extension/utils.js @@ -254,10 +254,11 @@ export async function setAdminAuthHeaderRule(project, index) { } const { owner, repo, authToken } = project; const rules = await chrome.declarativeNetRequest.getSessionRules(); - const removeRuleIds = rules.filter((r) => r.id === index).map((r) => r.id); - const options = { - removeRuleIds, - }; + const options = {}; + if (rules.find((r) => r.id === index)) { + // remove existing rule + options.removeRuleIds = [index]; + } if (authToken) { options.addRules = [{ id: index, @@ -278,7 +279,10 @@ export async function setAdminAuthHeaderRule(project, index) { }, }]; } - await chrome.declarativeNetRequest.updateSessionRules(options); + if (Object.keys(options).length) { + await chrome.declarativeNetRequest.updateSessionRules(options); + log.debug(`setAdminAuthHeaderRule: rule set for ${owner}/${repo}`); + } } async function getProjectConfig(owner, repo, ref = 'main') { From 637e3c2863f684a2f8077d9396ec2b4cd487f379 Mon Sep 17 00:00:00 2001 From: rofe Date: Wed, 5 Apr 2023 22:55:08 +0200 Subject: [PATCH 04/28] fix: safari error on localhost urls --- src/extension/manifest.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/extension/manifest.json b/src/extension/manifest.json index 65ccf456c..d7d00025c 100644 --- a/src/extension/manifest.json +++ b/src/extension/manifest.json @@ -49,8 +49,7 @@ "view-doc-source/js/content.js" ], "matches": [ - "https://*/*", - "http://localhost:3000/*" + "" ] } ], From 17bdb1b55bc524fcfa4278047f7d629227a38557 Mon Sep 17 00:00:00 2001 From: rofe Date: Wed, 5 Apr 2023 22:55:56 +0200 Subject: [PATCH 05/28] fix: only inject content script if config match --- src/extension/background.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/extension/background.js b/src/extension/background.js index 5b796977b..da0e6965e 100644 --- a/src/extension/background.js +++ b/src/extension/background.js @@ -221,19 +221,21 @@ function checkTab(id) { } const matches = await getProjectMatches(projects, checkUrl); log.debug('checking', id, checkUrl, matches); - try { - // execute content script - chrome.scripting.executeScript({ - target: { tabId: id }, - files: ['./content.js'], - }, () => { - // send config matches to tab - chrome.tabs.sendMessage(id, { - projectMatches: matches, + if (matches) { + try { + // execute content script + chrome.scripting.executeScript({ + target: { tabId: id }, + files: ['./content.js'], + }, () => { + // send config matches to tab + chrome.tabs.sendMessage(id, { + projectMatches: matches, + }); }); - }); - } catch (e) { - log.error('error enabling extension', id, e); + } catch (e) { + log.error('error enabling extension', id, e); + } } }); }); From 2d074057d60f280bbe68286fa44c60efc0a677f7 Mon Sep 17 00:00:00 2001 From: rofe Date: Wed, 5 Apr 2023 23:23:12 +0200 Subject: [PATCH 06/28] chore: extend chrome mock to fix tests --- test/extension/chromeMock.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/extension/chromeMock.js b/test/extension/chromeMock.js index 3ae2ad2ac..fd760efaf 100644 --- a/test/extension/chromeMock.js +++ b/test/extension/chromeMock.js @@ -73,4 +73,8 @@ export default { test: 'test', }), }, + declarativeNetRequest: { + getSessionRules: async () => ([]), + updateSessionRules: async () => undefined, + }, }; From b2de1726bba32e32b99bf2995d6aba2b3e775bc8 Mon Sep 17 00:00:00 2001 From: rofe Date: Thu, 6 Apr 2023 19:28:19 +0200 Subject: [PATCH 07/28] refactor: move auth token handling to background --- src/extension/background.js | 79 ++++++++++++++++++++++++++++++++++--- src/extension/sidekick.js | 3 +- src/extension/utils.js | 64 ++---------------------------- 3 files changed, 77 insertions(+), 69 deletions(-) diff --git a/src/extension/background.js b/src/extension/background.js index da0e6965e..c01817cc4 100644 --- a/src/extension/background.js +++ b/src/extension/background.js @@ -29,9 +29,7 @@ import { getGitHubSettings, setConfig, getConfig, - storeAuthToken, updateProjectConfigs, - setAdminAuthHeaderRule, } from './utils.js'; /** @@ -332,6 +330,66 @@ function checkViewDocSource(id) { }); } +async function updateAdminAuthHeaderRules() { + let id = 1; + const projects = await getConfig('sync', 'hlxSidekickProjects') || []; + projects.forEach(async (handle) => { + const project = await getConfig('sync', handle); + const { + owner, + repo, + authToken, + } = project; + const options = { + removeRuleIds: (await chrome.declarativeNetRequest.getSessionRules()) + .map((rule) => rule.id), + }; + if (authToken) { + options.addRules = [{ + id, + priority: 1, + action: { + type: 'modifyHeaders', + requestHeaders: [{ + operation: 'set', + header: 'x-auth-token', + value: authToken, + }], + }, + condition: { + regexFilter: `^https://admin.hlx.page/[a-z]+/${owner}/${repo}/.*`, + requestDomains: ['admin.hlx.page'], + requestMethods: ['get', 'post', 'delete'], + resourceTypes: ['xmlhttprequest'], + }, + }]; + id += 1; + } + if (Object.keys(options).length) { + await chrome.declarativeNetRequest.updateSessionRules(options); + log.debug(`setAdminAuthHeaderRule: rule set for ${owner}/${repo}`); + } + }); +} + +async function storeAuthToken(owner, repo, token) { + // find config tab with owner/repo + const project = await getProject({ owner, repo }); + if (project) { + if (token) { + project.authToken = token; + } else { + delete project.authToken; + } + await setProject(project); + log.debug(`updated auth token for ${owner}--${repo}`); + } else { + log.debug(`unable to update auth token for ${owner}--${repo}: no such config`); + } + // auth token changed, set/update admin auth header + updateAdminAuthHeaderRules(); +} + /** * Adds the listeners for the extension. */ @@ -340,6 +398,7 @@ function checkViewDocSource(id) { log.info(`sidekick extension installed (${reason})`); await updateHelpContent(); await updateProjectConfigs(); + await updateAdminAuthHeaderRules(); }); // register message listener @@ -434,10 +493,18 @@ function checkViewDocSource(id) { } }); - // for each project, listen for admin api requests and add auth to headers where needed - setAdminAuthHeaderRule(); - // for local debugging, add "declarativeNetRequestFeedback" to permissions in manifest.json - // and uncomment the following lines: + // listen for delete auth token calls from the content window + chrome.runtime.onMessage.addListener(async ({ deleteAuthToken }, { tab }) => { + // check if message contains project config and is sent from tab + if (tab && tab.id && typeof deleteAuthToken === 'object') { + const { owner, repo } = deleteAuthToken; + await storeAuthToken(owner, repo, ''); + } + }); + + // for local debugging of header modification rules: + // 1. add "declarativeNetRequestFeedback" to permissions in manifest.json + // 2. uncomment the following 3 lines: // chrome.declarativeNetRequest.onRuleMatchedDebug.addListener(({ request, rule }) => { // console.log('rule matched', request.method, request.url, rule.ruleId); // }); diff --git a/src/extension/sidekick.js b/src/extension/sidekick.js index 28c7d92c5..df2f7a21e 100644 --- a/src/extension/sidekick.js +++ b/src/extension/sidekick.js @@ -21,7 +21,6 @@ import { setConfig, setDisplay, i18n, - storeAuthToken, } from './utils.js'; export default async function injectSidekick(config, display) { @@ -108,7 +107,7 @@ export default async function injectSidekick(config, display) { sk.addEventListener('loggedout', async () => { // user clicked logout, delete the authToken from the config log.debug(`removing authToken from config ${owner}/${repo}`); - await storeAuthToken(owner, repo, ''); + chrome.runtime.sendMessage({ deleteAuthToken: { owner, repo } }); }); const helpOptOut = await getConfig('sync', 'hlxSidekickHelpOptOut'); if (!helpOptOut) { diff --git a/src/extension/utils.js b/src/extension/utils.js index d9c507a1b..da36d7058 100644 --- a/src/extension/utils.js +++ b/src/extension/utils.js @@ -243,48 +243,6 @@ export function isValidShareURL(shareurl) { return Object.keys(getShareSettings(shareurl)).length > 1; } -export async function setAdminAuthHeaderRule(project, index) { - if (!project) { - const projects = await getConfig('sync', 'hlxSidekickProjects'); - // loop through all projects - projects.forEach(async (handle, i) => { - await setAdminAuthHeaderRule(await getConfig('sync', handle), i); - }); - return; - } - const { owner, repo, authToken } = project; - const rules = await chrome.declarativeNetRequest.getSessionRules(); - const options = {}; - if (rules.find((r) => r.id === index)) { - // remove existing rule - options.removeRuleIds = [index]; - } - if (authToken) { - options.addRules = [{ - id: index, - priority: 1, - action: { - type: 'modifyHeaders', - requestHeaders: [{ - operation: 'set', - header: 'x-auth-token', - value: authToken, - }], - }, - condition: { - regexFilter: `^https://admin.hlx.page/[a-z]+/${owner}/${repo}/.*`, - requestDomains: ['admin.hlx.page'], - requestMethods: ['get', 'post', 'delete'], - resourceTypes: ['xmlhttprequest'], - }, - }]; - } - if (Object.keys(options).length) { - await chrome.declarativeNetRequest.updateSessionRules(options); - log.debug(`setAdminAuthHeaderRule: rule set for ${owner}/${repo}`); - } -} - async function getProjectConfig(owner, repo, ref = 'main') { const cfg = {}; let res; @@ -352,6 +310,7 @@ export async function getProject(project) { export async function setProject(project, cb) { const { owner, repo } = project; const handle = `${owner}/${repo}`; + // update project config await setConfig('sync', { [handle]: project, }); @@ -362,8 +321,6 @@ export async function setProject(project, cb) { await setConfig('sync', { hlxSidekickProjects: projects }); } log.info('updated project', project); - // set admin auth header rule - await setAdminAuthHeaderRule(projects, project); if (typeof cb === 'function') { cb(project); @@ -392,7 +349,8 @@ export async function deleteProject(handle, cb) { if (i >= 0) { if (confirm(i18n('config_delete_confirm'))) { // delete admin auth header rule - await setAdminAuthHeaderRule(projects, await getConfig('sync', handle)); + const [owner, repo] = handle.split('/'); + chrome.runtime.sendMessage({ deleteAuthToken: { owner, repo } }); // delete the project entry await removeConfig('sync', handle); // remove project entry from index @@ -427,22 +385,6 @@ export function toggleDisplay(cb) { }); } -export async function storeAuthToken(owner, repo, token) { - // find config tab with owner/repo - const project = await getProject({ owner, repo }); - if (project) { - if (token) { - project.authToken = token; - } else { - delete project.authToken; - } - await setProject(project); - log.debug(`updated auth token for ${owner}--${repo}`); - } else { - log.warn(`unable to update auth token for ${owner}--${repo}: no such config`); - } -} - export async function updateProjectConfigs() { const configs = await getConfig('sync', 'hlxSidekickConfigs'); const projects = await getConfig('sync', 'hlxSidekickProjects'); From be9fe4302fd17d8911bdbbb47ef156f488bd7f81 Mon Sep 17 00:00:00 2001 From: rofe Date: Thu, 6 Apr 2023 19:29:05 +0200 Subject: [PATCH 08/28] refactor: remove auth header setting from module --- src/extension/module.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/extension/module.js b/src/extension/module.js index b3c2d1ca2..9c9f64cb5 100644 --- a/src/extension/module.js +++ b/src/extension/module.js @@ -885,15 +885,12 @@ * @param {object} config * @returns {object} */ - function getAdminFetchOptions({ authToken }) { + function getAdminFetchOptions() { const opts = { cache: 'no-store', credentials: 'include', headers: {}, }; - if (authToken) { - opts.headers['x-auth-token'] = authToken; - } return opts; } @@ -1807,7 +1804,13 @@ const loginWindow = window.open(loginUrl.toString()); async function checkLoggedIn() { - if ((await fetch(profileUrl.href, getAdminFetchOptions(sk.config))).ok) { + const opts = getAdminFetchOptions(sk.config); + if (sk.config.authToken) { + opts.headers = { + 'x-auth-token': sk.config.authToken, + }; + } + if ((await fetch(profileUrl.href, opts)).ok) { window.setTimeout(() => { if (!loginWindow.closed) { loginWindow.close(); From 55f628d0565011aad4911f6d86f462de7748fecb Mon Sep 17 00:00:00 2001 From: rofe Date: Fri, 7 Apr 2023 17:15:01 +0200 Subject: [PATCH 09/28] fix: only inject content script if config match --- src/extension/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension/background.js b/src/extension/background.js index c01817cc4..a794682a1 100644 --- a/src/extension/background.js +++ b/src/extension/background.js @@ -219,7 +219,7 @@ function checkTab(id) { } const matches = await getProjectMatches(projects, checkUrl); log.debug('checking', id, checkUrl, matches); - if (matches) { + if (matches.length > 0) { try { // execute content script chrome.scripting.executeScript({ From 8835ae6e309023439e2c2f92add8cc754b0a2521 Mon Sep 17 00:00:00 2001 From: rofe Date: Fri, 21 Apr 2023 16:02:04 +0200 Subject: [PATCH 10/28] feat: new logout flow --- src/extension/module.js | 70 +++++++++++++++++++++------------------ src/extension/sidekick.js | 5 --- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/extension/module.js b/src/extension/module.js index 9c9f64cb5..32344ccb9 100644 --- a/src/extension/module.js +++ b/src/extension/module.js @@ -1784,12 +1784,7 @@ */ function login(sk, selectAccount) { sk.showWait(); - const loginUrl = getAdminUrl( - sk.config, - 'login', - sk.isProject() ? sk.location.pathname : '', - ); - loginUrl.searchParams.set('loginRedirect', 'https://www.hlx.live/tools/sidekick/login-success'); + const loginUrl = getAdminUrl(sk.config, 'login'); const extensionId = window.chrome?.runtime?.id; if (extensionId) { loginUrl.searchParams.set('extensionId', extensionId); @@ -1797,19 +1792,11 @@ if (selectAccount) { loginUrl.searchParams.set('selectAccount', true); } - const profileUrl = new URL('https://admin.hlx.page/profile'); - if (sk.config.adminVersion) { - profileUrl.searchParams.append('hlx-admin-version', sk.config.adminVersion); - } + const profileUrl = getAdminUrl(sk.config, 'profile'); const loginWindow = window.open(loginUrl.toString()); async function checkLoggedIn() { const opts = getAdminFetchOptions(sk.config); - if (sk.config.authToken) { - opts.headers = { - 'x-auth-token': sk.config.authToken, - }; - } if ((await fetch(profileUrl.href, opts)).ok) { window.setTimeout(() => { if (!loginWindow.closed) { @@ -1862,29 +1849,43 @@ * @param {Sidekick} sk The sidekick */ function logout(sk) { - const logoutUrl = new URL('https://admin.hlx.page/logout'); - if (sk.config.adminVersion) { - logoutUrl.searchParams.append('hlx-admin-version', sk.config.adminVersion); + sk.showWait(); + const logoutUrl = getAdminUrl(sk.config, 'logout'); + const extensionId = window.chrome?.runtime?.id; + if (extensionId) { + logoutUrl.searchParams.set('extensionId', extensionId); } + const logoutWindow = window.open(logoutUrl.toString()); - fetch(logoutUrl.href, { - ...getAdminFetchOptions(sk.config), - }) - .then(() => { + async function checkLoggedOut() { + if (logoutWindow.closed) { + delete sk.status.profile; delete sk.config.authToken; - sk.status = { - loggedOut: true, - }; - }) - .then(() => fireEvent(sk, 'loggedout')) - .then(() => sk.fetchStatus()) - .catch((e) => { - console.error('logout failed', e); + sk.addEventListener('statusfetched', () => sk.hideModal(), { once: true }); + sk.fetchStatus(); + fireEvent(sk, 'loggedout'); + return true; + } + return false; + } + + let seconds = 0; + const logoutCheck = window.setInterval(async () => { + // give up after 2 minutes or window closed + if (seconds >= 120) { + window.clearInterval(logoutCheck); + logoutWindow.close(); sk.showModal({ message: i18n(sk, 'error_logout_error'), - level: 0, + sticky: true, + level: 1, }); - }); + } + seconds += 1; + if (await checkLoggedOut()) { + window.clearInterval(logoutCheck); + } + }, 1000); } /** @@ -2634,7 +2635,10 @@ throw new Error('error_status_invalid'); } }) - .then((json) => Object.assign(this.status, json)) + .then((json) => { + this.status = json; + return json; + }) .then((json) => fireEvent(this, 'statusfetched', json)) .catch(({ message }) => { this.status.error = message; diff --git a/src/extension/sidekick.js b/src/extension/sidekick.js index df2f7a21e..b2110b2a1 100644 --- a/src/extension/sidekick.js +++ b/src/extension/sidekick.js @@ -104,11 +104,6 @@ export default async function injectSidekick(config, display) { sk.addEventListener('hidden', () => { setDisplay(false); }); - sk.addEventListener('loggedout', async () => { - // user clicked logout, delete the authToken from the config - log.debug(`removing authToken from config ${owner}/${repo}`); - chrome.runtime.sendMessage({ deleteAuthToken: { owner, repo } }); - }); const helpOptOut = await getConfig('sync', 'hlxSidekickHelpOptOut'); if (!helpOptOut) { // find next unacknowledged help topic with matching condition From 3b800f833a95df7d5fe6c1f3c7caf0b33e614b73 Mon Sep 17 00:00:00 2001 From: rofe Date: Fri, 21 Apr 2023 19:37:30 +0200 Subject: [PATCH 11/28] chore: adjust tests --- test/login.test.js | 8 ++++---- test/logout.test.js | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/login.test.js b/test/login.test.js index 5adb3aa07..9cb8e4ac6 100644 --- a/test/login.test.js +++ b/test/login.test.js @@ -42,7 +42,7 @@ describe('Test sidekick login', () => { nock.done(); }); - it('Opens login window and logs in via auth-cookie', async () => { + it.only('Opens login window and logs in via auth-cookie', async () => { let loggedIn = false; const test = new SidekickTest({ browser, @@ -62,13 +62,13 @@ describe('Test sidekick login', () => { } return [401]; }) - .get('/login/adobe/blog/main/en/topics/bla?loginRedirect=https%3A%2F%2Fwww.hlx.live%2Ftools%2Fsidekick%2Flogin-success') + .get('/login/adobe/blog/main') .times(DEBUG ? 2 : 1) // when dev-tools are enabled, browser makes 2 requests. .delay(1500) // delay so that 2 requests are made .reply(200, 'logged in!', { 'set-cookie': 'auth_token=foobar; Path=/; HttpOnly; Secure; SameSite=None', }) - .get('/profile') + .get('/profile/adobe/blog/main') .times(2) .reply(function req() { if (this.req.headers.cookie === 'auth_token=foobar') { @@ -85,7 +85,7 @@ describe('Test sidekick login', () => { new Promise((resolve) => { page.browser().on('targetdestroyed', async (target) => { const targetUrl = target.url(); - if (targetUrl.startsWith('https://admin.hlx.page/login/adobe/blog/main/en/topics/bla')) { + if (targetUrl === 'https://admin.hlx.page/login/adobe/blog/main') { loginClosed = true; resolve(); } diff --git a/test/logout.test.js b/test/logout.test.js index 7cc1e05c3..05a93e852 100644 --- a/test/logout.test.js +++ b/test/logout.test.js @@ -45,14 +45,15 @@ describe('Test sidekick logout', () => { it('Logout removes auth token from config', async () => { nock.admin(new Setup('blog')); nock('https://admin.hlx.page') - .get('/logout') - .reply(200, {}); + .get('/logout/adobe/blog/main') + .reply(200, ''); nock.admin(new Setup('blog')); + const test = new SidekickTest({ browser, page, plugin: 'user-logout', - sleep: 2000, + pluginSleep: 2000, checkPage: async (p) => p.evaluate(() => window.hlx.sidekick.config), }); test.sidekickConfig.authToken = 'foobar'; From bb3b986394dd84121e5162479e5bdf80cdd8e0e9 Mon Sep 17 00:00:00 2001 From: rofe Date: Fri, 21 Apr 2023 19:44:01 +0200 Subject: [PATCH 12/28] chore: remove only --- test/login.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/login.test.js b/test/login.test.js index 9cb8e4ac6..85137a4bd 100644 --- a/test/login.test.js +++ b/test/login.test.js @@ -42,7 +42,7 @@ describe('Test sidekick login', () => { nock.done(); }); - it.only('Opens login window and logs in via auth-cookie', async () => { + it('Opens login window and logs in via auth-cookie', async () => { let loggedIn = false; const test = new SidekickTest({ browser, From 68b3a75e5360c739359901a79c3bb8c645951b50 Mon Sep 17 00:00:00 2001 From: rofe Date: Fri, 21 Apr 2023 19:54:04 +0200 Subject: [PATCH 13/28] chore: adjust test --- test/login.test.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/login.test.js b/test/login.test.js index 85137a4bd..4d4703f34 100644 --- a/test/login.test.js +++ b/test/login.test.js @@ -110,13 +110,10 @@ describe('Test sidekick login', () => { nock('https://admin.hlx.page') .get('/status/adobe/blog/main/en/topics/bla?editUrl=auto') .reply(401) - .get('/profile') + .get('/profile/adobe/blog/main') .times(2) .reply(401) - .get('/login/adobe/blog/main/en/topics/bla') - .query({ - loginRedirect: 'https://www.hlx.live/tools/sidekick/login-success', - }) + .get('/login/adobe/blog/main') .reply(200, 'not logged in!'); const { popupTarget } = await test.run(); From 673976ce0827c9cde13af1997cbb80d315aa790b Mon Sep 17 00:00:00 2001 From: rofe Date: Sat, 22 Apr 2023 11:38:06 +0200 Subject: [PATCH 14/28] chore: stabilize deleteProject test --- test/extension/utils.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/extension/utils.test.js b/test/extension/utils.test.js index f6d07b003..272846813 100644 --- a/test/extension/utils.test.js +++ b/test/extension/utils.test.js @@ -224,9 +224,10 @@ describe('Test extension utils', () => { })).to.be.true; }); - it('deleteProject', async () => { + it('deleteProject', async (done) => { const spy = sandbox.spy(window.chrome.storage.sync, 'set'); const deleted = await new Promise((resolve) => { + done(); utils.deleteProject('test/add-project', resolve); }); expect(deleted).to.be.true; From 5311af76a1b52d8352bed959cc164ab32a64b12b Mon Sep 17 00:00:00 2001 From: rofe Date: Sat, 22 Apr 2023 14:32:05 +0200 Subject: [PATCH 15/28] chore: fix deleteProject test --- test/extension/chromeMock.js | 1 + test/extension/utils.test.js | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/extension/chromeMock.js b/test/extension/chromeMock.js index fd760efaf..5d899fff2 100644 --- a/test/extension/chromeMock.js +++ b/test/extension/chromeMock.js @@ -51,6 +51,7 @@ export default { getManifest: async () => readFile({ path: '../../src/extension/manifest.json' }).then((mf) => JSON.parse(mf)), getURL: (path) => `chrome-extension://${ID}${path}`, lastError: null, + sendMessage: () => {}, }, storage: { sync: new StorageMock({ diff --git a/test/extension/utils.test.js b/test/extension/utils.test.js index 272846813..92b22dd08 100644 --- a/test/extension/utils.test.js +++ b/test/extension/utils.test.js @@ -224,15 +224,14 @@ describe('Test extension utils', () => { })).to.be.true; }); - it('deleteProject', async (done) => { + it('deleteProject', async () => { const spy = sandbox.spy(window.chrome.storage.sync, 'set'); const deleted = await new Promise((resolve) => { - done(); - utils.deleteProject('test/add-project', resolve); + utils.deleteProject('adobe/blog', resolve); }); expect(deleted).to.be.true; expect(spy.calledWith({ - hlxSidekickProjects: ['adobe/blog'], + hlxSidekickProjects: ['test/add-project'], })).to.be.true; }); From 05be0a8965aba94534b1511bab89534efa7e79bc Mon Sep 17 00:00:00 2001 From: rofe Date: Sat, 22 Apr 2023 18:40:53 +0200 Subject: [PATCH 16/28] chore(safari): dev version [skip ci] --- .../project.pbxproj | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/safari/helix-sidekick-extension.xcodeproj/project.pbxproj b/src/safari/helix-sidekick-extension.xcodeproj/project.pbxproj index 7474cec08..d689435f7 100644 --- a/src/safari/helix-sidekick-extension.xcodeproj/project.pbxproj +++ b/src/safari/helix-sidekick-extension.xcodeproj/project.pbxproj @@ -719,7 +719,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "6.12.1"; + CURRENT_PROJECT_VERSION = "6.13.0"; DEVELOPMENT_TEAM = JQ525L2MZD; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS (Extension)/Info.plist"; @@ -731,7 +731,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = "6.12.1"; + MARKETING_VERSION = "6.13.0"; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -750,7 +750,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "6.12.1"; + CURRENT_PROJECT_VERSION = "6.13.0"; DEVELOPMENT_TEAM = JQ525L2MZD; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS (Extension)/Info.plist"; @@ -762,7 +762,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = "6.12.1"; + MARKETING_VERSION = "6.13.0"; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -785,7 +785,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "6.12.1"; + CURRENT_PROJECT_VERSION = "6.13.0"; DEVELOPMENT_TEAM = JQ525L2MZD; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS (App)/Info.plist"; @@ -801,7 +801,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = "6.12.1"; + MARKETING_VERSION = "6.13.0"; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -824,7 +824,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "6.12.1"; + CURRENT_PROJECT_VERSION = "6.13.0"; DEVELOPMENT_TEAM = JQ525L2MZD; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS (App)/Info.plist"; @@ -840,7 +840,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = "6.12.1"; + MARKETING_VERSION = "6.13.0"; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -862,7 +862,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/AEM Sidekick.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "6.12.1"; + CURRENT_PROJECT_VERSION = "6.13.0"; DEVELOPMENT_TEAM = JQ525L2MZD; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -875,7 +875,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = "6.12.1"; + MARKETING_VERSION = "6.13.0"; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -894,7 +894,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/AEM Sidekick.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "6.12.1"; + CURRENT_PROJECT_VERSION = "6.13.0"; DEVELOPMENT_TEAM = JQ525L2MZD; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -907,7 +907,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = "6.12.1"; + MARKETING_VERSION = "6.13.0"; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -929,7 +929,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "macOS (App)/AEM Sidekick.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "6.12.1"; + CURRENT_PROJECT_VERSION = "6.13.0"; DEVELOPMENT_TEAM = JQ525L2MZD; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -943,7 +943,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = "6.12.1"; + MARKETING_VERSION = "6.13.0"; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -966,7 +966,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "macOS (App)/AEM Sidekick.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "6.12.1"; + CURRENT_PROJECT_VERSION = "6.13.0"; DEVELOPMENT_TEAM = JQ525L2MZD; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -980,7 +980,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = "6.12.1"; + MARKETING_VERSION = "6.13.0"; OTHER_LDFLAGS = ( "-framework", SafariServices, From 713013fcb1284a285612ea3a2f5ca2564f569247 Mon Sep 17 00:00:00 2001 From: rofe Date: Tue, 25 Apr 2023 07:52:46 +0200 Subject: [PATCH 17/28] refactor: stabilize admin auth header rule handling --- src/extension/background.js | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/extension/background.js b/src/extension/background.js index a794682a1..73985875f 100644 --- a/src/extension/background.js +++ b/src/extension/background.js @@ -331,21 +331,19 @@ function checkViewDocSource(id) { } async function updateAdminAuthHeaderRules() { + // remove all rules first + await chrome.declarativeNetRequest.updateSessionRules({ + removeRuleIds: (await chrome.declarativeNetRequest.getSessionRules()) + .map((rule) => rule.id), + }); + // find projects with auth tokens and add rules for each let id = 1; const projects = await getConfig('sync', 'hlxSidekickProjects') || []; - projects.forEach(async (handle) => { - const project = await getConfig('sync', handle); - const { - owner, - repo, - authToken, - } = project; - const options = { - removeRuleIds: (await chrome.declarativeNetRequest.getSessionRules()) - .map((rule) => rule.id), - }; + const addRules = []; + const projectConfigs = await Promise.all(projects.map((handle) => getConfig('sync', handle))); + projectConfigs.forEach(({ owner, repo, authToken }) => { if (authToken) { - options.addRules = [{ + addRules.push({ id, priority: 1, action: { @@ -362,14 +360,17 @@ async function updateAdminAuthHeaderRules() { requestMethods: ['get', 'post', 'delete'], resourceTypes: ['xmlhttprequest'], }, - }]; + }); id += 1; - } - if (Object.keys(options).length) { - await chrome.declarativeNetRequest.updateSessionRules(options); - log.debug(`setAdminAuthHeaderRule: rule set for ${owner}/${repo}`); + log.debug('added admin auth header rule for ', owner, repo); } }); + if (addRules.length > 0) { + await chrome.declarativeNetRequest.updateSessionRules({ + addRules, + }); + log.debug(`setAdminAuthHeaderRule: ${addRules.length} rule(s) set`); + } } async function storeAuthToken(owner, repo, token) { From 6721b23ad7e2ce630e2a63ee4c3307b181864008 Mon Sep 17 00:00:00 2001 From: rofe Date: Tue, 25 Apr 2023 07:55:07 +0200 Subject: [PATCH 18/28] refactor: tiemout based login/logout --- src/extension/module.js | 116 +++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 62 deletions(-) diff --git a/src/extension/module.js b/src/extension/module.js index 32344ccb9..78036b6ac 100644 --- a/src/extension/module.js +++ b/src/extension/module.js @@ -1776,6 +1776,18 @@ } } + async function checkProfileStatus(sk, status) { + const url = getAdminUrl(sk.config, 'profile'); + const opts = getAdminFetchOptions(sk.config); + return fetch(url, opts) + .then((res) => res.json()) + .then((json) => { + console.log(json); + return json.status === status; + }) + .catch(() => false); + } + /** * Logs the user in. * @private @@ -1792,55 +1804,36 @@ if (selectAccount) { loginUrl.searchParams.set('selectAccount', true); } - const profileUrl = getAdminUrl(sk.config, 'profile'); const loginWindow = window.open(loginUrl.toString()); - async function checkLoggedIn() { - const opts = getAdminFetchOptions(sk.config); - if ((await fetch(profileUrl.href, opts)).ok) { - window.setTimeout(() => { - if (!loginWindow.closed) { - loginWindow.close(); - } - }, 500); - delete sk.status.status; - sk.addEventListener('statusfetched', () => sk.hideModal(), { once: true }); - sk.fetchStatus(); - fireEvent(sk, 'loggedin'); - return true; - } - return false; - } + let attempts = 0; - let seconds = 0; - const loginCheck = window.setInterval(async () => { - // give up after 2 minutes or window closed - if (seconds >= 120 || loginWindow.closed) { - window.clearInterval(loginCheck); - loginWindow.close(); - // last check - if (await checkLoggedIn()) { + async function checkLoggedIn() { + if (loginWindow.closed) { + attempts += 1; + // try 5 times after login window has been closed + if (await checkProfileStatus(sk, 200)) { + // logged in, stop checking + delete sk.status.status; + sk.addEventListener('statusfetched', () => sk.hideModal(), { once: true }); + sk.fetchStatus(); + fireEvent(sk, 'loggedin'); return; } - - if (seconds >= 120) { + if (attempts >= 5) { + // give up after 5 attempts sk.showModal({ message: i18n(sk, 'error_login_timeout'), sticky: true, level: 1, }); - } else { - sk.showModal({ - messsage: i18n(sk, 'error_login_aborted'), - }); + return; } } - - seconds += 1; - if (await checkLoggedIn()) { - window.clearInterval(loginCheck); - } - }, 1000); + // try again after 1s + window.setTimeout(checkLoggedIn, 1000); + } + window.setTimeout(checkLoggedIn, 1000); } /** @@ -1857,35 +1850,34 @@ } const logoutWindow = window.open(logoutUrl.toString()); + let attempts = 0; + async function checkLoggedOut() { if (logoutWindow.closed) { - delete sk.status.profile; - delete sk.config.authToken; - sk.addEventListener('statusfetched', () => sk.hideModal(), { once: true }); - sk.fetchStatus(); - fireEvent(sk, 'loggedout'); - return true; + attempts += 1; + // try 5 times after login window has been closed + if (await checkProfileStatus(sk, 401)) { + delete sk.status.profile; + delete sk.config.authToken; + sk.addEventListener('statusfetched', () => sk.hideModal(), { once: true }); + sk.fetchStatus(); + fireEvent(sk, 'loggedout'); + return; + } + if (attempts >= 5) { + // give up after 5 attempts + sk.showModal({ + message: i18n(sk, 'error_logout_error'), + sticky: true, + level: 1, + }); + return; + } } - return false; + // try again after 1s + window.setTimeout(checkLoggedOut, 1000); } - - let seconds = 0; - const logoutCheck = window.setInterval(async () => { - // give up after 2 minutes or window closed - if (seconds >= 120) { - window.clearInterval(logoutCheck); - logoutWindow.close(); - sk.showModal({ - message: i18n(sk, 'error_logout_error'), - sticky: true, - level: 1, - }); - } - seconds += 1; - if (await checkLoggedOut()) { - window.clearInterval(logoutCheck); - } - }, 1000); + window.setTimeout(checkLoggedOut, 1000); } /** From 4f49b702daba0367e72dda4600828e660890fc14 Mon Sep 17 00:00:00 2001 From: rofe Date: Tue, 25 Apr 2023 15:05:19 +0200 Subject: [PATCH 19/28] chore: fix login test --- test/login.test.js | 41 +++++++++++++---------------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/test/login.test.js b/test/login.test.js index 4d4703f34..4e82783ca 100644 --- a/test/login.test.js +++ b/test/login.test.js @@ -13,7 +13,7 @@ import assert from 'assert'; import { - DEBUG, IT_DEFAULT_TIMEOUT, Nock, sleep, TestBrowser, + IT_DEFAULT_TIMEOUT, Nock, TestBrowser, } from './utils.js'; import { SidekickTest } from './SidekickTest.js'; @@ -47,55 +47,40 @@ describe('Test sidekick login', () => { const test = new SidekickTest({ browser, page, - waitPopup: 2000, + pluginSleep: 2000, plugin: 'user-login', loadModule: true, }); nock('https://admin.hlx.page') .get('/status/adobe/blog/main/en/topics/bla?editUrl=auto') - .times(2) + .twice() .reply(function req() { if (this.req.headers.cookie === 'auth_token=foobar') { loggedIn = true; - return [200, '{}', { 'content-type': 'application/json' }]; + return [200, '{ "status": 200}', { 'content-type': 'application/json' }]; } - return [401]; + return [401, '{ "status": 401 }', { 'content-type': 'application/json' }]; }) .get('/login/adobe/blog/main') - .times(DEBUG ? 2 : 1) // when dev-tools are enabled, browser makes 2 requests. - .delay(1500) // delay so that 2 requests are made - .reply(200, 'logged in!', { + .reply(200, 'logged in', { 'set-cookie': 'auth_token=foobar; Path=/; HttpOnly; Secure; SameSite=None', }) .get('/profile/adobe/blog/main') - .times(2) .reply(function req() { if (this.req.headers.cookie === 'auth_token=foobar') { - return [200, '{}', { 'content-type': 'application/json' }]; + return [200, '{ "status": 200 }', { 'content-type': 'application/json' }]; } - return [401]; - }); + return [401, '{ "status": 401 }', { 'content-type': 'application/json' }]; + }) + // in debug mode, the browser requests /favicon.ico + .get('/favicon.ico') + .optionally() + .reply(404); await test.run(); - // wait until login window closes - let loginClosed = false; - await Promise.race([ - new Promise((resolve) => { - page.browser().on('targetdestroyed', async (target) => { - const targetUrl = target.url(); - if (targetUrl === 'https://admin.hlx.page/login/adobe/blog/main') { - loginClosed = true; - resolve(); - } - }); - }), - sleep(2000), - ]); - assert.ok(loggedIn, 'Sidekick did not send auth cookie.'); - assert.ok(loginClosed, 'Sidekick did not close login window.'); }).timeout(IT_DEFAULT_TIMEOUT); it('Opens login window and shows aborted modal', async () => { From eb36acdaa673c956a01dc0f78453f3467ebe9502 Mon Sep 17 00:00:00 2001 From: rofe Date: Tue, 25 Apr 2023 16:20:52 +0200 Subject: [PATCH 20/28] chore: fix login aborted test --- test/login.test.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/test/login.test.js b/test/login.test.js index 4e82783ca..947d9c0fd 100644 --- a/test/login.test.js +++ b/test/login.test.js @@ -88,23 +88,20 @@ describe('Test sidekick login', () => { browser, page, plugin: 'user-login', - pluginSleep: 2000, + pluginSleep: 7000, // sidekick tries 5 times before showing the login aborted modal loadModule: true, }); nock('https://admin.hlx.page') .get('/status/adobe/blog/main/en/topics/bla?editUrl=auto') - .reply(401) - .get('/profile/adobe/blog/main') - .times(2) - .reply(401) + .reply(401, '{ "status": 401 }', { 'content-type': 'application/json' }) .get('/login/adobe/blog/main') - .reply(200, 'not logged in!'); - - const { popupTarget } = await test.run(); + .reply(200, 'not logged in!') + .get('/profile/adobe/blog/main') + .times(5) + .reply(401, '{ "status": 401 }', { 'content-type': 'application/json' }); - // close login window - await (await popupTarget.page()).close(); + await test.run(); // wait for 'aborted' modal try { From 1cf1fc28c9229c51a3a9420079d20da5c87c4499 Mon Sep 17 00:00:00 2001 From: rofe Date: Tue, 25 Apr 2023 18:07:35 +0200 Subject: [PATCH 21/28] chore: fix logout test --- test/logout.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/logout.test.js b/test/logout.test.js index 05a93e852..b9e4b3b7f 100644 --- a/test/logout.test.js +++ b/test/logout.test.js @@ -46,7 +46,9 @@ describe('Test sidekick logout', () => { nock.admin(new Setup('blog')); nock('https://admin.hlx.page') .get('/logout/adobe/blog/main') - .reply(200, ''); + .reply(200, 'logged out') + .get('/profile/adobe/blog/main') + .reply(401, '{ "status": 401 }', { 'content-type': 'application/json' }); nock.admin(new Setup('blog')); const test = new SidekickTest({ From 87ba06fc319e232fd64eb895e6ab659d078a191a Mon Sep 17 00:00:00 2001 From: rofe Date: Wed, 26 Apr 2023 09:47:01 +0200 Subject: [PATCH 22/28] chore: add share url to host permissions [skip ci] --- src/extension/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extension/manifest.json b/src/extension/manifest.json index fd11c6bc0..55ea955b7 100644 --- a/src/extension/manifest.json +++ b/src/extension/manifest.json @@ -15,6 +15,7 @@ "storage" ], "host_permissions": [ + "https://www.hlx.live/tools/sidekick/*", "http://localhost:3000/*", "https://*/*" ], From df4740f7e51345a03a89934ea67bc54d47f90f55 Mon Sep 17 00:00:00 2001 From: rofe Date: Wed, 26 Apr 2023 15:47:42 +0200 Subject: [PATCH 23/28] chore: try/catch, jsdoc [skip ci] --- src/extension/background.js | 82 ++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/src/extension/background.js b/src/extension/background.js index 644fe371b..ac5c19d86 100644 --- a/src/extension/background.js +++ b/src/extension/background.js @@ -331,46 +331,54 @@ function checkViewDocSource(id) { }); } +/** + * Sets the x-auth-token header for all requests to admin.hlx.page if project config + * has an auth token. + */ async function updateAdminAuthHeaderRules() { - // remove all rules first - await chrome.declarativeNetRequest.updateSessionRules({ - removeRuleIds: (await chrome.declarativeNetRequest.getSessionRules()) - .map((rule) => rule.id), - }); - // find projects with auth tokens and add rules for each - let id = 1; - const projects = await getConfig('sync', 'hlxSidekickProjects') || []; - const addRules = []; - const projectConfigs = await Promise.all(projects.map((handle) => getConfig('sync', handle))); - projectConfigs.forEach(({ owner, repo, authToken }) => { - if (authToken) { - addRules.push({ - id, - priority: 1, - action: { - type: 'modifyHeaders', - requestHeaders: [{ - operation: 'set', - header: 'x-auth-token', - value: authToken, - }], - }, - condition: { - regexFilter: `^https://admin.hlx.page/[a-z]+/${owner}/${repo}/.*`, - requestDomains: ['admin.hlx.page'], - requestMethods: ['get', 'post', 'delete'], - resourceTypes: ['xmlhttprequest'], - }, - }); - id += 1; - log.debug('added admin auth header rule for ', owner, repo); - } - }); - if (addRules.length > 0) { + try { + // remove all rules first await chrome.declarativeNetRequest.updateSessionRules({ - addRules, + removeRuleIds: (await chrome.declarativeNetRequest.getSessionRules()) + .map((rule) => rule.id), + }); + // find projects with auth tokens and add rules for each + let id = 1; + const projects = await getConfig('sync', 'hlxSidekickProjects') || []; + const addRules = []; + const projectConfigs = await Promise.all(projects.map((handle) => getConfig('sync', handle))); + projectConfigs.forEach(({ owner, repo, authToken }) => { + if (authToken) { + addRules.push({ + id, + priority: 1, + action: { + type: 'modifyHeaders', + requestHeaders: [{ + operation: 'set', + header: 'x-auth-token', + value: authToken, + }], + }, + condition: { + regexFilter: `^https://admin.hlx.page/[a-z]+/${owner}/${repo}/.*`, + requestDomains: ['admin.hlx.page'], + requestMethods: ['get', 'post', 'delete'], + resourceTypes: ['xmlhttprequest'], + }, + }); + id += 1; + log.debug('added admin auth header rule for ', owner, repo); + } }); - log.debug(`setAdminAuthHeaderRule: ${addRules.length} rule(s) set`); + if (addRules.length > 0) { + await chrome.declarativeNetRequest.updateSessionRules({ + addRules, + }); + log.debug(`setAdminAuthHeaderRule: ${addRules.length} rule(s) set`); + } + } catch (e) { + log.error(`updateAdminAuthHeaderRules: ${e.message}`); } } From f650ee2ae293f15681e8ea3d79c2cdf6003b4b98 Mon Sep 17 00:00:00 2001 From: rofe Date: Wed, 26 Apr 2023 16:02:01 +0200 Subject: [PATCH 24/28] refactor: use cookie auth in safari [skip ci] --- src/extension/module.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension/module.js b/src/extension/module.js index d72ffd197..fc12e9016 100644 --- a/src/extension/module.js +++ b/src/extension/module.js @@ -1810,7 +1810,7 @@ sk.showWait(); const loginUrl = getAdminUrl(sk.config, 'login'); const extensionId = window.chrome?.runtime?.id; - if (extensionId) { + if (extensionId && !window.navigator.vendor.includes('Apple')) { // exclude safari loginUrl.searchParams.set('extensionId', extensionId); } if (selectAccount) { @@ -1857,7 +1857,7 @@ sk.showWait(); const logoutUrl = getAdminUrl(sk.config, 'logout'); const extensionId = window.chrome?.runtime?.id; - if (extensionId) { + if (extensionId && !window.navigator.vendor.includes('Apple')) { // exclude safari logoutUrl.searchParams.set('extensionId', extensionId); } const logoutWindow = window.open(logoutUrl.toString()); From c80b34bc1292b45ec386e50196cf7544e62e8083 Mon Sep 17 00:00:00 2001 From: rofe Date: Thu, 27 Apr 2023 16:17:59 +0200 Subject: [PATCH 25/28] chore: use login/logout redirects in cookie auth flow --- src/extension/module.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/extension/module.js b/src/extension/module.js index fc12e9016..9b044b436 100644 --- a/src/extension/module.js +++ b/src/extension/module.js @@ -1810,8 +1810,14 @@ sk.showWait(); const loginUrl = getAdminUrl(sk.config, 'login'); const extensionId = window.chrome?.runtime?.id; - if (extensionId && !window.navigator.vendor.includes('Apple')) { // exclude safari + const authHeaderEnabled = extensionId && !window.navigator.vendor.includes('Apple'); + if (authHeaderEnabled) { loginUrl.searchParams.set('extensionId', extensionId); + } else { + loginUrl.searchParams.set( + 'loginRedirect', + 'https://www.hlx.live/tools/sidekick/login-success', + ); } if (selectAccount) { loginUrl.searchParams.set('selectAccount', true); @@ -1859,6 +1865,11 @@ const extensionId = window.chrome?.runtime?.id; if (extensionId && !window.navigator.vendor.includes('Apple')) { // exclude safari logoutUrl.searchParams.set('extensionId', extensionId); + } else { + logoutUrl.searchParams.set( + 'logoutRedirect', + 'https://www.hlx.live/tools/sidekick/logout-success', + ); } const logoutWindow = window.open(logoutUrl.toString()); From 01f9c822167ea35e91b07fe45244dafce0c0e3ae Mon Sep 17 00:00:00 2001 From: rofe Date: Thu, 27 Apr 2023 16:49:54 +0200 Subject: [PATCH 26/28] chore: adjust tests --- test/login.test.js | 4 ++-- test/logout.test.js | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/test/login.test.js b/test/login.test.js index 947d9c0fd..0d5a4bce4 100644 --- a/test/login.test.js +++ b/test/login.test.js @@ -62,7 +62,7 @@ describe('Test sidekick login', () => { } return [401, '{ "status": 401 }', { 'content-type': 'application/json' }]; }) - .get('/login/adobe/blog/main') + .get('/login/adobe/blog/main?loginRedirect=https%3A%2F%2Fwww.hlx.live%2Ftools%2Fsidekick%2Flogin-success') .reply(200, 'logged in', { 'set-cookie': 'auth_token=foobar; Path=/; HttpOnly; Secure; SameSite=None', }) @@ -95,7 +95,7 @@ describe('Test sidekick login', () => { nock('https://admin.hlx.page') .get('/status/adobe/blog/main/en/topics/bla?editUrl=auto') .reply(401, '{ "status": 401 }', { 'content-type': 'application/json' }) - .get('/login/adobe/blog/main') + .get('/login/adobe/blog/main?loginRedirect=https%3A%2F%2Fwww.hlx.live%2Ftools%2Fsidekick%2Flogin-success') .reply(200, 'not logged in!') .get('/profile/adobe/blog/main') .times(5) diff --git a/test/logout.test.js b/test/logout.test.js index b9e4b3b7f..7fc1b02b3 100644 --- a/test/logout.test.js +++ b/test/logout.test.js @@ -45,17 +45,18 @@ describe('Test sidekick logout', () => { it('Logout removes auth token from config', async () => { nock.admin(new Setup('blog')); nock('https://admin.hlx.page') - .get('/logout/adobe/blog/main') + .get('/status/adobe/blog/main/en/topics/bla?editUrl=auto') + .reply(200, { status: 200 }) + .get('/logout/adobe/blog/main?logoutRedirect=https%3A%2F%2Fwww.hlx.live%2Ftools%2Fsidekick%2Flogout-success') .reply(200, 'logged out') .get('/profile/adobe/blog/main') .reply(401, '{ "status": 401 }', { 'content-type': 'application/json' }); - nock.admin(new Setup('blog')); const test = new SidekickTest({ browser, page, plugin: 'user-logout', - pluginSleep: 2000, + pluginSleep: 3000, checkPage: async (p) => p.evaluate(() => window.hlx.sidekick.config), }); test.sidekickConfig.authToken = 'foobar'; From ed366852ada4e9167701a35b8a777f5bba3f1c5a Mon Sep 17 00:00:00 2001 From: rofe Date: Fri, 28 Apr 2023 09:52:24 +0200 Subject: [PATCH 27/28] chore: stabilize flaky test --- test/bulk-preview.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/bulk-preview.test.js b/test/bulk-preview.test.js index f4708ce50..298066756 100644 --- a/test/bulk-preview.test.js +++ b/test/bulk-preview.test.js @@ -63,6 +63,7 @@ describe('Test bulk preview plugin', () => { const { plugins } = await new SidekickTest({ browser, page, + sleep: 1000, fixture: TESTS[0].fixture, url: setup.getUrl('edit', 'admin'), pre: (p) => p.evaluate(() => { From 2dfe0cb29af89b37413739c47bc8355788021571 Mon Sep 17 00:00:00 2001 From: rofe Date: Fri, 28 Apr 2023 10:32:24 +0200 Subject: [PATCH 28/28] chore: stabilize (and slow down) tests --- test/SidekickTest.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/SidekickTest.js b/test/SidekickTest.js index f0f67e77e..9918a39bf 100644 --- a/test/SidekickTest.js +++ b/test/SidekickTest.js @@ -85,7 +85,7 @@ import { * @param {string} o.env=preview The environment (preview or live) * @param {string} o.type=html The content type of the requested resource (html, xml or json) * @param {string} o.fixture=generic.html The fixture file to use as test bed - * @param {number} o.sleep=0 The number of milliseconds to wait after loading the sidekick + * @param {number} o.sleep=500 The number of milliseconds to wait after loading the sidekick * @param {string} o.plugin A plugin to execute after loading the sidekick * @param {number} o.pluginSleep=0 The number of milliseconds to wait after executing a plugin * @param {boolean} acceptDialogs=false Defines whether dialogs will be accepted or dismissed @@ -140,7 +140,7 @@ export class SidekickTest extends EventEmitter { this.env = o.env || 'preview'; this.type = o.type || 'html'; this.fixture = o.fixture || 'generic.html'; - this.sleep = o.sleep ?? 0; + this.sleep = o.sleep ?? 500; this.plugin = o.plugin; this.pluginSleep = o.pluginSleep ?? 0; this.acceptDialogs = o.acceptDialogs || false;