-
-
Notifications
You must be signed in to change notification settings - Fork 10.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refs #7688 Adds an `uploads/icon/` endpoint to the api route to get a seperate entry point for blog icon validations. The blog icon validation will specifically check for images which have icon extensions (`.ico` & `.png`) and throw errors if: - the icon file size is too big (>100kb) - the icon is not a squaer - the icon size is smaller than 32px - the icon size is larger than 1000px - the icon is not `.ico` or `.png` extension TODOs for this PR: - [X] get image dimensions - [X] validate for image - [X] size - [X] form (must be square) - [X] type - [X] dimenstion (min 32px and max 1,000px) - [X] return appropriate error messages - [X] write tests -------------------- TODOs for #7688: - [X] Figure out, which favicon should be used (uploaded or default) -> #7713 - [ ] Serve and redirect the favicon for any browser requests, incl. redirects -> #7700 [WIP] - [X] Upload favicon via `general/settings` and implement basic admin validations -> TryGhost/Admin#397 - [X] Build server side validations -> this PR
- Loading branch information
Showing
15 changed files
with
319 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
var errors = require('../../errors'), | ||
config = require('../../config'), | ||
ICO = require('icojs'), | ||
fs = require('fs'), | ||
Promise = require('bluebird'), | ||
sizeOf = require('image-size'), | ||
i18n = require('../../i18n'), | ||
_ = require('lodash'), | ||
validIconSize, | ||
getIconDimensions; | ||
|
||
validIconSize = function validIconSize(size) { | ||
return size / 1024 <= 100 ? true : false; | ||
}; | ||
|
||
getIconDimensions = function getIconDimensions(icon) { | ||
return new Promise(function getImageSize(resolve, reject) { | ||
var arrayBuffer; | ||
|
||
// image-size doesn't support .ico files | ||
if (icon.name.match(/.ico$/i)) { | ||
arrayBuffer = new Uint8Array(fs.readFileSync(icon.path)).buffer; | ||
ICO.parse(arrayBuffer).then(function (result, error) { | ||
if (error) { | ||
return reject(new errors.ValidationError({message: i18n.t('errors.api.icons.couldNotGetSize', {file: icon.name, error: error.message})})); | ||
} | ||
|
||
// CASE: ico file contains only one size | ||
if (result.length === 1) { | ||
return resolve({ | ||
width: result[0].width, | ||
height: result[0].height | ||
}); | ||
} else { | ||
// CASE: ico file contains multiple sizes, return only the max size | ||
return resolve({ | ||
width: _.maxBy(result, function (w) {return w.width;}).width, | ||
height: _.maxBy(result, function (h) {return h.height;}).height | ||
}); | ||
} | ||
}); | ||
} else { | ||
sizeOf(icon.path, function (err, dimensions) { | ||
if (err) { | ||
return reject(new errors.ValidationError({message: i18n.t('errors.api.icons.couldNotGetSize', {file: icon.name, error: err.message})})); | ||
} | ||
|
||
return resolve({ | ||
width: dimensions.width, | ||
height: dimensions.height | ||
}); | ||
}); | ||
} | ||
}); | ||
}; | ||
|
||
module.exports = function blogIcon() { | ||
// we checked for a valid image file, now we need to do validations for blog icons | ||
return function blogIconValidation(req, res, next) { | ||
var iconExtensions = (config.get('uploads').icons && config.get('uploads').icons.extensions) || []; | ||
|
||
// CASE: file should not be larger than 100kb | ||
if (!validIconSize(req.file.size)) { | ||
return next(new errors.RequestEntityTooLargeError({message: i18n.t('errors.api.icons.fileSizeTooLarge', {extensions: iconExtensions})})); | ||
} | ||
|
||
return getIconDimensions(req.file).then(function (dimensions) { | ||
// save the image dimensions in new property for file | ||
req.file.dimensions = dimensions; | ||
|
||
// CASE: file needs to be a square | ||
if (req.file.dimensions.width !== req.file.dimensions.height) { | ||
return next(new errors.ValidationError({message: i18n.t('errors.api.icons.iconNotSquare', {extensions: iconExtensions})})); | ||
} | ||
|
||
// CASE: icon needs to be bigger than 32px | ||
// .ico files can contain multiple sizes, we need at least a minimum of 32px (16px is ok, as long as 32px are present as well) | ||
if (req.file.dimensions.width < 32) { | ||
return next(new errors.ValidationError({message: i18n.t('errors.api.icons.fileTooSmall', {extensions: iconExtensions})})); | ||
} | ||
|
||
// CASE: icon needs to be smaller than 1000px | ||
if (req.file.dimensions.width > 1000) { | ||
return next(new errors.ValidationError({message: i18n.t('errors.api.icons.fileTooLarge', {extensions: iconExtensions})})); | ||
} | ||
|
||
next(); | ||
}); | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
exports.upload = require('./upload'); | ||
exports.blogIcon = require('./blog-icon'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
var testUtils = require('../../../utils'), | ||
/*jshint unused:false*/ | ||
should = require('should'), | ||
path = require('path'), | ||
fs = require('fs-extra'), | ||
supertest = require('supertest'), | ||
ghost = testUtils.startGhost, | ||
rewire = require('rewire'), | ||
config = require('../../../../../core/server/config'), | ||
request; | ||
|
||
describe('Upload Icon API', function () { | ||
var accesstoken = '', | ||
getIconDimensions, | ||
icons = []; | ||
|
||
before(function (done) { | ||
// starting ghost automatically populates the db | ||
// TODO: prevent db init, and manage bringing up the DB with fixtures ourselves | ||
ghost().then(function (ghostServer) { | ||
request = supertest.agent(ghostServer.rootApp); | ||
}).then(function () { | ||
return testUtils.doAuth(request); | ||
}).then(function (token) { | ||
accesstoken = token; | ||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
after(function (done) { | ||
icons.forEach(function (icon) { | ||
fs.removeSync(config.get('paths').appRoot + icon); | ||
}); | ||
|
||
testUtils.clearData().then(function () { | ||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
describe('success cases for icons', function () { | ||
it('valid png', function (done) { | ||
request.post(testUtils.API.getApiQuery('uploads/icon')) | ||
.set('Authorization', 'Bearer ' + accesstoken) | ||
.expect('Content-Type', /json/) | ||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/favicon.png')) | ||
.expect(200) | ||
.end(function (err, res) { | ||
if (err) { | ||
return done(err); | ||
} | ||
|
||
icons.push(res.body); | ||
done(); | ||
}); | ||
}); | ||
|
||
it('valid ico with multiple sizes', function (done) { | ||
request.post(testUtils.API.getApiQuery('uploads/icon')) | ||
.set('Authorization', 'Bearer ' + accesstoken) | ||
.expect('Content-Type', /json/) | ||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/favicon_multi_sizes.ico')) | ||
.expect(200) | ||
.end(function (err, res) { | ||
if (err) { | ||
return done(err); | ||
} | ||
|
||
icons.push(res.body); | ||
done(); | ||
}); | ||
}); | ||
it('valid ico with one size', function (done) { | ||
request.post(testUtils.API.getApiQuery('uploads/icon')) | ||
.set('Authorization', 'Bearer ' + accesstoken) | ||
.expect('Content-Type', /json/) | ||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/favicon_32x_single.ico')) | ||
.expect(200) | ||
.end(function (err, res) { | ||
if (err) { | ||
return done(err); | ||
} | ||
|
||
icons.push(res.body); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('error cases for icons', function () { | ||
it('import should fail without file', function (done) { | ||
request.post(testUtils.API.getApiQuery('uploads/icon')) | ||
.set('Authorization', 'Bearer ' + accesstoken) | ||
.set('Accept', 'application/json') | ||
.expect('Content-Type', /json/) | ||
.expect(403) | ||
.end(function (err) { | ||
if (err) { | ||
return done(err); | ||
} | ||
|
||
done(); | ||
}); | ||
}); | ||
|
||
it('import should fail with unsupported file', function (done) { | ||
request.post(testUtils.API.getApiQuery('uploads/icon')) | ||
.set('Authorization', 'Bearer ' + accesstoken) | ||
.expect('Content-Type', /json/) | ||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/ghosticon.jpg')) | ||
.expect(415) | ||
.end(function (err) { | ||
if (err) { | ||
return done(err); | ||
} | ||
|
||
done(); | ||
}); | ||
}); | ||
|
||
it('incorrect extension', function (done) { | ||
request.post(testUtils.API.getApiQuery('uploads/icon')) | ||
.set('Authorization', 'Bearer ' + accesstoken) | ||
.set('content-type', 'image/png') | ||
.expect('Content-Type', /json/) | ||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/ghost-logo.pngx')) | ||
.expect(415) | ||
.end(function (err) { | ||
if (err) { | ||
return done(err); | ||
} | ||
|
||
done(); | ||
}); | ||
}); | ||
it('import should fail, if icon is not square', function (done) { | ||
request.post(testUtils.API.getApiQuery('uploads/icon')) | ||
.set('Authorization', 'Bearer ' + accesstoken) | ||
.expect('Content-Type', /json/) | ||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/favicon_not_square.png')) | ||
.expect(422) | ||
.end(function (err) { | ||
if (err) { | ||
return done(err); | ||
} | ||
|
||
done(); | ||
}); | ||
}); | ||
it('import should fail, if icon file size is too large', function (done) { | ||
request.post(testUtils.API.getApiQuery('uploads/icon')) | ||
.set('Authorization', 'Bearer ' + accesstoken) | ||
.expect('Content-Type', /json/) | ||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/favicon_size_too_large.png')) | ||
.expect(413) | ||
.end(function (err) { | ||
if (err) { | ||
return done(err); | ||
} | ||
|
||
done(); | ||
}); | ||
}); | ||
it('import should fail, if icon dimensions are too large', function (done) { | ||
request.post(testUtils.API.getApiQuery('uploads/icon')) | ||
.set('Authorization', 'Bearer ' + accesstoken) | ||
.expect('Content-Type', /json/) | ||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/favicon_too_large.png')) | ||
.expect(422) | ||
.end(function (err) { | ||
if (err) { | ||
return done(err); | ||
} | ||
|
||
done(); | ||
}); | ||
}); | ||
it('import should fail, if png icon dimensions are too small', function (done) { | ||
request.post(testUtils.API.getApiQuery('uploads/icon')) | ||
.set('Authorization', 'Bearer ' + accesstoken) | ||
.expect('Content-Type', /json/) | ||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/favicon_too_small.png')) | ||
.expect(422) | ||
.end(function (err) { | ||
if (err) { | ||
return done(err); | ||
} | ||
|
||
done(); | ||
}); | ||
}); | ||
it('import should fail, if png icon dimensions are too small', function (done) { | ||
request.post(testUtils.API.getApiQuery('uploads/icon')) | ||
.set('Authorization', 'Bearer ' + accesstoken) | ||
.expect('Content-Type', /json/) | ||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/favicon_16x_single.ico')) | ||
.expect(422) | ||
.end(function (err) { | ||
if (err) { | ||
return done(err); | ||
} | ||
|
||
done(); | ||
}); | ||
}); | ||
}); | ||
}); |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters