import { Response, Request } from "express"; import { APIHelper } from "../helpers/APIHelper"; import { APIClient } from "./APIClient"; import { checkMail, removeHTMLNodes } from "../utils/StringUtils"; import { AccountHelper } from "../helpers/AccountHelper"; import { UploadedFile } from "express-fileupload"; import { prepareFileCreation, generateNewUserDataFileName, pathUserData } from "../utils/UserDataUtils"; import * as sharp from 'sharp'; import { UserHelper } from "../helpers/UserHelper"; import { GroupsAccessLevel } from "./Group"; import { GroupsHelper } from "../helpers/GroupsHelper"; import { checkVirtualDirectory } from "../utils/VirtualDirsUtils"; import { FriendsHelper } from "../helpers/FriendsHelper"; import { PostsHelper } from "../helpers/PostsHelper"; import { PostAccessLevel } from "./Post"; /** * Response to a request * * @author Pierre HUBERT */ export class RequestHandler { private client : APIClient = null; private userID : number = -1; private responseSent = false; public constructor(private req : Request, private response : Response) {} /** * Get a parameter included in the post request * * @param name Name of the parameter to get */ private getPostParam(name : string) : any { return this.req.body[name]; } /** * Get a String from the request * * @param name The name of the string to get * @param minLength Minimal required size of the string * @param required If set to true (true by default), an error will * be thrown if the string is not included in the request */ public postString(name : string, minLength : number = 1, required : boolean = true) : string { const param = this.getPostParam(name); // Check if parameter was not found if(param == undefined) { if(required) this.error(400, "Could not find required string: '"+name+"'"); return ""; } if(param.length < minLength) this.error(400, "Parameter "+name+" is too short!"); return param; } /** * Check out whether a post parameter is present into the request or not * * @param name The name of the post field to check */ public hasPostParameter(name: string) : boolean { return this.getPostParam(name) != undefined; } /** * Check out whether a POST string is present in the request or not * * @param name The name of the POST field to check * @param minLength Minimal length of the parameter */ public hasPostString(name: string, minLength: number = 0) : boolean { return this.hasPostParameter(name) && this.getPostParam(name).length >= minLength; } /** * Get some content for post and satinize it (remove HTML nodes) * * @param name The name of the POST field */ public postContent(name: string) : string { const content = this.postString(name); if(content.match(/data:image/)) this.error(401, "Please do not include inline images!"); return removeHTMLNodes(content); } /** * Get an email address included in a post request * * @param name The name of the POST filed */ public postEmail(name: string) : string { const email = this.postString(name, 3); if(!checkMail(email)) this.error(400, email + " is not a valid email address!"); return email; } /** * Get an integer included in the request * * @param name Name of POST field * @param fallback Fallback value (if none, throw an error) * @returns The number (throws in case of error) */ public postInt(name: string, fallback ?: number) : number { const param = this.getPostParam(name); if(param == undefined) { if(fallback == undefined) this.error(400, "Missing integer '"+name+"' in the request!"); return fallback; } // Check number if(Number.parseInt(param).toString() !== param.toString()) this.error(400, "'"+name+"' is an invalid integer!"); return Number.parseInt(param); } /** * Get a list of integeres included in the request * * @param name The name of the post field * @param minEntries Specify the minimum number of entries required */ public postNumbersList(name: string, minEntries : number = 1) : Array { const param = this.postString(name, minEntries < 1 ? 0 : minEntries, minEntries > 0); let list = []; for (const el of param.split(",")) { if(el == "") continue; if(Number.parseInt(el).toString() != el) this.error(400, "Invalid number detected in '"+name+"'!"); list.push(Number.parseInt(el)); } if(list.length < minEntries) this.error(400, "Not enough entries in '" + name + "'!") return list; } /** * Turn a list of string into a Set object * * @param name Name of POST field * @param minEntries Minimum number of entries to specify */ public postNumbersSet(name : string, minEntries : number = 1) : Set { return new Set(this.postNumbersList(name, minEntries)); } /** * Attempt to decode JSON included in a POST request * * @param name Name of POST field */ public postJSON(name: string) : any { const src = this.getPostParam(name); if(src == undefined) this.error(400, "Missing JSON '" + name + "' in the request!"); try { const response = JSON.parse(src); return response; } catch(e) { this.error(500, "'" + name + "' is not a valid JSON !"); } } /** * Get a boolean included in the request * * @param name The name of the POST field * @param fallback Fallback value to use if the value is not * found in the request */ public postBool(name: string, fallback ?: boolean) : boolean { const param = this.getPostParam(name); if(param == undefined) { if(fallback != undefined) return fallback; this.error(400, "Missing boolean '" + name + "' in the request!"); } return param === "true" || param === true; } /** * Get the ID of a user specified in a POST request * * @param name Name of the POST field */ public async postUserId(name: string) : Promise { const userID = this.postInt(name); if(userID < 1) this.error(400, "Invalid user ID specified in '" + name +"'!"); if(!await UserHelper.Exists(userID)) this.error(404, "User with ID " + userID + " not found!"); return userID; } /** * Get the ID of a friend included in a POST request * * @param name Name of the POST field */ public async postFriendId(name: string) : Promise { const friendID = await this.postUserId(name); if(!await FriendsHelper.AreFriend(this.getUserId(), friendID)) this.error(401, "You are not friend with this personn!"); return friendID; } /** * Find user ID based on its email address, included in a POST request * * @param name The name of the POST field containing the email address of the user */ public async postUserIdFromEmail(name: string) : Promise { const email = this.postEmail(name); const userID = await AccountHelper.FindIDFromEmail(email); if(userID < 1) this.error(404, "Email not found!"); return userID; } /** * Get a POST group ID * * @param name The name of the POST field */ public async postGroupID(name: string) : Promise { const groupID = this.postInt(name); if(!await GroupsHelper.Exists(groupID)) this.error(404, "Specified group not found!"); return groupID; } /** * Get a POST group ID with a check for access level of current user * * @param name The name of the POST field containing group ID * @param minVisibility Minimum visiblity requested to the group * @returns The ID of the group (throws in case of failure) */ public async postGroupIDWithAccess(name: string, minVisibility : GroupsAccessLevel) : Promise { const groupID = await this.postGroupID(name); const access = await GroupsHelper.GetAccessLevel(groupID, this.optionnalUserID); if(access == GroupsAccessLevel.NO_ACCESS) this.error(404, "Specified group not found!"); if(access < minVisibility) this.error(401, "You do not have enough rights to perform what you intend to do on this group!"); return groupID; } /** * Get the ID of post included in a POST request * * @param name The name of the POST field containing the id of the target post */ public async postPostID(name: string) : Promise { const postID = this.postInt(name); if(postID < 1) this.error(400, "Invalid post ID!"); if(!await PostsHelper.Exists(postID)) this.error(404, "Specified post does not exists!"); return postID; } /** * Get the ID of a post a user has access to * * @param name The name of the POST field containing the ID of the target post */ public async postPostIDWithAccess(name: string) : Promise { const postID = await this.postPostID(name); if(await PostsHelper.GetAccessLevelFromPostID(this.optionnalUserID, postID) == PostAccessLevel.NO_ACCESS) this.error(401, "Your are not allowed to access this post information!"); return postID; } /** * Get a virtual directory included in a POST request * * @param name The name of the POST variable * @return The virtual directory, if found as valid */ public postVirtualDirectory(name: string) : string { const dir = this.postString(name); if(!checkVirtualDirectory(dir)) this.error(401, "Specified directory seems to be invalid!"); return dir; } /** * Get information about an uploaded file * * @param name The name of the posted file */ private getUploadedFile(name: string) : undefined | UploadedFile { if(!this.req.files) return undefined; if(this.req.files[name] instanceof Array) this.error(500, "Multiple upload are not supported!"); return this.req.files[name]; } /** * Check out whether a file has been included in the request or not * * @param name The name of the file to check */ public hasFile(name: string) : boolean { return this.getUploadedFile(name) != undefined; } /** * Save an image in user data directory * * @param postField The name of the POST field * @param folder Target folder in user data directory * @param maxW Maximum width of the image * @param maxH Maximum height of the image */ public async savePostImage(postField: string, folder: string, maxW: number, maxH: number) : Promise { const file = this.getUploadedFile(postField); if(file == undefined) this.error(400, "File '"+postField+"' is missing in the request !"); const targetUserDataFolder = prepareFileCreation(this.getUserId(), folder); const targetFilePath = generateNewUserDataFileName(targetUserDataFolder, "png"); const targetSysPath = pathUserData(targetFilePath, true); // Process image size let img = sharp(file.data); const stats = await img.metadata(); if(stats.width > maxW || stats.height > maxH) { if(stats.width > maxW) img = img.resize(maxW, Math.floor((stats.height*maxW)/stats.width)); else img = img.resize(Math.floor((stats.width*maxH)/stats.height), maxH); } // Save image await img.png().toFile(targetSysPath); return targetFilePath; } /** * Validate API tokens * * @throws If the tokens could not be validated */ public async checkAPITokens() { // Extract API name & token from request const apiName = this.postString("serviceName"); const apiToken = this.postString("serviceToken"); // Validate the client const client = await APIHelper.GetClient(apiName, apiToken); if(client == null) this.error(400, "Client not recognized!"); if(client.domain) { const allowedOrigin = "http://" + client.domain; const referer = this.req.get("Referer"); if(!referer || !referer.startsWith(allowedOrigin)) this.error(401, "Use of this client is prohibited from this domain!"); this.response.set("Access-Control-Allow-Origin", allowedOrigin); } // Save client information for latter access this.client = client; } /** * Validate user tokens * * @param required Specify whether the user MUST be authenticated or not */ public async checkUserTokens(required ?: boolean) { const token1 = this.postString("userToken1", 0, false); const token2 = this.postString("userToken2", 0, false); if(token1.length < 1 || token2.length < 1) { if(required !== false) this.error(401, "This method requires the user to be signed in!"); return; } // Validate user tokens this.userID = await AccountHelper.GetUserIdFromTokens(this.getClientInfo(), token1, token2); if(this.userID < 1) this.error(412, "Please check your login tokens!"); } /** * Check the user password included in the request * * @param postField The name of the post field * containing user password */ public async needUserPostPassword(postField: string) { const password = this.postString(postField, 3); if(!await AccountHelper.CheckUserPassword(this.getUserId(), password)) this.error(401, "Invalid password!"); } /** * Get information about API client */ public getClientInfo() : APIClient { if(!this.client) throw Error("Try to get information about client but client has not been authenticated!"); return this.client; } /** * Get information about current user */ public getUserId() : number { if(this.userID < 1) throw Error("Trying to get user ID but none are available!"); return this.userID; } /** * Get the ID of the current user (if any) * or 0 if the user is not signed in */ public get optionnalUserID(): number { return this.userID >= 1 ? this.userID : 0; } /** * Check out whether user is signed in or not */ public get signedIn() : boolean { return this.userID > 0; } /** * Output an error code and throws an error * * @param code HTTP Status code * @param message The message to send */ public error(code : number, message : string, should_throw: boolean = true) { if(this.responseSent) return; this.response.status(code).send({ error: { code: code, message: message } }); this.responseSent = true; if(should_throw) throw Error("Could not complete request! ("+ message +")"); } /** * Return a successful operation * * @param message Message associated to success */ public success(message: string) { this.responseSent = true; this.response.send({ success: message }); } /** * Send some data to the server * * @param data The data to send back to the server */ public send(data: any) { this.responseSent = true; this.response.send(data); } }