mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-27 22:49:56 -05:00
cea598597b
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.
219 lines
6.9 KiB
JavaScript
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);
|
|
});
|
|
}
|
|
}
|
|
};
|