From d96be4907ee659e38446b9e033a2089fb9f4ccf3 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Mon, 5 Aug 2019 13:56:28 +0100 Subject: [PATCH] Fixed relative canonical_url values not being stored as root-relative (#10989) no issue - we try to store all urls as relative paths where possible in Ghost so that the `config.url` value can be changed - all relative paths are stored as root-relative except for the `post.canonical_url` field which was storing subdirectory-relative paths - adds a migration to put the subdirectory prefix onto any relative canonical_url paths - updates the canonical_url input serialiser to keep the subdirectory rather than stripping it to match all other url fields --- .../v2/utils/serializers/input/utils/url.js | 2 +- ...subdirectory-to-relative-canonical-urls.js | 98 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 core/server/data/migrations/versions/2.27/3-add-subdirectory-to-relative-canonical-urls.js 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 31c27bfb0a..a05360b2c7 100644 --- a/core/server/api/v2/utils/serializers/input/utils/url.js +++ b/core/server/api/v2/utils/serializers/input/utils/url.js @@ -12,7 +12,7 @@ const handleCanonicalUrl = (canonicalUrl) => { // 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, {withoutSubdirectory: true}); + return urlUtils.absoluteToRelative(canonicalUrl); } return canonicalUrl; diff --git a/core/server/data/migrations/versions/2.27/3-add-subdirectory-to-relative-canonical-urls.js b/core/server/data/migrations/versions/2.27/3-add-subdirectory-to-relative-canonical-urls.js new file mode 100644 index 0000000000..7dcfce7195 --- /dev/null +++ b/core/server/data/migrations/versions/2.27/3-add-subdirectory-to-relative-canonical-urls.js @@ -0,0 +1,98 @@ +const models = require('../../../../models'); +const common = require('../../../../lib/common'); +const config = require('../../../../config'); +const {URL} = require('url'); + +module.exports.config = { + transaction: true +}; + +// We've been incorrectly saving canonical_url fields without a subdirectory. +// We need this column to be consistent with all other relative URLs, so... + +// if we have a configured url with a subdirectory +// find all posts that have a canonical_url starting with / but not // +// prefix the subdirectory to the canonical_url and save the post +module.exports.up = (options) => { + // normalize config url to always have a trailing-slash + let configUrl = config.get('url'); + if (!configUrl.endsWith('/')) { + configUrl = `${configUrl}/`; + } + const url = new URL(configUrl); + + const localOptions = Object.assign({ + context: {internal: true} + }, options); + + if (url.pathname === '/') { + common.logging.info('Skipping posts.canonical_url subdirectory fix: no subdirectory configured'); + return Promise.resolve(); + } + + // perform a specific query for the type of canonical URLs we're looking for + // so we're not fetching and manually looping over a ton of post models + return models.Posts + .forge() + .query((qb) => { + qb.where('canonical_url', 'like', '/%'); + qb.whereNot('canonical_url', 'like', '//%'); + }) + .fetch(localOptions) + .then((posts) => { + if (posts) { + return Promise.mapSeries(posts, (post) => { + const canonicalUrl = post.get('canonical_url').replace('/', url.pathname); + post.set('canonical_url', canonicalUrl); + return post.save(null, localOptions); + }).then(() => { + common.logging.info(`Added subdirectory prefix to canonical_url in ${posts.length} posts`); + }); + } + + common.logging.info('Skipping posts.canonical_url subdirectory fix: no canonical_urls to fix'); + return Promise.resolve(); + }); +}; + +// if we have a configured url with a subdirectory +// find all posts with a canonical_url starting with the subdirectory +// remove it and save the post +module.exports.down = (options) => { + // normalize config url to always have a trailing-slash + let configUrl = config.get('url'); + if (!configUrl.endsWith('/')) { + configUrl = `${configUrl}/`; + } + const url = new URL(configUrl); + + const localOptions = Object.assign({ + context: {internal: true} + }, options); + + if (url.pathname === '/') { + common.logging.info('Skipping posts.canonical_url subdirectory fix: no subdirectory configured'); + return Promise.resolve(); + } + + return models.Posts + .forge() + .query((qb) => { + qb.where('canonical_url', 'LIKE', `${url.pathname}%`); + }) + .fetch() + .then((posts) => { + if (posts) { + return Promise.mapSeries(posts, (post) => { + const canonicalUrl = post.get('canonical_url').replace(url.pathname, '/'); + post.set('canonical_url', canonicalUrl); + return post.save(null, localOptions); + }).then(() => { + common.logging.info(`Removed subdirectory prefix from canonical_url in ${posts.length} posts`); + }); + } + + common.logging.info('Skipping posts.canonical_url subdirectory fix: no canonical_urls to fix'); + return Promise.resolve(); + }); +};