🐛 Fixed "unsaved changes" modal displaying when post has been saved

refs https://github.com/TryGhost/Ghost/issues/10477

The unsaved changes modal is displaying even when the post has been saved if images have been uploaded because the server is transforming absolute image urls to relative during input of the `mobiledoc` field but not transforming them back to absolute during output. The editor then thinks it's out of sync and shows the warning when trying to leave.

- `@tryghost/url-utils` has been updated with new methods for transforming URLs in mobiledoc content
- moves absolute->relative transformation from the API input serializers into the Post model
- transforms URLs in more fields for a more comprehensive transformation and fewer issues when re-configuring a site's domain
  - previously there could be problems with internal links between posts not being transformed so you could change the url config to newdomain.com but links in post content would still be pointing to olddomain.com
- updates the API post output serializers to transform all modified fields
- drops the `?absolute_urls=true` param switch from the `canary` API post output serializer so that all URLs are output as absolute
  - we're transforming more urls to relative when saving so this is necessary to ensure the unsaved changes modal is not triggered
  - the query param isn't documented and will disappear in v3
This commit is contained in:
Kevin Ansfield 2019-10-07 20:23:45 +01:00
parent fa4e68ba13
commit 32f3f9d2c3
14 changed files with 191 additions and 579 deletions

View File

@ -1,23 +1,5 @@
const _ = require('lodash');
const url = require('url');
const urlUtils = require('../../../../../../lib/url-utils');
const handleCanonicalUrl = (canonicalUrl) => {
const blogURl = urlUtils.getSiteUrl();
const isSameProtocol = url.parse(canonicalUrl).protocol === url.parse(blogURl).protocol;
const blogDomain = blogURl.replace(/^http(s?):\/\//, '').replace(/\/$/, '');
const absolute = canonicalUrl.replace(/^http(s?):\/\//, '');
// We only want to transform to a relative URL when the canonical URL matches the current
// Blog URL incl. the same protocol. This allows users to keep e.g. Facebook comments after
// a http -> https switch
if (absolute.startsWith(blogDomain) && isSameProtocol) {
return urlUtils.absoluteToRelative(canonicalUrl);
}
return canonicalUrl;
};
const handleImageUrl = (imageUrl) => {
const blogDomain = urlUtils.getSiteUrl().replace(/^http(s?):\/\//, '').replace(/\/$/, '');
const imageUrlAbsolute = imageUrl.replace(/^http(s?):\/\//, '');
@ -30,44 +12,7 @@ const handleImageUrl = (imageUrl) => {
return imageUrl;
};
const handleContentUrls = (content) => {
const blogDomain = urlUtils.getSiteUrl().replace(/^http(s?):\/\//, '').replace(/\/$/, '');
const imagePathRe = new RegExp(`(http(s?)://)?${blogDomain}/${urlUtils.STATIC_IMAGE_URL_PREFIX}`, 'g');
const matches = _.uniq(content.match(imagePathRe));
if (matches) {
matches.forEach((match) => {
const relative = urlUtils.absoluteToRelative(match);
content = content.replace(new RegExp(match, 'g'), relative);
});
}
return content;
};
const forPost = (attrs, options) => {
// make all content image URLs relative, ref: https://github.com/TryGhost/Ghost/issues/10477
if (attrs.mobiledoc) {
attrs.mobiledoc = handleContentUrls(attrs.mobiledoc);
}
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 (attrs.canonical_url) {
attrs.canonical_url = handleCanonicalUrl(attrs.canonical_url);
}
if (options && options.withRelated) {
options.withRelated.forEach((relation) => {
if (relation === 'tags' && attrs.tags) {

View File

@ -30,38 +30,28 @@ const forPost = (id, attrs, frame) => {
}
}
if (attrs.feature_image) {
attrs.feature_image = urlUtils.urlFor('image', {image: attrs.feature_image}, true);
}
if (attrs.og_image) {
attrs.og_image = urlUtils.urlFor('image', {image: attrs.og_image}, true);
}
if (attrs.twitter_image) {
attrs.twitter_image = urlUtils.urlFor('image', {image: attrs.twitter_image}, true);
}
if (attrs.canonical_url) {
attrs.canonical_url = urlUtils.relativeToAbsolute(attrs.canonical_url);
}
if (attrs.html) {
const urlOptions = {
assetsOnly: true
};
if (frame.options.absolute_urls) {
urlOptions.assetsOnly = false;
}
attrs.html = urlUtils.htmlRelativeToAbsolute(
attrs.html,
attrs.url,
urlOptions
if (attrs.mobiledoc) {
attrs.mobiledoc = urlUtils.mobiledocRelativeToAbsolute(
attrs.mobiledoc,
attrs.url
);
}
['html', 'codeinjection_head', 'codeinjection_foot'].forEach((attr) => {
if (attrs[attr]) {
attrs[attr] = urlUtils.htmlRelativeToAbsolute(
attrs[attr],
attrs.url
);
}
});
['feature_image', 'og_image', 'twitter_image', 'canonical_url'].forEach((attr) => {
if (attrs[attr]) {
attrs[attr] = urlUtils.relativeToAbsolute(attrs[attr]);
}
});
if (frame.options.columns && !frame.options.columns.includes('url')) {
delete attrs.url;
}

View File

@ -9,25 +9,27 @@ const urlsForPost = (id, attrs, options) => {
}
if (options && options.context && options.context.public && options.absolute_urls) {
if (attrs.feature_image) {
attrs.feature_image = urlUtils.urlFor('image', {image: attrs.feature_image}, true);
if (attrs.mobiledoc) {
attrs.mobiledoc = urlUtils.mobiledocRelativeToAbsolute(
attrs.mobiledoc,
attrs.url
);
}
if (attrs.og_image) {
attrs.og_image = urlUtils.urlFor('image', {image: attrs.og_image}, true);
}
['html', 'codeinjection_head', 'codeinjection_foot'].forEach((attr) => {
if (attrs[attr]) {
attrs[attr] = urlUtils.htmlRelativeToAbsolute(
attrs[attr],
attrs.url
);
}
});
if (attrs.twitter_image) {
attrs.twitter_image = urlUtils.urlFor('image', {image: attrs.twitter_image}, true);
}
if (attrs.html) {
attrs.html = urlUtils.htmlRelativeToAbsolute(attrs.html, attrs.url);
}
if (attrs.url) {
attrs.url = urlUtils.urlFor({relativeUrl: attrs.url}, true);
}
['feature_image', 'og_image', 'twitter_image', 'canonical_url', 'url'].forEach((attr) => {
if (attrs[attr]) {
attrs[attr] = urlUtils.relativeToAbsolute(attrs[attr]);
}
});
}
if (options && options.withRelated) {

View File

@ -1,23 +1,5 @@
const _ = require('lodash');
const url = require('url');
const urlUtils = require('../../../../../../lib/url-utils');
const handleCanonicalUrl = (canonicalUrl) => {
const siteUrl = urlUtils.getSiteUrl();
const isSameProtocol = url.parse(canonicalUrl).protocol === url.parse(siteUrl).protocol;
const siteDomain = siteUrl.replace(/^http(s?):\/\//, '').replace(/\/$/, '');
const absolute = canonicalUrl.replace(/^http(s?):\/\//, '');
// We only want to transform to a relative URL when the canonical URL matches the current
// Site URL incl. the same protocol. This allows users to keep e.g. Facebook comments after
// a http -> https switch
if (absolute.startsWith(siteDomain) && isSameProtocol) {
return urlUtils.absoluteToRelative(canonicalUrl);
}
return canonicalUrl;
};
const handleImageUrl = (imageUrl) => {
const siteDomain = urlUtils.getSiteUrl().replace(/^http(s?):\/\//, '').replace(/\/$/, '');
const imageUrlAbsolute = imageUrl.replace(/^http(s?):\/\//, '');
@ -30,44 +12,7 @@ const handleImageUrl = (imageUrl) => {
return imageUrl;
};
const handleContentUrls = (content) => {
const siteDomain = urlUtils.getSiteUrl().replace(/^http(s?):\/\//, '').replace(/\/$/, '');
const imagePathRe = new RegExp(`(http(s?)://)?${siteDomain}/${urlUtils.STATIC_IMAGE_URL_PREFIX}`, 'g');
const matches = _.uniq(content.match(imagePathRe));
if (matches) {
matches.forEach((match) => {
const relative = urlUtils.absoluteToRelative(match);
content = content.replace(new RegExp(match, 'g'), relative);
});
}
return content;
};
const forPost = (attrs, options) => {
// make all content image URLs relative, ref: https://github.com/TryGhost/Ghost/issues/10477
if (attrs.mobiledoc) {
attrs.mobiledoc = handleContentUrls(attrs.mobiledoc);
}
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 (attrs.canonical_url) {
attrs.canonical_url = handleCanonicalUrl(attrs.canonical_url);
}
if (options && options.withRelated) {
options.withRelated.forEach((relation) => {
if (relation === 'tags' && attrs.tags) {

View File

@ -30,38 +30,38 @@ const forPost = (id, attrs, frame) => {
}
}
if (attrs.feature_image) {
attrs.feature_image = urlUtils.urlFor('image', {image: attrs.feature_image}, true);
const urlOptions = {};
// v2 only transforms asset URLS, v3 will transform all urls so that
// input/output transformations are balanced and all URLs are absolute
if (!frame.options.absolute_urls) {
urlOptions.assetsOnly = true;
}
if (attrs.og_image) {
attrs.og_image = urlUtils.urlFor('image', {image: attrs.og_image}, true);
}
if (attrs.twitter_image) {
attrs.twitter_image = urlUtils.urlFor('image', {image: attrs.twitter_image}, true);
}
if (attrs.canonical_url) {
attrs.canonical_url = urlUtils.relativeToAbsolute(attrs.canonical_url);
}
if (attrs.html) {
const urlOptions = {
assetsOnly: true
};
if (frame.options.absolute_urls) {
urlOptions.assetsOnly = false;
}
attrs.html = urlUtils.htmlRelativeToAbsolute(
attrs.html,
if (attrs.mobiledoc) {
attrs.mobiledoc = urlUtils.mobiledocRelativeToAbsolute(
attrs.mobiledoc,
attrs.url,
urlOptions
);
}
['html', 'codeinjection_head', 'codeinjection_foot'].forEach((attr) => {
if (attrs[attr]) {
attrs[attr] = urlUtils.htmlRelativeToAbsolute(
attrs[attr],
attrs.url,
urlOptions
);
}
});
['feature_image', 'og_image', 'twitter_image', 'canonical_url'].forEach((attr) => {
if (attrs[attr]) {
attrs[attr] = urlUtils.relativeToAbsolute(attrs[attr], attrs.url, urlOptions);
}
});
if (frame.options.columns && !frame.options.columns.includes('url')) {
delete attrs.url;
}

View File

@ -11,6 +11,7 @@ const config = require('../config');
const settingsCache = require('../services/settings/cache');
const converters = require('../lib/mobiledoc/converters');
const relations = require('./relations');
const urlUtils = require('../lib/url-utils');
const MOBILEDOC_REVISIONS_COUNT = 10;
const ALL_STATUSES = ['published', 'draft', 'scheduled'];
@ -349,6 +350,39 @@ Post = ghostBookshelf.Model.extend({
this.set('mobiledoc', JSON.stringify(converters.mobiledocConverter.blankStructure()));
}
// ensure all URLs are stored as relative
// note: html is not necessary to change because it's a generated later from mobiledoc
const urlTransformMap = {
mobiledoc: 'mobiledocAbsoluteToRelative',
custom_excerpt: 'htmlAbsoluteToRelative',
codeinjection_head: 'htmlAbsoluteToRelative',
codeinjection_foot: 'htmlAbsoluteToRelative',
feature_image: 'absoluteToRelative',
og_image: 'absoluteToRelative',
twitter_image: 'absoluteToRelative',
canonical_url: {
method: 'absoluteToRelative',
options: {
ignoreProtocol: false
}
}
};
Object.entries(urlTransformMap).forEach(([attr, transform]) => {
let method = transform;
let options = {};
if (typeof transform === 'object') {
method = transform.method;
options = transform.options || {};
}
if (this.hasChanged(attr) && this.get(attr)) {
const transformedValue = urlUtils[method](this.get(attr), options);
this.set(attr, transformedValue);
}
});
// CASE: mobiledoc has changed, generate html
// CASE: html is null, but mobiledoc exists (only important for migrations & importing)
if (this.hasChanged('mobiledoc') || (!this.get('html') && (options.migrating || options.importing))) {

View File

@ -1134,6 +1134,44 @@ describe('Post Model', function () {
done();
}).catch(done);
});
it('transforms absolute urls to relative', function (done) {
const post = {
title: 'Absolute->Relative URL Transform Test',
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"http://127.0.0.1:2369/content/images/card.jpg"}]],"markups":[["a",["href","http://127.0.0.1:2369/test"]]],"sections":[[1,"p",[[0,[0],1,"Testing"]]],[10,0]]}',
custom_excerpt: 'Testing <a href="http://127.0.0.1:2369/internal">links</a> in custom excerpts',
codeinjection_head: '<script src="http://127.0.0.1:2369/assets/head.js"></script>',
codeinjection_foot: '<script src="http://127.0.0.1:2369/assets/foot.js"></script>',
feature_image: 'http://127.0.0.1:2369/content/images/feature.png',
og_image: 'http://127.0.0.1:2369/content/images/og.png',
twitter_image: 'http://127.0.0.1:2369/content/images/twitter.png',
canonical_url: 'http://127.0.0.1:2369/canonical'
};
models.Post.add(post, context).then((createdPost) => {
createdPost.get('mobiledoc').should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"/content/images/card.jpg"}]],"markups":[["a",["href","/test"]]],"sections":[[1,"p",[[0,[0],1,"Testing"]]],[10,0]]}');
createdPost.get('html').should.equal('<p><a href="/test">Testing</a></p><!--kg-card-begin: image--><figure class="kg-card kg-image-card"><img src="/content/images/card.jpg" class="kg-image"></figure><!--kg-card-end: image-->');
createdPost.get('custom_excerpt').should.equal('Testing <a href="/internal">links</a> in custom excerpts');
createdPost.get('codeinjection_head').should.equal('<script src="/assets/head.js"></script>');
createdPost.get('codeinjection_foot').should.equal('<script src="/assets/foot.js"></script>');
createdPost.get('feature_image').should.equal('/content/images/feature.png');
createdPost.get('og_image').should.equal('/content/images/og.png');
createdPost.get('twitter_image').should.equal('/content/images/twitter.png');
createdPost.get('canonical_url').should.equal('/canonical');
// ensure canonical_url is not transformed when protocol does not match
return createdPost.save({
canonical_url: 'https://127.0.0.1:2369/https-internal',
// sanity check for general absolute->relative transform during edits
feature_image: 'http://127.0.0.1:2369/content/images/updated_feature.png'
});
}).then((updatedPost) => {
updatedPost.get('canonical_url').should.equal('https://127.0.0.1:2369/https-internal');
updatedPost.get('feature_image').should.equal('/content/images/updated_feature.png');
done();
}).catch(done);
});
});
describe('destroy', function () {

View File

@ -221,174 +221,6 @@ describe('Unit: canary/utils/serializers/input/posts', function () {
});
describe('edit', function () {
describe('Ensure relative urls are returned for standard image urls', function () {
describe('no subdir', function () {
let sandbox;
after(function () {
sandbox.restore();
});
before(function () {
sandbox = sinon.createSandbox();
urlUtils.stubUrlUtils({url: 'https://mysite.com'}, sandbox);
});
it('when mobiledoc contains an absolute URL to image', function () {
const apiConfig = {};
const frame = {
options: {
context: {
user: 0,
api_key: {
id: 1,
type: 'content'
}
}
},
data: {
posts: [
{
id: 'id1',
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"https://mysite.com/content/images/2019/02/image.jpg"}]]}'
}
]
}
};
serializers.input.posts.edit(apiConfig, frame);
let postData = frame.data.posts[0];
postData.mobiledoc.should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"/content/images/2019/02/image.jpg"}]]}');
});
it('when mobiledoc contains multiple absolute URLs to images with different protocols', function () {
const apiConfig = {};
const frame = {
options: {
context: {
user: 0,
api_key: {
id: 1,
type: 'content'
}
}
},
data: {
posts: [
{
id: 'id1',
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"https://mysite.com/content/images/2019/02/image.jpg"}],["image",{"src":"http://mysite.com/content/images/2019/02/image.png"}]]'
}
]
}
};
serializers.input.posts.edit(apiConfig, frame);
let postData = frame.data.posts[0];
postData.mobiledoc.should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"/content/images/2019/02/image.jpg"}],["image",{"src":"/content/images/2019/02/image.png"}]]');
});
it('when blog url is without subdir', function () {
const apiConfig = {};
const frame = {
options: {
context: {
user: 0,
api_key: {
id: 1,
type: 'content'
}
},
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');
});
});
describe('with subdir', function () {
let sandbox;
after(function () {
sandbox.restore();
});
before(function () {
sandbox = sinon.createSandbox();
urlUtils.stubUrlUtils({url: 'https://mysite.com/blog'}, sandbox);
});
it('when blog url is with subdir', function () {
const apiConfig = {};
const frame = {
options: {
context: {
user: 0,
api_key: {
id: 1,
type: 'content'
}
},
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');
});
});
});
describe('Ensure html to mobiledoc conversion', function () {
it('no transformation when no html source option provided', function () {
const apiConfig = {};

View File

@ -9,7 +9,9 @@ describe('Unit: canary/utils/serializers/output/utils/url', function () {
beforeEach(function () {
sinon.stub(urlService, 'getUrlByResourceId').returns('getUrlByResourceId');
sinon.stub(urlUtils, 'urlFor').returns('urlFor');
sinon.stub(urlUtils, 'relativeToAbsolute').returns('relativeToAbsolute');
sinon.stub(urlUtils, 'htmlRelativeToAbsolute').returns({html: sinon.stub()});
sinon.stub(urlUtils, 'mobiledocRelativeToAbsolute').returns({});
});
afterEach(function () {
@ -28,21 +30,32 @@ describe('Unit: canary/utils/serializers/output/utils/url', function () {
it('meta & models & relations', function () {
const post = pageModel(testUtils.DataGenerator.forKnex.createPost({
id: 'id1',
feature_image: 'value'
mobiledoc: '{}',
html: 'html',
custom_excerpt: 'customExcerpt',
codeinjection_head: 'codeinjectionHead',
codeinjection_foot: 'codeinjectionFoot',
feature_image: 'featureImage',
og_image: 'ogImage',
twitter_image: 'twitterImage',
canonical_url: 'canonicalUrl'
}));
urlUtil.forPost(post.id, post, {options: {}});
post.hasOwnProperty('url').should.be.true();
urlUtils.urlFor.callCount.should.eql(1);
urlUtils.urlFor.getCall(0).args.should.eql(['image', {image: 'value'}, true]);
// feature_image, og_image, twitter_image, canonical_url
urlUtils.relativeToAbsolute.callCount.should.eql(4);
urlUtils.htmlRelativeToAbsolute.callCount.should.eql(1);
// mobiledoc
urlUtils.mobiledocRelativeToAbsolute.callCount.should.eql(1);
// html, codeinjection_head, codeinjection_foot
urlUtils.htmlRelativeToAbsolute.callCount.should.eql(3);
urlUtils.htmlRelativeToAbsolute.getCall(0).args.should.eql([
'## markdown',
'getUrlByResourceId',
{assetsOnly: true}
'html',
'getUrlByResourceId'
]);
urlService.getUrlByResourceId.callCount.should.eql(1);

View File

@ -13,45 +13,5 @@ describe('Unit: v2/utils/serializers/input/utils/url', function () {
afterEach(function () {
sinon.restore();
});
it('should transform canonical_url when protocol and domain match', function () {
const attrs = {
canonical_url: 'https://blogurl.com/hello-world'
};
url.forPost(attrs, {});
should.equal(attrs.canonical_url, '/hello-world');
});
it('should transform canonical_url when protocol and domain match with backslash in the end', function () {
const attrs = {
canonical_url: 'https://blogurl.com/hello-world/'
};
url.forPost(attrs, {});
should.equal(attrs.canonical_url, '/hello-world/');
});
it('should not transform canonical_url when different domains', function () {
const attrs = {
canonical_url: 'http://ghost.org/no-transform'
};
url.forPost(attrs, {});
should.equal(attrs.canonical_url, 'http://ghost.org/no-transform');
});
it('should not transform canonical_url when different protocols', function () {
const attrs = {
canonical_url: 'http://blogurl.com/no-transform'
};
url.forPost(attrs, {});
should.equal(attrs.canonical_url, 'http://blogurl.com/no-transform');
});
});
});

View File

@ -221,174 +221,6 @@ describe('Unit: v2/utils/serializers/input/posts', function () {
});
describe('edit', function () {
describe('Ensure relative urls are returned for standard image urls', function () {
describe('no subdir', function () {
let sandbox;
after(function () {
sandbox.restore();
});
before(function () {
sandbox = sinon.createSandbox();
urlUtils.stubUrlUtils({url: 'https://mysite.com'}, sandbox);
});
it('when mobiledoc contains an absolute URL to image', function () {
const apiConfig = {};
const frame = {
options: {
context: {
user: 0,
api_key: {
id: 1,
type: 'content'
}
}
},
data: {
posts: [
{
id: 'id1',
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"https://mysite.com/content/images/2019/02/image.jpg"}]]}'
}
]
}
};
serializers.input.posts.edit(apiConfig, frame);
let postData = frame.data.posts[0];
postData.mobiledoc.should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"/content/images/2019/02/image.jpg"}]]}');
});
it('when mobiledoc contains multiple absolute URLs to images with different protocols', function () {
const apiConfig = {};
const frame = {
options: {
context: {
user: 0,
api_key: {
id: 1,
type: 'content'
}
}
},
data: {
posts: [
{
id: 'id1',
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"https://mysite.com/content/images/2019/02/image.jpg"}],["image",{"src":"http://mysite.com/content/images/2019/02/image.png"}]]'
}
]
}
};
serializers.input.posts.edit(apiConfig, frame);
let postData = frame.data.posts[0];
postData.mobiledoc.should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"/content/images/2019/02/image.jpg"}],["image",{"src":"/content/images/2019/02/image.png"}]]');
});
it('when blog url is without subdir', function () {
const apiConfig = {};
const frame = {
options: {
context: {
user: 0,
api_key: {
id: 1,
type: 'content'
}
},
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');
});
});
describe('with subdir', function () {
let sandbox;
after(function () {
sandbox.restore();
});
before(function () {
sandbox = sinon.createSandbox();
urlUtils.stubUrlUtils({url: 'https://mysite.com/blog'}, sandbox);
});
it('when blog url is with subdir', function () {
const apiConfig = {};
const frame = {
options: {
context: {
user: 0,
api_key: {
id: 1,
type: 'content'
}
},
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');
});
});
});
describe('Ensure html to mobiledoc conversion', function () {
it('no transformation when no html source option provided', function () {
const apiConfig = {};

View File

@ -9,7 +9,9 @@ describe('Unit: v2/utils/serializers/output/utils/url', function () {
beforeEach(function () {
sinon.stub(urlService, 'getUrlByResourceId').returns('getUrlByResourceId');
sinon.stub(urlUtils, 'urlFor').returns('urlFor');
sinon.stub(urlUtils, 'relativeToAbsolute').returns('relativeToAbsolute');
sinon.stub(urlUtils, 'htmlRelativeToAbsolute').returns({html: sinon.stub()});
sinon.stub(urlUtils, 'mobiledocRelativeToAbsolute').returns({});
});
afterEach(function () {
@ -28,19 +30,31 @@ describe('Unit: v2/utils/serializers/output/utils/url', function () {
it('meta & models & relations', function () {
const post = pageModel(testUtils.DataGenerator.forKnex.createPost({
id: 'id1',
feature_image: 'value'
mobiledoc: '{}',
html: 'html',
custom_excerpt: 'customExcerpt',
codeinjection_head: 'codeinjectionHead',
codeinjection_foot: 'codeinjectionFoot',
feature_image: 'featureImage',
og_image: 'ogImage',
twitter_image: 'twitterImage',
canonical_url: 'canonicalUrl'
}));
urlUtil.forPost(post.id, post, {options: {}});
post.hasOwnProperty('url').should.be.true();
urlUtils.urlFor.callCount.should.eql(1);
urlUtils.urlFor.getCall(0).args.should.eql(['image', {image: 'value'}, true]);
// feature_image, og_image, twitter_image, canonical_url
urlUtils.relativeToAbsolute.callCount.should.eql(4);
urlUtils.htmlRelativeToAbsolute.callCount.should.eql(1);
// mobiledoc
urlUtils.mobiledocRelativeToAbsolute.callCount.should.eql(1);
// html, codeinjection_head, codeinjection_foot
urlUtils.htmlRelativeToAbsolute.callCount.should.eql(3);
urlUtils.htmlRelativeToAbsolute.getCall(0).args.should.eql([
'## markdown',
'html',
'getUrlByResourceId',
{assetsOnly: true}
]);

View File

@ -45,7 +45,7 @@
"@tryghost/members-ssr": "0.6.0",
"@tryghost/social-urls": "0.1.2",
"@tryghost/string": "^0.1.3",
"@tryghost/url-utils": "0.6.0",
"@tryghost/url-utils": "0.6.1",
"ajv": "6.10.2",
"amperize": "0.6.0",
"analytics-node": "3.3.0",

View File

@ -297,17 +297,15 @@
dependencies:
unidecode "^0.1.8"
"@tryghost/url-utils@0.6.0":
version "0.6.0"
resolved "https://registry.yarnpkg.com/@tryghost/url-utils/-/url-utils-0.6.0.tgz#12ad31781c03cfb6cd7eedbc8b4c690d4ef3e4d1"
integrity sha512-BN9l448lW2ykE0/QIeCijs1eVFGPtta1JCol6X4jzoqzy/hjL/YyGKj5ugLVOX+Fjl9Y/sblF6Yac+UzoaHkiA==
"@tryghost/url-utils@0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@tryghost/url-utils/-/url-utils-0.6.1.tgz#cb3a1c199ff855a131588258e43bbcb1599b856c"
integrity sha512-FfHc/OoMqKvKbQ8Rir09wkeFZPV7FZMfmnKaVFOUoJPuULetFmfS8yP0WNBHNfGj197aT+JyyJH2QpFokvPprQ==
dependencies:
cheerio "0.22.0"
moment "2.24.0"
moment-timezone "0.5.23"
remark-parse "^7.0.1"
remark-stringify "^7.0.3"
unified "^8.4.0"
remark "^11.0.1"
unist-util-visit "^2.0.0"
"@types/bluebird@^3.5.26", "@types/bluebird@^3.5.27":
@ -7310,7 +7308,7 @@ regexpp@^2.0.1:
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
remark-parse@^7.0.1:
remark-parse@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-7.0.1.tgz#0c13d67e0d7b82c2ad2d8b6604ec5fae6c333c2b"
integrity sha512-WOZLa545jYXtSy+txza6ACudKWByQac4S2DmGk+tAGO/3XnVTOxwyCIxB7nTcLlk8Aayhcuf3cV1WV6U6L7/DQ==
@ -7331,7 +7329,7 @@ remark-parse@^7.0.1:
vfile-location "^2.0.0"
xtend "^4.0.1"
remark-stringify@^7.0.3:
remark-stringify@^7.0.0:
version "7.0.3"
resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-7.0.3.tgz#9221e9770b0b395af83a0d5881a44b6fcb9d0a2a"
integrity sha512-+jgmjNjm2kR7y2Ns1BATXRlFr+iQ7sDcpSgytfU77nkw7UCd5yJNArSxB3MU3Uul7HuyYNTCjetoGfy8xLia1A==
@ -7351,6 +7349,15 @@ remark-stringify@^7.0.3:
unherit "^1.0.4"
xtend "^4.0.1"
remark@^11.0.1:
version "11.0.1"
resolved "https://registry.yarnpkg.com/remark/-/remark-11.0.1.tgz#3c16e1ed84c78a661299991bb8d5fa7ee5d18e3c"
integrity sha512-Fl2AvN+yU6sOBAjUz3xNC5iEvLkXV8PZicLOOLifjU8uKGusNvhHfGRCfETsqyvRHZ24JXqEyDY4hRLhoUd30A==
dependencies:
remark-parse "^7.0.0"
remark-stringify "^7.0.0"
unified "^8.2.0"
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@ -8709,7 +8716,7 @@ unidecode@^0.1.8:
resolved "https://registry.yarnpkg.com/unidecode/-/unidecode-0.1.8.tgz#efbb301538bc45246a9ac8c559d72f015305053e"
integrity sha1-77swFTi8RSRqmsjFWdcvAVMFBT4=
unified@^8.4.0:
unified@^8.2.0:
version "8.4.1"
resolved "https://registry.yarnpkg.com/unified/-/unified-8.4.1.tgz#99bd0393f58a139eaa51832cfbcc0e7f6573a1e1"
integrity sha512-YPj/uIIZSO7mMIZQj/5Z3hDl4lshWYRQGs5TgUCjHTVdklUWH+O94mK5Cy77SEcmEUwGhnUcudMuH/zIwporqw==