import gm from 'gm' import { URL } from 'url' import path from 'path' import crypto from 'crypto' import { v4 as uuid } from 'uuid' import { downloadURL } from '../../scripts/http' const fs = require('fs-extra') const gravatar = 'https://www.gravatar.com/avatar/' const uploads = path.join(__dirname, '../../', 'usercontent') const images = path.join(uploads, 'images') const maxFileSize = 1000000 export const imageTypes = { 'image/png': '.png', 'image/jpg': '.jpg', 'image/jpeg': '.jpeg' } function decodeBase64Image (dataString) { const matches = dataString.match(/^data:([A-Za-z-+/]+);base64,(.+)$/) const response = {} if (matches.length !== 3) { return null } response.type = matches[1] response.data = Buffer.from(matches[2], 'base64') return response } function saneFields (fields) { const out = {} for (const i in fields) { const entry = fields[i] if (typeof entry === 'object' && entry.length === 1 && !isNaN(parseInt(entry[0]))) { out[i] = parseInt(entry[0]) } } return out } async function bailOut (file, error) { await fs.unlink(file) throw new Error(error) } export async function imageBase64 (baseObj) { if (!baseObj) return null const imgData = decodeBase64Image(baseObj) if (!imgData) return null if (!imageTypes[imgData.type]) return null let imageName = 'base64-' + uuid() const ext = imageTypes[imgData.type] || '.png' imageName += ext const fpath = path.join(images, imageName) try { await fs.writeFile(fpath, imgData.data) } catch (e) { console.error(e) return null } return { file: fpath } } export function gravatarURL (email) { const sum = crypto.createHash('md5').update(email).digest('hex') return gravatar + sum + '.jpg' } export async function downloadImage (imgUrl, designation) { if (!imgUrl) return null if (!designation) designation = 'download' let imageName = designation + '-' + uuid() const uridata = new URL(imgUrl) const pathdata = path.parse(uridata.href) imageName += pathdata.ext || '.png' try { await downloadURL(imgUrl, path.join(images, imageName)) } catch (e) { return null } return imageName } export async function uploadImage (identifier, fields, files) { if (!files.image) throw new Error('No image file') let file = files.image[0] if (file.size > maxFileSize) return bailOut(file.path, 'Image is too large! 1 MB max') fields = saneFields(fields) // Get file info, generate a file name const fileHash = uuid() const contentType = file.headers['content-type'] if (!contentType) return bailOut(file.path, 'Invalid or missing content-type header') file = file.path // Make sure content type is allowed let match = false for (const i in imageTypes) { if (i === contentType) { match = true break } } if (!match) return bailOut(file, 'Invalid image type. Only PNG, JPG and JPEG files are allowed.') const extension = imageTypes[contentType] const fileName = identifier + '-' + fileHash + extension // Check for cropping if (fields.x == null || fields.y == null || fields.width == null || fields.height == null) { return bailOut(file, 'Images can only be cropped on the server side due to security reasons.') } if (fields.x < 0 || fields.y < 0 || fields.x > fields.width + fields.x || fields.y > fields.height + fields.y) { return bailOut(file, 'Impossible crop.') } // Check 1 : 1 aspect ratio if (Math.floor(fields.width / fields.height) !== 1) { return bailOut(file, 'Avatars can only have an aspect ratio of 1:1') } // Upscaling is not allowed if ((fields.scaleX != null && fields.scaleX > 1) || (fields.scaleY != null && fields.scaleY > 1)) { return bailOut(file, 'Image upscaling is not allowed.') } if (fields.scaleX != null) { fields.x *= fields.scaleX fields.width *= fields.scaleX } if (fields.scaleY != null) { fields.y *= fields.scaleY fields.height *= fields.scaleY } // Crop try { await new Promise(function (resolve, reject) { gm(file) .crop(fields.width, fields.height, fields.x, fields.y) .write(path.join(images, fileName), (err) => { if (err) return reject(err) resolve(fileName) }) }) await fs.unlink(file) } catch (e) { console.error(e) return bailOut(file, 'An error occured while cropping.') } return { file: fileName } }