diff --git a/core/server/api/canary/utils/serializers/input/utils/url.js b/core/server/api/canary/utils/serializers/input/utils/url.js index 7968b28001..bec5eeb5d3 100644 --- a/core/server/api/canary/utils/serializers/input/utils/url.js +++ b/core/server/api/canary/utils/serializers/input/utils/url.js @@ -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) { diff --git a/core/server/api/canary/utils/serializers/output/utils/url.js b/core/server/api/canary/utils/serializers/output/utils/url.js index 892585c63c..5e66a7bacb 100644 --- a/core/server/api/canary/utils/serializers/output/utils/url.js +++ b/core/server/api/canary/utils/serializers/output/utils/url.js @@ -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; } diff --git a/core/server/api/v0.1/decorators/urls.js b/core/server/api/v0.1/decorators/urls.js index aeb4e9f0cc..f288eda078 100644 --- a/core/server/api/v0.1/decorators/urls.js +++ b/core/server/api/v0.1/decorators/urls.js @@ -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) { diff --git a/core/server/api/v2/utils/serializers/input/utils/url.js b/core/server/api/v2/utils/serializers/input/utils/url.js index 1bba9a6fff..24272e8ee1 100644 --- a/core/server/api/v2/utils/serializers/input/utils/url.js +++ b/core/server/api/v2/utils/serializers/input/utils/url.js @@ -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) { diff --git a/core/server/api/v2/utils/serializers/output/utils/url.js b/core/server/api/v2/utils/serializers/output/utils/url.js index b688bee3a4..793a20879b 100644 --- a/core/server/api/v2/utils/serializers/output/utils/url.js +++ b/core/server/api/v2/utils/serializers/output/utils/url.js @@ -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; } diff --git a/core/server/models/post.js b/core/server/models/post.js index 59b99b692f..3f46723aa0 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -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))) { diff --git a/core/test/regression/models/model_posts_spec.js b/core/test/regression/models/model_posts_spec.js index 3b1b17c6c7..3f01f06ca0 100644 --- a/core/test/regression/models/model_posts_spec.js +++ b/core/test/regression/models/model_posts_spec.js @@ -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 links in custom excerpts', + codeinjection_head: '', + codeinjection_foot: '', + 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('

Testing

'); + createdPost.get('custom_excerpt').should.equal('Testing links in custom excerpts'); + createdPost.get('codeinjection_head').should.equal(''); + createdPost.get('codeinjection_foot').should.equal(''); + 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 () { diff --git a/core/test/unit/api/canary/utils/serializers/input/posts_spec.js b/core/test/unit/api/canary/utils/serializers/input/posts_spec.js index 0e93a70446..8943e97eae 100644 --- a/core/test/unit/api/canary/utils/serializers/input/posts_spec.js +++ b/core/test/unit/api/canary/utils/serializers/input/posts_spec.js @@ -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 = {}; diff --git a/core/test/unit/api/canary/utils/serializers/output/utils/url_spec.js b/core/test/unit/api/canary/utils/serializers/output/utils/url_spec.js index 9649030714..3078ab416d 100644 --- a/core/test/unit/api/canary/utils/serializers/output/utils/url_spec.js +++ b/core/test/unit/api/canary/utils/serializers/output/utils/url_spec.js @@ -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); diff --git a/core/test/unit/api/shared/serializers/input/utils/url_spec.js b/core/test/unit/api/shared/serializers/input/utils/url_spec.js index 78af13c36e..bafbd960a0 100644 --- a/core/test/unit/api/shared/serializers/input/utils/url_spec.js +++ b/core/test/unit/api/shared/serializers/input/utils/url_spec.js @@ -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'); - }); }); }); 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 257e6498f7..32d37c6fb7 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 @@ -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 = {}; diff --git a/core/test/unit/api/v2/utils/serializers/output/utils/url_spec.js b/core/test/unit/api/v2/utils/serializers/output/utils/url_spec.js index e9c16643ae..bb8e988f3a 100644 --- a/core/test/unit/api/v2/utils/serializers/output/utils/url_spec.js +++ b/core/test/unit/api/v2/utils/serializers/output/utils/url_spec.js @@ -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} ]); diff --git a/package.json b/package.json index 0f1054d90d..8842530296 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index ab4edfa6f9..0dea3688c4 100644 --- a/yarn.lock +++ b/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==