Ghost/core/server/api/themes.js
Katharina Irrgang 594b0c2d14 Custom post templates (#9073)
closes #9060

- Update `gscan` - it now extracts custom templates and exposes them to Ghost
- Add `custom_template` field to post schema w/ 1.13 migration
- Return `templates` array for the active theme in `/themes/` requests
- Users with Author/Editor roles can now request `/themes/`
- Front-end will render `custom_template` for posts if it exists, template priority is now:
  1. `post/page-{{slug}}.hbs`
  2. `{{custom_template}}.hbs`
  3. `post/page.hbs`
2017-10-10 13:36:35 +01:00

225 lines
8.2 KiB
JavaScript

// # Themes API
// RESTful API for Themes
var debug = require('ghost-ignition').debug('api:themes'),
Promise = require('bluebird'),
fs = require('fs-extra'),
apiUtils = require('./utils'),
errors = require('../errors'),
events = require('../events'),
i18n = require('../i18n'),
logging = require('../logging'),
settingsModel = require('../models/settings').Settings,
settingsCache = require('../settings/cache'),
themeUtils = require('../themes'),
themeList = themeUtils.list,
themes;
/**
* ## Themes API Methods
*
* **See:** [API Methods](index.js.html#api%20methods)
*/
themes = {
/**
* Every role can browse all themes. The response contains a list of all available themes in your content folder.
* The active theme get's marked as `active:true` and contains an extra object `templates`, which
* contains the custom templates of the active theme. These custom templates are used to show a dropdown
* in the PSM to be able to choose a custom post template.
*/
browse: function browse(options) {
return apiUtils
// Permissions
.handlePermissions('themes', 'browse')(options)
// Main action
.then(function makeApiResult() {
// Return JSON result
return themeUtils.toJSON();
});
},
activate: function activate(options) {
var themeName = options.name,
newSettings = [{
key: 'active_theme',
value: themeName
}],
loadedTheme,
checkedTheme;
return apiUtils
// Permissions
.handlePermissions('themes', 'activate')(options)
// Validation
.then(function validateTheme() {
loadedTheme = themeList.get(themeName);
if (!loadedTheme) {
return Promise.reject(new errors.ValidationError({
message: i18n.t('notices.data.validation.index.themeCannotBeActivated', {themeName: themeName}),
context: 'active_theme'
}));
}
return themeUtils.validate.check(loadedTheme);
})
// Update setting
.then(function changeActiveThemeSetting(_checkedTheme) {
checkedTheme = _checkedTheme;
// We use the model, not the API here, as we don't want to trigger permissions
return settingsModel.edit(newSettings, options);
})
// Call activate
.then(function hasEditedSetting() {
// Activate! (sort of)
debug('Activating theme (method B on API "activate")', themeName);
themeUtils.activate(loadedTheme, checkedTheme);
// Return JSON result
return themeUtils.toJSON(themeName, checkedTheme);
});
},
upload: function upload(options) {
options = options || {};
// consistent filename uploads
options.originalname = options.originalname.toLowerCase();
var zip = {
path: options.path,
name: options.originalname,
shortName: themeUtils.storage.getSanitizedFileName(options.originalname.split('.zip')[0])
},
checkedTheme;
// check if zip name is casper.zip
if (zip.name === 'casper.zip') {
throw new errors.ValidationError({message: i18n.t('errors.api.themes.overrideCasper')});
}
return apiUtils
// Permissions
.handlePermissions('themes', 'add')(options)
// Validation
.then(function validateTheme() {
return themeUtils.validate.check(zip, true);
})
// More validation (existence check)
.then(function checkExists(_checkedTheme) {
checkedTheme = _checkedTheme;
return themeUtils.storage.exists(zip.shortName);
})
// If the theme existed we need to delete it
.then(function removeOldTheme(themeExists) {
// delete existing theme
if (themeExists) {
return themeUtils.storage.delete(zip.shortName);
}
})
.then(function storeNewTheme() {
events.emit('theme.uploaded', zip.shortName);
// store extracted theme
return themeUtils.storage.save({
name: zip.shortName,
path: checkedTheme.path
});
})
.then(function loadNewTheme() {
// Loads the theme from the filesystem
// Sets the theme on the themeList
return themeUtils.loadOne(zip.shortName);
})
.then(function activateAndReturn(loadedTheme) {
// If this is the active theme, we are overriding
// This is a special case of activation
if (zip.shortName === settingsCache.get('active_theme')) {
// Activate! (sort of)
debug('Activating theme (method C, on API "override")', zip.shortName);
themeUtils.activate(loadedTheme, checkedTheme);
}
// @TODO: unify the name across gscan and Ghost!
return themeUtils.toJSON(zip.shortName, checkedTheme);
})
.finally(function () {
// @TODO we should probably do this as part of saving the theme
// remove zip upload from multer
// happens in background
Promise.promisify(fs.remove)(zip.path)
.catch(function (err) {
logging.error(new errors.GhostError({err: err}));
});
// @TODO we should probably do this as part of saving the theme
// remove extracted dir from gscan
// happens in background
if (checkedTheme) {
Promise.promisify(fs.remove)(checkedTheme.path)
.catch(function (err) {
logging.error(new errors.GhostError({err: err}));
});
}
});
},
download: function download(options) {
var themeName = options.name,
theme = themeList.get(themeName);
if (!theme) {
return Promise.reject(new errors.BadRequestError({message: i18n.t('errors.api.themes.invalidRequest')}));
}
return apiUtils
// Permissions
.handlePermissions('themes', 'read')(options)
.then(function sendTheme() {
events.emit('theme.downloaded', themeName);
return themeUtils.storage.serve({
name: themeName
});
});
},
/**
* remove theme zip
* remove theme folder
*/
destroy: function destroy(options) {
var themeName = options.name,
theme;
return apiUtils
// Permissions
.handlePermissions('themes', 'destroy')(options)
// Validation
.then(function validateTheme() {
if (themeName === 'casper') {
throw new errors.ValidationError({message: i18n.t('errors.api.themes.destroyCasper')});
}
if (themeName === settingsCache.get('active_theme')) {
throw new errors.ValidationError({message: i18n.t('errors.api.themes.destroyActive')});
}
theme = themeList.get(themeName);
if (!theme) {
throw new errors.NotFoundError({message: i18n.t('errors.api.themes.themeDoesNotExist')});
}
// Actually do the deletion here
return themeUtils.storage.delete(themeName);
})
// And some extra stuff to maintain state here
.then(function deleteTheme() {
themeList.del(themeName);
events.emit('theme.deleted', themeName);
// Delete returns an empty 204 response
});
}
};
module.exports = themes;