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 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(e) {
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
- let url = window.location.href;
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(url, qs);
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
- if (options.scrollcheck) {
529
- window.apos.adminBar.scrollPosition = {
530
- x: window.scrollX,
531
- y: window.scrollY
532
- };
533
- }
540
+ refreshable.innerHTML = content;
534
541
 
535
- if (refreshable) {
536
- refreshable.innerHTML = content;
537
- if (this.editMode && (!this.original)) {
538
- // the first time we enter edit mode on the page, we need to
539
- // establish a baseline for undo/redo. Use our
540
- // "@ notation" PATCH feature. Sort the areas by DOM depth
541
- // to ensure parents patch before children
542
- this.original = {};
543
- const els = Array.from(document.querySelectorAll('[data-apos-area-newly-editable]')).filter(el => el.getAttribute('data-doc-id') === this.context._id);
544
- els.sort((a, b) => {
545
- const da = depth(a);
546
- const db = depth(b);
547
- if (da < db) {
548
- return -1;
549
- } else if (db > da) {
550
- return 1;
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
- deep(context, `${path}._rendered`, areaRendered);
247
- deep(context, `${path}._fieldId`, undefined);
248
- deep(context, `${path}.items`, undefined);
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 deep(doc, parentDotPath) || doc;
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 = deep(doc, dotPath);
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 = deep(doc, dotPath);
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
- deep(doc, dotPath, {
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
- // 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.
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
- // Headless app to provide simple reactivity for the clipboard state
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
- // If we don't clone, the second paste will be a duplicate key error
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
- // When local storage changes, dump the list to
157
- // the console.
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 = req.url.replace(/\?.*$/, '');
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) !== req.url) {
55
+ if (self.local(doc._url) !== urlPathname) {
54
56
  req.statusCode = self.options.statusCode;
55
- req.redirect = self.local(doc._url);
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.1",
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
- "passport": "^0.3.2",
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",