Skip to content

Commit e64ffa1

Browse files
committed
Merge branch 'main' into pro-8080-checkbox-filter
* main: Resolved an issue affecting `withRelationships` with two or more steps. This issue could cause a document to appear to be related to the same document more than on (#5015) thanks! (#5014) Fix #4979: Replace connect-multiparty with multer to resolve security vulnerability (#5013) Hide rich text controls on interaction (#5008)
2 parents 3e4bbb8 + 681bac3 commit e64ffa1

8 files changed

Lines changed: 79 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,20 @@
99

1010
### Changes
1111

12-
- Changes handling of `order` and `groups` in the `admin-bar` module to respect, rather that reverse, the order of items
12+
* Changes handling of `order` and `groups` in the `admin-bar` module to respect, rather that reverse, the order of items
13+
* Interacting with the text inside a rich text widget will hide the widget controls to prevent awkawrd text selection.
1314

1415
### Fixes
1516

1617
* Let the `@apostrophecms/page:unpark` task unpark all parked pages with the given slug, not just the first one.
1718
* Exclude unknown page types from the page manager.
19+
* Resolved an issue affecting `withRelationships` with two or more steps. This issue could cause a document to appear to be related to the same document more than once.
20+
21+
22+
### Security
23+
24+
* Clear an npm audit warning by replacing `connect-multiparty` with `multer`. Thanks to [Radhakrishnan Mohan](https://github.com/RadhaKrishnan) for this contribution.
25+
* To be clear, this was never an actual security vulnerability. The CVE in question is disputed, and for good reasons. However, since `connect-multiparty` is no longer maintained, it makes sense to move to `multer`.
1826

1927
## 4.19.0 (2025-07-09)
2028

modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
class="apos-area-widget-controls apos-area-widget__label"
2626
:class="labelsClasses"
2727
>
28-
<ol class="apos-area-widget__breadcrumbs">
28+
<ol
29+
class="apos-area-widget__breadcrumbs"
30+
@click="isSuppressingWidgetControls = false"
31+
>
2932
<li
3033
class="
3134
apos-area-widget__breadcrumb
@@ -135,6 +138,7 @@
135138
:doc-id="docId"
136139
:focused="isFocused"
137140
@update="$emit('update', $event)"
141+
@suppress-widget-controls="isSuppressingWidgetControls = true"
138142
/>
139143
<component
140144
:is="widgetComponent(widget.type)"
@@ -285,12 +289,14 @@ export default {
285289
mounted: false, // hack around needing DOM to be rendered for computed classes
286290
isSuppressed: false,
287291
menuOpen: null,
292+
isSuppressingWidgetControls: false,
288293
classes: {
289294
show: 'apos-is-visible',
290295
open: 'apos-is-open',
291296
focus: 'apos-is-focused',
292297
highlight: 'apos-is-highlighted',
293-
adjust: 'apos-is-ui-adjusted'
298+
adjust: 'apos-is-ui-adjusted',
299+
suppressWidgetControls: 'apos-is-suppressing-widget-controls'
294300
},
295301
breadcrumbs: {
296302
$lastEl: null,
@@ -362,7 +368,8 @@ export default {
362368
},
363369
controlsClasses() {
364370
return {
365-
[this.classes.show]: this.isFocused
371+
[this.classes.show]: this.isFocused,
372+
[this.classes.suppressWidgetControls]: this.isSuppressingWidgetControls
366373
};
367374
},
368375
containerClasses() {
@@ -398,6 +405,7 @@ export default {
398405
} else {
399406
this.menuOpen = null;
400407
this.$refs.wrapper.removeEventListener('keydown', this.handleKeyboardUnfocus);
408+
this.isSuppressingWidgetControls = false;
401409
}
402410
}
403411
},
@@ -881,7 +889,7 @@ export default {
881889
}
882890
}
883891
884-
.apos-is-visible,
892+
.apos-is-visible:not(.apos-is-suppressing-widget-controls),
885893
.apos-is-focused {
886894
opacity: 1;
887895
pointer-events: auto;

modules/@apostrophecms/attachment/index.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,11 @@ module.exports = {
159159
post: {
160160
upload: [
161161
self.canUpload,
162-
require('connect-multiparty')(),
162+
require('multer')({ dest: require('os').tmpdir() }).single('file'),
163163
async function (req) {
164164
try {
165-
// The name attribute could be anything because of how fileupload
166-
// controls work; we don't really care.
167-
const file = Object.values(req.files || {})[0];
165+
// The file comes from multer middleware
166+
const file = req.file;
168167

169168
if (!file) {
170169
throw self.apos.error('invalid');
@@ -175,11 +174,11 @@ module.exports = {
175174

176175
return attachment;
177176
} finally {
178-
for (const file of (Object.values(req.files || {}))) {
177+
if (req.file) {
179178
try {
180-
fs.unlinkSync(file.path);
179+
fs.unlinkSync(req.file.path);
181180
} catch (e) {
182-
self.apos.util.warn(`Uploaded temporary file ${file.path} was already removed, this should have been the responsibility of the upload route`);
181+
self.apos.util.warn(`Uploaded temporary file ${req.file.path} was already removed, this should have been the responsibility of the upload route`);
183182
}
184183
}
185184
}
@@ -393,7 +392,9 @@ module.exports = {
393392
// object, suitable for passing to the `url` API and for use as the value
394393
// of a `type: 'attachment'` schema field.
395394
async insert(req, file, options = {}) {
396-
let extension = path.extname(file.name);
395+
// Handle both multer (originalname) and connect-multiparty (name) formats
396+
const fileName = file.originalname || file.name;
397+
let extension = path.extname(fileName);
397398
if (extension && extension.length) {
398399
extension = extension.substr(1);
399400
}
@@ -420,9 +421,9 @@ module.exports = {
420421
_id: options.attachmentId ?? self.apos.util.generateId(),
421422
group: group.name,
422423
createdAt: new Date(),
423-
name: self.apos.util.slugify(path.basename(file.name, path.extname(file.name))),
424+
name: self.apos.util.slugify(path.basename(fileName, path.extname(fileName))),
424425
title: self.apos.util.sortify(
425-
path.basename(file.name, path.extname(file.name))
426+
path.basename(fileName, path.extname(fileName))
426427
),
427428
extension,
428429
type: 'attachment',

modules/@apostrophecms/http/lib/big-upload-middleware.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const multiparty = require('connect-multiparty');
1+
const multer = require('multer');
22
const util = require('util');
33
const {
44
readFile, open, unlink
@@ -17,10 +17,10 @@ module.exports = (self) => ({
1717

1818
bigUploadMiddleware({ authorize } = {}) {
1919
return (req, res, next) => {
20-
// Chain the multiparty middleware to handle normal uploads
20+
// Chain the multer middleware to handle normal uploads
2121
// as chunks (more efficient than base64 etc)
22-
const multipartyFn = multiparty();
23-
return multipartyFn(req, res, () => {
22+
const multerFn = multer({ dest: require('os').tmpdir() }).any();
23+
return multerFn(req, res, () => {
2424
return body(req, res, next);
2525
});
2626
};
@@ -56,10 +56,10 @@ module.exports = (self) => ({
5656
});
5757
}
5858
} finally {
59-
// Clean up multiparty temporary files
60-
for (const { path } of Object.values(origFiles || {})) {
59+
// Clean up multer temporary files
60+
for (const file of (origFiles || [])) {
6161
try {
62-
await unlink(path);
62+
await unlink(file.path);
6363
} catch (e) {
6464
// OK if it is already gone
6565
}
@@ -136,7 +136,7 @@ module.exports = (self) => ({
136136
if ((chunk < 0) || (chunk >= info.chunks)) {
137137
throw self.apos.error('invalid', 'chunk out of range');
138138
}
139-
const file = req.files.chunk;
139+
const file = req.files.find(f => f.fieldname === 'chunk') || req.files[0];
140140
const ufs = self.getBigUploadFs();
141141
const ufsPath = `/big-uploads/${id}-${n}-${chunk}`;
142142
await ufs.copyIn(file.path, ufsPath);

modules/@apostrophecms/rich-text-widget/lib/apiRoutes.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const fs = require('node:fs');
2-
const connectMultiparty = require('connect-multiparty');
2+
const multer = require('multer');
33
const { pipeline } = require('stream/promises');
44
const { parse: csvParse } = require('csv-parse');
55
const { Transform } = require('stream');
@@ -9,14 +9,14 @@ module.exports = self => {
99
return {
1010
post: {
1111
generateCsvTable: [
12-
connectMultiparty(),
12+
multer({ dest: require('os').tmpdir() }).single('file'),
1313
async (req) => {
14-
const { file } = req.files || {};
14+
const file = req.file;
1515
if (!file) {
1616
throw self.apos.error('invalid', 'A file is required');
1717
}
1818

19-
const extension = file.name.split('.').pop();
19+
const extension = file.originalname.split('.').pop();
2020
if (extension !== 'csv') {
2121
throw self.apos.error('invalid', 'Only csv files are supported');
2222
}

modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ export default {
193193
default: false
194194
}
195195
},
196-
emits: [ 'update' ],
196+
emits: [ 'update', 'suppressWidgetControls' ],
197197
data() {
198198
return {
199199
editor: null,
@@ -209,6 +209,8 @@ export default {
209209
showPlaceholder: null,
210210
activeInsertMenuComponent: false,
211211
suppressInsertMenu: false,
212+
suppressWidgetControls: false,
213+
hasSelection: false,
212214
insertMenuKey: null,
213215
openedPopover: false
214216
};
@@ -377,8 +379,14 @@ export default {
377379
}
378380
},
379381
watch: {
382+
suppressWidgetControls(newVal) {
383+
if (newVal) {
384+
this.$emit('suppressWidgetControls');
385+
}
386+
},
380387
isFocused(newVal) {
381388
if (!newVal) {
389+
this.suppressWidgetControls = false;
382390
if (this.pending) {
383391
this.emitWidgetUpdate();
384392
}
@@ -463,6 +471,13 @@ export default {
463471
this.$nextTick(() => {
464472
this.showPlaceholder = true;
465473
});
474+
},
475+
onSelectionUpdate: ({ editor }) => {
476+
this.$nextTick(() => {
477+
if (!editor.view.state.selection.empty) {
478+
this.suppressWidgetControls = true;
479+
}
480+
});
466481
}
467482
});
468483
apos.bus.$on('apos-refreshing', this.onAposRefreshing);
@@ -501,6 +516,7 @@ export default {
501516
} else {
502517
this.suppressInsertMenu = false;
503518
}
519+
this.suppressWidgetControls = true;
504520
},
505521
doSuppressInsertMenu() {
506522
this.suppressInsertMenu = true;

modules/@apostrophecms/schema/lib/joinr.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ const joinr = module.exports = {
6868
getter,
6969
idMapper
7070
) {
71+
// This method never alters the items array itself, it alters
72+
// the objects within it. So it is safe to reduce that array to
73+
// its unique elements, and this simplifies calling code which does
74+
// not have to guard against this situation.
75+
// Note that we mean literal uniqueness (e.g. by reference), as this
76+
// is the only time we need to avoid appending the same joined objects
77+
// more than once.
78+
items = [ ...new Set(items) ];
7179
let otherIds = [];
7280
const othersById = {};
7381
for (const item of items) {
@@ -167,6 +175,14 @@ const joinr = module.exports = {
167175
getter,
168176
idMapper
169177
) {
178+
// This method never alters the items array itself, it alters
179+
// the objects within it. So it is safe to reduce that array to
180+
// its unique elements, and this simplifies calling code which does
181+
// not have to guard against this situation.
182+
// Note that we mean literal uniqueness (e.g. by reference), as this
183+
// is the only time we need to avoid appending the same joined objects
184+
// more than once.
185+
items = [ ...new Set(items) ];
170186
const itemIds = items.map(item => idMapper(item._id));
171187
for (const item of items) {
172188
if (!item[objectsField]) {

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
"chokidar": "^3.5.2",
6666
"common-tags": "^1.8.0",
6767
"connect-mongo": "^5.1.0",
68-
"connect-multiparty": "^2.1.1",
6968
"cookie-parser": "^1.4.5",
7069
"cors": "^2.8.5",
7170
"css-loader": "^5.2.4",
@@ -76,7 +75,6 @@
7675
"express-bearer-token": "^3.0.0",
7776
"express-cache-on-demand": "^1.0.3",
7877
"express-session": "^1.18.2",
79-
"form-data": "^4.0.0",
8078
"fs-extra": "^7.0.1",
8179
"glob": "^10.4.5",
8280
"he": "^1.2.0",
@@ -92,6 +90,7 @@
9290
"mini-css-extract-plugin": "^1.6.0",
9391
"minimatch": "^3.0.4",
9492
"mkdirp": "^0.5.5",
93+
"multer": "^2.0.2",
9594
"node-fetch": "^2.6.1",
9695
"nodemailer": "^6.6.1",
9796
"nunjucks": "^3.2.1",
@@ -134,6 +133,7 @@
134133
},
135134
"devDependencies": {
136135
"eslint-config-apostrophe": "^5.0.0",
136+
"form-data": "^4.0.4",
137137
"mocha": "^10.7.3",
138138
"nyc": "^15.1.0",
139139
"replace-in-file": "^6.1.0",

0 commit comments

Comments
 (0)