diff --git a/core/server/models/base/bookshelf.js b/core/server/models/base/bookshelf.js index 36f0fc8470..9f8307683d 100644 --- a/core/server/models/base/bookshelf.js +++ b/core/server/models/base/bookshelf.js @@ -52,6 +52,8 @@ ghostBookshelf.plugin(require('./raw-knex')); ghostBookshelf.plugin(require('./sanitize')); +ghostBookshelf.plugin(require('./generate-slug')); + // Manages nested updates (relationships) ghostBookshelf.plugin('bookshelf-relations', { allowedOptions: ['context', 'importing', 'migrating'], diff --git a/core/server/models/base/generate-slug.js b/core/server/models/base/generate-slug.js new file mode 100644 index 0000000000..3d5d04ec11 --- /dev/null +++ b/core/server/models/base/generate-slug.js @@ -0,0 +1,109 @@ +const _ = require('lodash'); +const security = require('@tryghost/security'); + +const urlUtils = require('../../../shared/url-utils'); + +/** + * @type {Bookshelf} Bookshelf + */ +module.exports = function (Bookshelf) { + Bookshelf.Model = Bookshelf.Model.extend({}, { + /** + * ### Generate Slug + * Create a string to act as the permalink for an object. + * @param {Bookshelf['Model']} Model Model type to generate a slug for + * @param {String} base The string for which to generate a slug, usually a title or name + * @param {Object} options Options to pass to findOne + * @return {Promise} Resolves to a unique slug string + */ + generateSlug: function generateSlug(Model, base, options) { + let slug; + let slugTryCount = 1; + const baseName = Model.prototype.tableName.replace(/s$/, ''); + + let longSlug; + + // Look for a matching slug, append an incrementing number if so + const checkIfSlugExists = function checkIfSlugExists(slugToFind) { + const args = {slug: slugToFind}; + + // status is needed for posts + if (options && options.status) { + args.status = options.status; + } + + return Model.findOne(args, options).then(function then(found) { + let trimSpace; + + if (!found) { + return slugToFind; + } + + slugTryCount += 1; + + // If we shortened, go back to the full version and try again + if (slugTryCount === 2 && longSlug) { + slugToFind = longSlug; + longSlug = null; + slugTryCount = 1; + return checkIfSlugExists(slugToFind); + } + + // If this is the first time through, add the hyphen + if (slugTryCount === 2) { + slugToFind += '-'; + } else { + // Otherwise, trim the number off the end + trimSpace = -(String(slugTryCount - 1).length); + slugToFind = slugToFind.slice(0, trimSpace); + } + + slugToFind += slugTryCount; + + return checkIfSlugExists(slugToFind); + }); + }; + + slug = security.string.safe(base, options); + + // the slug may never be longer than the allowed limit of 191 chars, but should also + // take the counter into count. We reduce a too long slug to 185 so we're always on the + // safe side, also in terms of checking for existing slugs already. + if (slug.length > 185) { + // CASE: don't cut the slug on import + if (!_.has(options, 'importing') || !options.importing) { + slug = slug.slice(0, 185); + } + } + + // If it's a user, let's try to cut it down (unless this is a human request) + if (baseName === 'user' && options && options.shortSlug && slugTryCount === 1 && slug !== 'ghost-owner') { + longSlug = slug; + slug = (slug.indexOf('-') > -1) ? slug.substr(0, slug.indexOf('-')) : slug; + } + + if (!_.has(options, 'importing') || !options.importing) { + // This checks if the first character of a tag name is a #. If it is, this + // is an internal tag, and as such we should add 'hash' to the beginning of the slug + if (baseName === 'tag' && /^#/.test(base)) { + slug = 'hash-' + slug; + } + } + + // Some keywords cannot be changed + slug = _.includes(urlUtils.getProtectedSlugs(), slug) ? slug + '-' + baseName : slug; + + // if slug is empty after trimming use the model name + if (!slug) { + slug = baseName; + } + + // Test for duplicate slugs. + return checkIfSlugExists(slug); + } + }); +}; + +/** + * @type {import('bookshelf')} Bookshelf + */ diff --git a/core/server/models/base/index.js b/core/server/models/base/index.js index a0971d306c..418d6393ef 100644 --- a/core/server/models/base/index.js +++ b/core/server/models/base/index.js @@ -11,14 +11,16 @@ const _ = require('lodash'); const moment = require('moment'); const ObjectId = require('bson-objectid'); const errors = require('@tryghost/errors'); -const security = require('@tryghost/security'); const schema = require('../../data/schema'); -const urlUtils = require('../../../shared/url-utils'); const bulkOperations = require('./bulk-operations'); const tpl = require('@tryghost/tpl'); const ghostBookshelf = require('./bookshelf'); +const messages = { + missingContext: 'missing context' +}; + let proto; // Cache an instance of the base model prototype @@ -368,100 +370,6 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ tableName = tableName || this.prototype.tableName; return bulkOperations.del(tableName, data); - }, - - /** - * ### Generate Slug - * Create a string to act as the permalink for an object. - * @param {ghostBookshelf.Model} Model Model type to generate a slug for - * @param {String} base The string for which to generate a slug, usually a title or name - * @param {Object} options Options to pass to findOne - * @return {Promise} Resolves to a unique slug string - */ - generateSlug: function generateSlug(Model, base, options) { - let slug; - let slugTryCount = 1; - const baseName = Model.prototype.tableName.replace(/s$/, ''); - - let longSlug; - - // Look for a matching slug, append an incrementing number if so - const checkIfSlugExists = function checkIfSlugExists(slugToFind) { - const args = {slug: slugToFind}; - - // status is needed for posts - if (options && options.status) { - args.status = options.status; - } - - return Model.findOne(args, options).then(function then(found) { - let trimSpace; - - if (!found) { - return slugToFind; - } - - slugTryCount += 1; - - // If we shortened, go back to the full version and try again - if (slugTryCount === 2 && longSlug) { - slugToFind = longSlug; - longSlug = null; - slugTryCount = 1; - return checkIfSlugExists(slugToFind); - } - - // If this is the first time through, add the hyphen - if (slugTryCount === 2) { - slugToFind += '-'; - } else { - // Otherwise, trim the number off the end - trimSpace = -(String(slugTryCount - 1).length); - slugToFind = slugToFind.slice(0, trimSpace); - } - - slugToFind += slugTryCount; - - return checkIfSlugExists(slugToFind); - }); - }; - - slug = security.string.safe(base, options); - - // the slug may never be longer than the allowed limit of 191 chars, but should also - // take the counter into count. We reduce a too long slug to 185 so we're always on the - // safe side, also in terms of checking for existing slugs already. - if (slug.length > 185) { - // CASE: don't cut the slug on import - if (!_.has(options, 'importing') || !options.importing) { - slug = slug.slice(0, 185); - } - } - - // If it's a user, let's try to cut it down (unless this is a human request) - if (baseName === 'user' && options && options.shortSlug && slugTryCount === 1 && slug !== 'ghost-owner') { - longSlug = slug; - slug = (slug.indexOf('-') > -1) ? slug.substr(0, slug.indexOf('-')) : slug; - } - - if (!_.has(options, 'importing') || !options.importing) { - // This checks if the first character of a tag name is a #. If it is, this - // is an internal tag, and as such we should add 'hash' to the beginning of the slug - if (baseName === 'tag' && /^#/.test(base)) { - slug = 'hash-' + slug; - } - } - - // Some keywords cannot be changed - slug = _.includes(urlUtils.getProtectedSlugs(), slug) ? slug + '-' + baseName : slug; - - // if slug is empty after trimming use the model name - if (!slug) { - slug = baseName; - } - - // Test for duplicate slugs. - return checkIfSlugExists(slug); } });