apostrophe 4.28.1 → 4.30.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 +35 -4
- package/README.md +2 -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/defaults.js +1 -0
- package/eslint.config.js +2 -1
- package/lib/safe-json-script.js +27 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +1 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +1 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +3 -5
- package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +13 -1
- package/modules/@apostrophecms/asset/lib/globalIcons.js +3 -0
- package/modules/@apostrophecms/attachment/index.js +43 -1
- package/modules/@apostrophecms/color-field/index.js +7 -1
- package/modules/@apostrophecms/db/index.js +68 -27
- package/modules/@apostrophecms/doc/index.js +11 -1
- package/modules/@apostrophecms/doc-type/index.js +165 -32
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +1 -1
- package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +104 -59
- package/modules/@apostrophecms/file/index.js +109 -8
- package/modules/@apostrophecms/http/index.js +1 -1
- package/modules/@apostrophecms/i18n/i18n/de.json +0 -2
- package/modules/@apostrophecms/i18n/i18n/en.json +40 -1
- package/modules/@apostrophecms/i18n/i18n/es.json +0 -1
- package/modules/@apostrophecms/i18n/i18n/fr.json +0 -1
- package/modules/@apostrophecms/i18n/i18n/it.json +0 -1
- package/modules/@apostrophecms/i18n/i18n/pt-BR.json +0 -1
- package/modules/@apostrophecms/i18n/i18n/sk.json +0 -1
- package/modules/@apostrophecms/i18n/ui/apos/apps/AposI18nBatchReporting.js +18 -1
- package/modules/@apostrophecms/i18n/ui/apos/apps/AposI18nLocalizeActions.js +50 -0
- package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +56 -13
- package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +8 -2
- package/modules/@apostrophecms/job/index.js +9 -7
- package/modules/@apostrophecms/layout-column-widget/index.js +156 -163
- package/modules/@apostrophecms/layout-widget/index.js +7 -2
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +6 -11
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridColumn.vue +3 -5
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +4 -4
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +0 -16
- package/modules/@apostrophecms/layout-widget/ui/apos/lib/grid-state.mjs +7 -27
- package/modules/@apostrophecms/layout-widget/views/column.html +7 -9
- package/modules/@apostrophecms/login/index.js +39 -40
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +17 -2
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +3 -2
- package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +1 -0
- package/modules/@apostrophecms/page/index.js +2 -0
- package/modules/@apostrophecms/piece-type/index.js +3 -1
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +1 -0
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +5 -0
- package/modules/@apostrophecms/recently-edited/index.js +831 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposCellTitle.vue +54 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedCombo.vue +454 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedFilterTag.vue +75 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedFilters.vue +287 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedIcon.vue +16 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedManager.vue +346 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedBatch.js +193 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedData.js +276 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedFetch.js +199 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedFilters.js +100 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +8 -4
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +1 -1
- package/modules/@apostrophecms/styles/index.js +10 -0
- package/modules/@apostrophecms/styles/lib/apiRoutes.js +6 -0
- package/modules/@apostrophecms/styles/lib/handlers.js +5 -0
- package/modules/@apostrophecms/styles/lib/methods.js +9 -3
- package/modules/@apostrophecms/styles/lib/presets.js +119 -0
- package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +3 -8
- package/modules/@apostrophecms/styles/ui/apos/composables/AposStyles.js +1 -3
- package/modules/@apostrophecms/styles/ui/apos/render-factory.js +29 -0
- package/modules/@apostrophecms/styles/ui/apos/universal/backgroundHelpers.mjs +140 -0
- package/modules/@apostrophecms/styles/ui/apos/universal/customRules.mjs +105 -0
- package/modules/@apostrophecms/styles/ui/apos/universal/render.mjs +195 -15
- package/modules/@apostrophecms/template/index.js +22 -6
- package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +2 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +18 -4
- package/modules/@apostrophecms/ui/ui/apos/composables/useInfiniteScroll.js +91 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +1 -0
- package/modules/@apostrophecms/ui/ui/apos/stores/modal.js +5 -2
- package/modules/@apostrophecms/ui/ui/apos/utils/index.js +9 -0
- package/modules/@apostrophecms/url/index.js +38 -4
- package/modules/@apostrophecms/widget-type/index.js +22 -6
- package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +8 -4
- package/package.json +19 -19
- package/test/add-missing-schema-fields-project/test.js +11 -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/job.js +1 -1
- package/test/layout-widget-migration.js +719 -0
- package/test/login-requirements.js +1 -1
- package/test/pieces-public-api.js +80 -0
- package/test/pieces.js +25 -0
- package/test/recently-edited.js +2311 -0
- package/test/schemas.js +39 -3
- package/test/static-build.js +642 -0
- package/test/styles.js +2569 -0
- package/test-lib/util.js +50 -14
- package/.claude/settings.local.json +0 -15
- package/lib/mongodb-connect.js +0 -62
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposLayoutColControlDialog.vue +0 -171
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +0 -131
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 4.
|
|
3
|
+
## 4.30.0-alpha.1
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### Adds
|
|
6
|
+
|
|
7
|
+
- Postgres and SQLite alpha release
|
|
8
|
+
|
|
9
|
+
## 4.29.0 (2026-04-15)
|
|
10
|
+
|
|
11
|
+
### Adds
|
|
12
|
+
|
|
13
|
+
- Added support for pretty URL file attachments in the static build metadata pipeline. When `@apostrophecms/file` has `options.prettyUrls` enabled, the `getAllUrlMetadata` API now annotates affected attachments properly. The backend streaming proxy route was also fixed to correctly resolve relative uploadfs URLs during static builds.
|
|
14
|
+
- Introduced Recently Edited manager as Admin Bar action, next to the existing Submitted Drafts. Allows modules to contribute filter choices.
|
|
15
|
+
- Fix batch operations executed in a modal in a different locale causing wrong browser URL rewrite
|
|
16
|
+
- Add background preset to the Styles Editor, supporting image, color, and gradient background CSS generation.
|
|
17
|
+
|
|
18
|
+
### Fixes
|
|
19
|
+
|
|
20
|
+
- Fix a focus trap bug where in the context menu focus would jump back to the first element when reaching the last one.
|
|
21
|
+
- Bug fix: the "pretty URLs" feature of `@apostrophecms/file` is now compatible with locale prefixes.
|
|
22
|
+
- Removed misleading return from `pruneDataForExternalFront`, a method intended to be overridden to modify data "in place" before it is sent to Astro or a similar frontend.
|
|
23
|
+
- Fix layout column breadcrumb operations leaking in layout edit mode.
|
|
24
|
+
- Fix edge case where widgets having styles and fields at the same time would show "Ungrouped" tab. Add `hideSingleTab` option that can be enabled in any widget to hide tabs from the widget editor when there is only one tab containing fields. This option can also be enabled globally in `@apostrophecms/widget-type` options.
|
|
25
|
+
- Add background preset, supporting image, color and gradient background CSS generation.
|
|
26
|
+
|
|
27
|
+
### Changes
|
|
28
|
+
|
|
29
|
+
- Combine Styles and Column configuration in a single Styles Editor experience.
|
|
30
|
+
- Use shorter placeholder text for relationship inputs in small/micro contexts.
|
|
31
|
+
|
|
32
|
+
### Security
|
|
6
33
|
|
|
7
|
-
-
|
|
34
|
+
- Fix an XSS vulnerability allowing arbitrary markup to be inserted via the "SEO Title" or "Meta Description" fields provided by the `@apostrophecms/seo` module. The fix requires upgrading BOTH `apostrophe` and `@apostrophecms/seo`. A new mechanism for safely emitting JSON nodes has been introduced to make this type of vulnerability unlikely in the future. Thanks to [K Shanmukha Srinivasulu Royal](https://github.com/Chittu13) for reporting the vulnerability.
|
|
35
|
+
- Fixed a security hole in the `.choices()` and `.counts()` query builders: formerly, these query builders could be used by the public to exfiltrate schema fields not included in the `publicApiProjection`, or fields locked down with a `viewPermission` property. Thanks to [offset](https://github.com/offset) for reporting this issue, which was not made public prior to the release of the fix.
|
|
36
|
+
- Fixed an XSS vulnerability in color fields, which formerly accepted `-` followed by anything, including `</style>`, which could be used to inject other markup. Thanks to [restriction](https://github.com/restriction) for reporting the issue and proposing the fix.
|
|
37
|
+
- Resolved a `publicApiProjection` bypass vulnerability for piece types. Thanks to [restriction](https://github.com/restriction) for reporting the issue and proposing the fix.
|
|
38
|
+
- Ensured a minimum 2-second delay in the password reset flow to avoid disclosing whether the email or username was valid or not. Thanks to [restriction](https://github.com/restriction) for reporting the issue and proposing the fix.
|
|
8
39
|
|
|
9
|
-
## 4.28.0
|
|
40
|
+
## 4.28.0 (2026-03-19)
|
|
10
41
|
|
|
11
42
|
### Adds
|
|
12
43
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<a href="https://github.com/apostrophecms/apostrophe">
|
|
3
|
-
<img src="logo.svg" alt="ApostropheCMS logo" width="80" height="80">
|
|
3
|
+
<img src="https://static.apostrophecms.com/apostrophecms/apostrophe/logo.svg" alt="ApostropheCMS logo" width="80" height="80">
|
|
4
4
|
</a>
|
|
5
5
|
|
|
6
6
|
<h1>ApostropheCMS</h1>
|
|
@@ -139,4 +139,4 @@ ApostropheCMS is open source software licensed under the [MIT License](https://g
|
|
|
139
139
|
<p>
|
|
140
140
|
<em>Built with ❤️ by the <a href="https://apostrophecms.com">ApostropheCMS team</a></em>
|
|
141
141
|
</p>
|
|
142
|
-
</div>
|
|
142
|
+
</div>
|
|
@@ -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/defaults.js
CHANGED
package/eslint.config.js
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Serialize `data` to a JSON string that is safe to embed inside an HTML
|
|
2
|
+
// `<script>` element. `JSON.stringify` on its own does NOT escape the
|
|
3
|
+
// sequences `</script>`, `<!--` or `<![CDATA[`, so untrusted data (e.g.
|
|
4
|
+
// editor-provided SEO fields) in a JSON body could otherwise break out of
|
|
5
|
+
// the surrounding script tag and inject arbitrary HTML/JS (stored XSS).
|
|
6
|
+
// Escaping `<` as its `\u003c` form keeps the JSON valid while neutralizing
|
|
7
|
+
// all of those sequences. Line and paragraph separators are also escaped
|
|
8
|
+
// since they are valid in JSON but illegal in some JavaScript parsers.
|
|
9
|
+
//
|
|
10
|
+
// This is the single source of truth for that escaping. The template
|
|
11
|
+
// `renderNodes` helper uses it to render `{ json: ... }` node bodies, so in
|
|
12
|
+
// most cases you should just build a node like:
|
|
13
|
+
//
|
|
14
|
+
// {
|
|
15
|
+
// name: 'script',
|
|
16
|
+
// attrs: { type: 'application/ld+json' },
|
|
17
|
+
// body: [ { json: data } ]
|
|
18
|
+
// }
|
|
19
|
+
//
|
|
20
|
+
// and let `renderNodes` do the right thing.
|
|
21
|
+
|
|
22
|
+
module.exports = function safeJsonForScript(data) {
|
|
23
|
+
return JSON.stringify(data, null, 2)
|
|
24
|
+
.replace(/</g, '\\u003c')
|
|
25
|
+
.replace(/\u2028/g, '\\u2028')
|
|
26
|
+
.replace(/\u2029/g, '\\u2029');
|
|
27
|
+
};
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
:tiny-screen="tinyScreen"
|
|
84
84
|
:widget="widget"
|
|
85
85
|
:options="options"
|
|
86
|
+
:breadcrumb-operations="widgetBreadcrumbOperations"
|
|
86
87
|
:disabled="disabled"
|
|
87
88
|
:is-focused="isFocused"
|
|
88
89
|
@widget-focus="getFocus"
|
|
@@ -304,10 +305,6 @@ export default {
|
|
|
304
305
|
type: Boolean,
|
|
305
306
|
default: false
|
|
306
307
|
},
|
|
307
|
-
breadcrumbDisabled: {
|
|
308
|
-
type: Boolean,
|
|
309
|
-
default: false
|
|
310
|
-
},
|
|
311
308
|
generation: {
|
|
312
309
|
type: Number,
|
|
313
310
|
required: false,
|
|
@@ -426,7 +423,8 @@ export default {
|
|
|
426
423
|
return apos.modules[this.moduleOptions?.widgetManagers[this.widget?.type]] ?? {};
|
|
427
424
|
},
|
|
428
425
|
widgetBreadcrumbOperations() {
|
|
429
|
-
return (this.widgetModuleOptions.widgetBreadcrumbOperations || [])
|
|
426
|
+
return (this.widgetModuleOptions.widgetBreadcrumbOperations || [])
|
|
427
|
+
.filter(op => op.hidden !== true);
|
|
430
428
|
},
|
|
431
429
|
shouldSkipEdit() {
|
|
432
430
|
return !this.widgetModuleOptions.widgetOperations
|
|
@@ -75,6 +75,15 @@ export default {
|
|
|
75
75
|
return {};
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
|
+
// Override module breadcrumb operations.
|
|
79
|
+
// If not provided (undefined or null), operations will be pulled from the
|
|
80
|
+
// widget's module options.
|
|
81
|
+
breadcrumbOperations: {
|
|
82
|
+
type: Array,
|
|
83
|
+
default() {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
78
87
|
isFocused: {
|
|
79
88
|
type: Boolean,
|
|
80
89
|
default: false
|
|
@@ -130,7 +139,10 @@ export default {
|
|
|
130
139
|
return apos.modules[this.moduleOptions?.widgetManagers[this.widget?.type]] ?? {};
|
|
131
140
|
},
|
|
132
141
|
widgetBreadcrumbOperations() {
|
|
133
|
-
return (
|
|
142
|
+
return (
|
|
143
|
+
this.breadcrumbOperations ||
|
|
144
|
+
this.widgetModuleOptions.widgetBreadcrumbOperations || []
|
|
145
|
+
)
|
|
134
146
|
.map((operation) => ({
|
|
135
147
|
component: this.getOperationComponent(operation),
|
|
136
148
|
props: this.getOperationProps(operation),
|
|
@@ -23,6 +23,7 @@ module.exports = {
|
|
|
23
23
|
'arrow-expand-vertical-icon': 'ArrowExpandVertical',
|
|
24
24
|
'arrow-left-icon': 'ArrowLeft',
|
|
25
25
|
'arrow-right-icon': 'ArrowRight',
|
|
26
|
+
'arrow-top-right-icon': 'ArrowTopRight',
|
|
26
27
|
'arrow-up-icon': 'ArrowUp',
|
|
27
28
|
'binoculars-icon': 'Binoculars',
|
|
28
29
|
'calendar-icon': 'Calendar',
|
|
@@ -109,6 +110,7 @@ module.exports = {
|
|
|
109
110
|
'keyboard-tab': 'KeyboardTab',
|
|
110
111
|
'label-icon': 'Label',
|
|
111
112
|
'lightbulb-on-icon': 'LightbulbOn',
|
|
113
|
+
'link-external-icon': 'OpenInNew',
|
|
112
114
|
'link-icon': 'Link',
|
|
113
115
|
'list-status-icon': 'ListStatus',
|
|
114
116
|
'lock-icon': 'Lock',
|
|
@@ -124,6 +126,7 @@ module.exports = {
|
|
|
124
126
|
'play-box-icon': 'PlayBox',
|
|
125
127
|
'playlist-edit-icon': 'PlaylistEdit',
|
|
126
128
|
'plus-icon': 'Plus',
|
|
129
|
+
'recently-edited-icon': 'ClockOutline',
|
|
127
130
|
'redo-icon': 'RedoVariant',
|
|
128
131
|
'refresh-icon': 'Refresh',
|
|
129
132
|
'resize-bottom-right-icon': 'ResizeBottomRight',
|
|
@@ -217,6 +217,11 @@ module.exports = {
|
|
|
217
217
|
|
|
218
218
|
await self.crop(req, _id, sanitizedCrop);
|
|
219
219
|
|
|
220
|
+
if (req.body.annotate) {
|
|
221
|
+
const attachment = await self.db.findOne({ _id });
|
|
222
|
+
return self.annotateAttachment(attachment, sanitizedCrop);
|
|
223
|
+
}
|
|
224
|
+
|
|
220
225
|
return true;
|
|
221
226
|
}
|
|
222
227
|
]
|
|
@@ -611,6 +616,42 @@ module.exports = {
|
|
|
611
616
|
height: sanitizeInteger(crop.height, 0, 0, 10000)
|
|
612
617
|
};
|
|
613
618
|
},
|
|
619
|
+
// Given an attachment object and an optional crop,
|
|
620
|
+
// return a clone with `_urls` fully populated for all
|
|
621
|
+
// configured image sizes. For non-image attachments
|
|
622
|
+
// a single `_url` is set instead.
|
|
623
|
+
annotateAttachment(attachment, crop) {
|
|
624
|
+
const result = { ...attachment };
|
|
625
|
+
result._isCroppable = self.isCroppable(result);
|
|
626
|
+
if (crop && crop.width) {
|
|
627
|
+
result._crop = _.pick(crop, 'width', 'height', 'top', 'left');
|
|
628
|
+
}
|
|
629
|
+
if (result.group === 'images') {
|
|
630
|
+
result._urls = {};
|
|
631
|
+
if (result._crop) {
|
|
632
|
+
result._urls.uncropped = {};
|
|
633
|
+
}
|
|
634
|
+
for (const size of self.imageSizes) {
|
|
635
|
+
result._urls[size.name] = self.url(result, { size: size.name });
|
|
636
|
+
if (result._crop) {
|
|
637
|
+
result._urls.uncropped[size.name] = self.url(result, {
|
|
638
|
+
size: size.name,
|
|
639
|
+
crop: false
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
result._urls.original = self.url(result, { size: 'original' });
|
|
644
|
+
if (result._crop) {
|
|
645
|
+
result._urls.uncropped.original = self.url(result, {
|
|
646
|
+
size: 'original',
|
|
647
|
+
crop: false
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
} else {
|
|
651
|
+
result._url = self.url(result);
|
|
652
|
+
}
|
|
653
|
+
return result;
|
|
654
|
+
},
|
|
614
655
|
// This method return a default icon url if an attachment is missing
|
|
615
656
|
// to avoid template errors
|
|
616
657
|
getMissingAttachmentUrl() {
|
|
@@ -1459,7 +1500,8 @@ module.exports = {
|
|
|
1459
1500
|
uploadsUrl: self.uploadfs.getUrl(),
|
|
1460
1501
|
croppable: self.croppable,
|
|
1461
1502
|
sized: self.sized,
|
|
1462
|
-
maxSize: self.options.maxSize
|
|
1503
|
+
maxSize: self.options.maxSize,
|
|
1504
|
+
imageSizes: self.imageSizes
|
|
1463
1505
|
};
|
|
1464
1506
|
},
|
|
1465
1507
|
// Middleware method used when only those with attachment privileges
|
|
@@ -32,9 +32,15 @@ module.exports = {
|
|
|
32
32
|
throw self.apos.error('required');
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const isVariable = destination[field.name].startsWith('--');
|
|
35
36
|
const test = new TinyColor(destination[field.name]);
|
|
36
|
-
if (!test.isValid && !
|
|
37
|
+
if (!test.isValid && !isVariable) {
|
|
37
38
|
destination[field.name] = null;
|
|
39
|
+
} else if (isVariable) {
|
|
40
|
+
// CSS custom property names: only allow alphanumeric, hyphens, underscores
|
|
41
|
+
if (!/^--[a-zA-Z0-9_-]+$/.test(destination[field.name])) {
|
|
42
|
+
destination[field.name] = null;
|
|
43
|
+
}
|
|
38
44
|
}
|
|
39
45
|
},
|
|
40
46
|
isEmpty: function (field, value) {
|
|
@@ -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;
|
|
@@ -1553,7 +1553,8 @@ module.exports = {
|
|
|
1553
1553
|
];
|
|
1554
1554
|
|
|
1555
1555
|
function validate ({
|
|
1556
|
-
action, context, type = 'modal', label, modal, conditions, if: ifProps
|
|
1556
|
+
action, context, type = 'modal', label, modal, conditions, if: ifProps,
|
|
1557
|
+
crossLocale
|
|
1557
1558
|
}) {
|
|
1558
1559
|
const allowedConditions = [
|
|
1559
1560
|
'canPublish',
|
|
@@ -1596,6 +1597,15 @@ module.exports = {
|
|
|
1596
1597
|
'invalid', 'The if property in addContextOperation must be an object containing properties and values that will be checked against the current document in order to show or not the context operation.'
|
|
1597
1598
|
);
|
|
1598
1599
|
}
|
|
1600
|
+
|
|
1601
|
+
if (
|
|
1602
|
+
crossLocale !== undefined &&
|
|
1603
|
+
typeof crossLocale !== 'boolean'
|
|
1604
|
+
) {
|
|
1605
|
+
throw self.apos.error(
|
|
1606
|
+
'invalid', 'The crossLocale property in addContextOperation must be a boolean.'
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1599
1609
|
}
|
|
1600
1610
|
},
|
|
1601
1611
|
getBrowserData(req) {
|