Added ability to install themes directly from GitHub (#12635)

refs https://github.com/TryGhost/Ghost/issues/12608

- adds `admin/canary/themes/install` endpoint to the Admin API
  - requires two query params. `source` must be set to "github". `ref` should refer to a GitHub repo in the format "{org}/{repo}"
  - downloads zip archive for the repo from github
  - runs downloaded zip through the same process as uploaded zips
This commit is contained in:
Kevin Ansfield 2021-02-12 09:19:17 +00:00 committed by GitHub
parent b707131bb7
commit 3e228072ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 101 additions and 5 deletions

View File

@ -19,7 +19,10 @@ module.exports.init = function () {
},
{
event: 'theme.uploaded',
name: 'Theme Uploaded'
name: 'Theme Uploaded',
// {keyOnSuppliedEventData: keyOnTrackedEventData}
// - used to extract specific properties from event data and give them meaningful names
data: {name: 'name'}
},
{
event: 'integration.added',
@ -28,8 +31,11 @@ module.exports.init = function () {
];
_.each(toTrack, function (track) {
events.on(track.event, function () {
analytics.track(_.extend(trackDefaults, {event: prefix + track.name}));
events.on(track.event, function (eventData = {}) {
// extract desired properties from eventData and rename keys if necessary
const data = _.mapValues(track.data || {}, v => eventData[v]);
analytics.track(_.extend(trackDefaults, data, {event: prefix + track.name}));
});
});
};

View File

@ -1,6 +1,13 @@
const fs = require('fs-extra');
const os = require('os');
const path = require('path');
const security = require('@tryghost/security');
const {events} = require('../../lib/common');
const themeService = require('../../../frontend/services/themes');
const models = require('../../models');
const request = require('../../lib/request');
const errors = require('@tryghost/errors/lib/errors');
const i18n = require('../../lib/common/i18n');
module.exports = {
docName: 'themes',
@ -46,6 +53,81 @@ module.exports = {
}
},
install: {
headers: {},
options: [
'source',
'ref'
],
validation: {
source: {
required: true,
values: ['github']
},
ref: {
required: true
}
},
permissions: {
method: 'add'
},
async query(frame) {
if (frame.options.source === 'github') {
const [org, repo] = frame.options.ref.toLowerCase().split('/');
// omit /:ref so we fetch the default branch
const zipUrl = `https://api.github.com/repos/${org}/${repo}/zipball`;
const zipName = `${repo}.zip`;
// store zip in a unique temporary folder to avoid conflicts
const downloadBase = path.join(os.tmpdir(), security.identifier.uid(10));
const downloadPath = path.join(downloadBase, zipName);
await fs.ensureDir(downloadBase);
try {
// download zip file
const response = await request(zipUrl, {
followRedirect: true,
headers: {
accept: 'application/vnd.github.v3+json'
},
encoding: null
});
await fs.writeFile(downloadPath, response.body);
// install theme from zip
const zip = {
path: downloadPath,
name: zipName
};
const {theme, themeOverridden} = await themeService.storage.setFromZip(zip);
if (themeOverridden) {
this.headers.cacheInvalidate = true;
}
events.emit('theme.uploaded', {name: theme.name});
return theme;
} catch (e) {
if (e.statusCode && e.statusCode === 404) {
return Promise.reject(new errors.BadRequestError({
message: i18n.t('errors.api.themes.repoDoesNotExist'),
context: zipUrl
}));
}
throw e;
} finally {
// clean up tmp dir with downloaded file
fs.remove(downloadBase);
}
}
}
},
upload: {
headers: {},
permissions: {
@ -66,7 +148,7 @@ module.exports = {
// CASE: clear cache
this.headers.cacheInvalidate = true;
}
events.emit('theme.uploaded');
events.emit('theme.uploaded', {name: theme.name});
return theme;
});
}

View File

@ -12,6 +12,11 @@ module.exports = {
this.browse(...arguments);
},
install() {
debug('install');
this.browse(...arguments);
},
activate() {
debug('activate');
this.browse(...arguments);

View File

@ -431,7 +431,8 @@
"invalidFile": "Please select a valid zip file.",
"overrideCasper": "Please rename your zip, it's not allowed to override the default casper theme.",
"destroyCasper": "Deleting the default casper theme is not allowed.",
"destroyActive": "Deleting the active theme is not allowed."
"destroyActive": "Deleting the active theme is not allowed.",
"repoDoesNotExist": "Supplied GitHub theme does not exist or is inaccessible"
},
"images": {
"missingFile": "Please select an image.",

View File

@ -142,6 +142,8 @@ module.exports = function apiRoutes() {
http(apiCanary.themes.upload)
);
router.post('/themes/install', mw.authAdminApi, http(apiCanary.themes.install));
router.put('/themes/:name/activate',
mw.authAdminApi,
http(apiCanary.themes.activate)