Add upload image dimension check/limitation for rank, squad and decoration images (CC-69)

pull/49/head
HardiReady 2019-02-03 12:16:22 +01:00
parent 68ad467e4d
commit 141a340aad
7 changed files with 207 additions and 149 deletions

View File

@ -4,7 +4,7 @@
const codes = require('../routes/http-codes');
// library to check image dimensions from file buffer
var sizeOf = require('buffer-image-size');
const sizeOf = require('buffer-image-size');
/**
* check if id has valid UUID format
@ -27,8 +27,6 @@ const idValidator = (req, res, next) => {
const imageDimensionValidator = (imageFileBuf, maxWidth, maxHeight) => {
const dimensions = sizeOf(imageFileBuf);
console.log(dimensions.width)
console.log(dimensions.height)
if (dimensions.width > maxWidth || dimensions.height > maxHeight) {
let err = new Error(`Image exceeds maximum dimensions of ${maxWidth}px width and ${maxHeight}px height`);
err.status = codes.wrongrequest;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -13,6 +13,7 @@ const codes = require('./http-codes');
const apiAuthenticationMiddleware = require('../middleware/auth-middleware');
const checkHl = require('../middleware/permission-check').checkHl;
const genericGetById = require('./_generic').genericGetById;
const routerHandling = require('../middleware/router-handling');
const idValidator = require('../middleware/validators').idValidator;
const resourceLocation = require('../middleware/resource-location').resourceLocation().concat('/decoration/');
@ -22,140 +23,175 @@ const DecorationModel = require('../models/decoration');
const AwardingsModel = require('../models/awarding');
// util
const genericGetById = require('./_generic').genericGetById;
const imageDimensionValidator = require('../middleware/validators').imageDimensionValidator;
const MAX_MEDAL_IMAGE_WIDTH = 100;
const MAX_MEDAL_IMAGE_HEIGHT = 150;
const MAX_RIBBON_IMAGE_WIDTH = 150;
const MAX_RIBBON_IMAGE_HEIGHT = 50;
const decoration = new express.Router();
const decorationRouter = new express.Router();
// routes **********************
decoration.route('/')
.get((req, res, next) => {
const filter = {};
if (req.query.fractFilter) {
filter.fraction = req.query.fractFilter.toUpperCase();
}
if (req.query.q) {
filter.name = {$regex: req.query.q, $options: 'i'};
}
DecorationModel.find(filter, {}, {
sort: {
fraction: 'asc',
isMedal: 'asc',
sortingNumber: 'asc',
name: 'asc',
},
}, (err, items) => {
if (err) {
err.status = codes.servererror;
return next(err);
}
if (items && items.length > 0) {
res.locals.items = items;
} else {
res.locals.items = [];
}
res.locals.processed = true;
next();
});
})
decorationRouter.route('/')
.get((req, res, next) => {
const filter = {};
if (req.query.fractFilter) {
filter.fraction = req.query.fractFilter.toUpperCase();
}
if (req.query.q) {
filter.name = {$regex: req.query.q, $options: 'i'};
}
DecorationModel.find(filter, {}, {
sort: {
fraction: 'asc',
isMedal: 'asc',
sortingNumber: 'asc',
name: 'asc',
},
}, (err, items) => {
if (err) {
err.status = codes.servererror;
return next(err);
}
if (items && items.length > 0) {
res.locals.items = items;
} else {
res.locals.items = [];
}
res.locals.processed = true;
next();
});
})
.post(apiAuthenticationMiddleware, checkHl, upload.single('image'), (req, res, next) => {
const decoration = new DecorationModel(req.body);
// timestamp and default are set automatically by Mongoose Schema Validation
decoration.save((err) => {
if (err) {
err.status = codes.wrongrequest;
err.message += ' in fields: ' + Object.getOwnPropertyNames(err.errors);
return next(err);
}
res.status(codes.created);
res.locals.items = decoration;
fs.appendFile(resourceLocation + decoration._id + '.png',
new Buffer(req.file.buffer),
(err) => {
if (err) next(err);
});
next();
});
})
.post(apiAuthenticationMiddleware, checkHl, upload.single('image'), (req, res, next) => {
if (req.file) {
const decoration = new DecorationModel(req.body);
.all(
routerHandling.httpMethodNotAllowed
);
const imageFileBuffer = req.file.buffer;
const err = imageDimensionValidator(imageFileBuffer,
(decoration.isMedal ? MAX_MEDAL_IMAGE_WIDTH : MAX_RIBBON_IMAGE_WIDTH),
(decoration.isMedal ? MAX_MEDAL_IMAGE_HEIGHT : MAX_RIBBON_IMAGE_HEIGHT));
if (err) {
return next(err);
}
decoration.route('/:id')
.get(idValidator, (req, res, next) => {
return genericGetById(req, res, next, DecorationModel);
})
// timestamp and default are set automatically by Mongoose Schema Validation
decoration.save((err) => {
if (err) {
err.status = codes.wrongrequest;
err.message += ' in fields: ' + Object.getOwnPropertyNames(err.errors);
return next(err);
}
res.status(codes.created);
res.locals.items = decoration;
fs.appendFile(resourceLocation + decoration._id + '.png',
new Buffer(req.file.buffer),
(err) => {
if (err) next(err);
});
next();
});
} else {
const err = new Error('no image file provided');
err.status = codes.wrongmediasend;
next(err);
}
})
.patch(apiAuthenticationMiddleware, checkHl, upload.single('image'), (req, res, next) => {
if (!req.body || (req.body._id && req.body._id !== req.params.id)) {
// little bit different as in PUT. :id does not need to be in data, but if the _id and url id must match
const err = new Error('id of PATCH resource and send JSON body are not equal ' + req.params.id + ' ' +
req.body._id);
err.status = codes.notfound;
next(err);
return; // prevent node to process this function further after next() has finished.
}
.all(
routerHandling.httpMethodNotAllowed
);
// optional task 3: increment version manually as we do not use .save(.)
req.body.updatedAt = new Date();
req.body.$inc = {__v: 1};
decorationRouter.route('/:id')
.get(idValidator, (req, res, next) => {
return genericGetById(req, res, next, DecorationModel);
})
if (req.file) {
const file = resourceLocation + req.params.id + '.png';
fs.unlink(file, (err) => {
if (err) next(err);
fs.appendFile(file, new Buffer(req.file.buffer), (err) => {
if (err) next(err);
});
});
}
.patch(apiAuthenticationMiddleware, checkHl, upload.single('image'), (req, res, next) => {
if (!req.body || (req.body._id && req.body._id !== req.params.id)) {
// little bit different as in PUT. :id does not need to be in data, but if the _id and url id must
// match
const err = new Error(
'id of PATCH resource and send JSON body are not equal ' + req.params.id + ' ' +
req.body._id);
err.status = codes.notfound;
next(err);
return; // prevent node to process this function further after next() has finished.
}
// PATCH is easier with mongoose than PUT. You simply update by all data that comes from outside. no need
// to reset attributes that are missing.
DecorationModel.findByIdAndUpdate(req.params.id, req.body, {new: true}, (err, item) => {
if (err) {
err.status = codes.wrongrequest;
} else if (!item) {
err = new Error('item not found');
err.status = codes.notfound;
} else {
res.locals.items = item;
}
next(err);
});
})
// optional task 3: increment version manually as we do not use .save(.)
req.body.updatedAt = new Date();
req.body.$inc = {__v: 1};
.delete(apiAuthenticationMiddleware, checkHl, (req, res, next) => {
const id = req.params.id;
DecorationModel.findByIdAndRemove(id, (err, item) => {
if (err) {
err.status = codes.wrongrequest;
} else if (!item) {
err = new Error('item not found');
err.status = codes.notfound;
}
DecorationModel.findById(req.body._id, (err, item) => {
if (err) {
return next(err);
}
if (req.file) {
const imageFileBuffer = req.file.buffer;
const imageDimensionError = imageDimensionValidator(imageFileBuffer,
(item.isMedal ? MAX_MEDAL_IMAGE_WIDTH : MAX_RIBBON_IMAGE_WIDTH),
(item.isMedal ? MAX_MEDAL_IMAGE_HEIGHT : MAX_RIBBON_IMAGE_HEIGHT));
if (imageDimensionError) {
return next(imageDimensionError);
}
// deleted all awardings linked to this decoration
AwardingsModel.find({decorationId: id}).remove().exec();
const file = resourceLocation + req.params.id + '.png';
fs.unlink(file, (err) => {
if (err) next(err);
fs.appendFile(file, new Buffer(req.file.buffer), (err) => {
if (err) next(err);
});
});
}
// delete graphic
fs.unlink(resourceLocation.concat(id).concat('.png'),
(err) => {
// we don't set res.locals.items and thus it will send a 204 (no content) at the end. see last handler
res.locals.processed = true;
next(err);
});
});
})
// PATCH is easier with mongoose than PUT. You simply update by all data that comes from outside. no
// need to reset attributes that are missing.
DecorationModel.findByIdAndUpdate(req.params.id, req.body, {new: true}, (err, item) => {
if (err) {
err.status = codes.wrongrequest;
} else if (!item) {
err = new Error('item not found');
err.status = codes.notfound;
} else {
res.locals.items = item;
}
next(err);
});
});
})
.all(
routerHandling.httpMethodNotAllowed
);
.delete(apiAuthenticationMiddleware, checkHl, (req, res, next) => {
const id = req.params.id;
DecorationModel.findByIdAndRemove(id, (err, item) => {
if (err) {
err.status = codes.wrongrequest;
} else if (!item) {
err = new Error('item not found');
err.status = codes.notfound;
}
// deleted all awardings linked to this decoration
AwardingsModel.find({decorationId: id}).remove().exec();
// delete graphic
fs.unlink(resourceLocation.concat(id).concat('.png'),
(err) => {
// we don't set res.locals.items and thus it will send a 204 (no content) at the end. see last
// handler
res.locals.processed = true;
next(err);
});
});
})
.all(
routerHandling.httpMethodNotAllowed
);
// this middleware function can be used, if you like or remove it
// it looks for object(s) in res.locals.items and if they exist, they are send to the client as json
decoration.use(routerHandling.emptyResponse);
decorationRouter.use(routerHandling.emptyResponse);
module.exports = decoration;
module.exports = decorationRouter;

View File

@ -56,27 +56,32 @@ ranks.route('/')
})
.post(apiAuthenticationMiddleware, checkHl, upload.single('image'), (req, res, next) => {
const rank = new RankModel(req.body);
const imageFileBuffer = req.file.buffer;
const err = imageDimensionValidator(imageFileBuffer, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT);
if(err) {
return next(err);
}
// timestamp and default are set automatically by Mongoose Schema Validation
rank.save((err) => {
if (req.file) {
const imageFileBuffer = req.file.buffer;
const err = imageDimensionValidator(imageFileBuffer, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT);
if (err) {
err.status = codes.wrongrequest;
next(err);
return next(err);
}
res.status(codes.created);
res.locals.items = rank;
fs.appendFile(resourceLocation + rank._id + '.png', new Buffer(imageFileBuffer),
(err) => {
const rank = new RankModel(req.body);
// timestamp and default are set automatically by Mongoose Schema Validation
rank.save((err) => {
if (err) {
err.status = codes.wrongrequest;
next(err);
});
});
}
res.status(codes.created);
res.locals.items = rank;
fs.appendFile(resourceLocation + rank._id + '.png', new Buffer(imageFileBuffer),
(err) => {
next(err);
});
});
} else {
const err = new Error('no image file provided');
err.status = codes.wrongmediasend;
next(err);
}
})
.all(
@ -106,7 +111,7 @@ ranks.route('/:id')
if (req.file) {
const imageFileBuffer = req.file.buffer;
const err = imageDimensionValidator(imageFileBuffer, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT);
if(err) {
if (err) {
return next(err);
}

View File

@ -22,6 +22,9 @@ const SquadModel = require('../models/squad');
// util
const genericGetById = require('./_generic').genericGetById;
const imageDimensionValidator = require('../middleware/validators').imageDimensionValidator;
const MAX_IMAGE_WIDTH = 150;
const MAX_IMAGE_HEIGHT = 150;
const squads = new express.Router();
@ -54,6 +57,12 @@ squads.route('/')
const squad = new SquadModel(req.body);
// timestamp and default are set automatically by Mongoose Schema Validation
if (req.file) {
const imageFileBuffer = req.file.buffer;
const err = imageDimensionValidator(imageFileBuffer, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT);
if (err) {
return next(err);
}
squad.save((err) => {
if (err) {
err.status = codes.wrongrequest;
@ -62,10 +71,9 @@ squads.route('/')
}
res.status(codes.created);
res.locals.items = squad;
fs.appendFile(resourceLocation.concat(squad._id).concat('.png'),
new Buffer(req.file.buffer), (err) => {
next(err);
});
fs.appendFile(resourceLocation.concat(squad._id).concat('.png'), new Buffer(imageFileBuffer), (err) => {
next(err);
});
});
} else {
const err = new Error('no image file provided');
@ -98,11 +106,16 @@ squads.route('/:id')
req.body.$inc = {__v: 1};
if (req.file) {
const imageFileBuffer = req.file.buffer;
const err = imageDimensionValidator(imageFileBuffer, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT);
if (err) {
return next(err);
}
const file = resourceLocation.concat(req.params.id)
.concat('.png');
fs.unlink(file, (err) => {
if (err) next(err);
fs.appendFile(file, new Buffer(req.file.buffer), (err) => {
fs.appendFile(file, new Buffer(imageFileBuffer), (err) => {
if (err) next(err);
});
});

View File

@ -66,7 +66,7 @@ export class EditDecorationComponent implements OnInit, OnDestroy {
if (this.fileList) {
file = this.fileList[0];
this.decorationService.submitDecoration(this.decoration, file)
.subscribe(rank => {
.subscribe(decoration => {
this.router.navigate(['..'], {relativeTo: this.route});
});
} else {
@ -83,12 +83,15 @@ export class EditDecorationComponent implements OnInit, OnDestroy {
}
delete this.decoration['__v'];
this.decorationService.submitDecoration(this.decoration, file)
.subscribe(rank => {
.subscribe(decoration => {
setTimeout(() => {
this.imagePreviewSrc = 'resource/decoration/' + this.decoration._id + '.png?' + Date.now();
}, 300);
fileInput.value = '';
this.snackBarService.showSuccess('generic.save.success');
}, error => {
const errorMsg = error._body ? JSON.parse(error._body).error.message : error.error.error.message;
this.snackBarService.showError('Error: '.concat(errorMsg), 15000);
});
}
}

View File

@ -69,7 +69,7 @@ export class EditSquadComponent implements OnInit, OnDestroy {
if (this.fileList) {
file = this.fileList[0];
this.squadService.submitSquad(this.squad, file)
.subscribe(rank => {
.subscribe(squad => {
this.saved = true;
this.router.navigate(['..'], {relativeTo: this.route});
});
@ -87,12 +87,15 @@ export class EditSquadComponent implements OnInit, OnDestroy {
}
delete this.squad['__v'];
this.squadService.submitSquad(this.squad, file)
.subscribe(rank => {
.subscribe(squad => {
setTimeout(() => {
this.imagePreviewSrc = 'resource/squad/' + this.squad._id + '.png?' + Date.now();
}, 300);
fileInput.value = '';
this.snackBarService.showSuccess('generic.save.success');
}, error => {
const errorMsg = error._body ? JSON.parse(error._body).error.message : error.error.error.message;
this.snackBarService.showError('Error: '.concat(errorMsg), 15000);
});
}
}