Skip to content

Commit ab45f28

Browse files
committed
Merge branch 'main' into pro-8052-render-widget
* main: PRO-8130: default node parseHTML now detects whether an element has the relevant class, or lack of one, before classifying it as a default node (#5017) Add regression tests for duplicate relationships resolution in some edge cases (#5016) 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 8663708 + 6abf937 commit ab45f28

10 files changed

Lines changed: 280 additions & 33 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,21 @@
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.
1819
* Remove multiple calls to render-widget when switch to edit mode.
20+
* 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.
21+
* Fixed a regression that prevented multiple variations of `p` with different classes from being recognized again when reopening the rich text editor, even if they are all on the style menu. This was caused by knock-on effects of upstream changes in tiptap and prosemirror and our previous efforts to mitigate these. Those upstream changes were correct, but they did have certain side effects in ApostropheCMS. By more fully specifying the desired behavior, we have now fully corrected the issue at the ApostropheCMS level.
22+
23+
### Security
24+
25+
* Clear an npm audit warning by replacing `connect-multiparty` with `multer`. Thanks to [Radhakrishnan Mohan](https://github.com/RadhaKrishnan) for this contribution.
26+
* 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`.
1927

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

@@ -25,8 +33,8 @@
2533
Thanks to [sombitganguly](https://github.com/sombitganguly) for this contribution.
2634
* Adds keyboard shortcuts for manipulating widgets in areas. Includes Cut, Copy, Paste, Delete, and Duplicate.
2735
* Automatic translation now supports a disclaimer and an help text for the checkbox. You can now set the disclaimer by setting `automaticTranslationDisclaimer` `i18n` key and the help text by setting `automaticTranslationCheckboxHelp` `i18n` key.
28-
* Adds dynamic choices working with piece manager filters.
29-
* Allow `import.imageTags` (array of image tag IDs) to be passed to the rich text widget when importing (see https://docs.apostrophecms.org/reference/api/rich-text.html#importing-inline-images).
36+
* Adds dynamic choices working with piece manager filters.
37+
* Allow `import.imageTags` (array of image tag IDs) to be passed to the rich text widget when importing (see <https://docs.apostrophecms.org/reference/api/rich-text.html#importing-inline-images>).
3038
* Adds a new way to make `GET` requests with a large query string. It can become a `POST` request containing the key `__aposGetWithQuery` in its body.
3139
A middleware checks for this key and converts the request back to a `GET` request with the right `req.query` property.
3240
* Adds a new batch operation to tag images.
@@ -46,7 +54,7 @@ A middleware checks for this key and converts the request back to a `GET` reques
4654

4755
* Adds MongoDB-style support (comparison operators) for conditional fields and all systems that use conditions. Conditional fields now have access to the `following` values from the parent schema fields.
4856
* Add `followingIgnore` option to the `string` field schema. A boolean `true` results in all `following` values being ignored (not attempted to be used as a value for the field). When array of strings, the UI will ignore every item that matches a `following` field name.
49-
* Adds link configuration to the `@apostrophecms/image-widget` UI and a new option `linkWithType` to control what document types can be linked to. Opt-out of the widget inline styles (reset) by setting `inlineStyles: false` in the widget configuration or contextual options (area).
57+
* Adds link configuration to the `@apostrophecms/image-widget` UI and a new option `linkWithType` to control what document types can be linked to. Opt-out of the widget inline styles (reset) by setting `inlineStyles: false` in the widget configuration or contextual options (area).
5058
* Use the link configuration of the Rich Text widget for image links too. It respects the existing `linkWithType` Rich Text option and uses the same schema (`linkFields`) used for text links. The fields from that schema can opt-in for specific tiptap extension now via a field property `extensions` (array) with possible array values `Link` and/or `Image`. You still need to specify the `htmlAttribute` property (the name of the attribute to be added to the link tag) in the schema when adding more fields. If the `extensions` property is not set, the field will be applied for both tiptap extensions.
5159
* Adds body style support for breakpoint preview mode. Created new `[data-apos-refreshable-body]` div inside the container during breapoint preview.
5260
Switch body attributes to this new div to keep supporting body styles in breakpoint preview mode.
@@ -2392,6 +2400,7 @@ myColorField: {
23922400
## 3.0.0-beta.3.1 - 2021-06-07
23932401

23942402
### Breaks
2403+
23952404
* This backwards compatibility break actually occurred in 3.0.0-beta.3 and was not documented at that time, but it is important to know that the following Rich Text tool names have been updated to match Tiptap2's convention:
23962405
-- `bullet_list` -> `bulletList`
23972406
-- `ordered_list` -> `orderedList`
@@ -2478,6 +2487,7 @@ The `nlbr` and `nlp` Nunjucks filters marked their output as safe to preserve th
24782487
* The rich text editor now supports specifying a `styles` array with no `p` tags properly. A newly added rich text widget initially contains an element with the first style, rather than always a paragraph. If no styles are configured, a `p` tag is assumed. Thanks to Stepan Jakl for reporting the issue.
24792488

24802489
### Changes
2490+
24812491
* Editor modal's Save button (publish / save draft / submit) now updated to use the `AposSplitButton` component. Editors can choose from several follow-up actions that occur after save, including creating another piece of content of the same type, being taken to the in-context version of the document, or being returned to the manager. Editor's selection is saved in localstorage, creating a remembered preference per content type.
24822492

24832493
## 3.0.0-beta.1.1 - 2021-05-07

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/rich-text-widget/ui/apos/tiptap-extensions/Default.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,27 @@ export default (options) => {
4444
HTMLAttributes: attrs,
4545
levels: def.options.level ? [ def.options.level ] : null
4646
};
47+
},
48+
parseHTML() {
49+
return [
50+
{
51+
tag: 'p',
52+
getAttrs: el => {
53+
// We must make sure the DefaultNode is only
54+
// returned for the subset of elements that actually
55+
// match its class, or lack of one. Otherwise it will
56+
// hoover up all elements sharing its element name,
57+
// breaking the ability to have alternates with classes
58+
// listed in "styles" in the toolbar "stick" when you
59+
// open the editor again -Tom
60+
if (def.options.class) {
61+
return el.getAttribute('class') === def.options.class;
62+
} else {
63+
return !el.hasAttribute('class');
64+
}
65+
}
66+
}
67+
];
4768
}
4869
});
4970
}

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)