0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-27 22:49:56 -05:00
ghost/core/server/api/v2/themes.js
Nazar Gargol cea598597b Restructured theme check logic
refs #10571

- Removes dependency on 'context' property being set in error when
checking a theme
- Refactoring was needed to be able to avoid passing checked theme as a
part of thrown error (logic was relying on error having this specific
data in context property). This created a problem where we controlled
the logic flow with data in error object.
- Introduced 2 different types of theme check handling, one behaves the
same way as before, the other gives more granulac control to the caller
to decide what to do with returned errors.
2019-04-22 22:34:12 +02:00

219 lines
6.9 KiB
JavaScript

const Promise = require('bluebird');
const fs = require('fs-extra');
const debug = require('ghost-ignition').debug('api:themes');
const common = require('../../lib/common');
const themeService = require('../../services/themes');
const settingsCache = require('../../services/settings/cache');
const models = require('../../models');
module.exports = {
docName: 'themes',
browse: {
permissions: true,
query() {
return themeService.toJSON();
}
},
activate: {
headers: {
cacheInvalidate: true
},
options: [
'name'
],
validation: {
options: {
name: {
required: true
}
}
},
permissions: true,
query(frame) {
let themeName = frame.options.name;
let checkedTheme;
const newSettings = [{
key: 'active_theme',
value: themeName
}];
const loadedTheme = themeService.list.get(themeName);
if (!loadedTheme) {
return Promise.reject(new common.errors.ValidationError({
message: common.i18n.t('notices.data.validation.index.themeCannotBeActivated', {themeName: themeName}),
context: 'active_theme'
}));
}
return themeService.validate.checkSafe(loadedTheme)
.then((_checkedTheme) => {
checkedTheme = _checkedTheme;
// @NOTE: we use the model, not the API here, as we don't want to trigger permissions
return models.Settings.edit(newSettings, frame.options);
})
.then(() => {
debug('Activating theme (method B on API "activate")', themeName);
themeService.activate(loadedTheme, checkedTheme);
return themeService.toJSON(themeName, checkedTheme);
});
}
},
upload: {
headers: {},
permissions: {
method: 'add'
},
query(frame) {
// @NOTE: consistent filename uploads
frame.options.originalname = frame.file.originalname.toLowerCase();
let zip = {
path: frame.file.path,
name: frame.file.originalname,
shortName: themeService.storage.getSanitizedFileName(frame.file.originalname.split('.zip')[0])
};
let checkedTheme;
// check if zip name is casper.zip
if (zip.name === 'casper.zip') {
throw new common.errors.ValidationError({
message: common.i18n.t('errors.api.themes.overrideCasper')
});
}
return themeService.validate.checkSafe(zip, true)
.then((_checkedTheme) => {
checkedTheme = _checkedTheme;
return themeService.storage.exists(zip.shortName);
})
.then((themeExists) => {
// CASE: delete existing theme
if (themeExists) {
return themeService.storage.delete(zip.shortName);
}
})
.then(() => {
// CASE: store extracted theme
return themeService.storage.save({
name: zip.shortName,
path: checkedTheme.path
});
})
.then(() => {
// CASE: loads the theme from the fs & sets the theme on the themeList
return themeService.loadOne(zip.shortName);
})
.then((loadedTheme) => {
// CASE: if this is the active theme, we are overriding
if (zip.shortName === settingsCache.get('active_theme')) {
debug('Activating theme (method C, on API "override")', zip.shortName);
themeService.activate(loadedTheme, checkedTheme);
// CASE: clear cache
this.headers.cacheInvalidate = true;
}
common.events.emit('theme.uploaded');
// @TODO: unify the name across gscan and Ghost!
return themeService.toJSON(zip.shortName, checkedTheme);
})
.finally(() => {
// @TODO: we should probably do this as part of saving the theme
// CASE: remove extracted dir from gscan
// happens in background
if (checkedTheme) {
fs.remove(checkedTheme.path)
.catch((err) => {
common.logging.error(new common.errors.GhostError({err: err}));
});
}
});
}
},
download: {
options: [
'name'
],
validation: {
options: {
name: {
required: true
}
}
},
permissions: {
method: 'read'
},
query(frame) {
let themeName = frame.options.name;
const theme = themeService.list.get(themeName);
if (!theme) {
return Promise.reject(new common.errors.BadRequestError({
message: common.i18n.t('errors.api.themes.invalidThemeName')
}));
}
return themeService.storage.serve({
name: themeName
});
}
},
destroy: {
statusCode: 204,
headers: {
cacheInvalidate: true
},
options: [
'name'
],
validation: {
options: {
name: {
required: true
}
}
},
permissions: true,
query(frame) {
let themeName = frame.options.name;
if (themeName === 'casper') {
throw new common.errors.ValidationError({
message: common.i18n.t('errors.api.themes.destroyCasper')
});
}
if (themeName === settingsCache.get('active_theme')) {
throw new common.errors.ValidationError({
message: common.i18n.t('errors.api.themes.destroyActive')
});
}
const theme = themeService.list.get(themeName);
if (!theme) {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.api.themes.themeDoesNotExist')
});
}
return themeService.storage.delete(themeName)
.then(() => {
themeService.list.del(themeName);
});
}
}
};