apostrophe 4.30.1-beta.1 → 4.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +15 -0
- package/CHANGELOG.md +22 -2
- package/claude-tools/detect-handles.js +46 -0
- package/claude-tools/minimal-hang-test.js +28 -0
- package/claude-tools/mongo-close-test.js +11 -0
- package/claude-tools/stdin-ref-test.js +14 -0
- package/eslint.config.js +3 -1
- package/modules/@apostrophecms/area/index.js +94 -2
- package/modules/@apostrophecms/area/lib/custom-tags/area.js +1 -40
- package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +0 -1
- package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +0 -1
- package/modules/@apostrophecms/attachment/index.js +4 -1
- package/modules/@apostrophecms/db/index.js +68 -27
- package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +5 -3
- package/modules/@apostrophecms/express/index.js +2 -0
- package/modules/@apostrophecms/file/index.js +9 -8
- package/modules/@apostrophecms/http/index.js +1 -1
- package/modules/@apostrophecms/i18n/i18n/en.json +3 -0
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +2 -2
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +3 -0
- package/modules/@apostrophecms/job/index.js +9 -7
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridColumn.vue +0 -1
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +0 -1
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +10 -2
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +3 -3
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +52 -23
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +6 -1
- package/modules/@apostrophecms/oembed/index.js +2 -1
- package/modules/@apostrophecms/piece-type/index.js +2 -1
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +7 -2
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposCellTitle.vue +1 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +21 -4
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputDateAndTime.vue +7 -2
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +1 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposSubform.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSubform.js +10 -0
- package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +1 -0
- package/modules/@apostrophecms/template/index.js +117 -11
- package/modules/@apostrophecms/template/lib/jsxLoader.js +128 -0
- package/modules/@apostrophecms/template/lib/jsxRender.js +490 -0
- package/modules/@apostrophecms/template/lib/jsxRuntime.js +276 -0
- package/modules/@apostrophecms/template/lib/nunjucksLoader.js +11 -36
- package/modules/@apostrophecms/template/lib/viewWatcher.js +113 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposCellLastEdited.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposSelect.vue +1 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +10 -4
- package/modules/@apostrophecms/ui/ui/apos/components/AposSlatList.vue +6 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposSubformPreview.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposTreeHeader.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +2 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +1 -0
- package/modules/@apostrophecms/uploadfs/index.js +3 -0
- package/modules/@apostrophecms/util/index.js +20 -3
- package/package.json +14 -10
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
- package/test/add-missing-schema-fields-project/test.js +22 -3
- package/test/assets.js +110 -67
- package/test/db-tools.js +365 -0
- package/test/db.js +24 -15
- package/test/default-adapter.js +256 -0
- package/test/external-front.js +419 -1
- package/test/files.js +28 -0
- package/test/job.js +1 -1
- package/test/modules/jsx-area-test/index.js +23 -0
- package/test/modules/jsx-area-test/views/bad-area.jsx +7 -0
- package/test/modules/jsx-area-test/views/with-area-ctx.jsx +13 -0
- package/test/modules/jsx-area-test/views/with-area.jsx +7 -0
- package/test/modules/jsx-area-test/views/with-widget-ctx.jsx +12 -0
- package/test/modules/jsx-area-test/views/with-widget.jsx +7 -0
- package/test/modules/jsx-async-widget/index.js +6 -0
- package/test/modules/jsx-async-widget/views/widget.jsx +11 -0
- package/test/modules/jsx-bridge-test/index.js +1 -0
- package/test/modules/jsx-bridge-test/views/cross-module.jsx +7 -0
- package/test/modules/jsx-bridge-test/views/disambig-name-only.jsx +7 -0
- package/test/modules/jsx-bridge-test/views/disambig-target.jsx +8 -0
- package/test/modules/jsx-bridge-test/views/disambig-with-template-name.jsx +7 -0
- package/test/modules/jsx-bridge-test/views/include-html.jsx +7 -0
- package/test/modules/jsx-bridge-test/views/include-target.html +4 -0
- package/test/modules/jsx-bridge-test/views/jsx-extends-via-extend.jsx +9 -0
- package/test/modules/jsx-bridge-test/views/jsx-extends.jsx +9 -0
- package/test/modules/jsx-bridge-test/views/jsx-layout.jsx +14 -0
- package/test/modules/jsx-bridge-test/views/njk-extends.jsx +14 -0
- package/test/modules/jsx-bridge-test/views/njk-layout.html +9 -0
- package/test/modules/jsx-bridge-test/views/short-form.jsx +7 -0
- package/test/modules/jsx-bridge-test/views/short-target.jsx +3 -0
- package/test/modules/jsx-component-test/index.js +15 -0
- package/test/modules/jsx-component-test/views/greet.html +1 -0
- package/test/modules/jsx-component-test/views/uses-component.jsx +8 -0
- package/test/modules/jsx-ctx-widget/index.js +6 -0
- package/test/modules/jsx-ctx-widget/views/widget.jsx +4 -0
- package/test/modules/jsx-mixed-test/index.js +9 -0
- package/test/modules/jsx-mixed-test/views/apos-full.jsx +21 -0
- package/test/modules/jsx-mixed-test/views/async-list.jsx +12 -0
- package/test/modules/jsx-mixed-test/views/lib/format.js +3 -0
- package/test/modules/jsx-mixed-test/views/localized.jsx +3 -0
- package/test/modules/jsx-mixed-test/views/partial.jsx +3 -0
- package/test/modules/jsx-mixed-test/views/safe-helper.jsx +3 -0
- package/test/modules/jsx-mixed-test/views/syntax-error.jsx +3 -0
- package/test/modules/jsx-mixed-test/views/throws.jsx +5 -0
- package/test/modules/jsx-mixed-test/views/uses-import.jsx +5 -0
- package/test/modules/jsx-mixed-test/views/uses-require.jsx +5 -0
- package/test/modules/jsx-watcher-cross-test/index.js +5 -0
- package/test/modules/jsx-watcher-cross-test/views/cross-template.jsx +3 -0
- package/test/modules/jsx-watcher-test/index.js +5 -0
- package/test/modules/jsx-watcher-test/views/watcher-test.jsx +3 -0
- package/test/modules/template-jsx-options-test/index.js +12 -0
- package/test/modules/template-jsx-options-test/views/options-test.jsx +9 -0
- package/test/modules/template-jsx-subclass-test/index.js +3 -0
- package/test/modules/template-jsx-subclass-test/views/override-test.jsx +3 -0
- package/test/modules/template-jsx-test/index.js +9 -0
- package/test/modules/template-jsx-test/views/boolean-attrs.jsx +11 -0
- package/test/modules/template-jsx-test/views/class-and-for.jsx +7 -0
- package/test/modules/template-jsx-test/views/dangerously-set.jsx +3 -0
- package/test/modules/template-jsx-test/views/escape-attr.jsx +3 -0
- package/test/modules/template-jsx-test/views/escape-body.jsx +3 -0
- package/test/modules/template-jsx-test/views/inherit-test.jsx +3 -0
- package/test/modules/template-jsx-test/views/list.jsx +7 -0
- package/test/modules/template-jsx-test/views/override-test.jsx +3 -0
- package/test/modules/template-jsx-test/views/svg-attrs.jsx +27 -0
- package/test/modules/template-jsx-test/views/test.jsx +3 -0
- package/test/modules/template-jsx-test/views/void-elements.jsx +9 -0
- package/test/templates-jsx-watcher.js +135 -0
- package/test/templates-jsx.js +537 -0
- package/test/utils.js +103 -0
- package/test-lib/util.js +50 -14
- package/lib/mongodb-connect.js +0 -62
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(timeout 180 npx mocha:*)",
|
|
5
|
+
"Bash(timeout 600 npx mocha:*)",
|
|
6
|
+
"Bash(npm ls:*)",
|
|
7
|
+
"Bash(timeout 540 npx mocha:*)",
|
|
8
|
+
"Bash(echo:*)",
|
|
9
|
+
"Bash(timeout 10 node:*)",
|
|
10
|
+
"Bash(timeout 300 npx mocha:*)",
|
|
11
|
+
"Bash(timeout 60 npx mocha:*)",
|
|
12
|
+
"Bash(timeout 120 npx mocha:*)"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 4.
|
|
3
|
+
## 4.31.0 (2026-06-10)
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### Adds
|
|
6
|
+
|
|
7
|
+
- Added support for `draggable: false` on non-inline `array` schema fields. Previously this option was only respected when `inline: true`. When set on a standard (modal-based) array field, drag-and-drop reordering and keyboard reordering are now disabled in the array editor's slat list.
|
|
8
|
+
- Introduced support for postgres://, sqlite://, and multipostgres:// database URIs in addition to mongodb://. The new db-connect API supports all of the database operations currently used in our own core, pro and multisite modules. For more information see the documentation.
|
|
9
|
+
- JSX support for templates within ApostropheCMS. JSX is now co-equal with Nunjucks, with a gradual migration strategy. Anyone who is familiar with React will be very comfortable writing JSX templates, which also offer a superior debugging experience, and templates can be migrated gradually. JSX is a great option for those who don't wish to create parallel Astro and ApostropheCMS projects, but still prefer a modern syntax. For more information, see the new [JSX templates guide](https://apostrophecms.com/docs/guide/jsx-templates.html).
|
|
10
|
+
- The session secret and the uploadfs `disabledFileKey` can now be supplied via the `APOS_SESSION_SECRET` and `APOS_UPLOADFS_DISABLED_FILE_KEY` environment variables. As with other Apostrophe environment variables, these take precedence over the corresponding `app.js` configuration.
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
6
13
|
|
|
14
|
+
- Fixed an issue where using the Tab key to navigate within modals could incorrectly jump focus to a wrong element instead of the next input field.
|
|
15
|
+
Fixed Tab navigation escaping out of modals when the form contained hidden sections or elements that became disabled after editing.
|
|
16
|
+
- Fixed adding or removing an area field from a schema breaking existing documents on an external front such as Astro.
|
|
17
|
+
- For Astro: `AposArea` now renders only schema-backed areas. A missing area no longer throws, and an area orphaned by removing its field from the schema (while its content remains in the document) renders nothing instead of breaking sibling areas in edit mode. Logged-in editors get a diagnostic message in place of an orphaned area; anonymous visitors see nothing.
|
|
18
|
+
- Editable documents sent to an external front (Asgtro) now materialize empty area objects for schema area fields added after the document was created, so they can be edited in context.
|
|
19
|
+
- `apos.util.getManagerOf` accepts a `{ log }` option to suppress its error log when probing objects that may not have a manager.
|
|
20
|
+
- Fix more admin UI a11y issues.
|
|
21
|
+
- Selecting an item in a relationship "browse" dialog no longer scrolls the title and Cancel/Select buttons out of view when the item is far down the list.
|
|
7
22
|
- Sites with a custom filterByIndexPage method no longer experience failures in the sitemap module and potential creeping CPU performance penalties. A regression introduced with our static site support, but not specific to static sites.
|
|
8
23
|
|
|
24
|
+
### Security
|
|
25
|
+
|
|
26
|
+
- Server-side prototype pollution (CWE-1321) via dot-notation paths. `apos.util.set()` and `apos.util.get()` now refuse to traverse `__proto__`, `constructor` and `prototype` path segments. Previously an authenticated editor could send a PATCH REST API request whose patch operators (for example `$pullAll` with a key of `__proto__.publicApiProjection`) wrote to `Object.prototype`. A polluted `publicApiProjection` defeated the `publicApiCheck()` authorization gate on piece-type REST endpoints for subsequent unauthenticated requests, for the lifetime of the Node.js process. All users should update. Thanks to [tonghuaroot](https://github.com/tonghuaroot), [H3xV0rT3x](https://github.com/H3xV0rT3x), and [5h1kh4r](https://github.com/5h1kh4r) for reporting the vulnerability.
|
|
27
|
+
- When `@apostrophecms/file` pretty URLs are enabled (`prettyUrls: true`), the upstream request used to serve the file is no longer built from the incoming `Host` header. The self-request is now resolved against the site's configured `baseUrl` (via `req.baseUrl`), falling back to the request host only when no `baseUrl` is configured. This closes a server-side request forgery (SSRF) vector in which the `Host` header could steer the proxied fetch at another host. The real-world risk was low: the path is constrained to an existing attachment's `/uploads/attachments/<cuid>-<slug>.<ext>`, and cuids are unique and immutable, so any reachable content was already public via the front door. Thanks to [EchoSkorJjj](https://github.com/EchoSkorJjj) for reporting the issue.
|
|
28
|
+
|
|
9
29
|
## 4.30.0
|
|
10
30
|
|
|
11
31
|
### Adds
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Require this before running mocha to detect what activates process.stdin
|
|
2
|
+
// Usage: npx mocha -t 10000 --require ./claude-tools/detect-handles.js test/assets.js
|
|
3
|
+
|
|
4
|
+
console.log(`stdin paused at startup: ${process.stdin.isPaused()}`);
|
|
5
|
+
console.log(`stdin readableFlowing at startup: ${process.stdin.readableFlowing}`);
|
|
6
|
+
|
|
7
|
+
// Monkey-patch stdin.resume to capture the call stack
|
|
8
|
+
const origResume = process.stdin.resume.bind(process.stdin);
|
|
9
|
+
process.stdin.resume = function(...args) {
|
|
10
|
+
console.log('\n=== process.stdin.resume() called ===');
|
|
11
|
+
console.log(new Error().stack);
|
|
12
|
+
return origResume(...args);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Monkey-patch stdin.on to detect 'data' listener additions
|
|
16
|
+
const origOn = process.stdin.on.bind(process.stdin);
|
|
17
|
+
process.stdin.on = function(event, ...args) {
|
|
18
|
+
if (event === 'data' || event === 'readable') {
|
|
19
|
+
console.log(`\n=== process.stdin.on('${event}') called ===`);
|
|
20
|
+
console.log(new Error().stack);
|
|
21
|
+
}
|
|
22
|
+
return origOn(event, ...args);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Periodically check stdin state changes
|
|
26
|
+
let lastState = process.stdin.readableFlowing;
|
|
27
|
+
const checker = setInterval(() => {
|
|
28
|
+
if (process.stdin.readableFlowing !== lastState) {
|
|
29
|
+
console.log(`\n=== stdin readableFlowing changed: ${lastState} -> ${process.stdin.readableFlowing} ===`);
|
|
30
|
+
console.log(new Error().stack);
|
|
31
|
+
lastState = process.stdin.readableFlowing;
|
|
32
|
+
}
|
|
33
|
+
}, 100);
|
|
34
|
+
checker.unref();
|
|
35
|
+
|
|
36
|
+
const origRun = require('mocha/lib/runner').prototype.run;
|
|
37
|
+
require('mocha/lib/runner').prototype.run = function(fn) {
|
|
38
|
+
return origRun.call(this, function(failures) {
|
|
39
|
+
console.log(`\nstdin paused at end: ${process.stdin.isPaused()}`);
|
|
40
|
+
console.log(`stdin readableFlowing at end: ${process.stdin.readableFlowing}`);
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
process.exit(failures ? 3 : 0);
|
|
43
|
+
}, 2000);
|
|
44
|
+
if (fn) fn(failures);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Minimal test to isolate what causes the hang.
|
|
2
|
+
// Must reference the test/ directory as root for proper module resolution.
|
|
3
|
+
const t = require('../test-lib/test.js');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// Fake a module object rooted in test/ like the real tests do
|
|
7
|
+
const fakeModule = {
|
|
8
|
+
id: path.join(__dirname, '../test/fake'),
|
|
9
|
+
filename: path.join(__dirname, '../test/fake.js'),
|
|
10
|
+
paths: [path.join(__dirname, '../test/node_modules')]
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe('Minimal hang test', function() {
|
|
14
|
+
this.timeout(60000);
|
|
15
|
+
let apos;
|
|
16
|
+
|
|
17
|
+
after(async function() {
|
|
18
|
+
await t.destroy(apos);
|
|
19
|
+
console.log('after: destroy complete');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should create and use apos without hanging', async function() {
|
|
23
|
+
apos = await t.create({
|
|
24
|
+
root: fakeModule
|
|
25
|
+
});
|
|
26
|
+
console.log('apos created successfully');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Test whether a MongoDB connection keeps the process alive after close()
|
|
2
|
+
const mongoConnect = require('../../../packages/db-connect/lib/mongodb-connect');
|
|
3
|
+
|
|
4
|
+
(async () => {
|
|
5
|
+
const uri = 'mongodb://localhost:27017/test_handle_leak';
|
|
6
|
+
console.log('Connecting...');
|
|
7
|
+
const client = await mongoConnect(uri);
|
|
8
|
+
console.log('Connected. Closing...');
|
|
9
|
+
await client.close();
|
|
10
|
+
console.log('Closed. Process should exit now if no leaked handles.');
|
|
11
|
+
})();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Check if process.stdin keeps the process alive
|
|
2
|
+
// If this script hangs, stdin is ref'd. If it exits, stdin is unref'd.
|
|
3
|
+
|
|
4
|
+
console.log(`stdin isTTY: ${process.stdin.isTTY}`);
|
|
5
|
+
console.log(`stdin readableFlowing: ${process.stdin.readableFlowing}`);
|
|
6
|
+
console.log(`stdin isPaused: ${process.stdin.isPaused()}`);
|
|
7
|
+
|
|
8
|
+
// Check ref status
|
|
9
|
+
if (typeof process.stdin.unref === 'function') {
|
|
10
|
+
console.log('stdin has unref method');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
console.log('Waiting to see if process exits on its own...');
|
|
14
|
+
// Don't do anything - just see if the process exits
|
package/eslint.config.js
CHANGED
|
@@ -140,7 +140,7 @@ module.exports = {
|
|
|
140
140
|
// so this logic is reproduced partially
|
|
141
141
|
self.apos.doc.walk(area, (o, k, v) => {
|
|
142
142
|
if (v && v.metaType === 'area') {
|
|
143
|
-
const manager = self.apos.util.getManagerOf(o);
|
|
143
|
+
const manager = self.apos.util.getManagerOf(o, { log: false });
|
|
144
144
|
if (!manager) {
|
|
145
145
|
self.apos.util.warnDevOnce(
|
|
146
146
|
'noManagerForDocInExternalFront',
|
|
@@ -285,6 +285,95 @@ module.exports = {
|
|
|
285
285
|
self.missingWidgetTypes[name] = true;
|
|
286
286
|
}
|
|
287
287
|
},
|
|
288
|
+
// Build an empty area and attach it to `parent[name]`. When the area's
|
|
289
|
+
// location can be resolved inside the *persisted* document, also stub it
|
|
290
|
+
// into the database so the backend recognizes it for later edits.
|
|
291
|
+
// Returns the area.
|
|
292
|
+
//
|
|
293
|
+
// Options:
|
|
294
|
+
// - `throwIfNotFound` (default `false`): when `parent` is doc-backed but
|
|
295
|
+
// the document or the container cannot be located in the database,
|
|
296
|
+
// throw a `notfound` error instead of returning an in-memory-only
|
|
297
|
+
// stub. The `{% area %}` tag opts in to preserve its historical
|
|
298
|
+
// behavior; the external front annotator leaves it off so a render is
|
|
299
|
+
// never brought down by such a case.
|
|
300
|
+
//
|
|
301
|
+
// Used by the `{% area %}` tag and the external front annotator as the
|
|
302
|
+
// single source of truth for stubbing schema areas that have no value
|
|
303
|
+
// yet.
|
|
304
|
+
async addMissingArea(parent, name, { throwIfNotFound = false } = {}) {
|
|
305
|
+
const area = {
|
|
306
|
+
metaType: 'area',
|
|
307
|
+
_id: self.apos.util.generateId(),
|
|
308
|
+
items: []
|
|
309
|
+
};
|
|
310
|
+
parent[name] = area;
|
|
311
|
+
|
|
312
|
+
const docId = parent._docId ??
|
|
313
|
+
(parent.metaType === 'doc' ? parent._id : null);
|
|
314
|
+
const areaDotPath = await self.resolvePersistedAreaDotPath(parent, name);
|
|
315
|
+
if (!areaDotPath) {
|
|
316
|
+
if (throwIfNotFound && docId) {
|
|
317
|
+
throw self.apos.error('notfound');
|
|
318
|
+
}
|
|
319
|
+
return area;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const result = await self.apos.doc.db.updateOne(
|
|
323
|
+
{
|
|
324
|
+
_id: docId,
|
|
325
|
+
// Idempotent and race-safe: only write when still absent.
|
|
326
|
+
[areaDotPath]: { $eq: null }
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
$set: { [areaDotPath]: self.apos.util.clonePermanent(area) }
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
if (result.modifiedCount === 0) {
|
|
333
|
+
// Another request stubbed it first (or it already existed): adopt
|
|
334
|
+
// the persisted `_id` so we render the same area.
|
|
335
|
+
const refreshed = await self.apos.doc.db.findOne({ _id: docId });
|
|
336
|
+
const persisted = refreshed && self.apos.util.get(refreshed, areaDotPath);
|
|
337
|
+
if (persisted?._id) {
|
|
338
|
+
area._id = persisted._id;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return area;
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
// Resolve the MongoDB dot-path at which `parent[name]` should be stored,
|
|
345
|
+
// computed from the *persisted* document so it always reflects real
|
|
346
|
+
// storage (not the in-memory graph with its loaded relationships). The
|
|
347
|
+
// `parent` object is located inside the freshly read document by its
|
|
348
|
+
// `_id`. Returns the dot-path string, or `null` when the area cannot be
|
|
349
|
+
// safely persisted (no doc id, doc not in the database, or `parent` is
|
|
350
|
+
// not part of the persisted document, e.g. loaded relationship data).
|
|
351
|
+
async resolvePersistedAreaDotPath(parent, name) {
|
|
352
|
+
const docId = parent._docId ??
|
|
353
|
+
(parent.metaType === 'doc' ? parent._id : null);
|
|
354
|
+
if (!docId) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const mainDoc = await self.apos.doc.db.findOne({ _id: docId });
|
|
358
|
+
if (!mainDoc) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
if (parent._id === docId) {
|
|
362
|
+
return name;
|
|
363
|
+
}
|
|
364
|
+
if (!parent._id) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
const found = self.apos.util.findNestedObjectAndDotPathById(
|
|
368
|
+
mainDoc,
|
|
369
|
+
parent._id,
|
|
370
|
+
{ ignoreDynamicProperties: true }
|
|
371
|
+
);
|
|
372
|
+
if (!found) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
return `${found.dotPath}.${name}`;
|
|
376
|
+
},
|
|
288
377
|
prepForRender(area, context, fieldName) {
|
|
289
378
|
const manager = self.apos.util.getManagerOf(context);
|
|
290
379
|
const field = manager.schema.find(field => field.name === fieldName);
|
|
@@ -451,7 +540,10 @@ module.exports = {
|
|
|
451
540
|
// Loop over the docs in the array passed in.
|
|
452
541
|
for (const doc of within) {
|
|
453
542
|
if (self.apos.externalFrontKey) {
|
|
454
|
-
self.apos.template.annotateDocForExternalFront(
|
|
543
|
+
await self.apos.template.annotateDocForExternalFront(
|
|
544
|
+
doc,
|
|
545
|
+
{ scene: req.scene }
|
|
546
|
+
);
|
|
455
547
|
}
|
|
456
548
|
|
|
457
549
|
const rendered = [];
|
|
@@ -59,46 +59,7 @@ module.exports = function(self) {
|
|
|
59
59
|
}
|
|
60
60
|
area = doc[name];
|
|
61
61
|
if (!area) {
|
|
62
|
-
|
|
63
|
-
// has a value, for instance the field could be new in the schema.
|
|
64
|
-
// But we need an area _id. Stub it into the db on the fly
|
|
65
|
-
// without race conditions
|
|
66
|
-
area = {
|
|
67
|
-
metaType: 'area',
|
|
68
|
-
_id: self.apos.util.generateId(),
|
|
69
|
-
items: []
|
|
70
|
-
};
|
|
71
|
-
doc[name] = area;
|
|
72
|
-
const docId = doc._docId || ((doc.metaType === 'doc') ? doc._id : null);
|
|
73
|
-
if (docId) {
|
|
74
|
-
let mainDoc = await self.apos.doc.db.findOne({ _id: docId });
|
|
75
|
-
if (!mainDoc) {
|
|
76
|
-
throw self.apos.error('notfound');
|
|
77
|
-
}
|
|
78
|
-
let docDotPath;
|
|
79
|
-
try {
|
|
80
|
-
docDotPath = (doc._id === docId) ? '' : self.apos.util.findNestedObjectAndDotPathById(mainDoc, doc._id).dotPath;
|
|
81
|
-
} catch (e) {
|
|
82
|
-
// Race condition: someone removed the area's parent object.
|
|
83
|
-
// Unlikely thanks to advisory locking
|
|
84
|
-
throw self.apos.error('notfound');
|
|
85
|
-
}
|
|
86
|
-
const areaDotPath = docDotPath ? `${docDotPath}.${name}` : name;
|
|
87
|
-
await self.apos.doc.db.updateOne({
|
|
88
|
-
_id: docId,
|
|
89
|
-
// Prevent race condition
|
|
90
|
-
[areaDotPath]: {
|
|
91
|
-
$eq: null
|
|
92
|
-
}
|
|
93
|
-
}, {
|
|
94
|
-
$set: {
|
|
95
|
-
[areaDotPath]: self.apos.util.clonePermanent(area)
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
mainDoc = await self.apos.doc.db.findOne({ _id: docId });
|
|
99
|
-
// Prevent race condition
|
|
100
|
-
area._id = self.apos.util.get(mainDoc, areaDotPath)._id;
|
|
101
|
-
}
|
|
62
|
+
area = await self.apos.area.addMissingArea(doc, name, { throwIfNotFound: true });
|
|
102
63
|
}
|
|
103
64
|
const manager = self.apos.util.getManagerOf(doc);
|
|
104
65
|
const field = manager.schema.find(field => field.name === name);
|
|
@@ -656,7 +656,10 @@ module.exports = {
|
|
|
656
656
|
// to avoid template errors
|
|
657
657
|
getMissingAttachmentUrl() {
|
|
658
658
|
const defaultIconUrl = '/modules/@apostrophecms/attachment/img/missing-icon.svg';
|
|
659
|
-
|
|
659
|
+
const e = new Error();
|
|
660
|
+
self.apos.util.warn('Template warning: Impossible to retrieve the attachment url since it is missing, a default icon has been set. Please fix this ASAP!\n\n' +
|
|
661
|
+
e.stack
|
|
662
|
+
);
|
|
660
663
|
// Convert static asset path to full URL, which matters when static
|
|
661
664
|
// assets are in uploadfs
|
|
662
665
|
return self.apos.asset.url(defaultIconUrl);
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
//
|
|
5
5
|
// ### `uri`
|
|
6
6
|
//
|
|
7
|
-
// The
|
|
7
|
+
// The databse connection URI. See the [MongoDB URI documentation](https://docs.mongodb.com/manual/reference/connection-string/)
|
|
8
|
+
// and the postgres documentation.
|
|
8
9
|
//
|
|
9
10
|
// ### `connect`
|
|
10
11
|
//
|
|
11
|
-
// If present, this object is passed on as options to
|
|
12
|
+
// If present, this object is passed on as options to the database adapters "connect"
|
|
12
13
|
// method, along with the uri. See the [MongoDB connect settings documentation](http://mongodb.github.io/node-mongodb-native/2.2/reference/connecting/connection-settings/).
|
|
13
14
|
//
|
|
14
15
|
// By default, Apostrophe sets options to retry lost connections forever,
|
|
@@ -20,9 +21,16 @@
|
|
|
20
21
|
//
|
|
21
22
|
// ### `client`
|
|
22
23
|
//
|
|
23
|
-
// An existing MongoDB
|
|
24
|
+
// An existing MongoDB-compatible client object. If present, it is used
|
|
24
25
|
// and `uri`, `host`, `connect`, etc. are ignored.
|
|
25
26
|
//
|
|
27
|
+
// ### `adapters`
|
|
28
|
+
//
|
|
29
|
+
// An array of adapters, each of which must provide `name`, `connect(uri, options)`,
|
|
30
|
+
// and `protocols` properties. `name` may be used to override a core adapter,
|
|
31
|
+
// such as `postgres` or `mongodb`. `connect` must resolve to a client object
|
|
32
|
+
// supporting a sufficient subset of the mongodb API.
|
|
33
|
+
//
|
|
26
34
|
// ### `versionCheck`
|
|
27
35
|
//
|
|
28
36
|
// If `true`, check to make sure the database does not belong to an
|
|
@@ -49,15 +57,15 @@
|
|
|
49
57
|
// in your project. However you may find it easier to just use the
|
|
50
58
|
// `client` option.
|
|
51
59
|
|
|
52
|
-
const
|
|
53
|
-
const escapeHost = require('../../../lib/escape-host');
|
|
60
|
+
const dbConnect = require('@apostrophecms/db-connect');
|
|
61
|
+
const escapeHost = require('../../../lib/escape-host.js');
|
|
54
62
|
|
|
55
63
|
module.exports = {
|
|
56
64
|
options: {
|
|
57
65
|
versionCheck: true
|
|
58
66
|
},
|
|
59
67
|
async init(self) {
|
|
60
|
-
await self.
|
|
68
|
+
await self.connectToDb();
|
|
61
69
|
await self.versionCheck();
|
|
62
70
|
},
|
|
63
71
|
handlers(self) {
|
|
@@ -81,14 +89,12 @@ module.exports = {
|
|
|
81
89
|
},
|
|
82
90
|
methods(self) {
|
|
83
91
|
return {
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
// for a persistent process that requires MongoDB in order to operate.
|
|
91
|
-
async connectToMongo() {
|
|
92
|
+
// Connect to the database and sets self.apos.dbClient
|
|
93
|
+
// and self.apos.db. Builds a mongodb URI by default,
|
|
94
|
+
// accepting host, port, user, password and name options
|
|
95
|
+
// if present. More typically a URI is specified via
|
|
96
|
+
// APOS_DB_URI, or via APOS_MONGODB_URI for bc.
|
|
97
|
+
async connectToDb() {
|
|
92
98
|
if (self.options.client) {
|
|
93
99
|
// Reuse a single client connection http://mongodb.github.io/node-mongodb-native/2.2/api/Db.html#db
|
|
94
100
|
self.apos.dbClient = self.options.client;
|
|
@@ -96,32 +102,67 @@ module.exports = {
|
|
|
96
102
|
self.connectionReused = true;
|
|
97
103
|
return;
|
|
98
104
|
}
|
|
99
|
-
let uri
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
let uri;
|
|
106
|
+
const viaEnv = process.env.APOS_DB_URI || process.env.APOS_MONGODB_URI;
|
|
107
|
+
if (viaEnv) {
|
|
108
|
+
uri = viaEnv;
|
|
102
109
|
} else if (self.options.uri) {
|
|
103
110
|
uri = self.options.uri;
|
|
104
111
|
} else {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
self.options.host = 'localhost';
|
|
110
|
-
}
|
|
111
|
-
if (!self.options.port) {
|
|
112
|
-
self.options.port = 27017;
|
|
112
|
+
const validAdapters = [ 'mongodb', 'sqlite', 'postgres', 'multipostgres' ];
|
|
113
|
+
const adapter = process.env.APOS_DEFAULT_DB_ADAPTER || self.options.defaultAdapter || 'mongodb';
|
|
114
|
+
if (!validAdapters.includes(adapter)) {
|
|
115
|
+
throw new Error(`Invalid defaultAdapter: "${adapter}". Must be one of: ${validAdapters.join(', ')}`);
|
|
113
116
|
}
|
|
114
117
|
if (!self.options.name) {
|
|
115
118
|
self.options.name = self.apos.shortName;
|
|
116
119
|
}
|
|
117
|
-
|
|
120
|
+
if (adapter === 'sqlite') {
|
|
121
|
+
const path = require('path');
|
|
122
|
+
uri = `sqlite://${path.resolve(self.apos.rootDir, 'data', self.options.name + '.sqlite')}`;
|
|
123
|
+
} else {
|
|
124
|
+
const credentials = self.options.user
|
|
125
|
+
? encodeURIComponent(self.options.user) + ':' + encodeURIComponent(self.options.password) + '@'
|
|
126
|
+
: '';
|
|
127
|
+
if (adapter === 'mongodb') {
|
|
128
|
+
if (!self.options.host) {
|
|
129
|
+
self.options.host = 'localhost';
|
|
130
|
+
}
|
|
131
|
+
if (!self.options.port) {
|
|
132
|
+
self.options.port = 27017;
|
|
133
|
+
}
|
|
134
|
+
uri = 'mongodb://' + credentials + escapeHost(self.options.host) + ':' + self.options.port + '/' + self.options.name;
|
|
135
|
+
} else {
|
|
136
|
+
// postgres or multipostgres
|
|
137
|
+
if (!self.options.host) {
|
|
138
|
+
self.options.host = 'localhost';
|
|
139
|
+
}
|
|
140
|
+
if (!self.options.port) {
|
|
141
|
+
self.options.port = 5432;
|
|
142
|
+
}
|
|
143
|
+
uri = adapter + '://' + credentials + escapeHost(self.options.host) + ':' + self.options.port + '/' + self.options.name;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
118
146
|
}
|
|
119
147
|
|
|
120
|
-
self.apos.dbClient = await
|
|
148
|
+
self.apos.dbClient = await dbConnect(uri, {
|
|
149
|
+
...self.options.connect,
|
|
150
|
+
adapters: self.options.adapters
|
|
151
|
+
});
|
|
121
152
|
self.uri = uri;
|
|
122
153
|
// Automatically uses the db name in the connection string
|
|
123
154
|
self.apos.db = self.apos.dbClient.db();
|
|
124
155
|
},
|
|
156
|
+
// Connect to a database using the appropriate adapter based on the URI protocol.
|
|
157
|
+
// Returns a client object compatible with the MongoDB driver interface.
|
|
158
|
+
// This method has no side effects — it does not set apos.db or apos.dbClient.
|
|
159
|
+
// It can be used to make temporary connections, e.g. for dropping a test database.
|
|
160
|
+
async connectToAdapter(uri, options) {
|
|
161
|
+
return dbConnect(uri, {
|
|
162
|
+
...options,
|
|
163
|
+
adapters: self.options.adapters
|
|
164
|
+
});
|
|
165
|
+
},
|
|
125
166
|
async versionCheck() {
|
|
126
167
|
if (!self.options.versionCheck) {
|
|
127
168
|
return;
|
|
@@ -273,13 +273,13 @@ export default {
|
|
|
273
273
|
});
|
|
274
274
|
},
|
|
275
275
|
moduleName() {
|
|
276
|
-
if (apos.modules[this.context.type]
|
|
276
|
+
if (apos.modules[this.context.type]?.action === apos.modules['@apostrophecms/page'].action) {
|
|
277
277
|
return '@apostrophecms/page';
|
|
278
278
|
}
|
|
279
279
|
return this.context.type;
|
|
280
280
|
},
|
|
281
281
|
moduleOptions() {
|
|
282
|
-
return apos.modules[this.moduleName];
|
|
282
|
+
return apos.modules[this.moduleName] || {};
|
|
283
283
|
},
|
|
284
284
|
isUpdateOperation() {
|
|
285
285
|
return !!this.context._id;
|
|
@@ -482,7 +482,9 @@ export default {
|
|
|
482
482
|
);
|
|
483
483
|
},
|
|
484
484
|
preview(doc) {
|
|
485
|
-
window.open(
|
|
485
|
+
// window.open() returns null when a popup blocker intercepts it,
|
|
486
|
+
// so guard before calling focus().
|
|
487
|
+
window.open(doc._url, '_blank')?.focus();
|
|
486
488
|
},
|
|
487
489
|
async copy(doc) {
|
|
488
490
|
// If there are changes warn the user before discarding them before
|
|
@@ -579,6 +579,8 @@ module.exports = {
|
|
|
579
579
|
name: self.apos.shortName + '.sid',
|
|
580
580
|
cookie: {}
|
|
581
581
|
});
|
|
582
|
+
// Env overrides config, per Apostrophe convention.
|
|
583
|
+
sessionOptions.secret = process.env.APOS_SESSION_SECRET || sessionOptions.secret;
|
|
582
584
|
_.defaults(sessionOptions.cookie, {
|
|
583
585
|
path: '/',
|
|
584
586
|
httpOnly: true,
|
|
@@ -248,14 +248,15 @@ module.exports = {
|
|
|
248
248
|
const uglyUrl = self.apos.attachment.url(file.attachment, {
|
|
249
249
|
prettyUrl: false
|
|
250
250
|
});
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
// `
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
251
|
+
// `uglyUrl` may be relative (local uploadfs) or absolute
|
|
252
|
+
// (S3/CDN). For the relative case `streamProxy` resolves it
|
|
253
|
+
// against `req.baseUrl`, which reflects the configured
|
|
254
|
+
// `baseUrl` (or locale hostname) and only falls back to the
|
|
255
|
+
// request host when the site has none. We deliberately do
|
|
256
|
+
// not build the upstream URL from the raw `Host` header
|
|
257
|
+
// here, as that is attacker-controlled and would allow the
|
|
258
|
+
// self-request to be redirected to an arbitrary host (SSRF).
|
|
259
|
+
return await streamProxy(req, uglyUrl, { error: self.apos.util.error });
|
|
259
260
|
} catch (e) {
|
|
260
261
|
self.apos.util.error('Error in pretty URL route:', e);
|
|
261
262
|
return res.status(500).send('error');
|
|
@@ -2,7 +2,7 @@ const _ = require('lodash');
|
|
|
2
2
|
const qs = require('qs');
|
|
3
3
|
const fetch = require('node-fetch');
|
|
4
4
|
const tough = require('tough-cookie');
|
|
5
|
-
const escapeHost = require('../../../lib/escape-host');
|
|
5
|
+
const escapeHost = require('../../../lib/escape-host.js');
|
|
6
6
|
const util = require('util');
|
|
7
7
|
|
|
8
8
|
module.exports = {
|
|
@@ -205,6 +205,7 @@
|
|
|
205
205
|
"editImageRelationshipTitle": "Adjust Image",
|
|
206
206
|
"editRelationship": "Edit Relationship",
|
|
207
207
|
"editRelationshipFor": "Edit Relationship for {{ title }}",
|
|
208
|
+
"editSubform": "Edit {{ label }}",
|
|
208
209
|
"editType": "Edit {{ type }}",
|
|
209
210
|
"editWidget": "Edit Widget",
|
|
210
211
|
"editWidgetForeignTooltip": "Click to edit this content in its natural context",
|
|
@@ -569,6 +570,7 @@
|
|
|
569
570
|
"richTextHighlight": "Mark",
|
|
570
571
|
"richTextHorizontalRule": "Horizontal Rule",
|
|
571
572
|
"richTextHorizontalRuleDescription": "Add a horizontal separator",
|
|
573
|
+
"richTextEditor": "Rich text editor",
|
|
572
574
|
"richTextInsertMenuHeading": "Insert element...",
|
|
573
575
|
"richTextItalic": "Italic",
|
|
574
576
|
"richTextLink": "Link",
|
|
@@ -632,6 +634,7 @@
|
|
|
632
634
|
"slugInUse": "Slug already in use",
|
|
633
635
|
"someoneElseTookControl": "{{ who }} took control of this document in another tab or window. A document can only be edited in one place at a time.",
|
|
634
636
|
"splitCell": "Split Cell",
|
|
637
|
+
"status": "Status",
|
|
635
638
|
"style": "Style",
|
|
636
639
|
"styleAlignment": "Alignment",
|
|
637
640
|
"styleBackground": "Background",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
v-if="activeMedia.attachment && activeMedia.attachment._urls"
|
|
15
15
|
class="apos-media-editor__thumb"
|
|
16
16
|
:src="activeMedia.attachment._urls[restoreOnly ? 'one-sixth' : 'one-third']"
|
|
17
|
-
:alt="activeMedia.description"
|
|
17
|
+
:alt="activeMedia.description || ''"
|
|
18
18
|
>
|
|
19
19
|
</div>
|
|
20
20
|
<ul class="apos-media-editor__details">
|
|
@@ -484,7 +484,7 @@ export default {
|
|
|
484
484
|
|
|
485
485
|
& {
|
|
486
486
|
line-height: var(--a-line-tallest);
|
|
487
|
-
color: var(--a-base-
|
|
487
|
+
color: var(--a-base-2);
|
|
488
488
|
}
|
|
489
489
|
}
|
|
490
490
|
|
|
@@ -252,6 +252,9 @@ export default {
|
|
|
252
252
|
@include apos-transition();
|
|
253
253
|
|
|
254
254
|
& {
|
|
255
|
+
// Contain the visually hidden (`.apos-sr-only`, position: absolute)
|
|
256
|
+
// file input so focusing it does not scroll a distant ancestor.
|
|
257
|
+
position: relative;
|
|
255
258
|
display: flex;
|
|
256
259
|
box-sizing: border-box;
|
|
257
260
|
align-items: center;
|