diff --git a/packages/url-utils/.eslintrc.js b/packages/url-utils/.eslintrc.js
new file mode 100644
index 000000000..6a5eab530
--- /dev/null
+++ b/packages/url-utils/.eslintrc.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: ['ghost'],
+ extends: [
+ 'plugin:ghost/node',
+ ]
+};
diff --git a/packages/url-utils/LICENSE b/packages/url-utils/LICENSE
new file mode 100644
index 000000000..1051206d3
--- /dev/null
+++ b/packages/url-utils/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Ghost Foundation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/url-utils/README.md b/packages/url-utils/README.md
new file mode 100644
index 000000000..91f4278f1
--- /dev/null
+++ b/packages/url-utils/README.md
@@ -0,0 +1,39 @@
+# Url Utils
+
+## Install
+
+`npm install @tryghost/url-utils --save`
+
+or
+
+`yarn add @tryghost/url-utils`
+
+
+## Usage
+
+
+## Develop
+
+This is a mono repository, managed with [lerna](https://lernajs.io/).
+
+Follow the instructions for the top-level repo.
+1. `git clone` this repo & `cd` into it as usual
+2. Run `yarn` to install top-level dependencies.
+
+
+## Run
+
+- `yarn dev`
+
+
+## Test
+
+- `yarn lint` run just eslint
+- `yarn test` run lint and tests
+
+
+
+
+# Copyright & License
+
+Copyright (c) 2019 Ghost Foundation - Released under the [MIT license](LICENSE).
diff --git a/packages/url-utils/index.js b/packages/url-utils/index.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/url-utils/lib/deduplicate-slashes.js b/packages/url-utils/lib/deduplicate-slashes.js
new file mode 100644
index 000000000..beda744d3
--- /dev/null
+++ b/packages/url-utils/lib/deduplicate-slashes.js
@@ -0,0 +1,5 @@
+function deduplicateDoubleSlashes(url) {
+ return url.replace(/\/\//g, '/');
+}
+
+module.exports = deduplicateDoubleSlashes;
diff --git a/packages/url-utils/lib/index.js b/packages/url-utils/lib/index.js
new file mode 100644
index 000000000..bf09cb743
--- /dev/null
+++ b/packages/url-utils/lib/index.js
@@ -0,0 +1,474 @@
+// Contains all path information to be used throughout the codebase.
+const _ = require('lodash');
+const url = require('url');
+const cheerio = require('cheerio');
+const replacePermalink = require('./replace-permalink');
+const deduplicateDoubleSlashes = require('./deduplicate-slashes');
+const isSSL = require('./is-ssl');
+
+/**
+ * Initialization method to pass in URL configurations
+ * @param {Object} options
+ * @param {String} options.url Ghost instance blog URL
+ * @param {String} options.adminUrl Ghost instance admin URL
+ * @param {Object} options.apiVersions configuration object which has defined `all` property which is an array of keys for other available properties
+ * @param {Object} options.slugs object with 2 properties reserved and protected containing arrays of special case slugs
+ * @param {Number} options.redirectCacheMaxAge
+ * @param {String} options.baseApiPath static prefix for serving API. Should not te passed in, unless the API is being run under custom URL
+ * @param {String} options.staticImageUrlPrefix static prefix for serving images. Should not be passed in, unless customizing ghost instance image storage
+ */
+module.exports = function urlUtils(options = {}) {
+ // NOTE: assumes all provided values, like URLs, are valid
+ const config = {
+ url: options.url,
+ adminUrl: options.adminUrl,
+ apiVersions: options.apiVersions,
+ slugs: options.slugs,
+ redirectCacheMaxAge: options.redirectCacheMaxAge,
+ baseApiPath: options.baseApiPath || '/ghost/api',
+ staticImageUrlPrefix: options.staticImageUrlPrefix || 'content/images'
+ };
+
+ /**
+ * Returns API path combining base path and path for specific version asked or deprecated by default
+ * @param {Object} options {version} for which to get the path(stable, actice, deprecated),
+ * {type} admin|content: defaults to {version: deprecated, type: content}
+ * @return {string} API Path for version
+ */
+ function getApiPath(options) {
+ const versionPath = getVersionPath(options);
+ return `${config.baseApiPath}${versionPath}`;
+ }
+
+ /**
+ * Returns path containing only the path for the specific version asked or deprecated by default
+ * @param {Object} options {version} for which to get the path(stable, active, deprecated),
+ * {type} admin|content: defaults to {version: deprecated, type: content}
+ * @return {string} API version path
+ */
+ function getVersionPath(options) {
+ let requestedVersion = options.version || 'v0.1';
+ let requestedVersionType = options.type || 'content';
+ let versionData = config.apiVersions[requestedVersion];
+ if (typeof versionData === 'string') {
+ versionData = config.apiVersions[versionData];
+ }
+ let versionPath = versionData[requestedVersionType];
+ return `/${versionPath}/`;
+ }
+
+ /**
+ * Returns the base URL of the blog as set in the config.
+ *
+ * Secure:
+ * If the request is secure, we want to force returning the blog url as https.
+ * Imagine Ghost runs with http, but nginx allows SSL connections.
+ *
+ * @param {boolean} secure
+ * @return {string} URL returns the url as defined in config, but always with a trailing `/`
+ */
+ function getBlogUrl(secure) {
+ var blogUrl;
+
+ if (secure) {
+ blogUrl = config.url.replace('http://', 'https://');
+ } else {
+ blogUrl = config.url;
+ }
+
+ if (!blogUrl.match(/\/$/)) {
+ blogUrl += '/';
+ }
+
+ return blogUrl;
+ }
+
+ /**
+ * Returns a subdirectory URL, if defined so in the config.
+ * @return {string} URL a subdirectory if configured.
+ */
+ function getSubdir() {
+ // Parse local path location
+ var localPath = url.parse(config.url).path,
+ subdir;
+
+ // Remove trailing slash
+ if (localPath !== '/') {
+ localPath = localPath.replace(/\/$/, '');
+ }
+
+ subdir = localPath === '/' ? '' : localPath;
+ return subdir;
+ }
+
+ function deduplicateSubDir(url) {
+ var subDir = getSubdir(),
+ subDirRegex;
+
+ if (!subDir) {
+ return url;
+ }
+
+ subDir = subDir.replace(/^\/|\/+$/, '');
+ // we can have subdirs that match TLDs so we need to restrict matches to
+ // duplicates that start with a / or the beginning of the url
+ subDirRegex = new RegExp('(^|/)' + subDir + '/' + subDir + '/');
+
+ return url.replace(subDirRegex, '$1' + subDir + '/');
+ }
+
+ function getProtectedSlugs() {
+ var subDir = getSubdir();
+
+ if (!_.isEmpty(subDir)) {
+ return config.slugs.protected.concat([subDir.split('/').pop()]);
+ } else {
+ return config.slugs.protected;
+ }
+ }
+
+ /** urlJoin
+ * Returns a URL/path for internal use in Ghost.
+ * @param {string} arguments takes arguments and concats those to a valid path/URL.
+ * @return {string} URL concatinated URL/path of arguments.
+ */
+ function urlJoin() {
+ var args = Array.prototype.slice.call(arguments),
+ prefixDoubleSlash = false,
+ url;
+
+ // Remove empty item at the beginning
+ if (args[0] === '') {
+ args.shift();
+ }
+
+ // Handle schemeless protocols
+ if (args[0].indexOf('//') === 0) {
+ prefixDoubleSlash = true;
+ }
+
+ // join the elements using a slash
+ url = args.join('/');
+
+ // Fix multiple slashes
+ url = url.replace(/(^|[^:])\/\/+/g, '$1/');
+
+ // Put the double slash back at the beginning if this was a schemeless protocol
+ if (prefixDoubleSlash) {
+ url = url.replace(/^\//, '//');
+ }
+
+ url = deduplicateSubDir(url);
+ return url;
+ }
+
+ /**
+ * admin:url is optional
+ */
+ function getAdminUrl() {
+ var adminUrl = config.adminUrl,
+ subDir = getSubdir();
+
+ if (!adminUrl) {
+ return;
+ }
+
+ if (!adminUrl.match(/\/$/)) {
+ adminUrl += '/';
+ }
+
+ adminUrl = urlJoin(adminUrl, subDir, '/');
+ adminUrl = deduplicateSubDir(adminUrl);
+ return adminUrl;
+ }
+
+ // ## createUrl
+ // Simple url creation from a given path
+ // Ensures that our urls contain the subdirectory if there is one
+ // And are correctly formatted as either relative or absolute
+ // Usage:
+ // createUrl('/', true) -> http://my-ghost-blog.com/
+ // E.g. /blog/ subdir
+ // createUrl('/welcome-to-ghost/') -> /blog/welcome-to-ghost/
+ // Parameters:
+ // - urlPath - string which must start and end with a slash
+ // - absolute (optional, default:false) - boolean whether or not the url should be absolute
+ // - secure (optional, default:false) - boolean whether or not to force SSL
+ // Returns:
+ // - a URL which always ends with a slash
+ function createUrl(urlPath, absolute, secure, trailingSlash) {
+ urlPath = urlPath || '/';
+ absolute = absolute || false;
+ var base;
+
+ // create base of url, always ends without a slash
+ if (absolute) {
+ base = getBlogUrl(secure);
+ } else {
+ base = getSubdir();
+ }
+
+ if (trailingSlash) {
+ if (!urlPath.match(/\/$/)) {
+ urlPath += '/';
+ }
+ }
+
+ return urlJoin(base, urlPath);
+ }
+
+ // ## urlFor
+ // Synchronous url creation for a given context
+ // Can generate a url for a named path and given path.
+ // Determines what sort of context it has been given, and delegates to the correct generation method,
+ // Finally passing to createUrl, to ensure any subdirectory is honoured, and the url is absolute if needed
+ // Usage:
+ // urlFor('home', true) -> http://my-ghost-blog.com/
+ // E.g. /blog/ subdir
+ // urlFor({relativeUrl: '/my-static-page/'}) -> /blog/my-static-page/
+ // Parameters:
+ // - context - a string, or json object describing the context for which you need a url
+ // - data (optional) - a json object containing data needed to generate a url
+ // - absolute (optional, default:false) - boolean whether or not the url should be absolute
+ // This is probably not the right place for this, but it's the best place for now
+ // @TODO: rewrite, very hard to read, create private functions!
+ function urlFor(context, data, absolute) {
+ var urlPath = '/',
+ secure, imagePathRe,
+ knownObjects = ['image', 'nav'], baseUrl,
+ hostname,
+
+ // this will become really big
+ knownPaths = {
+ home: '/',
+ sitemap_xsl: '/sitemap.xsl'
+ };
+
+ // Make data properly optional
+ if (_.isBoolean(data)) {
+ absolute = data;
+ data = null;
+ }
+
+ // Can pass 'secure' flag in either context or data arg
+ secure = (context && context.secure) || (data && data.secure);
+
+ if (_.isObject(context) && context.relativeUrl) {
+ urlPath = context.relativeUrl;
+ } else if (_.isString(context) && _.indexOf(knownObjects, context) !== -1) {
+ if (context === 'image' && data.image) {
+ urlPath = data.image;
+ imagePathRe = new RegExp('^' + getSubdir() + '/' + config.staticImageUrlPrefix);
+ absolute = imagePathRe.test(data.image) ? absolute : false;
+
+ if (absolute) {
+ // Remove the sub-directory from the URL because ghostConfig will add it back.
+ urlPath = urlPath.replace(new RegExp('^' + getSubdir()), '');
+ baseUrl = getBlogUrl(secure).replace(/\/$/, '');
+ urlPath = baseUrl + urlPath;
+ }
+
+ return urlPath;
+ } else if (context === 'nav' && data.nav) {
+ urlPath = data.nav.url;
+ secure = data.nav.secure || secure;
+ baseUrl = getBlogUrl(secure);
+ hostname = baseUrl.split('//')[1];
+
+ // If the hostname is present in the url
+ if (urlPath.indexOf(hostname) > -1
+ // do no not apply, if there is a subdomain, or a mailto link
+ && !urlPath.split(hostname)[0].match(/\.|mailto:/)
+ // do not apply, if there is a port after the hostname
+ && urlPath.split(hostname)[1].substring(0, 1) !== ':') {
+ // make link relative to account for possible mismatch in http/https etc, force absolute
+ urlPath = urlPath.split(hostname)[1];
+ urlPath = urlJoin('/', urlPath);
+ absolute = true;
+ }
+ }
+ } else if (context === 'home' && absolute) {
+ urlPath = getBlogUrl(secure);
+
+ // CASE: there are cases where urlFor('home') needs to be returned without trailing
+ // slash e. g. the `{{@site.url}}` helper. See https://github.com/TryGhost/Ghost/issues/8569
+ if (data && data.trailingSlash === false) {
+ urlPath = urlPath.replace(/\/$/, '');
+ }
+ } else if (context === 'admin') {
+ urlPath = getAdminUrl() || getBlogUrl();
+
+ if (absolute) {
+ urlPath += 'ghost/';
+ } else {
+ urlPath = '/ghost/';
+ }
+ } else if (context === 'api') {
+ urlPath = getAdminUrl() || getBlogUrl();
+ let apiPath = getApiPath({version: 'v0.1', type: 'content'});
+ // CASE: with or without protocol? If your blog url (or admin url) is configured to http, it's still possible that e.g. nginx allows both https+http.
+ // So it depends how you serve your blog. The main focus here is to avoid cors problems.
+ // @TODO: rename cors
+ if (data && data.cors) {
+ if (!urlPath.match(/^https:/)) {
+ urlPath = urlPath.replace(/^.*?:\/\//g, '//');
+ }
+ }
+
+ if (data && data.version) {
+ apiPath = getApiPath({version: data.version, type: data.versionType});
+ }
+
+ if (absolute) {
+ urlPath = urlPath.replace(/\/$/, '') + apiPath;
+ } else {
+ urlPath = apiPath;
+ }
+ } else if (_.isString(context) && _.indexOf(_.keys(knownPaths), context) !== -1) {
+ // trying to create a url for a named path
+ urlPath = knownPaths[context];
+ }
+
+ // This url already has a protocol so is likely an external url to be returned
+ // or it is an alternative scheme, protocol-less, or an anchor-only path
+ if (urlPath && (urlPath.indexOf('://') !== -1 || urlPath.match(/^(\/\/|#|[a-zA-Z0-9-]+:)/))) {
+ return urlPath;
+ }
+
+ return createUrl(urlPath, absolute, secure);
+ }
+
+ function redirect301(res, redirectUrl) {
+ res.set({'Cache-Control': 'public, max-age=' + config.redirectCacheMaxAge});
+ return res.redirect(301, redirectUrl);
+ }
+
+ function redirectToAdmin(status, res, adminPath) {
+ var redirectUrl = urlJoin(urlFor('admin'), adminPath, '/');
+
+ if (status === 301) {
+ return redirect301(res, redirectUrl);
+ }
+ return res.redirect(redirectUrl);
+ }
+
+ /**
+ * Make absolute URLs
+ * @param {string} html
+ * @param {string} siteUrl (blog URL)
+ * @param {string} itemUrl (URL of current context)
+ * @returns {object} htmlContent
+ * @description Takes html, blog url and item url and converts relative url into
+ * 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, options = {assetsOnly: false}) {
+ html = html || '';
+ const htmlContent = cheerio.load(html, {decodeEntities: false});
+ const staticImageUrlPrefixRegex = new RegExp(config.staticImageUrlPrefix);
+
+ // convert relative resource urls to absolute
+ ['href', 'src'].forEach(function forEach(attributeName) {
+ htmlContent('[' + attributeName + ']').each(function each(ix, el) {
+ el = htmlContent(el);
+
+ let attributeValue = el.attr(attributeName);
+
+ // if URL is absolute move on to the next element
+ try {
+ const parsed = url.parse(attributeValue);
+
+ if (parsed.protocol) {
+ return;
+ }
+
+ // Do not convert protocol relative URLs
+ if (attributeValue.lastIndexOf('//', 0) === 0) {
+ return;
+ }
+ } catch (e) {
+ return;
+ }
+
+ // CASE: don't convert internal links
+ if (attributeValue[0] === '#') {
+ return;
+ }
+
+ 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.
+ const baseUrl = attributeValue[0] === '/' ? siteUrl : itemUrl;
+ attributeValue = urlJoin(baseUrl, attributeValue);
+ el.attr(attributeName, attributeValue);
+ });
+ });
+
+ return htmlContent;
+ }
+
+ function absoluteToRelative(urlToModify, options) {
+ options = options || {};
+
+ const urlObj = url.parse(urlToModify);
+ const relativePath = urlObj.pathname;
+
+ if (options.withoutSubdirectory) {
+ const subDir = getSubdir();
+
+ if (!subDir) {
+ return relativePath;
+ }
+
+ const subDirRegex = new RegExp('^' + subDir);
+ return relativePath.replace(subDirRegex, '');
+ }
+
+ return relativePath;
+ }
+
+ function relativeToAbsolute(url) {
+ if (!url.startsWith('/') || url.startsWith('//')) {
+ return url;
+ }
+
+ return createUrl(url, true);
+ }
+
+ const utils = {};
+
+ utils.absoluteToRelative = absoluteToRelative;
+ utils.relativeToAbsolute = relativeToAbsolute;
+ utils.makeAbsoluteUrls = makeAbsoluteUrls;
+ utils.getProtectedSlugs = getProtectedSlugs;
+ utils.getSubdir = getSubdir;
+ utils.urlJoin = urlJoin;
+ utils.urlFor = urlFor;
+ utils.isSSL = isSSL;
+ utils.replacePermalink = replacePermalink;
+ utils.redirectToAdmin = redirectToAdmin;
+ utils.redirect301 = redirect301;
+ utils.createUrl = createUrl;
+ utils.deduplicateDoubleSlashes = deduplicateDoubleSlashes;
+ utils.getApiPath = getApiPath;
+ utils.getVersionPath = getVersionPath;
+ utils.getBlogUrl = getBlogUrl;
+ utils.getSiteUrl = getBlogUrl;
+
+ /**
+ * If you request **any** image in Ghost, it get's served via
+ * http://your-blog.com/content/images/2017/01/02/author.png
+ *
+ * /content/images/ is a static prefix for serving images!
+ *
+ * But internally the image is located for example in your custom content path:
+ * my-content/another-dir/images/2017/01/02/author.png
+ */
+ utils.STATIC_IMAGE_URL_PREFIX = config.staticImageUrlPrefix;
+
+ return utils;
+};
diff --git a/packages/url-utils/lib/is-ssl.js b/packages/url-utils/lib/is-ssl.js
new file mode 100644
index 000000000..012792836
--- /dev/null
+++ b/packages/url-utils/lib/is-ssl.js
@@ -0,0 +1,8 @@
+const url = require('url');
+
+function isSSL(urlToParse) {
+ var protocol = url.parse(urlToParse).protocol;
+ return protocol === 'https:';
+}
+
+module.exports = isSSL;
diff --git a/packages/url-utils/lib/replace-permalink.js b/packages/url-utils/lib/replace-permalink.js
new file mode 100644
index 000000000..2ce0e5102
--- /dev/null
+++ b/packages/url-utils/lib/replace-permalink.js
@@ -0,0 +1,48 @@
+const _ = require('lodash');
+const moment = require('moment-timezone');
+
+/**
+ * creates the url path for a post based on blog timezone and permalink pattern
+ */
+function replacePermalink(permalink, resource, timezone = 'UTC') {
+ let output = permalink,
+ primaryTagFallback = 'all',
+ publishedAtMoment = moment.tz(resource.published_at || Date.now(), timezone),
+ permalinkLookUp = {
+ year: function () {
+ return publishedAtMoment.format('YYYY');
+ },
+ month: function () {
+ return publishedAtMoment.format('MM');
+ },
+ day: function () {
+ return publishedAtMoment.format('DD');
+ },
+ author: function () {
+ return resource.primary_author.slug;
+ },
+ primary_author: function () {
+ return resource.primary_author ? resource.primary_author.slug : primaryTagFallback;
+ },
+ primary_tag: function () {
+ return resource.primary_tag ? resource.primary_tag.slug : primaryTagFallback;
+ },
+ slug: function () {
+ return resource.slug;
+ },
+ id: function () {
+ return resource.id;
+ }
+ };
+
+ // replace tags like :slug or :year with actual values
+ output = output.replace(/(:[a-z_]+)/g, function (match) {
+ if (_.has(permalinkLookUp, match.substr(1))) {
+ return permalinkLookUp[match.substr(1)]();
+ }
+ });
+
+ return output;
+}
+
+module.exports = replacePermalink;
diff --git a/packages/url-utils/package.json b/packages/url-utils/package.json
new file mode 100644
index 000000000..b5c8ec596
--- /dev/null
+++ b/packages/url-utils/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@tryghost/url-utils",
+ "version": "0.0.0",
+ "repository": "https://github.com/TryGhost/Ghost-SDKs/tree/master/packages/url-utils",
+ "author": "Ghost Foundation",
+ "license": "MIT",
+ "main": "lib/index.js",
+ "scripts": {
+ "dev": "echo \"Implement me!\"",
+ "test": "NODE_ENV=testing mocha './test/**/*.test.js'",
+ "lint": "eslint . --ext .js --cache",
+ "posttest": "yarn lint"
+ },
+ "files": [
+ "lib/"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "devDependencies": {
+ "mocha": "6.1.4",
+ "should": "13.2.3",
+ "sinon": "7.3.2"
+ },
+ "dependencies": {
+ "cheerio": "0.22.0",
+ "lodash": "4.17.11",
+ "moment-timezone": "0.5.25"
+ }
+}
diff --git a/packages/url-utils/test/.eslintrc.js b/packages/url-utils/test/.eslintrc.js
new file mode 100644
index 000000000..edb330863
--- /dev/null
+++ b/packages/url-utils/test/.eslintrc.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: ['ghost'],
+ extends: [
+ 'plugin:ghost/test',
+ ]
+};
diff --git a/packages/url-utils/test/utils.test.js b/packages/url-utils/test/utils.test.js
new file mode 100644
index 000000000..26698af67
--- /dev/null
+++ b/packages/url-utils/test/utils.test.js
@@ -0,0 +1,992 @@
+// Switch these lines once there are useful utils
+// const testUtils = require('./utils');
+require('./utils');
+
+const sinon = require('sinon');
+const moment = require('moment-timezone');
+const urlUtils = require('../lib/index');
+
+const constants = {
+ ONE_YEAR_S: 31536000
+};
+
+describe('Url', function () {
+ const defaultAPIVersions = {
+ all: ['v0.1', 'v2'],
+ v2: {
+ admin: 'v2/admin',
+ content: 'v2/content',
+ members: 'v2/members'
+ },
+ 'v0.1': {
+ admin: 'v0.1',
+ content: 'v0.1'
+ }
+ };
+
+ describe('absoluteToRelative', function () {
+ it('default', function () {
+ urlUtils().absoluteToRelative('http://myblog.com/test/').should.eql('/test/');
+ });
+
+ it('with subdir', function () {
+ urlUtils().absoluteToRelative('http://myblog.com/blog/test/').should.eql('/blog/test/');
+ });
+
+ it('with subdir, but request without', function () {
+ const utils = urlUtils({
+ url: 'http://myblog.com/blog/'
+ });
+
+ utils.absoluteToRelative('http://myblog.com/blog/test/', {withoutSubdirectory: true})
+ .should.eql('/test/');
+ });
+
+ it('with subdir, but request without', function () {
+ const utils = urlUtils({
+ url: 'http://myblog.com/blog'
+ });
+ utils.absoluteToRelative('http://myblog.com/blog/test/', {withoutSubdirectory: true})
+ .should.eql('/test/');
+ });
+ });
+
+ describe('relativeToAbsolute', function () {
+ it('default', function () {
+ const utils = urlUtils({
+ url: 'http://myblog.com/'
+ });
+ utils.relativeToAbsolute('/test/').should.eql('http://myblog.com/test/');
+ });
+
+ it('with subdir', function () {
+ const utils = urlUtils({
+ url: 'http://myblog.com/blog/'
+ });
+ utils.relativeToAbsolute('/test/').should.eql('http://myblog.com/blog/test/');
+ });
+
+ it('should not convert absolute url', function () {
+ urlUtils().relativeToAbsolute('http://anotherblog.com/blog/').should.eql('http://anotherblog.com/blog/');
+ });
+
+ it('should not convert absolute url', function () {
+ urlUtils().relativeToAbsolute('http://anotherblog.com/blog/').should.eql('http://anotherblog.com/blog/');
+ });
+
+ it('should not convert schemeless url', function () {
+ urlUtils().relativeToAbsolute('//anotherblog.com/blog/').should.eql('//anotherblog.com/blog/');
+ });
+ });
+
+ describe('getProtectedSlugs', function () {
+ it('defaults', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/',
+ slugs: {
+ protected: ['ghost', 'rss', 'amp']
+ }
+ });
+
+ utils.getProtectedSlugs().should.eql(['ghost', 'rss', 'amp']);
+ });
+
+ it('url has subdir', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog',
+ slugs: {
+ protected: ['ghost', 'rss', 'amp']
+ }
+ });
+
+ utils.getProtectedSlugs().should.eql(['ghost', 'rss', 'amp', 'blog']);
+ });
+ });
+
+ describe('getSubdir', function () {
+ it('url has no subdir', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/'
+ });
+ utils.getSubdir().should.eql('');
+ });
+
+ it('url has subdir', function () {
+ let utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog'
+ });
+ utils.getSubdir().should.eql('/blog');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog/'
+ });
+ utils.getSubdir().should.eql('/blog');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/my/blog'
+ });
+ utils.getSubdir().should.eql('/my/blog');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/my/blog/'
+ });
+ utils.getSubdir().should.eql('/my/blog');
+ });
+
+ it('should not return a slash for subdir', function () {
+ let utils = urlUtils({
+ url: 'http://my-ghost-blog.com'
+ });
+ utils.getSubdir().should.eql('');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/'
+ });
+ utils.getSubdir().should.eql('');
+ });
+ });
+
+ describe('urlJoin', function () {
+ it('should deduplicate slashes', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/'
+ });
+ utils.urlJoin('/', '/my/', '/blog/').should.equal('/my/blog/');
+ utils.urlJoin('/', '//my/', '/blog/').should.equal('/my/blog/');
+ utils.urlJoin('/', '/', '/').should.equal('/');
+ });
+
+ it('should not deduplicate slashes in protocol', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/'
+ });
+ utils.urlJoin('http://myurl.com', '/rss').should.equal('http://myurl.com/rss');
+ utils.urlJoin('https://myurl.com/', '/rss').should.equal('https://myurl.com/rss');
+ });
+
+ it('should permit schemeless protocol', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/'
+ });
+ utils.urlJoin('/', '/').should.equal('/');
+ utils.urlJoin('//myurl.com', '/rss').should.equal('//myurl.com/rss');
+ utils.urlJoin('//myurl.com/', '/rss').should.equal('//myurl.com/rss');
+ utils.urlJoin('//myurl.com//', 'rss').should.equal('//myurl.com/rss');
+ utils.urlJoin('', '//myurl.com', 'rss').should.equal('//myurl.com/rss');
+ });
+
+ it('should deduplicate subdir', function () {
+ let utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog'
+ });
+ utils.urlJoin('blog', 'blog/about').should.equal('blog/about');
+ utils.urlJoin('blog/', 'blog/about').should.equal('blog/about');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/my/blog'
+ });
+ utils.urlJoin('my/blog', 'my/blog/about').should.equal('my/blog/about');
+ utils.urlJoin('my/blog/', 'my/blog/about').should.equal('my/blog/about');
+ });
+
+ it('should handle subdir matching tld', function () {
+ const utils = urlUtils({
+ url: 'http://ghost.blog/blog'
+ });
+ utils.urlJoin('ghost.blog/blog', 'ghost/').should.equal('ghost.blog/blog/ghost/');
+ utils.urlJoin('ghost.blog', 'blog', 'ghost/').should.equal('ghost.blog/blog/ghost/');
+ });
+ });
+
+ describe('urlFor', function () {
+ it('should return the home url with no options', function () {
+ let utils = urlUtils({
+ url: 'http://ghost-blog.com/'
+ });
+ utils.urlFor().should.equal('/');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog'
+ });
+ utils.urlFor().should.equal('/blog/');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog/'
+ });
+ utils.urlFor().should.equal('/blog/');
+ });
+
+ it('should return home url when asked for', function () {
+ var testContext = 'home';
+
+ let utils = urlUtils({
+ url: 'http://my-ghost-blog.com'
+ });
+ utils.urlFor(testContext).should.equal('/');
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/');
+ utils.urlFor(testContext, {secure: true}, true).should.equal('https://my-ghost-blog.com/');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/'
+ });
+ utils.urlFor(testContext).should.equal('/');
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/');
+ utils.urlFor(testContext, {secure: true}, true).should.equal('https://my-ghost-blog.com/');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog'
+ });
+ utils.urlFor(testContext).should.equal('/blog/');
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/');
+ utils.urlFor(testContext, {secure: true}, true).should.equal('https://my-ghost-blog.com/blog/');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog/'
+ });
+ utils.urlFor(testContext).should.equal('/blog/');
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/');
+ utils.urlFor(testContext, {secure: true}, true).should.equal('https://my-ghost-blog.com/blog/');
+
+ // Output blog url without trailing slash
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com'
+ });
+ utils.urlFor(testContext).should.equal('/');
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/');
+ utils.urlFor(testContext, {
+ secure: true,
+ trailingSlash: false
+ }, true).should.equal('https://my-ghost-blog.com');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/'
+ });
+ utils.urlFor(testContext).should.equal('/');
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/');
+ utils.urlFor(testContext, {
+ secure: true,
+ trailingSlash: false
+ }, true).should.equal('https://my-ghost-blog.com');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog'
+ });
+ utils.urlFor(testContext).should.equal('/blog/');
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/');
+ utils.urlFor(testContext, {
+ secure: true,
+ trailingSlash: false
+ }, true).should.equal('https://my-ghost-blog.com/blog');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog/'
+ });
+ utils.urlFor(testContext).should.equal('/blog/');
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/');
+ utils.urlFor(testContext, {
+ secure: true,
+ trailingSlash: false
+ }, true).should.equal('https://my-ghost-blog.com/blog');
+ });
+
+ it('should handle weird cases by always returning /', function () {
+ const utils = urlUtils({
+ url: 'http://ghost-blog.com'
+ });
+ utils.urlFor('').should.equal('/');
+ utils.urlFor('post', {}).should.equal('/');
+ utils.urlFor('post', {post: {}}).should.equal('/');
+ utils.urlFor(null).should.equal('/');
+ utils.urlFor(undefined).should.equal('/');
+ utils.urlFor({}).should.equal('/');
+ utils.urlFor({relativeUrl: ''}).should.equal('/');
+ utils.urlFor({relativeUrl: null}).should.equal('/');
+ utils.urlFor({relativeUrl: undefined}).should.equal('/');
+ });
+
+ it('should return url for a random path when asked for', function () {
+ var testContext = {relativeUrl: '/about/'};
+
+ let utils = urlUtils({
+ url: 'http://my-ghost-blog.com'
+ });
+ utils.urlFor(testContext).should.equal('/about/');
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/about/');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog'
+ });
+ utils.urlFor(testContext).should.equal('/blog/about/');
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/about/');
+
+ testContext.secure = true;
+ utils.urlFor(testContext, true).should.equal('https://my-ghost-blog.com/blog/about/');
+
+ testContext.secure = false;
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/about/');
+
+ testContext.secure = false;
+
+ utils = urlUtils({
+ url: 'https://my-ghost-blog.com'
+ });
+ utils.urlFor(testContext, true).should.equal('https://my-ghost-blog.com/about/');
+ });
+
+ it('should deduplicate subdirectories in paths', function () {
+ var testContext = {relativeUrl: '/blog/about/'};
+
+ let utils = urlUtils({
+ url: 'http://my-ghost-blog.com'
+ });
+ utils.urlFor(testContext).should.equal('/blog/about/');
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/about/');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog'
+ });
+ utils.urlFor(testContext).should.equal('/blog/about/');
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/about/');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog/'
+ });
+ utils.urlFor(testContext).should.equal('/blog/about/');
+ utils.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/about/');
+ });
+
+ it('should return url for an image when asked for', function () {
+ var testContext = 'image',
+ testData;
+
+ let utils = urlUtils({
+ url: 'http://my-ghost-blog.com'
+ });
+
+ testData = {image: '/content/images/my-image.jpg'};
+ utils.urlFor(testContext, testData).should.equal('/content/images/my-image.jpg');
+ utils.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/content/images/my-image.jpg');
+
+ testData = {image: 'http://placekitten.com/500/200'};
+ utils.urlFor(testContext, testData).should.equal('http://placekitten.com/500/200');
+ utils.urlFor(testContext, testData, true).should.equal('http://placekitten.com/500/200');
+
+ testData = {image: '/blog/content/images/my-image2.jpg'};
+ utils.urlFor(testContext, testData).should.equal('/blog/content/images/my-image2.jpg');
+ // We don't make image urls absolute if they don't look like images relative to the image path
+ utils.urlFor(testContext, testData, true).should.equal('/blog/content/images/my-image2.jpg');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog/'
+ });
+
+ testData = {image: '/content/images/my-image3.jpg'};
+ utils.urlFor(testContext, testData).should.equal('/content/images/my-image3.jpg');
+ // We don't make image urls absolute if they don't look like images relative to the image path
+ utils.urlFor(testContext, testData, true).should.equal('/content/images/my-image3.jpg');
+
+ testData = {image: '/blog/content/images/my-image4.jpg'};
+ utils.urlFor(testContext, testData).should.equal('/blog/content/images/my-image4.jpg');
+ utils.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/blog/content/images/my-image4.jpg');
+
+ // Test case for blogs with optional https -
+ // they may be configured with http url but the actual connection may be over https (#8373)
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com'
+ });
+ testData = {image: '/content/images/my-image.jpg', secure: true};
+ utils.urlFor(testContext, testData, true).should.equal('https://my-ghost-blog.com/content/images/my-image.jpg');
+ });
+
+ it('should return a url for a nav item when asked for it', function () {
+ var testContext = 'nav',
+ testData;
+
+ let utils = urlUtils({
+ url: 'http://my-ghost-blog.com'
+ });
+
+ testData = {nav: {url: 'http://my-ghost-blog.com/'}};
+ utils.urlFor(testContext, testData).should.equal('http://my-ghost-blog.com/');
+
+ testData = {nav: {url: 'http://my-ghost-blog.com/short-and-sweet/'}};
+ utils.urlFor(testContext, testData).should.equal('http://my-ghost-blog.com/short-and-sweet/');
+
+ testData = {nav: {url: 'http://my-ghost-blog.com//short-and-sweet/'}, secure: true};
+ utils.urlFor(testContext, testData).should.equal('https://my-ghost-blog.com/short-and-sweet/');
+
+ testData = {nav: {url: 'http://my-ghost-blog.com:3000/'}};
+ utils.urlFor(testContext, testData).should.equal('http://my-ghost-blog.com:3000/');
+
+ testData = {nav: {url: 'http://my-ghost-blog.com:3000/short-and-sweet/'}};
+ utils.urlFor(testContext, testData).should.equal('http://my-ghost-blog.com:3000/short-and-sweet/');
+
+ testData = {nav: {url: 'http://sub.my-ghost-blog.com/'}};
+ utils.urlFor(testContext, testData).should.equal('http://sub.my-ghost-blog.com/');
+
+ testData = {nav: {url: '//sub.my-ghost-blog.com/'}};
+ utils.urlFor(testContext, testData).should.equal('//sub.my-ghost-blog.com/');
+
+ testData = {nav: {url: 'mailto:sub@my-ghost-blog.com/'}};
+ utils.urlFor(testContext, testData).should.equal('mailto:sub@my-ghost-blog.com/');
+
+ testData = {nav: {url: '#this-anchor'}};
+ utils.urlFor(testContext, testData).should.equal('#this-anchor');
+
+ testData = {nav: {url: 'http://some-external-page.com/my-ghost-blog.com'}};
+ utils.urlFor(testContext, testData).should.equal('http://some-external-page.com/my-ghost-blog.com');
+
+ testData = {nav: {url: 'http://some-external-page.com/stuff-my-ghost-blog.com-around'}};
+ utils.urlFor(testContext, testData).should.equal('http://some-external-page.com/stuff-my-ghost-blog.com-around');
+
+ testData = {nav: {url: 'mailto:marshmallow@my-ghost-blog.com'}};
+ utils.urlFor(testContext, testData).should.equal('mailto:marshmallow@my-ghost-blog.com');
+
+ utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog'
+ });
+ testData = {nav: {url: 'http://my-ghost-blog.com/blog/'}};
+ utils.urlFor(testContext, testData).should.equal('http://my-ghost-blog.com/blog/');
+
+ testData = {nav: {url: 'http://my-ghost-blog.com/blog/short-and-sweet/'}};
+ utils.urlFor(testContext, testData).should.equal('http://my-ghost-blog.com/blog/short-and-sweet/');
+
+ testData = {nav: {url: 'http://my-ghost-blog.com:3000/blog/'}};
+ utils.urlFor(testContext, testData).should.equal('http://my-ghost-blog.com:3000/blog/');
+
+ testData = {nav: {url: 'http://my-ghost-blog.com:3000/blog/short-and-sweet/'}};
+ utils.urlFor(testContext, testData).should.equal('http://my-ghost-blog.com:3000/blog/short-and-sweet/');
+
+ testData = {nav: {url: 'http://sub.my-ghost-blog.com/blog/'}};
+ utils.urlFor(testContext, testData).should.equal('http://sub.my-ghost-blog.com/blog/');
+
+ testData = {nav: {url: '//sub.my-ghost-blog.com/blog/'}};
+ utils.urlFor(testContext, testData).should.equal('//sub.my-ghost-blog.com/blog/');
+ });
+
+ it('sitemap: should return other known paths when requested', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com'
+ });
+ utils.urlFor('sitemap_xsl').should.equal('/sitemap.xsl');
+ utils.urlFor('sitemap_xsl', true).should.equal('http://my-ghost-blog.com/sitemap.xsl');
+ });
+
+ it('admin: relative', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com'
+ });
+
+ utils.urlFor('admin').should.equal('/ghost/');
+ });
+
+ it('admin: url is http', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com'
+ });
+
+ utils.urlFor('admin', true).should.equal('http://my-ghost-blog.com/ghost/');
+ });
+
+ it('admin: custom admin url is set', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com',
+ adminUrl: 'https://admin.my-ghost-blog.com'
+ });
+
+ utils.urlFor('admin', true).should.equal('https://admin.my-ghost-blog.com/ghost/');
+ });
+
+ it('admin: blog is on subdir', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog'
+ });
+
+ utils.urlFor('admin', true).should.equal('http://my-ghost-blog.com/blog/ghost/');
+ });
+
+ it('admin: blog is on subdir', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog/'
+ });
+
+ utils.urlFor('admin', true).should.equal('http://my-ghost-blog.com/blog/ghost/');
+ });
+
+ it('admin: blog is on subdir', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog'
+ });
+
+ utils.urlFor('admin').should.equal('/blog/ghost/');
+ });
+
+ it('admin: blog is on subdir', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog',
+ adminUrl: 'http://something.com'
+ });
+
+ utils.urlFor('admin', true).should.equal('http://something.com/blog/ghost/');
+ });
+
+ it('admin: blog is on subdir', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog',
+ adminUrl: 'http://something.com/blog'
+ });
+
+ utils.urlFor('admin', true).should.equal('http://something.com/blog/ghost/');
+ });
+
+ it('admin: blog is on subdir', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog',
+ adminUrl: 'http://something.com/blog/'
+ });
+
+ utils.urlFor('admin', true).should.equal('http://something.com/blog/ghost/');
+ });
+
+ it('admin: blog is on subdir', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog/',
+ adminUrl: 'http://something.com/blog'
+ });
+
+ utils.urlFor('admin', true).should.equal('http://something.com/blog/ghost/');
+ });
+
+ ['v0.1', 'v2'].forEach((apiVersion) => {
+ function getApiPath(options) {
+ const baseAPIPath = '/ghost/api/';
+
+ switch (options.version) {
+ case 'v0.1':
+ return `${baseAPIPath}v0.1/`;
+ case 'v2':
+ if (options.versionType === 'admin') {
+ return `${baseAPIPath}v2/admin/`;
+ } else {
+ return `${baseAPIPath}v2/content/`;
+ }
+ default:
+ return `${baseAPIPath}v0.1/`;
+ }
+ }
+
+ describe(`for api version: ${apiVersion}`, function () {
+ it('api: should return admin url is set', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com',
+ adminUrl: 'https://something.de',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils
+ .urlFor('api', {version: apiVersion, versionType: 'content'}, true)
+ .should.eql(`https://something.de${getApiPath({version: apiVersion, versionType: 'content'})}`);
+ });
+
+ it('api: url has subdir', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils
+ .urlFor('api', {version: apiVersion, versionType: 'content'}, true)
+ .should.eql(`http://my-ghost-blog.com/blog${getApiPath({version: apiVersion, versionType: 'content'})}`);
+ });
+
+ it('api: relative path is correct', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils
+ .urlFor('api', {version: apiVersion, versionType: 'content'})
+ .should.eql(getApiPath({version: apiVersion, versionType: 'content'}));
+ });
+
+ it('api: relative path with subdir is correct', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com/blog',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils
+ .urlFor('api', {version: apiVersion, versionType: 'content'})
+ .should.eql(`/blog${getApiPath({version: apiVersion, versionType: 'content'})}`);
+ });
+
+ it('api: should return http if config.url is http', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils
+ .urlFor('api', {version: apiVersion, versionType: 'content'}, true)
+ .should.eql(`http://my-ghost-blog.com${getApiPath({version: apiVersion, versionType: 'content'})}`);
+ });
+
+ it('api: should return https if config.url is https', function () {
+ const utils = urlUtils({
+ url: 'https://my-ghost-blog.com',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils
+ .urlFor('api', {version: apiVersion, versionType: 'content'}, true)
+ .should.eql(`https://my-ghost-blog.com${getApiPath({version: apiVersion, versionType: 'content'})}`);
+ });
+
+ it('api: with cors, blog url is http: should return no protocol', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils
+ .urlFor('api', {cors: true, version: apiVersion, versionType: 'content'}, true)
+ .should.eql(`//my-ghost-blog.com${getApiPath({version: apiVersion, versionType: 'content'})}`);
+ });
+
+ it('api: with cors, admin url is http: cors should return no protocol', function () {
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com',
+ adminUrl: 'http://admin.ghost.example',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils
+ .urlFor('api', {cors: true, version: apiVersion, versionType: 'content'}, true)
+ .should.eql(`//admin.ghost.example${getApiPath({version: apiVersion, versionType: 'content'})}`);
+ });
+
+ it('api: with cors, admin url is https: should return with protocol', function () {
+ const utils = urlUtils({
+ url: 'https://my-ghost-blog.com',
+ adminUrl: 'https://admin.ghost.example',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils
+ .urlFor('api', {cors: true, version: apiVersion, versionType: 'content'}, true)
+ .should.eql(`https://admin.ghost.example${getApiPath({version: apiVersion, versionType: 'content'})}`);
+ });
+
+ it('api: with cors, blog url is https: should return with protocol', function () {
+ const utils = urlUtils({
+ url: 'https://my-ghost-blog.com',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils
+ .urlFor('api', {cors: true, version: apiVersion, versionType: 'content'}, true)
+ .should.eql(`https://my-ghost-blog.com${getApiPath({version: apiVersion, versionType: 'content'})}`);
+ });
+
+ it('api: with stable version, blog url is https: should return stable content api path', function () {
+ const utils = urlUtils({
+ url: 'https://my-ghost-blog.com',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils
+ .urlFor('api', {cors: true, version: apiVersion, versionType: 'content'}, true)
+ .should.eql(`https://my-ghost-blog.com${getApiPath({version: apiVersion, versionType: 'content'})}`);
+ });
+
+ it('api: with stable version and admin true, blog url is https: should return stable admin api path', function () {
+ const utils = urlUtils({
+ url: 'https://my-ghost-blog.com',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils
+ .urlFor('api', {cors: true, version: apiVersion, versionType: 'admin'}, true)
+ .should.eql(`https://my-ghost-blog.com${getApiPath({version: apiVersion, versionType: 'admin'})}`);
+ });
+
+ it('api: with just version and no version type returns correct api path', function () {
+ const utils = urlUtils({
+ url: 'https://my-ghost-blog.com',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils
+ .urlFor('api', {cors: true, version: apiVersion}, true)
+ .should.eql(`https://my-ghost-blog.com${getApiPath({version: apiVersion})}`);
+ });
+ });
+ });
+
+ it('api: with active version, blog url is https: should return active content api path', function () {
+ const utils = urlUtils({
+ url: 'https://my-ghost-blog.com',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils.urlFor('api', {cors: true, version: 'v2', versionType: 'content'}, true).should.eql('https://my-ghost-blog.com/ghost/api/v2/content/');
+ });
+
+ it('api: with active version and admin true, blog url is https: should return active admin api path', function () {
+ const utils = urlUtils({
+ url: 'https://my-ghost-blog.com',
+ apiVersions: defaultAPIVersions
+ });
+
+ utils.urlFor('api', {cors: true, version: 'v2', versionType: 'admin'}, true).should.eql('https://my-ghost-blog.com/ghost/api/v2/admin/');
+ });
+ });
+
+ describe('replacePermalink', function () {
+ it('permalink is /:slug/, timezone is default', function () {
+ const testData = {
+ slug: 'short-and-sweet'
+ };
+ const postLink = '/short-and-sweet/';
+
+ urlUtils().replacePermalink('/:slug/', testData).should.equal(postLink);
+ });
+
+ it('permalink is /:year/:month/:day/:slug/, blog timezone is Los Angeles', function () {
+ const testData = {
+ slug: 'short-and-sweet',
+ published_at: new Date('2016-05-18T06:30:00.000Z')
+ };
+ const timezone = 'America/Los_Angeles';
+ const postLink = '/2016/05/17/short-and-sweet/';
+
+ urlUtils().replacePermalink('/:year/:month/:day/:slug/', testData, timezone).should.equal(postLink);
+ });
+
+ it('permalink is /:year/:month/:day/:slug/, blog timezone is Asia Tokyo', function () {
+ const testData = {
+ slug: 'short-and-sweet',
+ published_at: new Date('2016-05-18T06:30:00.000Z')
+ };
+ const timezone = 'Asia/Tokyo';
+ const postLink = '/2016/05/18/short-and-sweet/';
+
+ urlUtils().replacePermalink('/:year/:month/:day/:slug/', testData, timezone).should.equal(postLink);
+ });
+
+ it('permalink is /:year/:id/:author/, TZ is LA', function () {
+ const testData = {
+ id: 3,
+ primary_author: {slug: 'joe-blog'},
+ slug: 'short-and-sweet',
+ published_at: new Date('2016-01-01T00:00:00.000Z')
+ };
+ const timezone = 'America/Los_Angeles';
+ const postLink = '/2015/3/joe-blog/';
+
+ urlUtils().replacePermalink('/:year/:id/:author/', testData, timezone).should.equal(postLink);
+ });
+
+ it('permalink is /:year/:id:/:author/, TZ is Berlin', function () {
+ const testData = {
+ id: 3,
+ primary_author: {slug: 'joe-blog'},
+ slug: 'short-and-sweet',
+ published_at: new Date('2016-01-01T00:00:00.000Z')
+ };
+ const timezone = 'Europe/Berlin';
+ const postLink = '/2016/3/joe-blog/';
+
+ urlUtils().replacePermalink('/:year/:id/:author/', testData, timezone).should.equal(postLink);
+ });
+
+ it('permalink is /:primary_tag/:slug/ and there is a primary_tag', function () {
+ const testData = {
+ slug: 'short-and-sweet',
+ primary_tag: {slug: 'bitcoin'}
+ };
+ const timezone = 'Europe/Berlin';
+ const postLink = '/bitcoin/short-and-sweet/';
+
+ urlUtils().replacePermalink('/:primary_tag/:slug/', testData, timezone).should.equal(postLink);
+ });
+
+ it('permalink is /:primary_tag/:slug/ and there is NO primary_tag', function () {
+ const testData = {
+ slug: 'short-and-sweet'
+ };
+ const timezone = 'Europe/Berlin';
+ const postLink = '/all/short-and-sweet/';
+
+ urlUtils().replacePermalink('/:primary_tag/:slug/', testData, timezone).should.equal(postLink);
+ });
+
+ it('shows "undefined" for unknown route segments', function () {
+ const testData = {
+ slug: 'short-and-sweet'
+ };
+ const timezone = 'Europe/Berlin';
+ const postLink = '/undefined/short-and-sweet/';
+
+ urlUtils().replacePermalink('/:tag/:slug/', testData, timezone).should.equal(postLink);
+ });
+
+ it('post is not published yet', function () {
+ const testData = {
+ id: 3,
+ slug: 'short-and-sweet',
+ published_at: null
+ };
+ const timezone = 'Europe/London';
+
+ const nowMoment = moment().tz('Europe/London');
+
+ let postLink = '/YYYY/MM/DD/short-and-sweet/';
+
+ postLink = postLink.replace('YYYY', nowMoment.format('YYYY'));
+ postLink = postLink.replace('MM', nowMoment.format('MM'));
+ postLink = postLink.replace('DD', nowMoment.format('DD'));
+
+ urlUtils().replacePermalink('/:year/:month/:day/:slug/', testData, timezone).should.equal(postLink);
+ });
+ });
+
+ describe('isSSL', function () {
+ it('detects https protocol correctly', function () {
+ urlUtils().isSSL('https://my.blog.com').should.be.true();
+ urlUtils().isSSL('http://my.blog.com').should.be.false();
+ urlUtils().isSSL('http://my.https.com').should.be.false();
+ });
+ });
+
+ describe('redirects', function () {
+ it('performs 301 redirect correctly', function (done) {
+ var res = {};
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com',
+ redirectCacheMaxAge: constants.ONE_YEAR_S
+ });
+
+ res.set = sinon.spy();
+
+ res.redirect = function (code, path) {
+ code.should.equal(301);
+ path.should.eql('my/awesome/path');
+ res.set.calledWith({'Cache-Control': 'public, max-age=' + constants.ONE_YEAR_S}).should.be.true();
+
+ done();
+ };
+
+ utils.redirect301(res, 'my/awesome/path');
+ });
+
+ it('performs an admin 301 redirect correctly', function (done) {
+ var res = {};
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com',
+ redirectCacheMaxAge: constants.ONE_YEAR_S
+ });
+
+ res.set = sinon.spy();
+
+ res.redirect = function (code, path) {
+ code.should.equal(301);
+ path.should.eql('/ghost/#/my/awesome/path/');
+ res.set.calledWith({'Cache-Control': 'public, max-age=' + constants.ONE_YEAR_S}).should.be.true();
+
+ done();
+ };
+
+ utils.redirectToAdmin(301, res, '#/my/awesome/path');
+ });
+
+ it('performs an admin 302 redirect correctly', function (done) {
+ var res = {};
+
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com',
+ redirectCacheMaxAge: constants.ONE_YEAR_S
+ });
+
+ res.set = sinon.spy();
+
+ res.redirect = function (path) {
+ path.should.eql('/ghost/#/my/awesome/path/');
+ res.set.called.should.be.false();
+
+ done();
+ };
+
+ utils.redirectToAdmin(302, res, '#/my/awesome/path');
+ });
+ });
+
+ describe('make absolute urls ', function () {
+ const siteUrl = 'http://my-ghost-blog.com';
+ const itemUrl = 'my-awesome-post';
+
+ const utils = urlUtils({
+ url: 'http://my-ghost-blog.com'
+ });
+
+ it('[success] does not convert absolute URLs', function () {
+ var html = '',
+ result = utils.makeAbsoluteUrls(html, siteUrl, itemUrl).html();
+
+ result.should.match(//);
+ });
+ it('[failure] does not convert protocol relative `//` URLs', function () {
+ var html = '',
+ result = utils.makeAbsoluteUrls(html, siteUrl, itemUrl).html();
+
+ result.should.match(//);
+ });
+ it('[failure] does not convert internal links starting with "#"', function () {
+ var html = '',
+ result = utils.makeAbsoluteUrls(html, siteUrl, itemUrl).html();
+
+ result.should.match(//);
+ });
+ it('[success] converts a relative URL', function () {
+ var html = '',
+ result = utils.makeAbsoluteUrls(html, siteUrl, itemUrl).html();
+
+ result.should.match(//);
+ });
+ it('[success] converts a relative URL including subdirectories', function () {
+ var html = '',
+ result = utils.makeAbsoluteUrls(html, 'http://my-ghost-blog.com/blog', itemUrl).html();
+
+ result.should.match(//);
+ });
+
+ it('asset urls only', function () {
+ let html = '';
+ let result = utils.makeAbsoluteUrls(html, siteUrl, itemUrl, {assetsOnly: true}).html();
+
+ result.should.match(//);
+ result.should.match(//);
+
+ html = '';
+ result = utils.makeAbsoluteUrls(html, siteUrl, itemUrl, {assetsOnly: true}).html();
+
+ result.should.match(//);
+
+ html = '';
+ result = utils.makeAbsoluteUrls(html, siteUrl, itemUrl, {assetsOnly: true}).html();
+
+ result.should.match(//);
+
+ html = '';
+ result = utils.makeAbsoluteUrls(html, siteUrl, itemUrl, {assetsOnly: true}).html();
+
+ result.should.match(//);
+
+ html = '';
+ result = utils.makeAbsoluteUrls(html, siteUrl, itemUrl, {assetsOnly: true}).html();
+
+ result.should.match(//);
+ });
+ });
+});
diff --git a/packages/url-utils/test/utils/assertions.js b/packages/url-utils/test/utils/assertions.js
new file mode 100644
index 000000000..7364ee8aa
--- /dev/null
+++ b/packages/url-utils/test/utils/assertions.js
@@ -0,0 +1,11 @@
+/**
+ * Custom Should Assertions
+ *
+ * Add any custom assertions to this file.
+ */
+
+// Example Assertion
+// should.Assertion.add('ExampleAssertion', function () {
+// this.params = {operator: 'to be a valid Example Assertion'};
+// this.obj.should.be.an.Object;
+// });
diff --git a/packages/url-utils/test/utils/index.js b/packages/url-utils/test/utils/index.js
new file mode 100644
index 000000000..0d67d86ff
--- /dev/null
+++ b/packages/url-utils/test/utils/index.js
@@ -0,0 +1,11 @@
+/**
+ * Test Utilities
+ *
+ * Shared utils for writing tests
+ */
+
+// Require overrides - these add globals for tests
+require('./overrides');
+
+// Require assertions - adds custom should assertions
+require('./assertions');
diff --git a/packages/url-utils/test/utils/overrides.js b/packages/url-utils/test/utils/overrides.js
new file mode 100644
index 000000000..90203424e
--- /dev/null
+++ b/packages/url-utils/test/utils/overrides.js
@@ -0,0 +1,10 @@
+// This file is required before any test is run
+
+// Taken from the should wiki, this is how to make should global
+// Should is a global in our eslint test config
+global.should = require('should').noConflict();
+should.extend();
+
+// Sinon is a simple case
+// Sinon is a global in our eslint test config
+global.sinon = require('sinon');
diff --git a/yarn.lock b/yarn.lock
index 918db39cb..415b39065 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1156,11 +1156,21 @@ binary-extensions@^1.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
+bluebird@3.5.5:
+ version "3.5.5"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
+ integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
+
bluebird@^3.5.3:
version "3.5.4"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.4.tgz#d6cc661595de30d5b3af5fcedd3c0b3ef6ec5714"
integrity sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==
+boolbase@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+ integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -1296,6 +1306,28 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+cheerio@0.22.0:
+ version "0.22.0"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e"
+ integrity sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=
+ dependencies:
+ css-select "~1.2.0"
+ dom-serializer "~0.1.0"
+ entities "~1.1.1"
+ htmlparser2 "^3.9.1"
+ lodash.assignin "^4.0.9"
+ lodash.bind "^4.1.4"
+ lodash.defaults "^4.0.1"
+ lodash.filter "^4.4.0"
+ lodash.flatten "^4.2.0"
+ lodash.foreach "^4.3.0"
+ lodash.map "^4.4.0"
+ lodash.merge "^4.4.0"
+ lodash.pick "^4.2.1"
+ lodash.reduce "^4.4.0"
+ lodash.reject "^4.4.0"
+ lodash.some "^4.4.0"
+
chokidar@^2.0.4:
version "2.1.5"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.5.tgz#0ae8434d962281a5f56c72869e79cb6d9d86ad4d"
@@ -1464,6 +1496,21 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
shebang-command "^1.2.0"
which "^1.2.9"
+css-select@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+ integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
+ dependencies:
+ boolbase "~1.0.0"
+ css-what "2.1"
+ domutils "1.5.1"
+ nth-check "~1.0.1"
+
+css-what@2.1:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
+ integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
+
cssom@0.3.x, cssom@^0.3.4:
version "0.3.6"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.6.tgz#f85206cee04efa841f3c5982a74ba96ab20d65ad"
@@ -1608,6 +1655,19 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
+dom-serializer@0, dom-serializer@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+ integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
+ dependencies:
+ domelementtype "^1.3.0"
+ entities "^1.1.1"
+
+domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+ integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+
domexception@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
@@ -1615,6 +1675,29 @@ domexception@^1.0.1:
dependencies:
webidl-conversions "^4.0.2"
+domhandler@^2.3.0:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+ integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+ dependencies:
+ domelementtype "1"
+
+domutils@1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+ integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+domutils@^1.5.1:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+ integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
dtrace-provider@~0.8:
version "0.8.7"
resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.7.tgz#dc939b4d3e0620cfe0c1cd803d0d2d7ed04ffd04"
@@ -1659,6 +1742,11 @@ end-of-stream@^1.1.0:
dependencies:
once "^1.4.0"
+entities@^1.1.1, entities@~1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+ integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+
es-abstract@^1.11.0, es-abstract@^1.5.1, es-abstract@^1.7.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
@@ -2155,7 +2243,7 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
-ghost-ignition@^3.0.0, ghost-ignition@^3.0.4:
+ghost-ignition@3.1.0, ghost-ignition@^3.0.0, ghost-ignition@^3.0.4:
version "3.1.0"
resolved "https://registry.yarnpkg.com/ghost-ignition/-/ghost-ignition-3.1.0.tgz#4a0d7f0a15e54fbc0e16d398da62ff68737e4e23"
integrity sha512-ae0r/yBJDo9SkeLfy0ecrHVqO0gt9l07wiCOWmdzdvbmoU24AB881PjymnM/DLP9SY6E33mLYtT8K3ximgieLg==
@@ -2325,6 +2413,18 @@ html-encoding-sniffer@^1.0.2:
dependencies:
whatwg-encoding "^1.0.1"
+htmlparser2@^3.9.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+ integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+ dependencies:
+ domelementtype "^1.3.1"
+ domhandler "^2.3.0"
+ domutils "^1.5.1"
+ entities "^1.1.1"
+ inherits "^2.0.1"
+ readable-stream "^3.1.1"
+
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
@@ -2374,7 +2474,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@^2.0.3, inherits@~2.0.3:
+inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
@@ -2867,6 +2967,36 @@ lodash-es@^4.17.11:
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0"
integrity sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==
+lodash.assignin@^4.0.9:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2"
+ integrity sha1-uo31+4QesKPoBEIysOJjqNxqKKI=
+
+lodash.bind@^4.1.4:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35"
+ integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=
+
+lodash.defaults@^4.0.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
+ integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
+
+lodash.filter@^4.4.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
+ integrity sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=
+
+lodash.flatten@^4.2.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
+ integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
+
+lodash.foreach@^4.3.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
+ integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=
+
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
@@ -2897,17 +3027,47 @@ lodash.isstring@^4.0.1:
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
+lodash.map@^4.4.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
+ integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=
+
+lodash.merge@^4.4.0:
+ version "4.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
+ integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==
+
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
+lodash.pick@^4.2.1:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
+ integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
+
+lodash.reduce@^4.4.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b"
+ integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=
+
+lodash.reject@^4.4.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415"
+ integrity sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=
+
+lodash.some@^4.4.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
+ integrity sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=
+
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
-lodash@^4.16.4, lodash@^4.17.11:
+lodash@4.17.11, lodash@^4.16.4, lodash@^4.17.11:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
@@ -3115,7 +3275,14 @@ mocha@6.1.4:
yargs-parser "13.0.0"
yargs-unparser "1.5.0"
-moment@^2.10.6, moment@^2.15.2, moment@^2.18.1:
+moment-timezone@0.5.25:
+ version "0.5.25"
+ resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.25.tgz#a11bfa2f74e088327f2cd4c08b3e7bdf55957810"
+ integrity sha512-DgEaTyN/z0HFaVcVbSyVCUU6HeFdnNC3vE4c9cgu2dgMTvjBUBdBzWfasTBmAW45u5OIMeCJtU8yNjM22DHucw==
+ dependencies:
+ moment ">= 2.9.0"
+
+"moment@>= 2.9.0", moment@^2.10.6, moment@^2.15.2, moment@^2.18.1:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
@@ -3320,6 +3487,13 @@ npmlog@^4.0.2:
gauge "~2.7.3"
set-blocking "~2.0.0"
+nth-check@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+ integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+ dependencies:
+ boolbase "~1.0.0"
+
number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
@@ -3669,6 +3843,15 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
+readable-stream@^3.1.1:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc"
+ integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
readdirp@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
@@ -4265,6 +4448,13 @@ string-width@^3.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
+string_decoder@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
+ integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
+ dependencies:
+ safe-buffer "~5.1.0"
+
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
@@ -4553,7 +4743,7 @@ use@^3.1.0:
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
-util-deprecate@~1.0.1:
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=