mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-27 18:52:14 +03:00
🐛 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:
parent
fa4e68ba13
commit
32f3f9d2c3
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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))) {
|
||||
|
@ -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 () {
|
||||
|
@ -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 = {};
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 = {};
|
||||
|
@ -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}
|
||||
]);
|
||||
|
@ -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",
|
||||
|
27
yarn.lock
27
yarn.lock
@ -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==
|
||||
|
Loading…
Reference in New Issue
Block a user