Skip to content

Commit

Permalink
Merge branch 'main' into pro-6585
Browse files Browse the repository at this point in the history
  • Loading branch information
boutell authored Sep 20, 2024
2 parents a341939 + 4869e54 commit fafd5ec
Show file tree
Hide file tree
Showing 15 changed files with 937 additions and 322 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,28 @@

## UNRELEASED

### Adds

* Adds focus states for media library's Uploader tile
* Adds focus states file attachment's input UI
* Simplified importing rich text widgets via the REST API. If you you have HTML that contains `img` tags pointing to existing images, you can now import them all quickly. When supplying the rich text widget object, include an `import` property with an `html` subproperty, rather than the usual `content` property. You can optionally provide a `baseUrl` subproperty as well. Any images present in `html` will be imported automatically and the correct `figure` tags will be added to the new rich text widget, along with any other markup acceptable to the widget's configuration.

### Changes

* The various implementations of `newInstance` found in Apostrophe, e.g. for widgets, array items, relationship fields and documents themselves, have been consolidated in one implementation. The same code is now reused both on the front and the back end, ensuring the same result without the need to introduce additional back end API calls.

### Fixes

* The `@apostrophecms/page` module APIs no longer allow a page to become a child of itself. Thanks to [Maarten Marx](https://github.com/Pixelguymm) for reporting the issue.
* Uploaded SVGs now permit `<use>` tags granted their `xlink:href` property is a local reference and begins with the `#` character. This improves SVG support while mitgating XSS vulnerabilities.
* Default properties of object fields present in a widget now populate correctly even if never focused in the editor.

## 4.7.0 (2024-09-05)

### Changes

* UI and UX of inline arrays and their table styles

### Adds

* To aid debugging, when a file extension is unacceptable as an Apostrophe attachment the rejected extension is now printed as part of the error message.
Expand Down Expand Up @@ -49,6 +61,9 @@ This resolves the issue for new uploads.
* Fix widget focus state so that the in-context Add Content menu stays visible during animation.
* Fix UI of areas in schemas so that their context menus are layered overtop sibling schema fields UI.

### Removes
* Inline array option for `alwaysOpen` replaced with UI toggles

## 4.6.0 (2024-08-08)

### Adds
Expand Down Expand Up @@ -76,6 +91,7 @@ The shape of the relationship field is still validated.

### Fixes

* Fixes the rendering of conditional fields in arrays where the `inline: true` option is used.
* Fixes the rich text link tool's detection and display of the Remove Link button for removing existing links
* Fixes the rich text link tool's detection and display of Apostrophe Page relationship field.
* Overriding standard Vue.js components with `editorModal` and `managerModal` are now applied all the time.
Expand Down
3 changes: 3 additions & 0 deletions modules/@apostrophecms/asset/lib/globalIcons.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ module.exports = {
'apple-keyboard-shift': 'AppleKeyboardShift',
'archive-arrow-down-icon': 'ArchiveArrowDown',
'archive-arrow-up-icon': 'ArchiveArrowUp',
'arrow-collapse-vertical-icon': 'ArrowCollapseVertical',
'arrow-down-icon': 'ArrowDown',
'arrow-expand-vertical-icon': 'ArrowExpandVertical',
'arrow-left-icon': 'ArrowLeft',
'arrow-right-icon': 'ArrowRight',
'arrow-up-icon': 'ArrowUp',
Expand Down Expand Up @@ -107,6 +109,7 @@ module.exports = {
'sign-text-icon': 'SignText',
'tag-icon': 'Tag',
'text-box-icon': 'TextBox',
'text-box-multiple-icon': 'TextBoxMultiple',
'text-box-remove-icon': 'TextBoxRemove',
'trash-can-icon': 'TrashCan',
'trash-can-outline-icon': 'TrashCanOutline',
Expand Down
9 changes: 8 additions & 1 deletion modules/@apostrophecms/attachment/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -519,8 +519,15 @@ module.exports = {
const writeFile = require('util').promisify(fs.writeFile);
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
DOMPurify.addHook('afterSanitizeAttributes', node => {
if (node.hasAttribute('xlink:href') && !node.getAttribute('xlink:href').match(/^#/)) {
node.remove();
}
});
const dirty = await readFile(path);
const clean = DOMPurify.sanitize(dirty);
const clean = DOMPurify.sanitize(dirty, {
ADD_TAGS: [ 'use' ]
});
return writeFile(path, clean);
},
getFileGroup(extension) {
Expand Down
6 changes: 6 additions & 0 deletions modules/@apostrophecms/i18n/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"clone": "Clone",
"close": "Close",
"closeGlobal": "Close Global Site Settings",
"collapseAll": "Collapse all",
"commandMenuArchiveSelected": "Archive selected",
"commandMenuContent": "Content",
"commandMenuCreateNew": "Create new {{ type }}",
Expand Down Expand Up @@ -152,6 +153,7 @@
"draftSavedNoPreview": "Draft saved but could not navigate to a preview.",
"dropMedia": "Drop new media here",
"duplicate": "Duplicate...",
"duplicateOf": "Duplicate of",
"duplicateError": "Duplicate",
"edit": "Edit",
"editImageAdjustments": "Image Adjustments",
Expand Down Expand Up @@ -193,6 +195,7 @@
"errorWhileUnpublishing": "An error occurred while unpublishing the document.",
"everythingElse": "Everything Else",
"exit": "Exit",
"expandAll": "Expand all",
"fetchPublishedVersionFailed": "An error occurred fetching the published version of the document.",
"fieldHasUnpublishedChanges": "This field has unpublished changes",
"file": "File",
Expand Down Expand Up @@ -309,6 +312,8 @@
"modifyOrDelete": "Modify / Delete",
"moreOperations": "More Operations",
"moreOptions": "More Options",
"moveDown": "Move Down",
"moveUp": "Move Up",
"multipleEditors": "Multiple Editors",
"newDocType": "New {{ type }}",
"newItem": "New item",
Expand All @@ -319,6 +324,7 @@
"nextPage": "Next Page",
"no": "No",
"noDraftSubmissions": "No Draft Submissions to Manage",
"noItemsAdded": "No Items Added",
"noItemsSelected": "No Items Selected",
"noJustSwitchLocales": "No, just switch locales",
"noLongerPublished": "No longer published.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<label
:class="dropzoneClasses"
class="apos-media-manager-display__cell apos-media-uploader"
:class="{'apos-media-uploader--enabled': !disabled}"
:disabled="disabled"
@drop.prevent="uploadMedia"
@dragover.prevent=""
Expand All @@ -9,6 +10,7 @@
>
<div
class="apos-media-uploader__inner"
:class="{'apos-is-dragging': dragover}"
tabindex="0"
@keydown="onUploadDragAndDropKeyDown"
>
Expand Down Expand Up @@ -70,15 +72,7 @@ export default {
};
},
computed: {
dropzoneClasses () {
return [
'apos-media-manager-display__cell',
'apos-media-uploader',
{
'apos-is-dragging': this.dragover
}
].concat(this.disabled ? [] : [ 'apos-media-uploader--enabled' ]);
}
},
mounted() {
apos.bus.$on('command-menu-manager-create-new', this.create);
Expand Down Expand Up @@ -262,7 +256,7 @@ export default {
}
}
.apos-media-uploader--enabled {
.apos-media-uploader--enabled .apos-media-uploader__inner {
&::after {
@include apos-transition($duration: 0.3s);
Expand Down Expand Up @@ -290,7 +284,7 @@ export default {
&:active,
&:focus,
&.apos-is-dragging {
border-width: 0;
outline: 2px dashed var(--a-primary);
&::after {
width: 102%;
Expand All @@ -302,11 +296,6 @@ export default {
transform: translateY(-2px);
}
}
&:active,
&:focus {
outline: 1px solid var(--a-primary);
}
}
.apos-media-uploader__inner {
Expand Down
66 changes: 66 additions & 0 deletions modules/@apostrophecms/rich-text-widget/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

const sanitizeHtml = require('sanitize-html');
const cheerio = require('cheerio');
const { createWriteStream, unlinkSync } = require('fs');
const { Readable, pipeline } = require('stream');
const util = require('util');

module.exports = {
extend: '@apostrophecms/widget-type',
Expand Down Expand Up @@ -869,6 +872,69 @@ module.exports = {
const output = await _super(req, input, rteOptions);
const finalOptions = self.optionsToSanitizeHtml(rteOptions);

if (input.import?.html) {
if ((typeof input.import.html) !== 'string') {
throw self.apos.error('invalid', 'import.html must be a string');
}
if (input.import.baseUrl && ((typeof input.import.html) !== 'string')) {
throw self.apos.error('invalid', 'If present, import.baseUrl must be a string');
}
const $ = cheerio.load(input.import.html);
const $images = $('img');
// Build an array of cheerio objects because
// we need to iterate while doing async work,
// which .each() can't do
const $$images = [];
$images.each((i, el) => {
const $image = $(el);
$$images.push($image);
});
for (const $image of $$images) {
const src = $image.attr('src');
const alt = $image.attr('alt') && self.apos.util.escapeHtml($image.attr('alt'));
const url = new URL(src, input.import.baseUrl || self.apos.baseUrl);
const res = await fetch(url);
if (res.status >= 400) {
self.apos.util.warn(`Error ${res.status} while importing ${src}, ignoring image`);
continue;
}
const id = self.apos.util.generateId();
const temp = self.apos.attachment.uploadfs.getTempPath() + `/${id}`;
const matches = src.match(/\/([^/]+\.\w+)$/);
if (!matches) {
self.apos.util.warn('img URL has no extension, skipping:', src);
continue;
}
const name = matches[1];
try {
await util.promisify(pipeline)(Readable.fromWeb(res.body), createWriteStream(temp));
const attachment = await self.apos.attachment.insert(req, {
name,
path: temp
});
const image = await self.apos.image.insert(req, {
title: name,
attachment
});
const newSrc = `${self.apos.image.action}/${image.aposDocId}/src`;
$image.replaceWith(
`<figure>
<img src="${newSrc}" ${alt && `alt="${alt}"`} />
<figcaption></figcaption>
</figure>
`
);
} finally {
try {
unlinkSync(temp);
} catch (e) {
// It's OK if we never created it
}
}
}
input.content = $.html();
}

output.content = self.sanitizeHtml(input.content, finalOptions);

const permalinkAnchors = output.content.match(/"#apostrophe-permalink-[^"?]*?\?/g);
Expand Down
Loading

0 comments on commit fafd5ec

Please sign in to comment.