Ghost/core/server/utils/url.js
Katharina Irrgang 2a52af1d99 🔥 remove imageRelPath (#7927)
refs #7488

- remove imageRelPath
- instead add a static image prefix to the url helper
2017-02-03 14:42:05 +00:00

343 lines
12 KiB
JavaScript

// Contains all path information to be used throughout
// the codebase.
var moment = require('moment-timezone'),
_ = require('lodash'),
url = require('url'),
config = require('./../config'),
settingsCache = require('./../api/settings').cache,
// @TODO: unify this with routes.apiBaseUrl
apiPath = '/ghost/api/v0.1',
STATIC_IMAGE_URL_PREFIX = 'content/images';
/** getBaseUrl
* Returns the base URL of the blog as set in the config. If called with secure options, returns the ssl URL.
* @param {boolean} secure
* @return {string} URL returns the url as defined in config, but always with a trailing `/`
*/
function getBaseUrl(secure) {
var base;
// CASE: a specified SSL URL is configured (e. g. https://secure.blog.org/)
// see: https://github.com/TryGhost/Ghost/issues/6270#issuecomment-168939865
if (secure && config.get('urlSSL')) {
base = config.get('urlSSL');
} else {
// CASE: no specified SSL URL configured, but user request is secure. In this case we force SSL
// and therefore replace the protocol.
if (secure) {
base = config.get('url').replace('http://', 'https://');
} else {
base = config.get('url');
}
}
if (!base.match(/\/$/)) {
base += '/';
}
return base;
}
/** getSubdir
* Returns a subdirectory URL, if defined so in the config.
* @return {string} URL a subdirectory if configured.
*/
function getSubdir() {
var localPath, subdir;
// Parse local path location
if (config.get('url')) {
localPath = url.parse(config.get('url')).path;
// Remove trailing slash
if (localPath !== '/') {
localPath = localPath.replace(/\/$/, '');
}
}
subdir = localPath === '/' ? '' : localPath;
return subdir;
}
function getProtectedSlugs() {
var subdir = getSubdir();
if (!_.isEmpty(subdir)) {
return config.get('slugs').protected.concat([subdir.split('/').pop()]);
} else {
return config.get('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,
subdir = getSubdir().replace(/^\/|\/+$/, ''),
subdirRegex,
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(/^\//, '//');
}
// Deduplicate subdirectory
if (subdir) {
subdirRegex = new RegExp(subdir + '\/' + subdir + '\/');
url = url.replace(subdirRegex, subdir + '/');
}
return url;
}
// ## 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 use urlSSL or url config
// Returns:
// - a URL which always ends with a slash
function createUrl(urlPath, absolute, secure) {
urlPath = urlPath || '/';
absolute = absolute || false;
var base;
// create base of url, always ends without a slash
if (absolute) {
base = getBaseUrl(secure);
} else {
base = getSubdir();
}
return urlJoin(base, urlPath);
}
/**
* creates the url path for a post based on blog timezone and permalink pattern
*
* @param {JSON} post
* @returns {string}
*/
function urlPathForPost(post) {
var output = '',
permalinks = settingsCache.get('permalinks'),
publishedAtMoment = moment.tz(post.published_at || Date.now(), settingsCache.get('activeTimezone')),
tags = {
year: function () { return publishedAtMoment.format('YYYY'); },
month: function () { return publishedAtMoment.format('MM'); },
day: function () { return publishedAtMoment.format('DD'); },
author: function () { return post.author.slug; },
slug: function () { return post.slug; },
id: function () { return post.id; }
};
if (post.page) {
output += '/:slug/';
} else {
output += permalinks;
}
// replace tags like :slug or :year with actual values
output = output.replace(/(:[a-z]+)/g, function (match) {
if (_.has(tags, match.substr(1))) {
return tags[match.substr(1)]();
}
});
return output;
}
// ## urlFor
// Synchronous url creation for a given context
// Can generate a url for a named path, given path, or known object (post)
// 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/
// E.g. if post object represents welcome post, and slugs are set to standard
// urlFor('post', {...}) -> /welcome-to-ghost/
// E.g. if post object represents welcome post, and slugs are set to date
// urlFor('post', {...}) -> /2014/01/01/welcome-to-ghost/
// 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!
function urlFor(context, data, absolute) {
var urlPath = '/',
secure, imagePathRe,
knownObjects = ['post', 'tag', 'author', 'image', 'nav'], baseUrl,
hostname,
// this will become really big
knownPaths = {
home: '/',
rss: '/rss/',
api: apiPath,
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) {
// trying to create a url for an object
if (context === 'post' && data.post) {
urlPath = data.post.url;
secure = data.secure;
} else if (context === 'tag' && data.tag) {
urlPath = urlJoin('/', config.get('routeKeywords').tag, data.tag.slug, '/');
secure = data.tag.secure;
} else if (context === 'author' && data.author) {
urlPath = urlJoin('/', config.get('routeKeywords').author, data.author.slug, '/');
secure = data.author.secure;
} else if (context === 'image' && data.image) {
urlPath = data.image;
imagePathRe = new RegExp('^' + getSubdir() + '/' + STATIC_IMAGE_URL_PREFIX);
absolute = imagePathRe.test(data.image) ? absolute : false;
secure = data.image.secure;
if (absolute) {
// Remove the sub-directory from the URL because ghostConfig will add it back.
urlPath = urlPath.replace(new RegExp('^' + getSubdir()), '');
baseUrl = getBaseUrl(secure).replace(/\/$/, '');
urlPath = baseUrl + urlPath;
}
return urlPath;
} else if (context === 'nav' && data.nav) {
urlPath = data.nav.url;
secure = data.nav.secure || secure;
baseUrl = getBaseUrl(secure);
hostname = baseUrl.split('//')[1] + getSubdir();
if (urlPath.indexOf(hostname) > -1
&& !urlPath.split(hostname)[0].match(/\.|mailto:/)
&& urlPath.split(hostname)[1].substring(0,1) !== ':') {
// make link relative to account for possible
// mismatch in http/https etc, force absolute
// do not do so if link is a subdomain of blog url
// or if hostname is inside of the slug
// or if slug is a port
urlPath = urlPath.split(hostname)[1];
if (urlPath.substring(0, 1) !== '/') {
urlPath = '/' + urlPath;
}
absolute = true;
}
}
} else if (context === 'home' && absolute) {
urlPath = getBaseUrl(secure);
// other objects are recognised but not yet supported
} else if (context === 'admin') {
if (config.get('forceAdminSSL')) {
urlPath = getBaseUrl(true);
} else {
urlPath = getBaseUrl();
}
if (absolute) {
urlPath += 'ghost/';
} else {
urlPath = '/ghost/';
}
} 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);
}
/**
* CASE: generate api url for CORS
* - we delete the http protocol if your blog runs with http and https (configured by nginx)
* - in that case your config.js configures Ghost with http and no admin ssl force
* - the browser then reads the protocol dynamically
*/
function apiUrl(options) {
options = options || {cors: false};
// @TODO unify this with urlFor
var url;
if (config.get('forceAdminSSL')) {
url = (config.get('urlSSL') || getBaseUrl(true)).replace(/^.*?:\/\//g, 'https://');
} else if (config.get('urlSSL')) {
url = config.get('urlSSL').replace(/^.*?:\/\//g, 'https://');
} else if (config.get('url').match(/^https:/)) {
url = config.get('url');
} else {
if (options.cors === false) {
url = config.get('url');
} else {
url = config.get('url').replace(/^.*?:\/\//g, '//');
}
}
return url.replace(/\/$/, '') + apiPath + '/';
}
module.exports.getProtectedSlugs = getProtectedSlugs;
module.exports.getSubdir = getSubdir;
module.exports.urlJoin = urlJoin;
module.exports.urlFor = urlFor;
module.exports.urlPathForPost = urlPathForPost;
module.exports.apiUrl = apiUrl;
/**
* 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
*/
module.exports.STATIC_IMAGE_URL_PREFIX = STATIC_IMAGE_URL_PREFIX;