From b5bdd5b7b1bf254e07bf253f71c5257503360905 Mon Sep 17 00:00:00 2001 From: Miro Yovchev <2827783+myovchev@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:38:22 +0300 Subject: [PATCH 01/13] Fix choices IDs (#5379) * Fix choices IDs * Cleanup dots from ID values * Update changelog Co-authored-by: Robert Means --------- Co-authored-by: Robert Means (cherry picked from commit 9f458b5d9544d33239a0971b0050fff8bffe028a) --- .changeset/clean-actors-laugh.md | 5 +++++ .../schema/ui/apos/logic/AposInputCheckboxes.js | 2 +- .../@apostrophecms/schema/ui/apos/logic/AposInputRadio.js | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/clean-actors-laugh.md diff --git a/.changeset/clean-actors-laugh.md b/.changeset/clean-actors-laugh.md new file mode 100644 index 0000000000..5d78fab358 --- /dev/null +++ b/.changeset/clean-actors-laugh.md @@ -0,0 +1,5 @@ +--- +"apostrophe": patch +--- + +Fix illegal HTML id attribute values generated by the admin UI diff --git a/packages/apostrophe/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js b/packages/apostrophe/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js index 45a8a4c6d0..73fa374d04 100644 --- a/packages/apostrophe/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js +++ b/packages/apostrophe/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js @@ -11,7 +11,7 @@ export default { }, methods: { getChoiceId(uid, value) { - return (uid + JSON.stringify(value)).replace(/\s+/g, ''); + return (uid + String(value)).replace(/[\s"'.]/g, ''); }, watchValue () { this.error = this.modelValue.error; diff --git a/packages/apostrophe/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js b/packages/apostrophe/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js index 97dffa11ed..f7d1b6f6e9 100644 --- a/packages/apostrophe/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js +++ b/packages/apostrophe/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js @@ -8,7 +8,7 @@ export default { mixins: [ AposInputMixin, AposInputChoicesMixin ], methods: { getChoiceId(uid, value) { - return (uid + JSON.stringify(value)).replace(/\s+/g, ''); + return (uid + String(value)).replace(/[\s"'.]/g, ''); }, validate(value) { const validValue = this.choices.some((choice) => choice.value === value); From 77fa6f03e6091f26a3a3e7e96f0f18d924b16eb1 Mon Sep 17 00:00:00 2001 From: Miro Yovchev <2827783+myovchev@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:01:04 +0300 Subject: [PATCH 02/13] Bump CLI dependencies (#5383) (cherry picked from commit a5e1a4a35d01e09cbfd2c503370c95a8c2490867) --- .changeset/cozy-wombats-burn.md | 5 +++++ packages/cli/lib/util.js | 4 ++-- packages/cli/package.json | 3 +-- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 .changeset/cozy-wombats-burn.md diff --git a/.changeset/cozy-wombats-burn.md b/.changeset/cozy-wombats-burn.md new file mode 100644 index 0000000000..e04a363fcd --- /dev/null +++ b/.changeset/cozy-wombats-burn.md @@ -0,0 +1,5 @@ +--- +"@apostrophecms/cli": patch +--- + +Bump and clean up depdendencies. diff --git a/packages/cli/lib/util.js b/packages/cli/lib/util.js index f531f47e77..566b3e75fc 100644 --- a/packages/cli/lib/util.js +++ b/packages/cli/lib/util.js @@ -7,7 +7,7 @@ const util = {}; const _ = require('lodash'); const cliVersion = require('../package.json').version; const confUtils = require('./conf-utils'); -const packageJson = require('package-json'); +const { execSync } = require('child_process'); const semver = require('semver'); const ora = require('ora'); const { spawn } = require('child_process'); @@ -218,7 +218,7 @@ util.spawnWithSpinner = async function (command, options = { async function checkIfUpdated () { try { // Get the latest published version number. - const { version: latest } = await packageJson('@apostrophecms/cli'); + const latest = execSync('npm show @apostrophecms/cli version', { encoding: 'utf8' }).trim(); // Check if they've been notified for this version already. If so, bail out. const latestChecked = await confUtils.getConf('versionNotified'); diff --git a/packages/cli/package.json b/packages/cli/package.json index 25c13dcde7..ff9c72b249 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -23,12 +23,11 @@ "conf": "^6.2.4", "lodash": "^4.18.1", "ora": "^5.4.1", - "package-json": "^6.5.0", "pkginfo": "^0.4.1", "prompts": "^2.3.2", "semver": "^7.3.2", "shell-quote": "^1.8.1", - "shelljs": "~0.3.x", + "shelljs": "^0.10.0", "sync-request": "^6.0.0", "uuid": "^8.0.0" }, From 0a080d208fbbfe6430ada87f2805875d92b28261 Mon Sep 17 00:00:00 2001 From: Miro Yovchev <2827783+myovchev@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:30:56 +0300 Subject: [PATCH 03/13] Native browser shortcuts work again (#5384) (cherry picked from commit b9b32bdd446add927c75d85e44d0a26814ba62b0) --- .changeset/full-symbols-obey.md | 5 +++++ .../modules/@apostrophecms/area/index.js | 15 ++++++++++----- .../ui/apos/components/TheAposCommandMenu.vue | 12 +++++++++++- 3 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 .changeset/full-symbols-obey.md diff --git a/.changeset/full-symbols-obey.md b/.changeset/full-symbols-obey.md new file mode 100644 index 0000000000..903395756e --- /dev/null +++ b/.changeset/full-symbols-obey.md @@ -0,0 +1,5 @@ +--- +"apostrophe": patch +--- + +Keyboard shortcuts for widget operations (copy, cut, paste, duplicate, remove) no longer block the browser's native clipboard behavior when no widget is focused. Previously, selecting and copying text on a page while logged in was prevented by the admin UI intercepting those shortcuts unconditionally. diff --git a/packages/apostrophe/modules/@apostrophecms/area/index.js b/packages/apostrophe/modules/@apostrophecms/area/index.js index 47b83516f1..a96d8248c8 100644 --- a/packages/apostrophe/modules/@apostrophecms/area/index.js +++ b/packages/apostrophe/modules/@apostrophecms/area/index.js @@ -19,7 +19,8 @@ module.exports = { action: { type: 'command-menu-area-cut-widget' }, - shortcut: 'Ctrl+X Meta+X' + shortcut: 'Ctrl+X Meta+X', + requireWidgetFocus: true }, [`${self.__meta.name}:copy-widget`]: { type: 'item', @@ -27,7 +28,8 @@ module.exports = { action: { type: 'command-menu-area-copy-widget' }, - shortcut: 'Ctrl+C Meta+C' + shortcut: 'Ctrl+C Meta+C', + requireWidgetFocus: true }, [`${self.__meta.name}:paste-widget`]: { type: 'item', @@ -35,7 +37,8 @@ module.exports = { action: { type: 'command-menu-area-paste-widget' }, - shortcut: 'Ctrl+V Meta+V' + shortcut: 'Ctrl+V Meta+V', + requireWidgetFocus: true }, [`${self.__meta.name}:duplicate-widget`]: { type: 'item', @@ -43,7 +46,8 @@ module.exports = { action: { type: 'command-menu-area-duplicate-widget' }, - shortcut: 'Ctrl+Shift+D Meta+Shift+D' + shortcut: 'Ctrl+Shift+D Meta+Shift+D', + requireWidgetFocus: true }, [`${self.__meta.name}:remove-widget`]: { type: 'item', @@ -51,7 +55,8 @@ module.exports = { action: { type: 'command-menu-area-remove-widget' }, - shortcut: 'Backspace' + shortcut: 'Backspace', + requireWidgetFocus: true } }, modal: { diff --git a/packages/apostrophe/modules/@apostrophecms/command-menu/ui/apos/components/TheAposCommandMenu.vue b/packages/apostrophe/modules/@apostrophecms/command-menu/ui/apos/components/TheAposCommandMenu.vue index 6712ec169e..6003a5830d 100644 --- a/packages/apostrophe/modules/@apostrophecms/command-menu/ui/apos/components/TheAposCommandMenu.vue +++ b/packages/apostrophe/modules/@apostrophecms/command-menu/ui/apos/components/TheAposCommandMenu.vue @@ -10,6 +10,7 @@ import { mapActions, mapState } from 'pinia'; import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin'; import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal'; +import { useWidgetStore } from 'Modules/@apostrophecms/ui/stores/widget'; export default { name: 'TheAposCommandMenu', @@ -50,7 +51,13 @@ export default { .flatMap(command => { return command.shortcut .split(' ') - .map(shortcut => [ shortcut.toUpperCase(), command.action ]); + .map(shortcut => [ + shortcut.toUpperCase(), + { + ...command.action, + requireWidgetFocus: command.requireWidgetFocus || false + } + ]); }); }) ); @@ -111,6 +118,9 @@ export default { ? keys.slice('SHIFT+'.length) : keys]; if (action) { + if (action.requireWidgetFocus && !useWidgetStore().focusedWidget) { + return; + } event.preventDefault(); apos.bus.$emit(action.type, action.payload); return; From 0b5f6b83e5f1be689b257371fabdbbe853248b14 Mon Sep 17 00:00:00 2001 From: Robert Means Date: Wed, 22 Apr 2026 08:00:35 -0400 Subject: [PATCH 04/13] Pro 8838 charset (#5385) * Removes encoding option and comments hardcoded encoding meta * add changeset * Response to first comments (cherry picked from commit 08845c5f23d95ead552ab9743a878daf997793a1) --- .changeset/fifty-hornets-follow.md | 7 +++++++ packages/apostrophe/modules/@apostrophecms/i18n/index.js | 9 +-------- .../@apostrophecms/template/views/outerLayoutBase.html | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 .changeset/fifty-hornets-follow.md diff --git a/.changeset/fifty-hornets-follow.md b/.changeset/fifty-hornets-follow.md new file mode 100644 index 0000000000..52da86bff8 --- /dev/null +++ b/.changeset/fifty-hornets-follow.md @@ -0,0 +1,7 @@ +--- +"apostrophe": minor +--- + +- Removed duplicate tag from `outerLayoutBase.html` +- Standardized charset to utf-8 (removed legacy configuration option) +- Altered unused/legacy i18n template helper to return `utf-8` (BC) diff --git a/packages/apostrophe/modules/@apostrophecms/i18n/index.js b/packages/apostrophe/modules/@apostrophecms/i18n/index.js index 96de6ad5ac..66d0aa9f8d 100644 --- a/packages/apostrophe/modules/@apostrophecms/i18n/index.js +++ b/packages/apostrophe/modules/@apostrophecms/i18n/index.js @@ -29,10 +29,6 @@ // in the same language as the website content. // Example: `defaultAdminLocale: 'fr'`. // -// ### `encoding` -// -// Defaults to `'utf-8'`. You almost certainly do not want to change this. -// // ### `slugDirection` // // Controls the default `direction` value of slug schema. Can be `ltr`, `rtl` or @@ -81,8 +77,6 @@ module.exports = { }, // If true, slugifying will strip accents from Latin characters stripUrlAccents: false, - // You almost certainly do not want to change this - encoding: 'utf-8', slugDirection: 'ltr' }, async init(self) { @@ -166,7 +160,6 @@ module.exports = { await self.i18next.init(i18nextOptions); self.addInitialResources(); self.enableBrowserData(); - self.encoding = self.options.encoding; }, handlers(self) { return { @@ -1369,7 +1362,7 @@ module.exports = { helpers(self) { return { encoding() { - return self.encoding; + return 'utf-8'; } }; } diff --git a/packages/apostrophe/modules/@apostrophecms/template/views/outerLayoutBase.html b/packages/apostrophe/modules/@apostrophecms/template/views/outerLayoutBase.html index 3ef25932f2..12c60de95f 100644 --- a/packages/apostrophe/modules/@apostrophecms/template/views/outerLayoutBase.html +++ b/packages/apostrophe/modules/@apostrophecms/template/views/outerLayoutBase.html @@ -2,6 +2,7 @@ {% block encoding %} + {# Per spec (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta#charset) the only allowed value for this attribute is `utf-8` and this meta element must be in the first 1kb of the document #} {% endblock %} {% block startHead %} @@ -15,7 +16,6 @@ {% block standardHead %} - {% endblock %} {% component '@apostrophecms/template:inject' with { where: 'head', end: 'append', when: 'hmr' } %} {% component '@apostrophecms/template:inject' with { where: 'head', end: 'append' } %} From bfc5d4c4702a495e553888fb777c39420a8e1c80 Mon Sep 17 00:00:00 2001 From: haroun <1765606+haroun@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:10:15 +0200 Subject: [PATCH 05/13] Log aposResponse errors (#5386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * log aposResponse errors * add changeset --------- Co-authored-by: Harouna Traoré (cherry picked from commit d45e27f6d66cd25fde0cf9c7199ee9d014fa1518) --- .changeset/wild-lies-film.md | 5 +++++ packages/apostrophe-astro/lib/aposResponse.js | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .changeset/wild-lies-film.md diff --git a/.changeset/wild-lies-film.md b/.changeset/wild-lies-film.md new file mode 100644 index 0000000000..4070e1aea6 --- /dev/null +++ b/.changeset/wild-lies-film.md @@ -0,0 +1,5 @@ +--- +"@apostrophecms/apostrophe-astro": minor +--- + +Log aposResponse errors diff --git a/packages/apostrophe-astro/lib/aposResponse.js b/packages/apostrophe-astro/lib/aposResponse.js index ea4dcfb509..1f9f676080 100644 --- a/packages/apostrophe-astro/lib/aposResponse.js +++ b/packages/apostrophe-astro/lib/aposResponse.js @@ -49,7 +49,7 @@ export default async function aposResponse(req) { const aposUrl = new URL(aposHost + pathname); aposUrl.search = url.search; - + // Prepare headers, excluding any specified in config const requestHeaders = {}; for (const [name, value] of req.headers) { @@ -111,6 +111,7 @@ export default async function aposResponse(req) { } // Skip unknown encodings silently } catch (decompressError) { + console.error(decompressError); // If decompression fails, return original response return new Response(new Uint8Array(bodyArrayBuffer), { ...rest, @@ -133,10 +134,12 @@ export default async function aposResponse(req) { headers: responseHeaders }); } catch (bodyError) { + console.error(bodyError); // If we can't process the body, fall back to the original response return new Response(res.body, { ...rest, status: statusCode, headers: responseHeaders }); } } catch (error) { + console.error(error); // Handle any unexpected errors return new Response(`Server error: ${error.message}`, { status: 500 }); } From c27b62c7f2629fb7c430ab214d8ea806525b14a0 Mon Sep 17 00:00:00 2001 From: Tom Boutell Date: Thu, 23 Apr 2026 09:21:40 -0400 Subject: [PATCH 06/13] merge back the thanks (#5388) (cherry picked from commit f3501f4de0448d4d0f8a01a1db4dc2fcf52c6218) --- .changeset/wild-lies-film.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/wild-lies-film.md b/.changeset/wild-lies-film.md index 4070e1aea6..3b608070dd 100644 --- a/.changeset/wild-lies-film.md +++ b/.changeset/wild-lies-film.md @@ -2,4 +2,4 @@ "@apostrophecms/apostrophe-astro": minor --- -Log aposResponse errors +Log aposResponse errors server side in the Astro process. Thanks to [Harouna Traore](https://github.com/haroun). From d872b5bbbea3882e9f7b25abd4479b790f2b5cec Mon Sep 17 00:00:00 2001 From: Stuart Romanek Date: Wed, 29 Apr 2026 14:00:17 -0400 Subject: [PATCH 07/13] ignore inline table array as draggable ui for windows (#5392) (cherry picked from commit b360b05e8d4e479a36fcae45a5a6dbf820b8fba3) --- .changeset/stale-shirts-read.md | 5 +++++ .../@apostrophecms/modal/ui/apos/components/AposModal.vue | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/stale-shirts-read.md diff --git a/.changeset/stale-shirts-read.md b/.changeset/stale-shirts-read.md new file mode 100644 index 0000000000..271cd86872 --- /dev/null +++ b/.changeset/stale-shirts-read.md @@ -0,0 +1,5 @@ +--- +"apostrophe": patch +--- + +fixes issue where orderable table array items drag the entire floating window diff --git a/packages/apostrophe/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue b/packages/apostrophe/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue index 6402d79c49..a83770ff35 100644 --- a/packages/apostrophe/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +++ b/packages/apostrophe/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue @@ -212,7 +212,8 @@ const nonDraggableElements = [ '.apos-field--inline-array-field', '.apos-field--inline-array-table-with-remove-button-field', '.apos-field--inline-array-table-field', - '.apos-input-color__sample-picker' + '.apos-input-color__sample-picker', + '.apos-input-array-inline-table' ]; const resizeSides = [ From 1cd315a1a166de07bd12380c165e7469d009d788 Mon Sep 17 00:00:00 2001 From: Miro Yovchev <2827783+myovchev@users.noreply.github.com> Date: Mon, 4 May 2026 13:26:48 +0300 Subject: [PATCH 08/13] Layout focus orchestration (#5393) (cherry picked from commit 77a2968206b0208883ff45de37feb43ba0b5d5b1) --- .changeset/quick-guests-join.md | 5 +++++ .../area/ui/apos/components/AposAreaWidget.vue | 2 ++ .../ui/apos/components/AposAreaLayoutEditor.vue | 6 ++++++ 3 files changed, 13 insertions(+) create mode 100644 .changeset/quick-guests-join.md diff --git a/.changeset/quick-guests-join.md b/.changeset/quick-guests-join.md new file mode 100644 index 0000000000..8bab7b0384 --- /dev/null +++ b/.changeset/quick-guests-join.md @@ -0,0 +1,5 @@ +--- +"apostrophe": minor +--- + +Fix layout widget not regaining full focus on switching back to Edit content mode. diff --git a/packages/apostrophe/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue b/packages/apostrophe/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue index a8ac47dde0..7b83852860 100644 --- a/packages/apostrophe/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +++ b/packages/apostrophe/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue @@ -536,6 +536,7 @@ export default { apos.bus.$on('widget-focus-parent', this.focusParent); apos.bus.$on('context-menu-toggled', this.getFocusForMenu); apos.bus.$on('suppress-focused-widget-controls', this.doSuppressWidgetControls); + apos.bus.$on('clear-focused-widget-control-suppression', this.clearSuppressionFlags); this.breadcrumbs.$lastEl = this.$el; @@ -573,6 +574,7 @@ export default { // Remove the focus parent listener when unmounted apos.bus.$off('widget-focus-parent', this.focusParent); apos.bus.$off('suppress-focused-widget-controls', this.doSuppressWidgetControls); + apos.bus.$off('clear-focused-widget-control-suppression', this.clearSuppressionFlags); window.removeEventListener('scroll', this.stickyControlsScroll); window.removeEventListener('resize', this.stickyControlsResize); this.unregisterFromGraph(); diff --git a/packages/apostrophe/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue b/packages/apostrophe/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue index 17a32f0202..9f4f2387d4 100644 --- a/packages/apostrophe/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +++ b/packages/apostrophe/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue @@ -259,6 +259,12 @@ export default { this.emphasizeGrid(); } else { this.deEmphasizeGrid(); + // Leaving layout mode: clear the control-suppression flag that was + // set on the focused widget when we entered layout mode. Without + // this the layout widget keeps `isSuppressingWidgetControls=true` + // and its side operation bar stays hidden even though it is the + // focused widget. + apos.bus.$emit('clear-focused-widget-control-suppression'); } } }, From 2f978f2d985cc82eeafc54f705a9f52d8f51fb70 Mon Sep 17 00:00:00 2001 From: Miro Yovchev <2827783+myovchev@users.noreply.github.com> Date: Tue, 5 May 2026 18:02:03 +0300 Subject: [PATCH 09/13] Bump dependencies (#5398) * Bump dependencies * Fix missing test await resulting in random failures (cherry picked from commit 008417fa9814dba4899b53b9394778dbd46ccf3c) --- packages/cli/lib/commands/create.js | 12 ++++++------ packages/cli/package.json | 2 +- packages/login-totp/test/test.js | 2 +- packages/oembetter/package.json | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/cli/lib/commands/create.js b/packages/cli/lib/commands/create.js index 9837cf9a1f..8596c3c8bb 100644 --- a/packages/cli/lib/commands/create.js +++ b/packages/cli/lib/commands/create.js @@ -86,7 +86,7 @@ module.exports = function (program) { const expressIndexPath = resolvePath('lib/modules/apostrophe-express/index.js'); if (fs.existsSync(expressIndexPath)) { util.replaceInFiles( - [expressIndexPath], + [ expressIndexPath ], /secret: undefined/, `secret: '${secret}'` ); @@ -96,7 +96,7 @@ module.exports = function (program) { secret = util.secret(); util.replaceInFiles( - [resolvePath('app.js')], + [ resolvePath('app.js') ], /disabledFileKey: undefined/, `disabledFileKey: '${secret}'` ); @@ -124,7 +124,7 @@ module.exports = function (program) { }); } catch (error) { await util.error('create', 'Error installing backend packages'); - /* eslint-disable-next-line no-console */ + console.error(error); } @@ -136,7 +136,7 @@ module.exports = function (program) { }); } catch (error) { await util.error('create', 'Error installing frontend packages'); - /* eslint-disable-next-line no-console */ + console.error(error); } } else { @@ -147,7 +147,7 @@ module.exports = function (program) { }); } catch (error) { await util.error('create', 'Error installing packages'); - /* eslint-disable-next-line no-console */ + console.error(error); } } @@ -192,6 +192,6 @@ module.exports = function (program) { }; function replaceInConfig(regex, replacement, resolvePath) { - const files = [resolvePath('app.js'), resolvePath('package.json')]; + const files = [ resolvePath('app.js'), resolvePath('package.json') ]; util.replaceInFiles(files, regex, replacement); } diff --git a/packages/cli/package.json b/packages/cli/package.json index ff9c72b249..20ff3c1395 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,7 +29,7 @@ "shell-quote": "^1.8.1", "shelljs": "^0.10.0", "sync-request": "^6.0.0", - "uuid": "^8.0.0" + "uuid": "^11.1.0" }, "devDependencies": { "eslint": "^9.39.1", diff --git a/packages/login-totp/test/test.js b/packages/login-totp/test/test.js index 0a298c2a9a..75e536c8e3 100644 --- a/packages/login-totp/test/test.js +++ b/packages/login-totp/test/test.js @@ -67,7 +67,7 @@ describe('totp module', function () { user.role = 'editor'; const doc = await apos.user.insert(apos.task.getReq(), user); - apos.user.safe.updateOne({ + await apos.user.safe.updateOne({ _id: doc._id }, { $set: { diff --git a/packages/oembetter/package.json b/packages/oembetter/package.json index 3b01690fec..63ee9c08c2 100644 --- a/packages/oembetter/package.json +++ b/packages/oembetter/package.json @@ -29,7 +29,7 @@ "dependencies": { "async": "^0.9.0", "cheerio": "^1.1.0", - "fast-xml-parser": "^4.5.6", + "fast-xml-parser": "^5.7.0", "node-fetch": "^2.6.7", "urls": "0.0.4" }, From a0be3c5282eea045f077e7237cb2f2e734899b73 Mon Sep 17 00:00:00 2001 From: Tom Boutell Date: Wed, 6 May 2026 14:13:29 -0400 Subject: [PATCH 10/13] PRO-9467: remove defunct and nonpublic oembed providers and improve developer control to ensure security in the future (no risk exists today) (#5400) * PRO-9467: remove defunct and nonpublic oembed providers and improve developer control to ensure security in the future (no risk exists today) * allow newer twitter domain * infogr.am still around * facebook no longer does oembed (cherry picked from commit e9b3bac692315414da47fe05202e8854dd31d7ee) --- .changeset/wild-forks-fetch.md | 15 ++++++++ .../modules/@apostrophecms/oembed/index.js | 31 +++++++++------- packages/oembetter/index.js | 36 +++++-------------- 3 files changed, 42 insertions(+), 40 deletions(-) create mode 100644 .changeset/wild-forks-fetch.md diff --git a/.changeset/wild-forks-fetch.md b/.changeset/wild-forks-fetch.md new file mode 100644 index 0000000000..f5f944c79e --- /dev/null +++ b/.changeset/wild-forks-fetch.md @@ -0,0 +1,15 @@ +--- +"apostrophe": minor +--- + +apostrophe and oembetter have been updated to eliminate a number of services that formerly supported +oembed for the general public, but no longer do so. While there is no security risk today, removing +these ensures that if these domains are ever allowed to lapse, they do not become an XSS +attack vector in the future. + +Because oembed responses are not always iframes, it is important that this list be maintained +over time. In addition, developers always have the option to prune it on their own by setting +the new minimumAllowlist and minimumEndpoints options of the @apostrophecms/oembed module. + +Thanks to [Sainithin0309](https://github.com/Sainithin0309) for pointing out the potential +long-term security concern. diff --git a/packages/apostrophe/modules/@apostrophecms/oembed/index.js b/packages/apostrophe/modules/@apostrophecms/oembed/index.js index 9e4e83ad8a..d21e458da7 100644 --- a/packages/apostrophe/modules/@apostrophecms/oembed/index.js +++ b/packages/apostrophe/modules/@apostrophecms/oembed/index.js @@ -16,10 +16,13 @@ const cheerio = require('cheerio'); // widely trusted sites are already allowlisted. // // Your `allowlist` option is concatenated with `oembetter`'s standard -// allowlist, plus wufoo.com, infogr.am, and slideshare.net. +// allowlist, plus wufoo.com, infogr.am and slideshare.net. // // Your `endpoints` option is concatenated with `oembetter`'s standard // endpoints list. +// +// If you wish to completely override the behavior, set +// `minimumAllowlist` and `minimumEndpoints` instead. module.exports = { options: { @@ -42,28 +45,30 @@ module.exports = { // Don't permit oembed of untrusted sites, which could // lead to XSS attacks - self.oembetter.allowlist(self.oembetter.suggestedAllowlist.concat( - self.options.allowlist || [], - [ - 'wufoo.com', - 'infogr.am', - 'slideshare.net' - ]) - ); + const minimumAllowlist = self.options.minimumAllowlist || [ + ...self.oembetter.suggestedAllowlist, + 'wufoo.com', + 'infogr.am', + 'slideshare.net' + ]; + + self.oembetter.allowlist(minimumAllowlist.concat(self.options.allowlist || [])); + + const minimumEndpoints = self.options.minimumEndpoints || self.oembetter.suggestedEndpoints; self.oembetter.endpoints( - self.oembetter.suggestedEndpoints.concat(self.options.endpoints || []) + minimumEndpoints.concat(self.options.endpoints || []) ); }, // Enhances oembetter to support services better or to support services - // that have no oembed support by default. Called by `afterConstruct`. - // Extend this method to add additional `oembetter` filters. + // that have no oembed support by default. + // + // Extend or override this method to change or add oembetter filters. enhanceOembetter() { require('./lib/youtube.js')(self, self.oembetter); require('./lib/vimeo.js')(self, self.oembetter); require('./lib/wufoo.js')(self, self.oembetter); - require('./lib/infogram.js')(self, self.oembetter); }, // This method fetches the specified URL, determines its best embedded diff --git a/packages/oembetter/index.js b/packages/oembetter/index.js index 6f33de895f..4c3d168def 100644 --- a/packages/oembetter/index.js +++ b/packages/oembetter/index.js @@ -187,41 +187,15 @@ module.exports = function(options) { self.suggestedAllowlist = [ 'youtube.com', 'youtu.be', - 'blip.tv', 'dailymotion.com', 'flickr.com', - 'hulu.com', - 'nfb.ca', - 'qik.com', - 'revision3.com', - 'scribd.com', - 'viddler.com', 'vimeo.com', - 'youtube.com', - 'dotsub.com', - 'yfrog.com', - 'photobucket.com', 'soundcloud.com', - 'instagram.com', 'twitter.com', - 'facebook.com' + 'x.com' ]; self.suggestedEndpoints = [ - { - domain: 'instagram.com', - endpoint: 'http://api.instagram.com/oembed' - }, - { - domain: 'facebook.com', - path: /\/videos\//, - endpoint: 'https://www.facebook.com/plugins/video/oembed.json/' - }, - { - domain: 'facebook.com', - path: /\/posts\//, - endpoint: 'https://www.facebook.com/plugins/post/oembed.json/' - }, { domain: 'vimeo.com', endpoint: 'https://vimeo.com/api/oembed.json' @@ -233,6 +207,14 @@ module.exports = function(options) { { domain: 'youtu.be', endpoint: 'https://www.youtube.com/oembed' + }, + { + domain: 'twitter.com', + endpoint: 'https://publish.twitter.com/oembed' + }, + { + domain: 'x.com', + endpoint: 'https://publish.twitter.com/oembed' } ]; From 1d6a10453f8c4ab47005b75595a101a9f1724a05 Mon Sep 17 00:00:00 2001 From: Miro Yovchev <2827783+myovchev@users.noreply.github.com> Date: Thu, 7 May 2026 18:21:28 +0300 Subject: [PATCH 11/13] Layout editable gap (#5397) (cherry picked from commit bc8f7bed381d4462a082976dd92b8d7f28180a95) --- .changeset/seven-emus-vanish.md | 6 + .../widgets/LayoutWidget.astro | 32 +- .../modules/@apostrophecms/i18n/i18n/en.json | 2 + .../@apostrophecms/layout-widget/index.js | 126 ++++- .../apos/components/AposAreaLayoutEditor.vue | 89 ++- .../ui/apos/components/AposGridLayout.vue | 4 +- .../ui/apos/components/AposGridManager.vue | 4 +- .../layout-widget/ui/apos/layout.css | 8 + .../layout-widget/ui/src/layout.css | 2 +- .../layout-widget/views/widget.html | 6 +- .../modules/@apostrophecms/styles/index.js | 16 + .../@apostrophecms/styles/lib/handlers.js | 6 + .../@apostrophecms/styles/lib/methods.js | 93 +++ .../@apostrophecms/styles/lib/presets.js | 17 + .../styles/ui/apos/universal/render.mjs | 11 +- .../modules/@apostrophecms/util/index.js | 4 + .../@apostrophecms/widget-type/index.js | 6 + .../ui/apos/components/AposWidgetEditor.vue | 32 ++ packages/apostrophe/test/layout-widget-gap.js | 530 ++++++++++++++++++ packages/apostrophe/test/styles.js | 50 ++ 20 files changed, 1022 insertions(+), 22 deletions(-) create mode 100644 .changeset/seven-emus-vanish.md create mode 100644 packages/apostrophe/test/layout-widget-gap.js diff --git a/.changeset/seven-emus-vanish.md b/.changeset/seven-emus-vanish.md new file mode 100644 index 0000000000..564a10e231 --- /dev/null +++ b/.changeset/seven-emus-vanish.md @@ -0,0 +1,6 @@ +--- +"@apostrophecms/apostrophe-astro": minor +"apostrophe": minor +--- + +Editors can now control the layout-widget gap through the styles system, both site-wide via a global `layoutGap` preset and per widget via a `gap` styles field. New Layout widget option `className` allows for additional CSS class names to be added to the widget Grid container. diff --git a/packages/apostrophe-astro/widgets/LayoutWidget.astro b/packages/apostrophe-astro/widgets/LayoutWidget.astro index ec25dfaa5b..596cbecffa 100644 --- a/packages/apostrophe-astro/widgets/LayoutWidget.astro +++ b/packages/apostrophe-astro/widgets/LayoutWidget.astro @@ -2,6 +2,31 @@ const { widget, options } = Astro.props; import AposArea from "../components/AposArea.astro"; import LayoutColumn from "./LayoutColumn.astro"; + +const gapMeta = widget._options || {}; +const resolvedGap = options._gap ?? gapMeta._gap ?? null; +const gapHasGlobal = options._gapHasGlobal ?? gapMeta._gapHasGlobal ?? false; + +// Resolve the inline `--grid-gap` value +const hasWidgetGap = resolvedGap != null; +const omitGridGap = !hasWidgetGap && gapHasGlobal; +const gridGap = hasWidgetGap + ? resolvedGap + : omitGridGap + ? undefined + : options.gap || "0"; + +// Mirror the Nunjucks `parentOptionsForArea` helper so the in-place +// layout editor (AposAreaLayoutEditor) sees the same `gap` signal in +// its `data-parent-options` JSON as it would on the Nunjucks side: +// - widget value present → carries through (string with unit). +// - widget value absent + global enabled → `gap: null` (signal omit). +// - otherwise → no `gap` key (use the static module default / BC). +const parentOptions = { + ...options, + widgetId: widget._id, + ...(hasWidgetGap ? { gap: resolvedGap } : omitGridGap ? { gap: null } : {}), +}; --- diff --git a/packages/apostrophe/modules/@apostrophecms/i18n/i18n/en.json b/packages/apostrophe/modules/@apostrophecms/i18n/i18n/en.json index d2ae0d8af6..e6b5ed913d 100644 --- a/packages/apostrophe/modules/@apostrophecms/i18n/i18n/en.json +++ b/packages/apostrophe/modules/@apostrophecms/i18n/i18n/en.json @@ -643,6 +643,8 @@ "styleGradientAngle": "Angle", "styleGradientEnd": "End Color", "styleGradientStart": "Start Color", + "styleLayoutGap": "Layout Gap", + "styleLayoutGapHelp": "Sets the spacing between columns inside layout sections across the site.", "styleLeft": "Left", "styleMargin": "Margin", "styleOverlayColor": "Overlay Color", diff --git a/packages/apostrophe/modules/@apostrophecms/layout-widget/index.js b/packages/apostrophe/modules/@apostrophecms/layout-widget/index.js index 71377222d0..b6d2060bd7 100644 --- a/packages/apostrophe/modules/@apostrophecms/layout-widget/index.js +++ b/packages/apostrophe/modules/@apostrophecms/layout-widget/index.js @@ -19,6 +19,10 @@ module.exports = { gap: '1.5rem', defaultCellHorizontalAlignment: null, defaultCellVerticalAlignment: null, + // Extra class name(s) to append to the rendered layout-widget area + // wrapper, in addition to the built-in `layout-widget` class. Accepts + // a string of space-separated class names. + className: '', injectStyles: true, minifyStyles: true }, @@ -98,6 +102,25 @@ module.exports = { validateAndIdentifyTypes() { const { column } = self.validateAndIdentifyTypes(); self.columnWidgetName = column; + }, + // Detect the widget-style "gap" field (any styles field whose + // CSS `property` resolves to `gap`). Only the first match is used. + // Also detect whether the @apostrophecms/styles module has a + // site-wide layout gap field configured (via the `layoutGap` + // preset / `layoutGapDefault: true` marker). + detectGapFields() { + const widgetGapFields = self.apos.styles + .fieldsWithProperty(self.schema, 'gap'); + if (widgetGapFields.length > 1) { + self.apos.util.warn( + `[${self.__meta.name}] Multiple style fields produce the ` + + `CSS \`gap\` property (${widgetGapFields.join(', ')}). ` + + 'Only the first one will be honoured as the widget-scope gap.' + ); + } + self.widgetGapFieldName = widgetGapFields[0] || null; + self.globalGapEnabled = !!self.apos.modules['@apostrophecms/styles'] + ?.layoutGapFieldName; } } }; @@ -118,6 +141,17 @@ module.exports = { defaultCellHorizontalAlignment: self.options.defaultCellHorizontalAlignment, defaultCellVerticalAlignment: self.options.defaultCellVerticalAlignment }, + widgetGapFieldName: self.widgetGapFieldName || null, + widgetGapFieldUnit: self.widgetGapFieldName + ? (self.apos.styles + .getFieldByPath(self.schema, self.widgetGapFieldName) + ?.unit || '') + : '', + globalGapEnabled: !!self.globalGapEnabled, + // Opt-in flag read by the generic widget editor: when true, + // it broadcasts `apos-widget-live-preview` events on the apos bus + // during the style-only fast path. + subscribesToLivePreview: !!self.widgetGapFieldName, columnWidgetName: self.columnWidgetName }; }, @@ -132,6 +166,7 @@ module.exports = { defaultCellHorizontalAlignment, defaultCellVerticalAlignment } = self.options; + const widgetGap = self.resolveWidgetGap(widget); return { ..._super(widget, { scene }), columns, @@ -141,13 +176,53 @@ module.exports = { tablet, gap, defaultCellHorizontalAlignment, - defaultCellVerticalAlignment + defaultCellVerticalAlignment, + _gap: widgetGap, + _gapHasGlobal: !!self.globalGapEnabled }; } }; }, methods(self) { return { + // Resolve the widget-scope gap for a widget instance, if this + // module declares a styles field with `property: 'gap'`. Returns + // the resolved value (with unit, when applicable). When the + // widget has no explicit value, falls back to the field's `def`. + // Returns `null` only when no widget gap field is + // configured, no widget value is set, and no `def` is declared + // on the field. + resolveWidgetGap(widget) { + if (!self.widgetGapFieldName || !widget) { + return null; + } + const field = self.apos.styles.getFieldByPath( + self.schema, self.widgetGapFieldName + ); + let value = self.apos.util.get(widget, self.widgetGapFieldName); + if (value === null || value === undefined || value === '') { + if (field?.def === null || field?.def === undefined || field?.def === '') { + return null; + } + value = field.def; + } + if (field?.unit && typeof value !== 'string') { + return `${value}${field.unit}`; + } + return value; + }, + // Determine whether the inline `--grid-gap` CSS variable should + // be omitted on the grid container so the global cascade + // (`var(--apos-layout-gap, …)`) can take effect. The inline var + // is omitted whenever: + // - no widget-scope gap value is set, AND + // - the global layout-gap field is configured. + shouldOmitInlineGap(widget, global) { + if (!self.globalGapEnabled) { + return false; + } + return self.resolveWidgetGap(widget) === null; + }, publicCssNodes(req) { return [ { @@ -277,11 +352,13 @@ module.exports = { const mobileBreakpointPlus = mobileBreakpoint + 1; const tabletBreakpoint = self.options.tablet?.breakpoint || 1024; const tabletBreakpointPlus = tabletBreakpoint + 1; + const gapDefault = self.options.gap || '0'; cssContent = cssContent .replace(/\{\$mobile\}/g, mobileBreakpoint) .replace(/\{\$mobile-plus\}/g, mobileBreakpointPlus) .replace(/\{\$tablet\}/g, tabletBreakpoint) - .replace(/\{\$tablet-plus\}/g, tabletBreakpointPlus); + .replace(/\{\$tablet-plus\}/g, tabletBreakpointPlus) + .replace(/\{\$gap-default\}/g, gapDefault); return self.processCss(cssContent, scene); }, @@ -371,6 +448,51 @@ module.exports = { (a.order ?? 0) - (b.order ?? 0) ); return items[items.length - 1]._id; + }, + // Compute the `--grid-gap: ;` declaration to inline on the + // grid container, or an empty string when the cascade should + // resolve gap via `var(--apos-layout-gap, …)` instead. + // Honours the priority order: + // 1. Widget-style gap (when set on this widget instance). + // 2. Static module option (BC) — when no global gap field is + // configured, or it has no value. + // 3. Otherwise, omit the inline var so the global cascade wins. + // Must be invoked via the widget's own module namespace — + // `apos.modules[data.manager.__meta.name].gapInlineCss(...)` — + // so that `self` resolves to the actual subclass and picks up + // its `widgetGapFieldName` / `globalGapEnabled`. + gapInlineCss(widget, options, global) { + const widgetGap = self.resolveWidgetGap(widget); + if (widgetGap !== null) { + return ` --grid-gap: ${widgetGap};`; + } + if (self.shouldOmitInlineGap(widget, global)) { + return ''; + } + const fallback = (options && options.gap) || self.options.gap || '0'; + return ` --grid-gap: ${fallback};`; + }, + // Build the `aposParentOptions` payload passed by the rendered + // layout-widget area to the in-place editor (AposAreaLayoutEditor). + // Includes the widget's resolved gap (from its `gap` styles field, + // when present) so the live editor's grid container reflects the + // saved per-widget value rather than only the static module + // option. Sets `gap: null` to signal the editor to omit + // `--grid-gap` so the global cascade resolves it through + // `:root { --apos-layout-gap }`. Must be invoked via the widget's + // own module namespace, like `gapInlineCss`. + parentOptionsForArea(widget, options, global) { + const opts = { + ...(options || {}), + widgetId: widget._id + }; + const widgetGap = self.resolveWidgetGap(widget); + if (widgetGap !== null) { + opts.gap = widgetGap; + } else if (self.shouldOmitInlineGap(widget, global)) { + opts.gap = null; + } + return opts; } }; } diff --git a/packages/apostrophe/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue b/packages/apostrophe/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue index 9f4f2387d4..9e6d4bd4d8 100644 --- a/packages/apostrophe/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +++ b/packages/apostrophe/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue @@ -122,6 +122,7 @@