Skip to content

Commit

Permalink
Returned relative paths in html for Content API V2 by default (#10091)
Browse files Browse the repository at this point in the history
refs #10083

- you can send `?absolute_urls=true` and Ghost will also transform the paths in the content (this is optional/conditional)
  • Loading branch information
kirrg001 committed Nov 5, 2018
1 parent 94b3735 commit 1b9c61e
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 24 deletions.
3 changes: 2 additions & 1 deletion core/server/api/v2/pages.js
Expand Up @@ -39,7 +39,8 @@ module.exports = {
'fields',
'status',
'formats',
'debug'
'debug',
'absolute_urls'
],
data: [
'id',
Expand Down
6 changes: 4 additions & 2 deletions core/server/api/v2/posts.js
Expand Up @@ -16,7 +16,8 @@ module.exports = {
'limit',
'order',
'page',
'debug'
'debug',
'absolute_urls'
],
validation: {
options: {
Expand All @@ -42,7 +43,8 @@ module.exports = {
'fields',
'status',
'formats',
'debug'
'debug',
'absolute_urls'
],
data: [
'id',
Expand Down
28 changes: 20 additions & 8 deletions core/server/api/v2/utils/serializers/output/utils/url.js
@@ -1,23 +1,35 @@
const urlService = require('../../../../../../services/url');
const {urlFor, makeAbsoluteUrls} = require('../../../../../../services/url/utils');

const forPost = (id, attrs, options) => {
attrs.url = urlService.getUrlByResourceId(id, {absolute: true});

if (attrs.feature_image) {
attrs.feature_image = urlFor('image', {image: attrs.feature_image}, true);
attrs.feature_image = urlService.utils.urlFor('image', {image: attrs.feature_image}, true);
}

if (attrs.og_image) {
attrs.og_image = urlFor('image', {image: attrs.og_image}, true);
attrs.og_image = urlService.utils.urlFor('image', {image: attrs.og_image}, true);
}

if (attrs.twitter_image) {
attrs.twitter_image = urlFor('image', {image: attrs.twitter_image}, true);
attrs.twitter_image = urlService.utils.urlFor('image', {image: attrs.twitter_image}, true);
}

if (attrs.html) {
attrs.html = makeAbsoluteUrls(attrs.html, urlFor('home', true), attrs.url).html();
const urlOptions = {
assetsOnly: true
};

if (options.absolute_urls) {
urlOptions.assetsOnly = false;
}

attrs.html = urlService.utils.makeAbsoluteUrls(
attrs.html,
urlService.utils.urlFor('home', true),
attrs.url,
urlOptions
).html();
}

if (options.columns && !options.columns.includes('url')) {
Expand Down Expand Up @@ -50,11 +62,11 @@ const forUser = (id, attrs) => {
attrs.url = urlService.getUrlByResourceId(id, {absolute: true});

if (attrs.profile_image) {
attrs.profile_image = urlFor('image', {image: attrs.profile_image}, true);
attrs.profile_image = urlService.utils.urlFor('image', {image: attrs.profile_image}, true);
}

if (attrs.cover_image) {
attrs.cover_image = urlFor('image', {image: attrs.cover_image}, true);
attrs.cover_image = urlService.utils.urlFor('image', {image: attrs.cover_image}, true);
}

return attrs;
Expand All @@ -64,7 +76,7 @@ const forTag = (id, attrs) => {
attrs.url = urlService.getUrlByResourceId(id, {absolute: true});

if (attrs.feature_image) {
attrs.feature_image = urlFor('image', {image: attrs.feature_image}, true);
attrs.feature_image = urlService.utils.urlFor('image', {image: attrs.feature_image}, true);
}

return attrs;
Expand Down
1 change: 0 additions & 1 deletion core/server/apps/amp/lib/helpers/amp_content.js
Expand Up @@ -124,7 +124,6 @@ function getAmperizeHTML(html, post) {
amperize = amperize || new Amperize();

// make relative URLs abolute
// @TODO: API v2 already makes the urls absolute. Remove if we drop v0.1.
html = urlService.utils.makeAbsoluteUrls(html, urlService.utils.urlFor('home', true), post.url).html();

if (!amperizeCache[post.id] || moment(new Date(amperizeCache[post.id].updated_at)).diff(new Date(post.updated_at)) < 0) {
Expand Down
21 changes: 11 additions & 10 deletions core/server/services/url/utils.js
Expand Up @@ -381,23 +381,20 @@ function redirectToAdmin(status, res, adminPath) {
* absolute urls. Returns an object. The html string can be accessed by calling `html()` on
* the variable that takes the result of this function
*/
function makeAbsoluteUrls(html, siteUrl, itemUrl) {
var htmlContent = cheerio.load(html, {decodeEntities: false});
function makeAbsoluteUrls(html, siteUrl, itemUrl, options = {assetsOnly: false}) {
const htmlContent = cheerio.load(html, {decodeEntities: false});
const staticImageUrlPrefixRegex = new RegExp(STATIC_IMAGE_URL_PREFIX);

// convert relative resource urls to absolute
['href', 'src'].forEach(function forEach(attributeName) {
htmlContent('[' + attributeName + ']').each(function each(ix, el) {
var baseUrl,
attributeValue,
parsed;

el = htmlContent(el);

attributeValue = el.attr(attributeName);
let attributeValue = el.attr(attributeName);

// if URL is absolute move on to the next element
try {
parsed = url.parse(attributeValue);
const parsed = url.parse(attributeValue);

if (parsed.protocol) {
return;
Expand All @@ -415,11 +412,15 @@ function makeAbsoluteUrls(html, siteUrl, itemUrl) {
if (attributeValue[0] === '#') {
return;
}
// compose an absolute URL

if (options.assetsOnly && !attributeValue.match(staticImageUrlPrefixRegex)) {
return;
}

// compose an absolute URL
// if the relative URL begins with a '/' use the blog URL (including sub-directory)
// as the base URL, otherwise use the post's URL.
baseUrl = attributeValue[0] === '/' ? siteUrl : itemUrl;
const baseUrl = attributeValue[0] === '/' ? siteUrl : itemUrl;
attributeValue = urlJoin(baseUrl, attributeValue);
el.attr(attributeName, attributeValue);
});
Expand Down
5 changes: 5 additions & 0 deletions core/test/functional/api/v2/content/posts_spec.js
Expand Up @@ -89,6 +89,11 @@ describe('Posts', function () {
should.exist(urlParts.protocol);
should.exist(urlParts.host);

res.body.posts[7].slug.should.eql('not-so-short-bit-complex');
res.body.posts[7].html.should.match(/<a href="\/about#nowhere" title="Relative URL/);
res.body.posts[9].slug.should.eql('ghostly-kitchen-sink');
res.body.posts[9].html.should.match(/<img src="http:\/\/127.0.0.1:2369\/content\/images\/lol.jpg"/);

done();
});
});
Expand Down
44 changes: 42 additions & 2 deletions core/test/unit/api/v2/utils/serializers/output/posts_spec.js
Expand Up @@ -68,14 +68,54 @@ describe('Unit: v2/utils/serializers/output/posts', function () {
urlService.utils.urlFor.getCall(3).args.should.eql(['home', true]);

urlService.utils.makeAbsoluteUrls.callCount.should.eql(2);
urlService.utils.makeAbsoluteUrls.getCall(0).args.should.eql(['## markdown', 'urlFor', 'getUrlByResourceId']);
urlService.utils.makeAbsoluteUrls.getCall(1).args.should.eql(['<img href=/content/test.jpf', 'urlFor', 'getUrlByResourceId']);
urlService.utils.makeAbsoluteUrls.getCall(0).args.should.eql([
'## markdown',
'urlFor',
'getUrlByResourceId',
{assetsOnly: true}
]);

urlService.utils.makeAbsoluteUrls.getCall(1).args.should.eql([
'<img href=/content/test.jpf',
'urlFor',
'getUrlByResourceId',
{assetsOnly: true}
]);

urlService.getUrlByResourceId.callCount.should.eql(4);
urlService.getUrlByResourceId.getCall(0).args.should.eql(['id1', {absolute: true}]);
urlService.getUrlByResourceId.getCall(1).args.should.eql(['id3', {absolute: true}]);
urlService.getUrlByResourceId.getCall(2).args.should.eql(['id4', {absolute: true}]);
urlService.getUrlByResourceId.getCall(3).args.should.eql(['id2', {absolute: true}]);
});

it('absolute_urls = true', function () {
const apiConfig = {};
const frame = {
options: {
withRelated: ['tags', 'authors'],
absolute_urls: true
}
};

const ctrlResponse = {
data: [
postModel(testUtils.DataGenerator.forKnex.createPost({
id: 'id2',
html: '<img href=/content/test.jpf'
}))
],
meta: {}
};

serializers.output.posts.all(ctrlResponse, apiConfig, frame);
urlService.utils.makeAbsoluteUrls.callCount.should.eql(1);
urlService.utils.makeAbsoluteUrls.getCall(0).args.should.eql([
'<img href=/content/test.jpf',
'urlFor',
'getUrlByResourceId',
{assetsOnly: false}
]);
});
});
});
28 changes: 28 additions & 0 deletions core/test/unit/services/url/utils_spec.js
Expand Up @@ -813,5 +813,33 @@ describe('Url', function () {

result.should.match(/<a href="http:\/\/my-ghost-blog.com\/blog\/about#nowhere" title="Relative URL">/);
});

it('asset urls only', function () {
let html = '<a href="/about" title="Relative URL"><img src="/content/images/1.jpg">';
let result = urlService.utils.makeAbsoluteUrls(html, siteUrl, itemUrl, {assetsOnly: true}).html();

result.should.match(/<img src="http:\/\/my-ghost-blog.com\/content\/images\/1.jpg">/);
result.should.match(/<a href="\/about\" title="Relative URL">/);

html = '<a href="/content/images/09/01/image.jpg">';
result = urlService.utils.makeAbsoluteUrls(html, siteUrl, itemUrl, {assetsOnly: true}).html();

result.should.match(/<a href="http:\/\/my-ghost-blog.com\/content\/images\/09\/01\/image.jpg">/);

html = '<a href="/blog/content/images/09/01/image.jpg">';
result = urlService.utils.makeAbsoluteUrls(html, siteUrl, itemUrl, {assetsOnly: true}).html();

result.should.match(/<a href="http:\/\/my-ghost-blog.com\/blog\/content\/images\/09\/01\/image.jpg">/);

html = '<img src="http://my-ghost-blog.de/content/images/09/01/image.jpg">';
result = urlService.utils.makeAbsoluteUrls(html, siteUrl, itemUrl, {assetsOnly: true}).html();

result.should.match(/<img src="http:\/\/my-ghost-blog.de\/content\/images\/09\/01\/image.jpg">/);

html = '<img src="http://external.com/image.jpg">';
result = urlService.utils.makeAbsoluteUrls(html, siteUrl, itemUrl, {assetsOnly: true}).html();

result.should.match(/<img src="http:\/\/external.com\/image.jpg">/);
});
});
});

0 comments on commit 1b9c61e

Please sign in to comment.