apostrophe 3.40.1 → 3.40.2-alpha
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 +13 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +37 -33
- package/modules/@apostrophecms/area/index.js +7 -8
- package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +11 -10
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +13 -6
- package/modules/@apostrophecms/soft-redirect/index.js +5 -3
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## UNRELEASED
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
* Replace `deep-get-set` dependency with `lodash`'s `get` and `set` functions to fix the [Prototype Pollution in deep-get-set](https://github.com/advisories/GHSA-mjjj-6p43-vhhv) vulnerability. There was no actual vulnerability in Apostrophe due to the way the module was actually used, and this was done to address vulnerability scan reports.
|
|
8
|
+
* The "soft redirects" for former URLs of documents now work better with localization. Thanks to [Waldemar Pankratz](https://github.com/waldemar-p).
|
|
9
|
+
* Destroy `AreaEditor` Vue apps when the page content is refreshed in edit mode. This avoids a leak of Vue apps components being recreated while instances of old ones are still alive.
|
|
10
|
+
|
|
11
|
+
### Security
|
|
12
|
+
|
|
13
|
+
* Upgrades passport to the latest version in order to ensure session regeneration when logging in or out. This adds additional security to logins by mitigating any risks due to XSS attacks. Apostrophe is already robust against XSS attacks. For passport methods that are internally used by Apostrophe everything is still working. For projects that are accessing the passport instance directly through `self.apos.login.passport`, some verifications may be necessary to avoid any compatibility issue. The internally used methods are `authenticate`, `use`, `serializeUser`, `deserializeUser`, `initialize`, `session`.
|
|
14
|
+
|
|
3
15
|
## 3.40.1 (2023-02-18)
|
|
4
16
|
|
|
5
17
|
* No code change. Patch level bump for package update.
|
|
18
|
+
|
|
6
19
|
## 3.40.0 (2023-02-17)
|
|
7
20
|
|
|
8
21
|
### Adds
|
|
@@ -291,7 +291,7 @@ export default {
|
|
|
291
291
|
}, 1100);
|
|
292
292
|
}
|
|
293
293
|
},
|
|
294
|
-
async onPublish(
|
|
294
|
+
async onPublish() {
|
|
295
295
|
if (!this.canPublish) {
|
|
296
296
|
const submitted = await this.submitDraft(this.context);
|
|
297
297
|
if (submitted) {
|
|
@@ -505,7 +505,20 @@ export default {
|
|
|
505
505
|
}
|
|
506
506
|
},
|
|
507
507
|
async refresh(options = {}) {
|
|
508
|
-
|
|
508
|
+
const refreshable = document.querySelector('[data-apos-refreshable]');
|
|
509
|
+
if (options.scrollcheck) {
|
|
510
|
+
window.apos.adminBar.scrollPosition = {
|
|
511
|
+
x: window.scrollX,
|
|
512
|
+
y: window.scrollY
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (!refreshable) {
|
|
517
|
+
apos.bus.$emit('refreshed');
|
|
518
|
+
this.rememberLastBaseContext();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
509
522
|
const qs = {
|
|
510
523
|
...apos.http.parseQuery(window.location.search),
|
|
511
524
|
aposRefresh: '1',
|
|
@@ -514,7 +527,7 @@ export default {
|
|
|
514
527
|
aposEdit: '1'
|
|
515
528
|
} : {})
|
|
516
529
|
};
|
|
517
|
-
url = apos.http.addQueryToUrl(
|
|
530
|
+
const url = apos.http.addQueryToUrl(window.location.href, qs);
|
|
518
531
|
const content = await apos.http.get(url, {
|
|
519
532
|
qs,
|
|
520
533
|
headers: {
|
|
@@ -523,39 +536,30 @@ export default {
|
|
|
523
536
|
draft: true,
|
|
524
537
|
busy: true
|
|
525
538
|
});
|
|
526
|
-
const refreshable = document.querySelector('[data-apos-refreshable]');
|
|
527
539
|
|
|
528
|
-
|
|
529
|
-
window.apos.adminBar.scrollPosition = {
|
|
530
|
-
x: window.scrollX,
|
|
531
|
-
y: window.scrollY
|
|
532
|
-
};
|
|
533
|
-
}
|
|
540
|
+
refreshable.innerHTML = content;
|
|
534
541
|
|
|
535
|
-
if (
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
} else {
|
|
552
|
-
return 0;
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
for (const el of els) {
|
|
556
|
-
const data = JSON.parse(el.getAttribute('data'));
|
|
557
|
-
this.original[`@${data._id}`] = data;
|
|
542
|
+
if (this.editMode && (!this.original)) {
|
|
543
|
+
// the first time we enter edit mode on the page, we need to
|
|
544
|
+
// establish a baseline for undo/redo. Use our
|
|
545
|
+
// "@ notation" PATCH feature. Sort the areas by DOM depth
|
|
546
|
+
// to ensure parents patch before children
|
|
547
|
+
this.original = {};
|
|
548
|
+
const els = Array.from(document.querySelectorAll('[data-apos-area-newly-editable]')).filter(el => el.getAttribute('data-doc-id') === this.context._id);
|
|
549
|
+
els.sort((a, b) => {
|
|
550
|
+
const da = depth(a);
|
|
551
|
+
const db = depth(b);
|
|
552
|
+
if (da < db) {
|
|
553
|
+
return -1;
|
|
554
|
+
} else if (db > da) {
|
|
555
|
+
return 1;
|
|
556
|
+
} else {
|
|
557
|
+
return 0;
|
|
558
558
|
}
|
|
559
|
+
});
|
|
560
|
+
for (const el of els) {
|
|
561
|
+
const data = JSON.parse(el.getAttribute('data'));
|
|
562
|
+
this.original[`@${data._id}`] = data;
|
|
559
563
|
}
|
|
560
564
|
}
|
|
561
565
|
apos.bus.$emit('refreshed');
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
|
-
const deep = require('deep-get-set');
|
|
3
2
|
const { stripIndent } = require('common-tags');
|
|
4
3
|
|
|
5
4
|
// An area is a series of zero or more widgets, in which users can add
|
|
@@ -243,15 +242,15 @@ module.exports = {
|
|
|
243
242
|
|
|
244
243
|
const areaRendered = await self.apos.area.renderArea(req, preppedArea, context);
|
|
245
244
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
_.set(context, [ path, '_rendered' ], areaRendered);
|
|
246
|
+
_.set(context, [ path, '_fieldId' ], undefined);
|
|
247
|
+
_.set(context, [ path, 'items' ], undefined);
|
|
249
248
|
}
|
|
250
249
|
|
|
251
250
|
function findParent(doc, dotPath) {
|
|
252
251
|
const pathSplit = dotPath.split('.');
|
|
253
252
|
const parentDotPath = pathSplit.slice(0, pathSplit.length - 1).join('.');
|
|
254
|
-
return
|
|
253
|
+
return _.get(doc, parentDotPath, doc);
|
|
255
254
|
}
|
|
256
255
|
},
|
|
257
256
|
// Sanitize an input array of items intended to become
|
|
@@ -365,12 +364,12 @@ module.exports = {
|
|
|
365
364
|
// always okay - unless it already exists
|
|
366
365
|
// and is not an area.
|
|
367
366
|
if (components.length > 1) {
|
|
368
|
-
const existing =
|
|
367
|
+
const existing = _.get(doc, dotPath);
|
|
369
368
|
if (existing && existing.metaType !== 'area') {
|
|
370
369
|
throw self.apos.error('forbidden');
|
|
371
370
|
}
|
|
372
371
|
}
|
|
373
|
-
const existingArea =
|
|
372
|
+
const existingArea = _.get(doc, dotPath);
|
|
374
373
|
const existingItems = existingArea && (existingArea.items || []);
|
|
375
374
|
if (_.isEqual(self.apos.util.clonePermanent(items), self.apos.util.clonePermanent(existingItems))) {
|
|
376
375
|
// No real change — don't waste a version and clutter the database.
|
|
@@ -378,7 +377,7 @@ module.exports = {
|
|
|
378
377
|
// nothing has changed. -Tom
|
|
379
378
|
return;
|
|
380
379
|
}
|
|
381
|
-
|
|
380
|
+
_.set(doc, dotPath, {
|
|
382
381
|
metaType: 'area',
|
|
383
382
|
items: items
|
|
384
383
|
});
|
|
@@ -93,11 +93,12 @@ export default function() {
|
|
|
93
93
|
const component = window.apos.area.components.editor;
|
|
94
94
|
|
|
95
95
|
if (apos.area.activeEditor && (apos.area.activeEditor.id === data._id)) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
// Editing a piece causes a refresh of the main content area,
|
|
97
|
+
// but this may contain the area we originally intended to add
|
|
98
|
+
// a widget to when we created a piece for that purpose. Preserve
|
|
99
|
+
// the editing experience by restoring that widget's editor to the DOM
|
|
100
|
+
// rather than creating a new one.
|
|
101
|
+
|
|
101
102
|
el.parentNode.replaceChild(apos.area.activeEditor.$el, el);
|
|
102
103
|
} else {
|
|
103
104
|
return new Vue({
|
|
@@ -131,7 +132,7 @@ export default function() {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
function createWidgetClipboardApp() {
|
|
134
|
-
|
|
135
|
+
// Headless app to provide simple reactivity for the clipboard state
|
|
135
136
|
apos.area.widgetClipboard = new Vue({
|
|
136
137
|
el: null,
|
|
137
138
|
data: () => {
|
|
@@ -149,12 +150,12 @@ export default function() {
|
|
|
149
150
|
localStorage.setItem('aposWidgetClipboard', JSON.stringify(this.widgetClipboard));
|
|
150
151
|
},
|
|
151
152
|
get() {
|
|
152
|
-
|
|
153
|
+
// If we don't clone, the second paste will be a duplicate key error
|
|
153
154
|
return klona(this.widgetClipboard);
|
|
154
155
|
},
|
|
155
156
|
onStorage() {
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
// When local storage changes, dump the list to
|
|
158
|
+
// the console.
|
|
158
159
|
const contents = window.localStorage.getItem('aposWidgetClipboard');
|
|
159
160
|
if (contents) {
|
|
160
161
|
this.widgetClipboard = JSON.parse(contents);
|
|
@@ -164,4 +165,4 @@ export default function() {
|
|
|
164
165
|
});
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
}
|
|
168
|
+
}
|
|
@@ -210,22 +210,24 @@ export default {
|
|
|
210
210
|
}
|
|
211
211
|
},
|
|
212
212
|
mounted() {
|
|
213
|
-
apos.bus.$on('area-updated', this.areaUpdatedHandler);
|
|
214
|
-
apos.bus.$on('widget-hover', this.updateWidgetHovered);
|
|
215
|
-
apos.bus.$on('widget-focus', this.updateWidgetFocused);
|
|
216
213
|
this.bindEventListeners();
|
|
217
214
|
},
|
|
218
215
|
beforeDestroy() {
|
|
219
|
-
apos.bus.$off('area-updated', this.areaUpdatedHandler);
|
|
220
|
-
apos.bus.$off('widget-hover', this.updateWidgetHovered);
|
|
221
|
-
apos.bus.$off('widget-focus', this.updateWidgetFocused);
|
|
222
216
|
this.unbindEventListeners();
|
|
223
217
|
},
|
|
224
218
|
methods: {
|
|
225
219
|
bindEventListeners() {
|
|
220
|
+
apos.bus.$on('area-updated', this.areaUpdatedHandler);
|
|
221
|
+
apos.bus.$on('widget-hover', this.updateWidgetHovered);
|
|
222
|
+
apos.bus.$on('widget-focus', this.updateWidgetFocused);
|
|
223
|
+
apos.bus.$on('refreshed', this.destroyParentComponent);
|
|
226
224
|
window.addEventListener('keydown', this.focusParentEvent);
|
|
227
225
|
},
|
|
228
226
|
unbindEventListeners() {
|
|
227
|
+
apos.bus.$off('area-updated', this.areaUpdatedHandler);
|
|
228
|
+
apos.bus.$off('widget-hover', this.updateWidgetHovered);
|
|
229
|
+
apos.bus.$off('widget-focus', this.updateWidgetFocused);
|
|
230
|
+
apos.bus.$off('refreshed', this.destroyParentComponent);
|
|
229
231
|
window.removeEventListener('keydown', this.focusParentEvent);
|
|
230
232
|
},
|
|
231
233
|
areaUpdatedHandler(area) {
|
|
@@ -582,6 +584,11 @@ export default {
|
|
|
582
584
|
}
|
|
583
585
|
});
|
|
584
586
|
return widget;
|
|
587
|
+
},
|
|
588
|
+
destroyParentComponent() {
|
|
589
|
+
if (!document.body.contains(this.$parent.$el)) {
|
|
590
|
+
this.$parent.$destroy();
|
|
591
|
+
}
|
|
585
592
|
}
|
|
586
593
|
}
|
|
587
594
|
};
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
// }
|
|
22
22
|
// ```
|
|
23
23
|
|
|
24
|
+
const parseurl = require('parseurl');
|
|
25
|
+
|
|
24
26
|
module.exports = {
|
|
25
27
|
async init(self) {
|
|
26
28
|
self.options.statusCode = self.options.statusCode || 302;
|
|
@@ -37,7 +39,7 @@ module.exports = {
|
|
|
37
39
|
return {
|
|
38
40
|
'@apostrophecms/page:notFound': {
|
|
39
41
|
async notFoundRedirect(req) {
|
|
40
|
-
const urlPathname =
|
|
42
|
+
const urlPathname = parseurl.original(req).pathname;
|
|
41
43
|
|
|
42
44
|
const doc = await self.apos.doc
|
|
43
45
|
.find(req, {
|
|
@@ -50,9 +52,9 @@ module.exports = {
|
|
|
50
52
|
if (!(doc && doc._url)) {
|
|
51
53
|
return;
|
|
52
54
|
}
|
|
53
|
-
if (self.local(doc._url) !==
|
|
55
|
+
if (self.local(doc._url) !== urlPathname) {
|
|
54
56
|
req.statusCode = self.options.statusCode;
|
|
55
|
-
req.redirect =
|
|
57
|
+
req.redirect = doc._url;
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apostrophe",
|
|
3
|
-
"version": "3.40.
|
|
3
|
+
"version": "3.40.2-alpha",
|
|
4
4
|
"description": "The Apostrophe Content Management System.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -64,7 +64,6 @@
|
|
|
64
64
|
"cuid": "^2.1.8",
|
|
65
65
|
"dayjs": "^1.9.8",
|
|
66
66
|
"debounce-async": "0.0.2",
|
|
67
|
-
"deep-get-set": "^1.1.1",
|
|
68
67
|
"dompurify": "^2.3.1",
|
|
69
68
|
"express": "^4.16.4",
|
|
70
69
|
"express-bearer-token": "^2.4.0",
|
|
@@ -91,7 +90,8 @@
|
|
|
91
90
|
"nodemailer": "^6.6.1",
|
|
92
91
|
"nunjucks": "^3.2.1",
|
|
93
92
|
"oembetter": "^1.0.1",
|
|
94
|
-
"
|
|
93
|
+
"parseurl": "^1.3.3",
|
|
94
|
+
"passport": "^0.6.0",
|
|
95
95
|
"passport-local": "^1.0.0",
|
|
96
96
|
"path-to-regexp": "^1.8.0",
|
|
97
97
|
"performance-now": "^2.1.0",
|