2019-07-01 17:56:23 +03:00
|
|
|
const fs = require('fs-extra');
|
|
|
|
|
Moved activate from themes to the bridge
refs: https://github.com/TryGhost/Ghost/commit/bf0823c9a2ddbc93ad0ddcec36eed130c8c5a203
- continuing the work of splitting up the theme service into logical components
Theme activations are a trickier piece of the theme split puzzle because they are called from the API and theme service on boot in different ways.
Activations require a theme to have been validated at different levels. Validations are also tricky - do they belong to the theme engine, or the theme service?
There are then several different flows for activations:
- On Boot
- API "activate" call
- API override on upload or install via setFromZip, which is a method in the storage layer
These calls all have quite different logical flows at the moment, and need to be unified
For now, I've moved the existing "activate" function onto the bridge. This allows the theme service to be split from the frontend, and refactoring can start from there.
I hope to move this so there is less code in the actual bridge very soon, but my goal is not to require any server packages in the frontend part of this
I think ideally:
- all activation code, including validation, should probably be part of the theme engine
- the theme engine should offer 3 methods: getActive() canActivate() and activate()
- the theme service is then only responsible for loading themes in and out of storage, JSON responses for the API, and handing themes to the frontend via the bridge at the appropriate moment
2021-04-27 14:49:48 +03:00
|
|
|
const bridge = require('../../../bridge');
|
2019-07-01 17:56:23 +03:00
|
|
|
const validate = require('./validate');
|
|
|
|
const list = require('./list');
|
2019-07-09 17:35:18 +03:00
|
|
|
const ThemeStorage = require('./ThemeStorage');
|
2019-07-01 17:56:23 +03:00
|
|
|
const themeLoader = require('./loader');
|
|
|
|
const toJSON = require('./to-json');
|
|
|
|
|
2021-04-27 15:28:04 +03:00
|
|
|
const settingsCache = require('../settings/cache');
|
2021-04-27 16:18:04 +03:00
|
|
|
const i18n = require('../../../server/lib/common/i18n');
|
2020-05-28 21:30:23 +03:00
|
|
|
const logging = require('../../../shared/logging');
|
2020-05-22 21:22:20 +03:00
|
|
|
const errors = require('@tryghost/errors');
|
2019-07-01 17:56:23 +03:00
|
|
|
const debug = require('ghost-ignition').debug('api:themes');
|
2020-06-03 18:29:34 +03:00
|
|
|
const ObjectID = require('bson-objectid');
|
2019-07-01 17:56:23 +03:00
|
|
|
|
|
|
|
let themeStorage;
|
|
|
|
|
|
|
|
const getStorage = () => {
|
2019-07-09 17:35:18 +03:00
|
|
|
themeStorage = themeStorage || new ThemeStorage();
|
2019-07-01 17:56:23 +03:00
|
|
|
|
|
|
|
return themeStorage;
|
|
|
|
};
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
getZip: (themeName) => {
|
|
|
|
const theme = list.get(themeName);
|
|
|
|
|
|
|
|
if (!theme) {
|
2020-05-22 21:22:20 +03:00
|
|
|
return Promise.reject(new errors.BadRequestError({
|
|
|
|
message: i18n.t('errors.api.themes.invalidThemeName')
|
2019-07-01 17:56:23 +03:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
return getStorage().serve({
|
|
|
|
name: themeName
|
|
|
|
});
|
|
|
|
},
|
|
|
|
setFromZip: (zip) => {
|
|
|
|
const shortName = getStorage().getSanitizedFileName(zip.name.split('.zip')[0]);
|
2020-06-03 18:29:34 +03:00
|
|
|
const backupName = `${shortName}_${ObjectID()}`;
|
2019-07-01 17:56:23 +03:00
|
|
|
|
|
|
|
// check if zip name is casper.zip
|
|
|
|
if (zip.name === 'casper.zip') {
|
2020-05-22 21:22:20 +03:00
|
|
|
throw new errors.ValidationError({
|
|
|
|
message: i18n.t('errors.api.themes.overrideCasper')
|
2019-07-01 17:56:23 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let checkedTheme;
|
2021-02-23 11:49:48 +03:00
|
|
|
let renamedExisting = false;
|
2019-07-01 17:56:23 +03:00
|
|
|
|
|
|
|
return validate.checkSafe(zip, true)
|
|
|
|
.then((_checkedTheme) => {
|
|
|
|
checkedTheme = _checkedTheme;
|
|
|
|
|
|
|
|
return getStorage().exists(shortName);
|
|
|
|
})
|
|
|
|
.then((themeExists) => {
|
2020-06-03 18:29:34 +03:00
|
|
|
// CASE: move the existing theme to a backup folder
|
2019-07-01 17:56:23 +03:00
|
|
|
if (themeExists) {
|
2021-02-23 11:49:48 +03:00
|
|
|
renamedExisting = true;
|
2020-06-03 18:29:34 +03:00
|
|
|
return getStorage().rename(shortName, backupName);
|
2019-07-01 17:56:23 +03:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.then(() => {
|
|
|
|
// CASE: store extracted theme
|
|
|
|
return getStorage().save({
|
|
|
|
name: shortName,
|
|
|
|
path: checkedTheme.path
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.then(() => {
|
|
|
|
// CASE: loads the theme from the fs & sets the theme on the themeList
|
|
|
|
return themeLoader.loadOneTheme(shortName);
|
|
|
|
})
|
|
|
|
.then((loadedTheme) => {
|
2019-07-11 10:28:08 +03:00
|
|
|
const overrideTheme = (shortName === settingsCache.get('active_theme'));
|
2019-07-01 17:56:23 +03:00
|
|
|
// CASE: if this is the active theme, we are overriding
|
2019-07-11 10:28:08 +03:00
|
|
|
if (overrideTheme) {
|
2019-07-01 17:56:23 +03:00
|
|
|
debug('Activating theme (method C, on API "override")', shortName);
|
Moved activate from themes to the bridge
refs: https://github.com/TryGhost/Ghost/commit/bf0823c9a2ddbc93ad0ddcec36eed130c8c5a203
- continuing the work of splitting up the theme service into logical components
Theme activations are a trickier piece of the theme split puzzle because they are called from the API and theme service on boot in different ways.
Activations require a theme to have been validated at different levels. Validations are also tricky - do they belong to the theme engine, or the theme service?
There are then several different flows for activations:
- On Boot
- API "activate" call
- API override on upload or install via setFromZip, which is a method in the storage layer
These calls all have quite different logical flows at the moment, and need to be unified
For now, I've moved the existing "activate" function onto the bridge. This allows the theme service to be split from the frontend, and refactoring can start from there.
I hope to move this so there is less code in the actual bridge very soon, but my goal is not to require any server packages in the frontend part of this
I think ideally:
- all activation code, including validation, should probably be part of the theme engine
- the theme engine should offer 3 methods: getActive() canActivate() and activate()
- the theme service is then only responsible for loading themes in and out of storage, JSON responses for the API, and handing themes to the frontend via the bridge at the appropriate moment
2021-04-27 14:49:48 +03:00
|
|
|
bridge.activateTheme(loadedTheme, checkedTheme);
|
2019-07-01 17:56:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// @TODO: unify the name across gscan and Ghost!
|
2019-07-11 10:28:08 +03:00
|
|
|
return {
|
|
|
|
themeOverridden: overrideTheme,
|
|
|
|
theme: toJSON(shortName, checkedTheme)
|
|
|
|
};
|
2019-07-01 17:56:23 +03:00
|
|
|
})
|
2021-02-23 11:49:48 +03:00
|
|
|
.catch((error) => {
|
|
|
|
// restore backup if we renamed an existing theme but saving failed
|
|
|
|
if (renamedExisting) {
|
|
|
|
return getStorage().exists(shortName).then((themeExists) => {
|
|
|
|
if (!themeExists) {
|
|
|
|
return getStorage().rename(backupName, shortName).then(() => {
|
|
|
|
throw error;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
})
|
2019-07-01 17:56:23 +03:00
|
|
|
.finally(() => {
|
|
|
|
// @TODO: we should probably do this as part of saving the theme
|
2020-06-03 18:29:34 +03:00
|
|
|
// CASE: remove extracted dir from gscan happens in background
|
2019-07-01 17:56:23 +03:00
|
|
|
if (checkedTheme) {
|
|
|
|
fs.remove(checkedTheme.path)
|
|
|
|
.catch((err) => {
|
2020-05-22 21:22:20 +03:00
|
|
|
logging.error(new errors.GhostError({err: err}));
|
2019-07-01 17:56:23 +03:00
|
|
|
});
|
|
|
|
}
|
2020-06-03 18:29:34 +03:00
|
|
|
|
|
|
|
// CASE: remove the backup we created earlier
|
|
|
|
getStorage()
|
|
|
|
.delete(backupName)
|
|
|
|
.catch((err) => {
|
|
|
|
logging.error(new errors.GhostError({err: err}));
|
|
|
|
});
|
2019-07-01 17:56:23 +03:00
|
|
|
});
|
|
|
|
},
|
|
|
|
destroy: function (themeName) {
|
|
|
|
if (themeName === 'casper') {
|
2020-05-22 21:22:20 +03:00
|
|
|
throw new errors.ValidationError({
|
|
|
|
message: i18n.t('errors.api.themes.destroyCasper')
|
2019-07-01 17:56:23 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (themeName === settingsCache.get('active_theme')) {
|
2020-05-22 21:22:20 +03:00
|
|
|
throw new errors.ValidationError({
|
|
|
|
message: i18n.t('errors.api.themes.destroyActive')
|
2019-07-01 17:56:23 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const theme = list.get(themeName);
|
|
|
|
|
|
|
|
if (!theme) {
|
2020-05-22 21:22:20 +03:00
|
|
|
throw new errors.NotFoundError({
|
|
|
|
message: i18n.t('errors.api.themes.themeDoesNotExist')
|
2019-07-01 17:56:23 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return getStorage().delete(themeName)
|
|
|
|
.then(() => {
|
|
|
|
list.del(themeName);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|