From 915d5612a1691121f6485bc64beb2eb62e5995ff Mon Sep 17 00:00:00 2001 From: Rishabh Garg Date: Thu, 18 Oct 2018 16:48:47 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20relative=20image=20URLs?= =?UTF-8?q?=20becoming=20absolute=20URLs=20on=20save=20(#10025)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #10024 - Updated input serializers for posts/tags/users to handle absolute urls conversion ------- 1. Ghost stores relative images urls 2. API V2 returns images with absolute urls 3. Ghost-Admin sends absolute urls back on any save e.g. update user **Current behavior**: This will override the relative image path in db to absolute, which in turn won't get updated in future if domain or protocol changes for e.g. **Fix**: On save/update, input serializers converts any absolute image url paths back to relative if the base URL from image fields matches the configured URL --- .../api/v2/utils/serializers/input/index.js | 4 + .../api/v2/utils/serializers/input/posts.js | 3 + .../api/v2/utils/serializers/input/tags.js | 14 +++ .../api/v2/utils/serializers/input/users.js | 3 + .../v2/utils/serializers/input/utils/url.js | 67 ++++++++++++++ core/server/services/url/utils.js | 1 + .../v2/utils/serializers/input/posts_spec.js | 87 +++++++++++++++++++ 7 files changed, 179 insertions(+) create mode 100644 core/server/api/v2/utils/serializers/input/tags.js create mode 100644 core/server/api/v2/utils/serializers/input/utils/url.js diff --git a/core/server/api/v2/utils/serializers/input/index.js b/core/server/api/v2/utils/serializers/input/index.js index fe2b070e5c..02bca0aaaa 100644 --- a/core/server/api/v2/utils/serializers/input/index.js +++ b/core/server/api/v2/utils/serializers/input/index.js @@ -13,5 +13,9 @@ module.exports = { get users() { return require('./users'); + }, + + get tags() { + return require('./tags'); } }; diff --git a/core/server/api/v2/utils/serializers/input/posts.js b/core/server/api/v2/utils/serializers/input/posts.js index 9fb2e6e6da..2db60c3edf 100644 --- a/core/server/api/v2/utils/serializers/input/posts.js +++ b/core/server/api/v2/utils/serializers/input/posts.js @@ -1,5 +1,6 @@ const _ = require('lodash'); const debug = require('ghost-ignition').debug('api:v2:utils:serializers:input:posts'); +const url = require('./utils/url'); module.exports = { all(apiConfig, frame) { @@ -79,6 +80,8 @@ module.exports = { }); } } + + frame.data.posts[0] = url.forPost(Object.assign({}, frame.data.posts[0]), frame.options); }, edit(apiConfig, frame) { diff --git a/core/server/api/v2/utils/serializers/input/tags.js b/core/server/api/v2/utils/serializers/input/tags.js new file mode 100644 index 0000000000..97bf0a34ed --- /dev/null +++ b/core/server/api/v2/utils/serializers/input/tags.js @@ -0,0 +1,14 @@ +const debug = require('ghost-ignition').debug('api:v2:utils:serializers:input:tags'); +const url = require('./utils/url'); + +module.exports = { + add(apiConfig, frame) { + debug('add'); + frame.data.tags[0] = url.forTag(Object.assign({}, frame.data.tags[0])); + }, + + edit(apiConfig, frame) { + debug('edit'); + this.add(apiConfig, frame); + } +}; diff --git a/core/server/api/v2/utils/serializers/input/users.js b/core/server/api/v2/utils/serializers/input/users.js index 6b03cad86c..bd6d2262ba 100644 --- a/core/server/api/v2/utils/serializers/input/users.js +++ b/core/server/api/v2/utils/serializers/input/users.js @@ -1,4 +1,5 @@ const debug = require('ghost-ignition').debug('api:v2:utils:serializers:input:users'); +const url = require('./utils/url'); module.exports = { read(apiConfig, frame) { @@ -19,5 +20,7 @@ module.exports = { if (frame.data.users[0].password) { delete frame.data.users[0].password; } + + frame.data.users[0] = url.forUser(Object.assign({}, frame.data.users[0])); } }; diff --git a/core/server/api/v2/utils/serializers/input/utils/url.js b/core/server/api/v2/utils/serializers/input/utils/url.js new file mode 100644 index 0000000000..551df8387d --- /dev/null +++ b/core/server/api/v2/utils/serializers/input/utils/url.js @@ -0,0 +1,67 @@ +const {absoluteToRelative, getBlogUrl, STATIC_IMAGE_URL_PREFIX} = require('../../../../../../services/url/utils'); + +const handleImageUrl = (imageUrl) => { + const blogUrl = getBlogUrl().replace(/^http(s?):\/\//, '').replace(/\/$/, ''); + const imageUrlAbsolute = imageUrl.replace(/^http(s?):\/\//, ''); + const imagePathRe = new RegExp(`^${blogUrl}/${STATIC_IMAGE_URL_PREFIX}`); + if (imagePathRe.test(imageUrlAbsolute)) { + return absoluteToRelative(imageUrl); + } + return imageUrl; +}; + +const forPost = (attrs, options) => { + if (attrs.feature_image) { + attrs.feature_image = handleImageUrl(attrs.feature_image); + } + + if (attrs.og_image) { + attrs.og_image = handleImageUrl(attrs.og_image); + } + + if (attrs.twitter_image) { + attrs.twitter_image = handleImageUrl(attrs.twitter_image); + } + + if (options && options.withRelated) { + options.withRelated.forEach((relation) => { + if (relation === 'tags' && attrs.tags) { + attrs.tags = attrs.tags.map(tag => forTag(tag)); + } + + if (relation === 'author' && attrs.author) { + attrs.author = forUser(attrs.author, options); + } + + if (relation === 'authors' && attrs.authors) { + attrs.authors = attrs.authors.map(author => forUser(author, options)); + } + }); + } + + return attrs; +}; + +const forUser = (attrs) => { + if (attrs.profile_image) { + attrs.profile_image = handleImageUrl(attrs.profile_image); + } + + if (attrs.cover_image) { + attrs.cover_image = handleImageUrl(attrs.cover_image); + } + + return attrs; +}; + +const forTag = (attrs) => { + if (attrs.feature_image) { + attrs.feature_image = handleImageUrl(attrs.feature_image); + } + + return attrs; +}; + +module.exports.forPost = forPost; +module.exports.forUser = forUser; +module.exports.forTag = forTag; diff --git a/core/server/services/url/utils.js b/core/server/services/url/utils.js index c6d5262328..06fc120198 100644 --- a/core/server/services/url/utils.js +++ b/core/server/services/url/utils.js @@ -465,6 +465,7 @@ module.exports.redirect301 = redirect301; module.exports.createUrl = createUrl; module.exports.deduplicateDoubleSlashes = deduplicateDoubleSlashes; module.exports.getApiPath = getApiPath; +module.exports.getBlogUrl = getBlogUrl; /** * If you request **any** image in Ghost, it get's served via diff --git a/core/test/unit/api/v2/utils/serializers/input/posts_spec.js b/core/test/unit/api/v2/utils/serializers/input/posts_spec.js index 4625e0d8a0..d29ff7fcf7 100644 --- a/core/test/unit/api/v2/utils/serializers/input/posts_spec.js +++ b/core/test/unit/api/v2/utils/serializers/input/posts_spec.js @@ -1,5 +1,6 @@ const should = require('should'); const serializers = require('../../../../../../../server/api/v2/utils/serializers'); +const configUtils = require('../../../../../../utils/configUtils'); describe('Unit: v2/utils/serializers/input/posts', function () { it('default', function () { @@ -75,4 +76,90 @@ describe('Unit: v2/utils/serializers/input/posts', function () { serializers.input.posts.all(apiConfig, frame); frame.options.filter.should.eql('page:false'); }); + + describe('Ensure relative urls are returned for standard image urls', function () { + after(function () { + configUtils.restore(); + }); + + it('when blog url is without subdir', function () { + configUtils.set({url: 'https://mysite.com'}); + const apiConfig = {}; + const frame = { + options: { + context: { + user: 0, + api_key_id: 1 + }, + withRelated: ['tags', 'authors'] + }, + data: { + posts: [ + { + id: 'id1', + feature_image: 'https://mysite.com/content/images/image.jpg', + og_image: 'https://mysite.com/mycustomstorage/images/image.jpg', + twitter_image: 'https://mysite.com/blog/content/images/image.jpg', + tags: [{ + id: 'id3', + feature_image: 'http://mysite.com/content/images/image.jpg' + }], + authors: [{ + id: 'id4', + name: 'Ghosty', + profile_image: 'https://somestorage.com/blog/images/image.jpg' + }] + } + ] + } + }; + serializers.input.posts.edit(apiConfig, frame); + let postData = frame.data.posts[0]; + postData.feature_image.should.eql('/content/images/image.jpg'); + postData.og_image.should.eql('https://mysite.com/mycustomstorage/images/image.jpg'); + postData.twitter_image.should.eql('https://mysite.com/blog/content/images/image.jpg'); + postData.tags[0].feature_image.should.eql('/content/images/image.jpg'); + postData.authors[0].profile_image.should.eql('https://somestorage.com/blog/images/image.jpg'); + }); + + it('when blog url is with subdir', function () { + configUtils.set({url: 'https://mysite.com/blog'}); + const apiConfig = {}; + const frame = { + options: { + context: { + user: 0, + api_key_id: 1 + }, + withRelated: ['tags', 'authors'] + }, + data: { + posts: [ + { + id: 'id1', + feature_image: 'https://mysite.com/blog/content/images/image.jpg', + og_image: 'https://mysite.com/content/images/image.jpg', + twitter_image: 'https://mysite.com/mycustomstorage/images/image.jpg', + tags: [{ + id: 'id3', + feature_image: 'http://mysite.com/blog/mycustomstorage/content/images/image.jpg' + }], + authors: [{ + id: 'id4', + name: 'Ghosty', + profile_image: 'https://somestorage.com/blog/content/images/image.jpg' + }] + } + ] + } + }; + serializers.input.posts.edit(apiConfig, frame); + let postData = frame.data.posts[0]; + postData.feature_image.should.eql('/blog/content/images/image.jpg'); + postData.og_image.should.eql('https://mysite.com/content/images/image.jpg'); + postData.twitter_image.should.eql('https://mysite.com/mycustomstorage/images/image.jpg'); + postData.tags[0].feature_image.should.eql('http://mysite.com/blog/mycustomstorage/content/images/image.jpg'); + postData.authors[0].profile_image.should.eql('https://somestorage.com/blog/content/images/image.jpg'); + }); + }); });