Skip to content

Commit

Permalink
feature: upload/download themes
Browse files Browse the repository at this point in the history
closes TryGhost#7204
[ci skip]
  • Loading branch information
kirrg001 committed Aug 19, 2016
1 parent 61d66a4 commit ca4cf3a
Show file tree
Hide file tree
Showing 16 changed files with 439 additions and 14 deletions.
2 changes: 1 addition & 1 deletion core/client
14 changes: 14 additions & 0 deletions core/server/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

var _ = require('lodash'),
Promise = require('bluebird'),
stream = require('stream'),
config = require('../config'),
configuration = require('./configuration'),
db = require('./db'),
Expand Down Expand Up @@ -198,6 +199,13 @@ addHeaders = function addHeaders(apiMethod, req, res, result) {
});
}

if (apiMethod === themes.download) {
res.set({
'Content-disposition': 'attachment; filename=theme.zip',
'Content-Type': 'application/zip'
});
}

return contentDisposition;
};

Expand Down Expand Up @@ -236,10 +244,16 @@ http = function http(apiMethod) {
if (req.method === 'DELETE') {
return res.status(204).end();
}

// Keep CSV header and formatting
if (res.get('Content-Type') && res.get('Content-Type').indexOf('text/csv') === 0) {
return res.status(200).send(response);
}

if (response instanceof stream.Readable) {
return response.pipe(res);
}

// Send a properly formatting HTTP response containing the data with correct headers
res.json(response || {});
}).catch(function onAPIError(error) {
Expand Down
134 changes: 125 additions & 9 deletions core/server/api/themes.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
// # Themes API
// RESTful API for Themes
var Promise = require('bluebird'),
_ = require('lodash'),
config = require('../config'),
errors = require('../errors'),
settings = require('./settings'),
pipeline = require('../utils/pipeline'),
utils = require('./utils'),
i18n = require('../i18n'),

var Promise = require('bluebird'),
_ = require('lodash'),
gscan = require('gscan'),
fs = require('fs-extra'),
config = require('../config'),
errors = require('../errors'),
storage = require('../storage'),
execFile = require('child_process').execFile,
settings = require('./settings'),
pipeline = require('../utils/pipeline'),
utils = require('./utils'),
i18n = require('../i18n'),
docName = 'themes',
themes;

Expand Down Expand Up @@ -186,6 +189,119 @@ themes = {
];

return pipeline(tasks, options || {});
},

upload: function upload(options) {
var zip = {
path: options.path,
name: options.originalname,
shortName: options.originalname.split('.zip')[0]
}, theme, store = storage.getStorage();

// Check if zip name is casper.zip
if (zip.name === 'casper.zip') {
throw new errors.ValidationError(i18n.t('errors.api.themes.overrideCasper'));
}

return utils.handlePermissions('themes', 'add')(options)
.then(function () {
return gscan.checkZip(zip, {keepExtractedDir: true})
})
.then(function (_theme) {
theme = _theme;
theme = gscan.format(theme);

if (theme.results.error.length) {
var validationErrors = [];

_.each(theme.results.error, function (error) {
validationErrors.push(new errors.ValidationError(i18n.t('errors.api.themes.invalidTheme', {reason: error.rule})));
});

throw validationErrors;
}
})
.then(function () {
//@TODO: what if store.exists fn is undefined?
return store.exists(config.paths.themePath + '/' + zip.shortName);
})
.then(function (zipExists) {
// override theme, remove zip and extracted folder
if (zipExists) {
fs.removeSync(config.paths.themePath + '/' + zip.shortName);
}

// store extracted theme
return store.save({
name: zip.shortName,
path: theme.path
}, config.paths.themePath);
})
.then(function () {
// force reload of availableThemes
// right now the logic is in the ConfigManager
// if we create a theme collection, we don't have to read them from disk
return config.loadThemesAndApps();
})
.then(function () {
// the settings endpoint is used to fetch the availableThemes
// so we have to force updating the in process cache
return settings.updateSettingsCache();
})
.then(function () {
// gscan returns the name of the package.json
// the whole theme handling in ghost relies on folder-name
theme.name = zip.shortName;
return {themes: [theme]};
})
.finally(function () {
//remove uploaded zip
fs.removeSync(zip.path);

//remove extracted dir
//@TODO: theme.path is a relative path?
//@TODO: recursive dir remove, because /uuid/zip-name
if (theme) {
fs.removeSync(theme.path);
}
})
},

download: function download(object, options) {
if (!_.isArray(object.themes)) {
return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.themes.invalidRequest')));
}

var themeName = object.themes[0].uuid,
theme = config.paths.availableThemes[themeName],
themePath = config.paths.themePath + '/' + themeName,
zipName = themeName + '.zip',
zipPath = config.paths.themePath + '/' + zipName;

if (!theme) {
return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.themes.invalidRequest')));
}

return utils.handlePermissions('themes', 'read')(options)
.then(function () {
if (fs.existsSync(zipPath)) {
return Promise.resolve();
}

return new Promise(function (resolve, reject) {
execFile('zip', ['-r', '-j', zipPath, themePath], function (err) {
if (err) {
return reject(err);
}

resolve();
});
});
})
.then(function () {
var stream = fs.createReadStream(zipPath);
return stream;
})
}
};

Expand Down
12 changes: 11 additions & 1 deletion core/server/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ ConfigManager.prototype.init = function (rawConfig) {
// just the object appropriate for this NODE_ENV
self.set(rawConfig);

return this.loadThemesAndApps();
};

ConfigManager.prototype.loadThemesAndApps = function () {
var self = this;

return Promise.all([readThemes(self._config.paths.themePath), readDirectory(self._config.paths.appPath)]).then(function (paths) {
self._config.paths.availableThemes = paths[0];
self._config.paths.availableApps = paths[1];
Expand Down Expand Up @@ -230,7 +236,7 @@ ConfigManager.prototype.set = function (config) {
uploads: {
subscribers: {
extensions: ['.csv'],
contentTypes: ['text/csv','application/csv']
contentTypes: ['text/csv', 'application/csv']
},
images: {
extensions: ['.jpg', '.jpeg', '.gif', '.png', '.svg', '.svgz'],
Expand All @@ -239,6 +245,10 @@ ConfigManager.prototype.set = function (config) {
db: {
extensions: ['.json'],
contentTypes: ['application/octet-stream', 'application/json']
},
themes: {
extensions: ['.zip'],
contentTypes: ['application/zip']
}
},
deprecatedItems: ['updateCheck', 'mail.fromaddress'],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
var utils = require('../utils'),
resource = 'theme';

function getPermissions() {
return utils.findModelFixtures('Permission', {object_type: resource});
}

function getRelations() {
return utils.findPermissionRelationsForObject(resource);
}

function printResult(logger, result, message) {
if (result.done === result.expected) {
logger.info(message);
} else {
logger.warn('(' + result.done + '/' + result.expected + ') ' + message);
}
}

module.exports = function addThemePermissions(options, logger) {
var modelToAdd = getPermissions(),
relationToAdd = getRelations();

return utils.addFixturesForModel(modelToAdd, options).then(function (result) {
printResult(logger, result, 'Adding permissions fixtures for ' + resource + 's');
return utils.addFixturesForRelation(relationToAdd, options);
}).then(function (result) {
printResult(logger, result, 'Adding permissions_roles fixtures for ' + resource + 's');
});
};
3 changes: 3 additions & 0 deletions core/server/data/migration/fixtures/007/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = [
require('./01-add-themes-permissions')
];
10 changes: 10 additions & 0 deletions core/server/data/migration/fixtures/fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,16 @@
"action_type": "edit",
"object_type": "theme"
},
{
"name": "Upload themes",
"action_type": "add",
"object_type": "theme"
},
{
"name": "Download themes",
"action_type": "read",
"object_type": "theme"
},
{
"name": "Browse users",
"action_type": "browse",
Expand Down
2 changes: 1 addition & 1 deletion core/server/data/schema/default-settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"core": {
"databaseVersion": {
"defaultValue": "006"
"defaultValue": "007"
},
"dbHash": {
"defaultValue": null
Expand Down
9 changes: 9 additions & 0 deletions core/server/routes/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,16 @@ apiRoutes = function apiRoutes(middleware) {

// ## Themes
router.get('/themes', authenticatePrivate, api.http(api.themes.browse));
router.post('/themes/upload',
authenticatePrivate,
middleware.upload.single('theme'),
middleware.validation.upload({type: 'themes'}),
api.http(api.themes.upload)
);

router.get('/themes/download', authenticatePrivate, api.http(api.themes.download));
router.put('/themes/:name', authenticatePrivate, api.http(api.themes.edit));
//router.del('/themes/:name', authenticatePrivate, api.http(api.themes.destroy));

// ## Notifications
router.get('/notifications', authenticatePrivate, api.http(api.notifications.browse));
Expand Down
5 changes: 4 additions & 1 deletion core/server/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,10 @@
"noPermissionToBrowseThemes": "You do not have permission to browse themes.",
"noPermissionToEditThemes": "You do not have permission to edit themes.",
"themeDoesNotExist": "Theme does not exist.",
"invalidRequest": "Invalid request."
"invalidTheme": "Theme is invalid: {reason}",
"missingFile": "Please select a theme.",
"invalidFile": "Please select a valid zip file.",
"overrideCasper": "Please rename your zip, it's not allowed to override the default casper theme."
},
"images": {
"missingFile": "Please select an image.",
Expand Down

0 comments on commit ca4cf3a

Please sign in to comment.