From 7c3d75ade7eafe3ce3d91df7e3562eec7638c1d5 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Thu, 10 Nov 2022 14:43:15 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20pasting=20newlines=20in?= =?UTF-8?q?=20post=20titles=20(#15794)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes https://github.com/TryGhost/Team/issues/2193 - When pasting a title with a newline, we now trim the string and clear newslines before pasting. - When sending the slug to the backend to generate a unique slug, we now sluggify it in the frontend before adding it to the URL to prevent issues with unsupported characters (causing possible routing problems in Pro). --- .../app/components/gh-koenig-editor-lexical.hbs | 3 ++- .../app/components/gh-koenig-editor-lexical.js | 14 ++++++++++++++ ghost/admin/app/components/gh-koenig-editor.hbs | 3 ++- ghost/admin/app/components/gh-koenig-editor.js | 14 ++++++++++++++ ghost/admin/app/services/slug-generator.js | 4 +++- .../integration/services/slug-generator-test.js | 2 +- 6 files changed, 36 insertions(+), 4 deletions(-) diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs index 3ed24c5326..2919f96159 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs @@ -32,6 +32,7 @@ {{on "blur" (fn (mut this.titleIsFocused) false)}} {{on "mouseover" (fn (mut this.titleIsHovered) true)}} {{on "mouseleave" (fn (mut this.titleIsHovered) false)}} + {{on "paste" this.cleanPastedTitle}} data-test-editor-title-input={{true}} /> @@ -61,4 +62,4 @@ @postType={{@postType}} /> --}} - \ No newline at end of file + diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.js b/ghost/admin/app/components/gh-koenig-editor-lexical.js index 5736be4f3e..879f1d1092 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.js +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.js @@ -56,6 +56,20 @@ export default class GhKoenigEditorReactComponent extends Component { this.args.onTitleChange?.(event.target.value); } + @action + cleanPastedTitle(event) { + const pastedText = (event.clipboardData || window.clipboardData).getData('text'); + + if (!pastedText) { + return; + } + + event.preventDefault(); + + const cleanValue = pastedText.replace(/(\n|\r)+/g, ' ').trim(); + document.execCommand('insertText', false, cleanValue); + } + @action focusTitle() { this.titleElement.focus(); diff --git a/ghost/admin/app/components/gh-koenig-editor.hbs b/ghost/admin/app/components/gh-koenig-editor.hbs index 32c4bf0a0d..a1a9f142ea 100644 --- a/ghost/admin/app/components/gh-koenig-editor.hbs +++ b/ghost/admin/app/components/gh-koenig-editor.hbs @@ -32,6 +32,7 @@ {{on "blur" (fn (mut this.titleIsFocused) false)}} {{on "mouseover" (fn (mut this.titleIsHovered) true)}} {{on "mouseleave" (fn (mut this.titleIsHovered) false)}} + {{on "paste" this.cleanPastedTitle}} data-test-editor-title-input={{true}} /> @@ -56,4 +57,4 @@ @postType={{@postType}} /> - \ No newline at end of file + diff --git a/ghost/admin/app/components/gh-koenig-editor.js b/ghost/admin/app/components/gh-koenig-editor.js index e21a18427a..93f0ee6fee 100644 --- a/ghost/admin/app/components/gh-koenig-editor.js +++ b/ghost/admin/app/components/gh-koenig-editor.js @@ -43,6 +43,20 @@ export default class GhKoenigEditorComponent extends Component { this.args.onTitleChange?.(event.target.value); } + @action + cleanPastedTitle(event) { + const pastedText = (event.clipboardData || window.clipboardData).getData('text'); + + if (!pastedText) { + return; + } + + event.preventDefault(); + + const cleanValue = pastedText.replace(/(\n|\r)+/g, ' ').trim(); + document.execCommand('insertText', false, cleanValue); + } + @action focusTitle() { this.titleElement.focus(); diff --git a/ghost/admin/app/services/slug-generator.js b/ghost/admin/app/services/slug-generator.js index f6ab19ad3b..397340e075 100644 --- a/ghost/admin/app/services/slug-generator.js +++ b/ghost/admin/app/services/slug-generator.js @@ -1,6 +1,7 @@ import RSVP from 'rsvp'; import Service, {inject as service} from '@ember/service'; import classic from 'ember-classic-decorator'; +import {slugify} from '@tryghost/string'; const {resolve} = RSVP; @@ -16,7 +17,8 @@ export default class SlugGeneratorService extends Service { return resolve(''); } - url = this.get('ghostPaths.url').api('slugs', slugType, encodeURIComponent(textToSlugify)); + // We already do a partial slugify at the client side to prevent issues with Pro returning a 404 page because of invalid (encoded) characters (a newline, %0A, for example) + url = this.get('ghostPaths.url').api('slugs', slugType, encodeURIComponent(slugify(textToSlugify))); return this.ajax.request(url).then((response) => { let [firstSlug] = response.slugs; diff --git a/ghost/admin/tests/integration/services/slug-generator-test.js b/ghost/admin/tests/integration/services/slug-generator-test.js index 29bbc8c869..2ac4961e34 100644 --- a/ghost/admin/tests/integration/services/slug-generator-test.js +++ b/ghost/admin/tests/integration/services/slug-generator-test.js @@ -42,7 +42,7 @@ describe('Integration: Service: slug-generator', function () { it('calls correct endpoint and returns correct data', function (done) { let rawSlug = 'a test post'; - stubSlugEndpoint(server, 'post', rawSlug); + stubSlugEndpoint(server, 'post', 'a-test-post'); let service = this.owner.lookup('service:slug-generator');