apostrophe 4.30.0 → 4.31.0-alpha.1
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/CHANGELOG.md +67 -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/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/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-page-type/index.js +7 -0
- 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/_theme.scss +1 -0
- package/modules/@apostrophecms/uploadfs/index.js +3 -0
- package/modules/@apostrophecms/util/index.js +3 -3
- package/package.json +14 -10
- 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/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-lib/util.js +50 -14
- package/.claude/settings.local.json +0 -15
- package/lib/mongodb-connect.js +0 -62
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +0 -131
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.31.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 33bb4c0: 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
|
+
- dfd25cf: 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
|
+
- 950927d: 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.
|
|
10
|
+
- cf1a639: 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.
|
|
11
|
+
|
|
12
|
+
Fixed Tab navigation escaping out of modals when the form contained hidden sections or elements that became disabled after editing.
|
|
13
|
+
|
|
14
|
+
- 77a2968: Fix layout widget not regaining full focus on switching back to Edit content mode.
|
|
15
|
+
- f67c272: Fixed adding or removing an area field from a schema breaking existing documents on an external front such as Astro.
|
|
16
|
+
|
|
17
|
+
- `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 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
|
+
|
|
21
|
+
- bc8f7be: 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.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 2e2f3b4: Accessibility: corrected ARIA semantics on the top admin navigation bar.
|
|
26
|
+
- 2e2f3b4: Accessibility: improvements to the document context title (admin bar middle group) and the underlying `AposContextMenu` machinery.
|
|
27
|
+
- 2e2f3b4: Accessibility: improve the locale switcher (`AposLocalePicker`).
|
|
28
|
+
- 2e2f3b4: Accessibility: the Recently Edited Documents tray icon (admin bar) now exposes its action through `aria-label`.
|
|
29
|
+
- 2e2f3b4: Accessibility: fix `.apos-sr-only` so screen-reader-only content is exposed to the accessibility tree.
|
|
30
|
+
- 2e2f3b4: Accessibility: icon-only context-utility buttons in the admin bar tray (e.g. the global settings cog) now expose their action through `aria-label`.
|
|
31
|
+
- 41670a3: Security fix: the password reset request feature will refuse to operate unless the baseUrl option or the APOS_BASE_URL environment variable has been set (note that this is automatic in multisite projects). This fix is necessary to prevent a vulnerability that can be used to convince ApostropheCMS to send emails containing links to other sites. It is only a vulnerability if you have enabled the passwordReset: true option for the login module. Thanks to [SPIDY](https://github.com/Mujahidkhan525) for reporting the issue.
|
|
32
|
+
- 9f458b5: Fix illegal HTML id attribute values generated by the admin UI
|
|
33
|
+
- 08845c5: - Removed duplicate <meta charset> tag from `outerLayoutBase.html`
|
|
34
|
+
- Standardized charset to utf-8 (the legacy configuration option is now ignored). Per the spec this is the only legal setting, so we classify this as a bug fix
|
|
35
|
+
- Altered unused/legacy i18n template helper to return `utf-8`, ensuring backwards compatibility
|
|
36
|
+
- b9b32bd: 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.
|
|
37
|
+
- 2191c8a: Fix more admin UI a11y issues
|
|
38
|
+
- 08dfcac: Security: a malicious full name containing HTML was executed as HTML in the tooltip displayed with an "i" icon next to the title of the current page, creating an
|
|
39
|
+
XSS attack risk versus other users. Since most projects permit users to change their full name (the "title" property), All projects with multiple users should be
|
|
40
|
+
updated promptly to close this vulnerability. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting the issue.
|
|
41
|
+
- 5d1a028: Security: launder now uses and exports the best available naughtyHref function for detecting malicious URLs. sanitize-html now depends on it, and apostrophe now uses type: 'url' for the link URL field of image widgets, which leverages it. Prior to this fix, it was possible for any user with editing privileges, including a contributor, to trigger arbitrary JavaScript via a javascript: URL in the link URL field of an image widget. A migration has been included to strip any such malicious URLs already present in the database. All users of apostrophe are encouraged to upgrade to get this security fix. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting the issue.
|
|
42
|
+
- 8098017: Security: the HTML import feature of the rich text widget no longer permits images to be fetched
|
|
43
|
+
from arbitrary hosts. This could be used to probe internal networks, and to exfiltrate images from
|
|
44
|
+
internal hosts if their URLs were known. Instead, the `imageImportAllowedHostnames` option of the
|
|
45
|
+
`@apostrophecms/rich-text-widget` must be configured to opt into that feature.
|
|
46
|
+
|
|
47
|
+
Thanks to [Yiğit Şengezer](https://github.com/yigitsengezer) and [Sainithin0309](https://github.com/Sainithin0309) for reporting this issue.
|
|
48
|
+
|
|
49
|
+
- b7f9ad5: 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.
|
|
50
|
+
- b360b05: fixes issue where orderable table array items drag the entire floating window
|
|
51
|
+
- e9b3bac: apostrophe and oembetter have been updated to eliminate a number of services that formerly supported
|
|
52
|
+
oembed for the general public, but no longer do so. While there is no security risk today, removing
|
|
53
|
+
these ensures that if these domains are ever allowed to lapse, they do not become an XSS
|
|
54
|
+
attack vector in the future.
|
|
55
|
+
|
|
56
|
+
Because oembed responses are not always iframes, it is important that this list be maintained
|
|
57
|
+
over time. In addition, developers always have the option to prune it on their own by setting
|
|
58
|
+
the new minimumAllowlist and minimumEndpoints options of the @apostrophecms/oembed module.
|
|
59
|
+
|
|
60
|
+
Thanks to [Sainithin0309](https://github.com/Sainithin0309) for pointing out the potential
|
|
61
|
+
long-term security concern.
|
|
62
|
+
|
|
63
|
+
- Updated dependencies [862d760]
|
|
64
|
+
- Updated dependencies [5d1a028]
|
|
65
|
+
- Updated dependencies [8d4c882]
|
|
66
|
+
- sanitize-html@2.17.5
|
|
67
|
+
- launder@1.7.2
|
|
68
|
+
|
|
3
69
|
## 4.30.0
|
|
4
70
|
|
|
5
71
|
### Adds
|
|
@@ -21,7 +87,7 @@
|
|
|
21
87
|
- **XSS via full name field:** A malicious full name containing HTML was executed in the page title tooltip in the admin bar, posing an XSS risk to other users. All multi-user projects should update promptly. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting.
|
|
22
88
|
- **XSS via image widget link URL:** Users with editing privileges could trigger arbitrary JavaScript via a `javascript:` URL in the image widget's link URL field. A migration is included to strip any such URLs already in the database. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting.
|
|
23
89
|
- **SSRF via rich text HTML import:** The rich text widget's HTML import feature no longer fetches images from arbitrary hosts, which could be used to probe internal networks or exfiltrate internal images. Configure `imageImportAllowedHostnames` on `@apostrophecms/rich-text-widget` to opt in. Thanks to [Yiğit Şengezer](https://github.com/yigitsengezer) and [Sainithin0309](https://github.com/Sainithin0309) for reporting.
|
|
24
|
-
-
|
|
90
|
+
- **the xmp tag could be used to pass forbidden markup through sanitize-html**, even when xmp itself. This was fixed in `sanitize-html` and the dependency was bumped. Thanks to [Vincenzo Turturro](https://github.com/sushi-gif) for reporting the vulnerability.
|
|
25
91
|
- **the `linkHref` field of image widgets was an XSS vulnerability** because it did not use the `url` field type. This means that a user with editing privileges could potentially carry out XSS. In addition, we have updated the `launder` module to sanitize URLs more robustly for the `url` field type, and bumped that dependency. Also, a database migration is included to clean any XSS attacks that could be present in existing links. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting the issue.
|
|
26
92
|
|
|
27
93
|
### Accessibility
|
|
@@ -33,7 +99,6 @@
|
|
|
33
99
|
- Fixed `.apos-sr-only` so screen-reader-only content is correctly exposed to the accessibility tree.
|
|
34
100
|
- Icon-only context-utility buttons in the admin bar tray (e.g. the global settings cog) now expose their action via `aria-label`.
|
|
35
101
|
|
|
36
|
-
|
|
37
102
|
## 4.29.0 (2026-04-15)
|
|
38
103
|
|
|
39
104
|
### 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,
|
|
@@ -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
|
|