0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

🐛 Fixed relative image URLs becoming absolute URLs on save (#10025)

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
This commit is contained in:
Rishabh Garg 2018-10-18 16:48:47 +05:30 committed by Kevin Ansfield
parent e865d2218c
commit 915d5612a1
7 changed files with 179 additions and 0 deletions

View file

@ -13,5 +13,9 @@ module.exports = {
get users() {
return require('./users');
},
get tags() {
return require('./tags');
}
};

View file

@ -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) {

View file

@ -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);
}
};

View file

@ -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]));
}
};

View file

@ -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;

View file

@ -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

View file

@ -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');
});
});
});