mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
📡 Add debug for the 3 theme activation methods There are 3 different ways that a theme can be activated in Ghost: A. On boot: we load the active theme from the file system, according to the `activeTheme` setting B. On API "activate": when an /activate/ request is triggered for a theme, we validate & change the `activeTheme` setting C. On API "override": if uploading a theme with the same name, we override. Using a dirty hack to make this work. A: setting is done, should load & validate + next request does mounting B: load is done, should validate & change setting + next request does mounting C: load, validate & setting are all done + a hack is needed to ensure the next request does mounting ✨ Validate w/ gscan when theme activating on boot - use the new gscan validation validate.check() method when activating on boot ✨ New concept of active theme - add ActiveTheme class - make it possible to set a theme to be active, and to get the active theme - call the new themes.activate() method in all 3 cases where we activate a theme 🎨 Use new activeTheme to simplify theme code - make use of the new concept where we can, to reduce & simplify code - use new hasPartials() method so we don't have to do file lookups - use path & name getters to reduce use of getContentPath etc - remove requirement on req.app.get('activeTheme') from static-theme middleware (more on this soon) 🚨 Improve theme unit tests (TODO: fix inter-dep) - The theme unit tests are borked! They all pass because they don't test the right things. - This improves them, but they are still dependent on each-other - configHbsForContext tests don't pass if the activateTheme tests aren't run first - I will fix this in a later PR
198 lines
7.3 KiB
JavaScript
198 lines
7.3 KiB
JavaScript
// # Themes API
|
|
// RESTful API for Themes
|
|
var debug = require('debug')('ghost:api:themes'),
|
|
Promise = require('bluebird'),
|
|
fs = require('fs-extra'),
|
|
config = require('../config'),
|
|
errors = require('../errors'),
|
|
events = require('../events'),
|
|
logging = require('../logging'),
|
|
storage = require('../storage'),
|
|
apiUtils = require('./utils'),
|
|
utils = require('./../utils'),
|
|
i18n = require('../i18n'),
|
|
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 = {
|
|
browse: function browse() {
|
|
return Promise.resolve(themeUtils.toJSON());
|
|
},
|
|
|
|
activate: function activate(options) {
|
|
var themeName = options.name,
|
|
newSettings = [{
|
|
key: 'activeTheme',
|
|
value: themeName
|
|
}],
|
|
loadedTheme,
|
|
checkedTheme;
|
|
|
|
return apiUtils
|
|
.handlePermissions('themes', 'activate')(options)
|
|
.then(function activateTheme() {
|
|
loadedTheme = themeList.get(themeName);
|
|
|
|
if (!loadedTheme) {
|
|
return Promise.reject(new errors.ValidationError({
|
|
message: i18n.t('notices.data.validation.index.themeCannotBeActivated', {themeName: themeName}),
|
|
context: 'activeTheme'
|
|
}));
|
|
}
|
|
|
|
return themeUtils.validate.check(loadedTheme);
|
|
})
|
|
.then(function haveValidTheme(_checkedTheme) {
|
|
checkedTheme = _checkedTheme;
|
|
// We use the model, not the API here, as we don't want to trigger permissions
|
|
return settingsModel.edit(newSettings, options);
|
|
})
|
|
.then(function hasEditedSetting() {
|
|
// Activate! (sort of)
|
|
debug('Activating theme (method B on API "activate")', themeName);
|
|
themeUtils.activate(loadedTheme, checkedTheme);
|
|
|
|
return themeUtils.toJSON(themeName, checkedTheme);
|
|
});
|
|
},
|
|
|
|
upload: function upload(options) {
|
|
options = options || {};
|
|
|
|
// consistent filename uploads
|
|
options.originalname = options.originalname.toLowerCase();
|
|
|
|
var storageAdapter = storage.getStorage('themes'),
|
|
zip = {
|
|
path: options.path,
|
|
name: options.originalname,
|
|
shortName: storageAdapter.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.handlePermissions('themes', 'add')(options)
|
|
.then(function validateTheme() {
|
|
return themeUtils.validate.check(zip, true);
|
|
})
|
|
.then(function checkExists(_checkedTheme) {
|
|
checkedTheme = _checkedTheme;
|
|
|
|
return storageAdapter.exists(utils.url.urlJoin(config.getContentPath('themes'), zip.shortName));
|
|
})
|
|
.then(function (themeExists) {
|
|
// delete existing theme
|
|
if (themeExists) {
|
|
return storageAdapter.delete(zip.shortName, config.getContentPath('themes'));
|
|
}
|
|
})
|
|
.then(function () {
|
|
events.emit('theme.uploaded', zip.shortName);
|
|
// store extracted theme
|
|
return storageAdapter.save({
|
|
name: zip.shortName,
|
|
path: checkedTheme.path
|
|
}, config.getContentPath('themes'));
|
|
})
|
|
.then(function () {
|
|
// Loads the theme from the filesystem
|
|
// Sets the theme on the themeList
|
|
return themeUtils.loadOne(zip.shortName);
|
|
})
|
|
.then(function (loadedTheme) {
|
|
// If this is the active theme, we are overriding
|
|
// This is a special case of activation
|
|
if (zip.shortName === settingsCache.get('activeTheme')) {
|
|
// 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.removeSync)(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.removeSync)(checkedTheme.path)
|
|
.catch(function (err) {
|
|
logging.error(new errors.GhostError({err: err}));
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
download: function download(options) {
|
|
var themeName = options.name,
|
|
theme = themeList.get(themeName),
|
|
storageAdapter = storage.getStorage('themes');
|
|
|
|
if (!theme) {
|
|
return Promise.reject(new errors.BadRequestError({message: i18n.t('errors.api.themes.invalidRequest')}));
|
|
}
|
|
|
|
return apiUtils.handlePermissions('themes', 'read')(options)
|
|
.then(function () {
|
|
events.emit('theme.downloaded', themeName);
|
|
return storageAdapter.serve({isTheme: true, name: themeName});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* remove theme zip
|
|
* remove theme folder
|
|
*/
|
|
destroy: function destroy(options) {
|
|
var name = options.name,
|
|
theme,
|
|
storageAdapter = storage.getStorage('themes');
|
|
|
|
return apiUtils.handlePermissions('themes', 'destroy')(options)
|
|
.then(function () {
|
|
if (name === 'casper') {
|
|
throw new errors.ValidationError({message: i18n.t('errors.api.themes.destroyCasper')});
|
|
}
|
|
|
|
if (name === settingsCache.get('activeTheme')) {
|
|
throw new errors.ValidationError({message: i18n.t('errors.api.themes.destroyActive')});
|
|
}
|
|
|
|
theme = themeList.get(name);
|
|
|
|
if (!theme) {
|
|
throw new errors.NotFoundError({message: i18n.t('errors.api.themes.themeDoesNotExist')});
|
|
}
|
|
|
|
return storageAdapter.delete(name, config.getContentPath('themes'));
|
|
})
|
|
.then(function () {
|
|
themeList.del(name);
|
|
events.emit('theme.deleted', name);
|
|
// Delete returns an empty 204 response
|
|
});
|
|
}
|
|
};
|
|
|
|
module.exports = themes;
|