From 6e117e63fba07f8d24148baceebcdfb0881d3cdb Mon Sep 17 00:00:00 2001 From: Aileen Nowak Date: Fri, 11 May 2018 13:12:15 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20gscan=20errors=20not=20c?= =?UTF-8?q?aught=20for=20corrupted=20zips?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes TryGhost/Support#426 refs TryGhost/gscan#106 needs TryGhost/gscan#107 GScan can return errors, which was not handled in our theme validator and caused Ghost to crash completely. GScan will now return an Ignition error when its not able to read the `.zip` file. e. g.: `{"errors":[{"message":"Failed to read zip file","context":"tife.zip","errorType":"ValidationError","errorDetails":"invalid relative path: ../tife/"}]}` --- core/server/services/themes/validate.js | 35 +++-- .../unit/services/themes/validate_spec.js | 141 ++++++++++++++++++ 2 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 core/test/unit/services/themes/validate_spec.js diff --git a/core/server/services/themes/validate.js b/core/server/services/themes/validate.js index e4d9d452f6e..fe46ce90dd6 100644 --- a/core/server/services/themes/validate.js +++ b/core/server/services/themes/validate.js @@ -16,24 +16,27 @@ checkTheme = function checkTheme(theme, isZip) { checkPromise = gscan.check(theme.path); } - return checkPromise.then(function resultHandler(checkedTheme) { - checkedTheme = gscan.format(checkedTheme, { - onlyFatalErrors: config.get('env') === 'production' - }); + return checkPromise + .then(function resultHandler(checkedTheme) { + checkedTheme = gscan.format(checkedTheme, { + onlyFatalErrors: config.get('env') === 'production' + }); - // CASE: production and no fatal errors - // CASE: development returns fatal and none fatal errors, theme is only invalid if fatal errors - if (!checkedTheme.results.error.length || - config.get('env') === 'development' && !checkedTheme.results.hasFatalErrors) { - return checkedTheme; - } + // CASE: production and no fatal errors + // CASE: development returns fatal and none fatal errors, theme is only invalid if fatal errors + if (!checkedTheme.results.error.length || + config.get('env') === 'development' && !checkedTheme.results.hasFatalErrors) { + return checkedTheme; + } - return Promise.reject(new common.errors.ThemeValidationError({ - message: common.i18n.t('errors.api.themes.invalidTheme'), - errorDetails: checkedTheme.results.error, - context: checkedTheme - })); - }); + return Promise.reject(new common.errors.ThemeValidationError({ + message: common.i18n.t('errors.api.themes.invalidTheme'), + errorDetails: checkedTheme.results.error, + context: checkedTheme + })); + }).catch(function (error) { + return Promise.reject(error); + }); }; module.exports.check = checkTheme; diff --git a/core/test/unit/services/themes/validate_spec.js b/core/test/unit/services/themes/validate_spec.js new file mode 100644 index 00000000000..d06e603647b --- /dev/null +++ b/core/test/unit/services/themes/validate_spec.js @@ -0,0 +1,141 @@ +'use strict'; + +const should = require('should'); +const sinon = require('sinon'); +const _ = require('lodash'); + +const themes = require('../../../../server/services/themes'); +const validate = themes.validate; + +const gscan = require('gscan'); +const common = require('../../../../server/lib/common'); +const sandbox = sinon.sandbox.create(); + +describe('Themes', function () { + let checkZipStub; + let checkStub; + let formatStub; + + beforeEach(function () { + checkZipStub = sandbox.stub(gscan, 'checkZip'); + checkStub = sandbox.stub(gscan, 'check'); + formatStub = sandbox.stub(gscan, 'format'); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('Validate', function () { + const testTheme = { + name: 'supertheme', + version: '1.0.0', + path: '/path/to/theme' + }; + + it('[success] validates a valid zipped theme', function () { + checkZipStub.resolves({}); + formatStub.returns({results: {error: []}}); + + return validate.check(testTheme, true) + .then((checkedTheme) => { + checkZipStub.calledOnce.should.be.true(); + checkZipStub.calledWith(testTheme).should.be.true(); + checkStub.callCount.should.be.equal(0); + formatStub.calledOnce.should.be.true(); + checkedTheme.should.be.an.Object(); + }); + }); + + it('[success] validates a valid unzipped theme', function () { + checkStub.resolves({}); + formatStub.returns({results: {error: []}}); + + return validate.check(testTheme, false) + .then((checkedTheme) => { + checkZipStub.callCount.should.be.equal(0); + checkStub.calledOnce.should.be.true(); + checkStub.calledWith(testTheme.path).should.be.true(); + formatStub.calledOnce.should.be.true(); + checkedTheme.should.be.an.Object(); + }); + }); + + it('[failure] validates an invalid zipped theme', function () { + checkZipStub.resolves({}); + formatStub.returns({ + results: { + error: [ + { + fatal: true, + level: 'error', + rule: 'Replace the {{#if author.cover}} helper with {{#if author.cover_image}}', + details: 'The cover attribute was replaced with cover_image.
Instead of {{#if author.cover}} you need to use {{#if author.cover_image}}.
See the object attributes of author here.', + failures: [ {} ], + code: 'GS001-DEPR-CON-AC' + } + ] + } + }); + + return validate.check(testTheme, true) + .then((checkedTheme) => { + checkedTheme.should.not.exist(); + }).catch((error) => { + error.should.be.an.Object(); + (error instanceof common.errors.ThemeValidationError).should.eql(true); + checkZipStub.calledOnce.should.be.true(); + checkZipStub.calledWith(testTheme).should.be.true(); + checkStub.callCount.should.be.equal(0); + formatStub.calledOnce.should.be.true(); + }); + }); + + it('[failure] validates an invalid unzipped theme', function () { + checkStub.resolves({}); + formatStub.returns({ + results: { + error: [ + { + fatal: true, + level: 'error', + rule: 'Replace the {{#if author.cover}} helper with {{#if author.cover_image}}', + details: 'The cover attribute was replaced with cover_image.
Instead of {{#if author.cover}} you need to use {{#if author.cover_image}}.
See the object attributes of author here.', + failures: [ {} ], + code: 'GS001-DEPR-CON-AC' + } + ] + } + }); + + return validate.check(testTheme, false) + .then((checkedTheme) => { + checkedTheme.should.not.exist(); + }).catch((error) => { + error.should.be.an.Object(); + (error instanceof common.errors.ThemeValidationError).should.eql(true); + checkStub.calledOnce.should.be.true(); + checkStub.calledWith(testTheme.path).should.be.true(); + checkZipStub.callCount.should.be.equal(0); + formatStub.calledOnce.should.be.true(); + }); + }); + + it('[failure] can handle a corrupt zip file', function () { + checkZipStub.rejects(new Error('invalid zip file')); + formatStub.returns({results: {error: []}}); + + return validate.check(testTheme, true) + .then((checkedTheme) => { + checkedTheme.should.not.exist(); + }).catch((error) => { + error.should.be.an.Object(); + error.message.should.be.equal('invalid zip file'); + checkZipStub.calledOnce.should.be.true(); + checkZipStub.calledWith(testTheme).should.be.true(); + checkStub.callCount.should.be.equal(0); + formatStub.calledOnce.should.be.false(); + }); + }); + }); +});