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
53 changes: 53 additions & 0 deletions tests/tools/scheduler/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
deletePageSchedule,
deleteSnapshotSchedule,
fetchSchedule,
isPageHost,
parseSidekickParams,
} from '../../../tools/scheduler/utils.js';

describe('scheduler:utils.js', () => {
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 9 additions & 3 deletions tools/scheduler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
46 changes: 45 additions & 1 deletion tools/scheduler/schedule.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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 || '';
Expand Down Expand Up @@ -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();
}

Expand Down
17 changes: 15 additions & 2 deletions tools/scheduler/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down