diff --git a/tests/tools/scheduler/utils.test.js b/tests/tools/scheduler/utils.test.js index 53335b75..ea852798 100644 --- a/tests/tools/scheduler/utils.test.js +++ b/tests/tools/scheduler/utils.test.js @@ -6,6 +6,8 @@ import { deletePageSchedule, deleteSnapshotSchedule, fetchSchedule, + isPageHost, + parseSidekickParams, } from '../../../tools/scheduler/utils.js'; describe('scheduler:utils.js', () => { @@ -58,6 +60,57 @@ describe('scheduler:utils.js', () => { }); }); + describe('isPageHost', () => { + it('matches the default preview/live/review hosts', () => { + assert.equal(isPageHost('main--site--org.aem.page'), true); + assert.equal(isPageHost('main--site--org.aem.live'), true); + assert.equal(isPageHost('default--main--site--org.aem.reviews'), true); + }); + + it('rejects anything else, including authoring surfaces and a custom prod domain', () => { + assert.equal(isPageHost('org-my.sharepoint.com'), false); + assert.equal(isPageHost('docs.google.com'), false); + assert.equal(isPageHost('www.example.com'), false); + }); + }); + + describe('parseSidekickParams', () => { + it('derives path directly from a preview/live referrer', () => { + const search = '?owner=org&repo=site&referrer=https%3A%2F%2Fmain--site--org.aem.page%2Ffoo%2Fbar'; + const result = parseSidekickParams(search); + assert.deepEqual(result, { + org: 'org', + site: 'site', + path: '/foo/bar', + referrer: 'https://main--site--org.aem.page/foo/bar', + isProject: true, + }); + }); + + it('leaves path empty for an authoring-surface referrer (e.g. SharePoint)', () => { + const referrer = 'https://org-my.sharepoint.com/:w:/r/personal/foo/Documents/page.docx'; + const search = `?owner=org&repo=site&referrer=${encodeURIComponent(referrer)}`; + const result = parseSidekickParams(search); + assert.equal(result.path, ''); + assert.equal(result.isProject, false); + assert.equal(result.referrer, referrer); + }); + + it('leaves path empty for a custom prod domain referrer (resolved via Admin instead)', () => { + const search = '?owner=org&repo=site&referrer=https%3A%2F%2Fwww.example.com%2Ffoo'; + const result = parseSidekickParams(search); + assert.equal(result.path, ''); + assert.equal(result.isProject, false); + }); + + it('handles a missing or unparsable referrer', () => { + assert.deepEqual(parseSidekickParams('?owner=org&repo=site'), { + org: 'org', site: 'site', path: '', referrer: '', isProject: false, + }); + assert.equal(parseSidekickParams('?referrer=not-a-url').path, ''); + }); + }); + describe('worker-call signatures', () => { it('schedulePage POSTs body { path, scheduledPublish, nonce } without userId', async () => { let captured; diff --git a/tools/scheduler/README.md b/tools/scheduler/README.md index 55e06e6f..e5e73bac 100644 --- a/tools/scheduler/README.md +++ b/tools/scheduler/README.md @@ -46,9 +46,15 @@ this entry to `/{org}/sites/{site}/sidekick.json` under `plugins`: `passConfig` injects `owner`, `repo`, `ref`, etc. as query parameters and `passReferrer` injects the current page URL — the popover uses -`owner`/`repo` as `org`/`site` and the referrer's pathname as the page -path. The site must already be enabled for scheduling for the popover to -schedule successfully. +`owner`/`repo` as `org`/`site`. For the page path, Sidekick's `referrer` is +just `window.location.href` of the tab the plugin was opened from. On the +default `.aem.page`/`.aem.live`/`.aem.reviews` hosts that's the page itself, +so its URL pathname is the resource path. On any other host — SharePoint, +Google Docs, etc. — the tab isn't guaranteed to be the +page, so the popover instead resolves the path via the Admin API's +`GET /status/{org}/{site}/{ref}?editUrl={referrer}` (`webPath` on the +response), the same mechanism Sidekick itself uses while editing. The site must already +be enabled for scheduling for the popover to schedule successfully. ## Authentication diff --git a/tools/scheduler/schedule.js b/tools/scheduler/schedule.js index 4b3a195f..36b1897a 100644 --- a/tools/scheduler/schedule.js +++ b/tools/scheduler/schedule.js @@ -28,7 +28,33 @@ async function writeIntent(org, site, entry) { }; } +// Resolves the resource path behind an authoring-surface referrer (SharePoint, +// Google Docs, the admin.hlx.page editor, etc) by asking the Admin API to +// reverse-map the source document URL via its `editUrl` status lookup — the +// same mechanism Sidekick itself uses while editing, since the referrer URL +// there has no relation to the page's web path. +async function resolvePagePath(org, site, editUrl) { + const res = await scheduleAdmin.status({ org, site }).get('', { params: { editUrl } }); + if (!res.ok) { + return { path: '', error: res.error || 'Could not resolve the page being edited.', resp: res }; + } + let json; + try { + json = await res.json(); + } catch { + return { path: '', error: 'Could not parse status response.', resp: res }; + } + const path = json?.webPath || ''; + return { + path, + error: path ? '' : 'Could not resolve the page being edited.', + resp: res, + }; +} + const missingContext = document.getElementById('missing-context'); +const missingContextMessage = missingContext.querySelector('p'); +const defaultMissingContextMessage = missingContextMessage.textContent; const formWrap = document.getElementById('schedule-form-wrap'); const pathValue = document.getElementById('target-path-value'); const siteValue = document.getElementById('target-site-value'); @@ -37,7 +63,11 @@ const timezoneLabel = document.getElementById('schedule-timezone'); const statusText = document.getElementById('status-text'); const scheduleBtn = document.getElementById('schedule-btn'); -const { org, site, path } = api.parseSidekickParams(window.location.search); +const sidekickParams = api.parseSidekickParams(window.location.search); +const { + org, site, referrer, isProject, +} = sidekickParams; +let { path } = sidekickParams; function setStatus(message, kind = 'info') { statusText.textContent = message || ''; @@ -134,6 +164,20 @@ async function init() { }); scheduleBtn.addEventListener('click', handleSchedule); + // Referrers from an authoring surface (SharePoint, Google Docs, etc) + // carry the source document's URL, not the page's web path. + // resolve it via the Admin API instead of guessing. + if (!path && !isProject && org && site && referrer) { + setStatus('Resolving page…'); + const resolved = await resolvePagePath(org, site, referrer); + if (resolved.path) { + path = resolved.path; + } else { + missingContextMessage.textContent = resolved.error || defaultMissingContextMessage; + } + setStatus(''); + } + initContext(); } diff --git a/tools/scheduler/utils.js b/tools/scheduler/utils.js index beb68cfe..1bcf65da 100644 --- a/tools/scheduler/utils.js +++ b/tools/scheduler/utils.js @@ -139,21 +139,34 @@ export function isAtLeastFiveMinAhead(localDatetimeValue) { return selected.getTime() - Date.now() >= 5 * 60 * 1000; } +// Default EDS hosts where the referrer *is* the page, so its URL pathname is +// the resource's web path. Anything else — SharePoint, Google Docs, etc. +// — is treated as edit mode, where Sidekick's `referrer` is +// the source document's URL instead (see resolvePagePath in schedule.js) +const PAGE_HOST_SUFFIXES = ['.aem.page', '.aem.live', '.aem.reviews']; + +export function isPageHost(hostname) { + return PAGE_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix)); +} + export function parseSidekickParams(searchString) { const params = new URLSearchParams(searchString); const org = params.get('owner') || ''; const site = params.get('repo') || ''; const referrer = params.get('referrer') || ''; let path = ''; + let isProject = false; if (referrer) { try { - path = new URL(referrer).pathname; + const referrerUrl = new URL(referrer); + isProject = isPageHost(referrerUrl.hostname); + if (isProject) path = referrerUrl.pathname; } catch { path = ''; } } return { - org, site, path, referrer, + org, site, path, referrer, isProject, }; }