diff --git a/src/controllers/GroupsController.ts b/src/controllers/GroupsController.ts index f8f3961..8769a49 100644 --- a/src/controllers/GroupsController.ts +++ b/src/controllers/GroupsController.ts @@ -4,6 +4,10 @@ import { GroupsAccessLevel, GroupInfo, GroupVisibilityLevel, GroupPostsCreationL import { GroupMembershipLevels } from "../entities/GroupMember"; import { time } from "../utils/DateUtils"; import { LikesHelper, LikesType } from "../helpers/LikesHelper"; +import { GroupSettings } from "../entities/GroupSettings"; +import { removeHTMLNodes, checkURL } from "../utils/StringUtils"; +import { findKey } from "../utils/ArrayUtils"; +import { checkVirtualDirectoryAvailability, VirtualDirType } from "../utils/VirtualDirsUtils"; /** * Groups API controller @@ -14,7 +18,7 @@ import { LikesHelper, LikesType } from "../helpers/LikesHelper"; /** * API groups registration levels */ -const GROUPS_REGISTRATION_LEVELS = []; +const GROUPS_REGISTRATION_LEVELS = {}; GROUPS_REGISTRATION_LEVELS[GroupRegistrationLevel.OPEN_REGISTRATION] = "open"; GROUPS_REGISTRATION_LEVELS[GroupRegistrationLevel.MODERATED_REGISTRATION] = "moderated"; GROUPS_REGISTRATION_LEVELS[GroupRegistrationLevel.CLOSED_REGISTRATION] = "closed"; @@ -22,7 +26,7 @@ GROUPS_REGISTRATION_LEVELS[GroupRegistrationLevel.CLOSED_REGISTRATION] = "closed /** * API groups membership levels */ -const GROUPS_MEMBERSHIP_LEVELS = []; +const GROUPS_MEMBERSHIP_LEVELS = {}; GROUPS_MEMBERSHIP_LEVELS[GroupMembershipLevels.ADMINISTRATOR] = "administrator"; GROUPS_MEMBERSHIP_LEVELS[GroupMembershipLevels.MODERATOR] = "moderator"; GROUPS_MEMBERSHIP_LEVELS[GroupMembershipLevels.MEMBER] = "member"; @@ -34,7 +38,7 @@ GROUPS_MEMBERSHIP_LEVELS[GroupMembershipLevels.VISITOR] = "visitor"; /** * API groups visibility levels */ -const GROUPS_VISIBILITY_LEVELS = []; +const GROUPS_VISIBILITY_LEVELS = {}; GROUPS_VISIBILITY_LEVELS[GroupVisibilityLevel.OPEN_GROUP] = "open"; GROUPS_VISIBILITY_LEVELS[GroupVisibilityLevel.PRIVATE_GROUP] = "private"; GROUPS_VISIBILITY_LEVELS[GroupVisibilityLevel.SECRETE_GROUP] = "secrete"; @@ -144,6 +148,74 @@ export class GroupsController { h.send(await this.GroupInfoToAPI(group, h, true)); } + /** + * Set (update) group settings + * + * @param h Request handler + */ + public static async SetSettings(h: RequestHandler) { + const groupID = await h.postGroupIDWithAccess("id", GroupsAccessLevel.ADMIN_ACCESS); + + // Check group visibility + const visibilityKey = findKey(GROUPS_VISIBILITY_LEVELS, h.postString("visibility", 3)); + if(visibilityKey == null) + h.error(400, "Group visibility level not recognized!"); + + // Check group registration level + const registrationKey = findKey(GROUPS_REGISTRATION_LEVELS, h.postString("registration_level", 3)); + if(registrationKey == null) + h.error(400, "Group registration level not recognized!"); + + // Check post creation level + const postLevelKey = findKey(GROUPS_POSTS_LEVELS, h.postString("posts_level", 3)); + if(postLevelKey == null) + h.error(400, "Group post creation level not recognized!"); + + + // Check URL + const url = h.postString("url", 0); + if(url.length > 0 && ! checkURL(url)) + h.error(401, "Invalid group URL!"); + + + // Check virtual directory + let virtualDirectory = ""; + if(h.hasPostString("virtual_directory", 1)) { + virtualDirectory = h.postVirtualDirectory("virtual_directory"); + + // Check out whether virtual directory is available or not + if(!await checkVirtualDirectoryAvailability(virtualDirectory, groupID, VirtualDirType.GROUP)) + h.error(401, "Requested virtual directory is not available!"); + } + + const settings = new GroupSettings({ + // Basic information + id: groupID, + name: removeHTMLNodes(h.postString("name", 3)), + visiblity: Number(visibilityKey), + registrationLevel: Number(registrationKey), + postsCreationLevel: Number(postLevelKey), + + // Useless info + membersCount: -1, + timeCreate: -1, + + // Optionnal + description: removeHTMLNodes(h.postString("description", 0)), + + // Optionnal + url: url, + + // Optionnal + virtualDirectory: virtualDirectory, + + }); + + await GroupsHelper.SetSettings(settings); + + h.success("Group settings have been successfully updated!"); + } + /** * Turn a GroupInfo object into a valid API object * diff --git a/src/controllers/Routes.ts b/src/controllers/Routes.ts index 4f2a66e..854d78d 100644 --- a/src/controllers/Routes.ts +++ b/src/controllers/Routes.ts @@ -96,4 +96,6 @@ export const Routes : Route[] = [ {path: "/groups/get_advanced_info", cb: (h) => GroupsController.GetAdvancedInfo(h), needLogin: false}, {path: "/groups/get_settings", cb: (h) => GroupsController.GetSettings(h)}, + + {path: "/groups/set_settings", cb: (h) => GroupsController.SetSettings(h)}, ] \ No newline at end of file diff --git a/src/entities/GroupSettings.ts b/src/entities/GroupSettings.ts new file mode 100644 index 0000000..07bda53 --- /dev/null +++ b/src/entities/GroupSettings.ts @@ -0,0 +1,11 @@ +import { GroupInfo } from "./Group"; + +/** + * Group settings + * + * @author Pierre HUBERT + */ + +export class GroupSettings extends GroupInfo { + +} \ No newline at end of file diff --git a/src/entities/RequestHandler.ts b/src/entities/RequestHandler.ts index fd89973..ab7269c 100644 --- a/src/entities/RequestHandler.ts +++ b/src/entities/RequestHandler.ts @@ -9,6 +9,7 @@ import * as sharp from 'sharp'; import { UserHelper } from "../helpers/UserHelper"; import { GroupsAccessLevel } from "./Group"; import { GroupsHelper } from "../helpers/GroupsHelper"; +import { checkVirtualDirectory } from "../utils/VirtualDirsUtils"; /** * Response to a request @@ -67,6 +68,16 @@ export class RequestHandler { 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 an email address included in a post request * @@ -233,6 +244,21 @@ export class RequestHandler { return groupID; } + /** + * 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 * diff --git a/src/helpers/AccountHelper.ts b/src/helpers/AccountHelper.ts index d4dcf3c..0c8945a 100644 --- a/src/helpers/AccountHelper.ts +++ b/src/helpers/AccountHelper.ts @@ -2,6 +2,7 @@ import { crypt, sha1, randomStr } from "../utils/CryptUtils"; import { APIClient } from "../entities/APIClient"; import { UserLoginTokens } from "../entities/UserLoginTokens"; import { DatabaseHelper } from "./DatabaseHelper"; +import { UserHelper } from "./UserHelper"; /** * Account helper @@ -139,4 +140,17 @@ export class AccountHelper { token2: row.token2 }; } + + + /** + * Check out whether a virtual directory is available or not + * + * @param dir Virtual directory to check + * @param userID Target user ID + */ + public static async CheckUserDirectoryAvailability(dir: string, userID: number = -1) : Promise { + const foundUser = await UserHelper.FindByFolder(dir); + + return foundUser < 1 || userID == foundUser; + } } \ No newline at end of file diff --git a/src/helpers/GroupsHelper.ts b/src/helpers/GroupsHelper.ts index 92919dc..aebea10 100644 --- a/src/helpers/GroupsHelper.ts +++ b/src/helpers/GroupsHelper.ts @@ -3,6 +3,7 @@ import { GroupsAccessLevel, GroupVisibilityLevel, GroupInfo } from "../entities/ import { GroupMembershipLevels, GroupMember } from "../entities/GroupMember"; import { NewGroup } from "../entities/NewGroup"; import { time } from "../utils/DateUtils"; +import { GroupSettings } from "../entities/GroupSettings"; /** * Groups helper @@ -98,6 +99,23 @@ export class GroupsHelper { return this.DbToGroupInfo(row); } + /** + * Update (set) group settings + * + * @param settings Group settings + */ + public static async SetSettings(settings: GroupSettings) { + const dbEntry = this.GroupSettingsToDB(settings); + + await DatabaseHelper.UpdateRows({ + table: GROUPS_LIST_TABLE, + where: { + id: settings.id + }, + set: dbEntry + }); + } + /** * Get the visibility level of a group * @@ -222,6 +240,36 @@ export class GroupsHelper { return this.DbToGroupMember(data); } + /** + * Find a group by its virtual directory + * + * @param dir Target directory + * @returns The ID of the target directory / -1 if none found + */ + public static async FindByVirtualDirectory(dir: string) : Promise { + const result = await DatabaseHelper.QueryRow({ + table: GROUPS_LIST_TABLE, + where: { + virtual_directory: dir + }, + fields: ["id"] + }); + + return result == null ? -1 : result.id; + } + + /** + * Check out whether a virtual directory is available or not + * + * @param dir The directory to check + * @param groupID The ID of the group making the request + */ + public static async CheckDirectoryAvailability(dir: string, groupID: number = -1) : Promise { + const currID = await this.FindByVirtualDirectory(dir); + + return currID < 1 || currID == groupID; + } + /** * Count the number of members of a group @@ -277,4 +325,40 @@ export class GroupsHelper { following: row.following == 1 }); } + + /** + * Turn a GroupSettings object into a database entry object + * + * @param settings Group settings object to transform + * @return Generated database entry + */ + private static GroupSettingsToDB(settings: GroupSettings) : Object { + let data = {}; + + if(settings.name != null) + data['name'] = settings.name; + + if(settings.hasLogo) + data["path_logo"] = settings.logo; + + if(settings.visiblity != null) + data["visibility"] = settings.visiblity; + + if(settings.registrationLevel != null) + data["registration_level"] = settings.registrationLevel; + + if(settings.postsCreationLevel != null) + data["posts_level"] = settings.postsCreationLevel; + + if(settings.virtualDirectory != null) + data["virtual_directory"] = settings.virtualDirectory; + + if(settings.description != null) + data["description"] = settings.description; + + if(settings.url != null) + data["url"] = settings.url; + + return data; + } } \ No newline at end of file diff --git a/src/helpers/UserHelper.ts b/src/helpers/UserHelper.ts index b3cb93a..96c3518 100644 --- a/src/helpers/UserHelper.ts +++ b/src/helpers/UserHelper.ts @@ -68,6 +68,24 @@ export class UserHelper { return request.map((e) => e.ID); } + /** + * Search for user by virtual directory + * + * @param dir Target directory + * @returns The ID of the user found / -1 if none found + */ + public static async FindByFolder(dir: string) : Promise { + const result = await DatabaseHelper.QueryRow({ + table: TABLE_NAME, + where: { + sous_repertoire: dir + }, + fields: ["ID"] + }); + + return result == null ? -1 : Number(result.ID); + } + private static async DbToUser(row: any) : Promise { return new User({ diff --git a/src/utils/ArrayUtils.ts b/src/utils/ArrayUtils.ts new file mode 100644 index 0000000..b734e8c --- /dev/null +++ b/src/utils/ArrayUtils.ts @@ -0,0 +1,24 @@ +/** + * Array utilities + * + * @author Pierre HUBERT + */ + +/** + * Find the key matching a given value in an object + * + * @param object Object to search in + * @param value The value to search for + * @returns Matching key, or null if not found + */ +export function findKey(object: Object, value: any): string { + for (const key in object) { + if (!object.hasOwnProperty(key)) + continue; + + if(object[key] == value) + return key; + } + + return null; +} \ No newline at end of file diff --git a/src/utils/StringUtils.ts b/src/utils/StringUtils.ts index ec2dd03..cefb068 100644 --- a/src/utils/StringUtils.ts +++ b/src/utils/StringUtils.ts @@ -14,6 +14,20 @@ export function checkMail(emailAddress: string): boolean { return (emailAddress.match(/^[a-zA-Z0-9_.]+@[a-zA-Z0-9-.]{1,}[.][a-zA-Z]{2,8}$/) === null ? false : true); } +/** + * Check a URL validity + * + * Source: https://gist.github.com/729294 + * + * @param {string} url The URL to check + * @return {boolean} TRUE if the URL is valid + */ +export function checkURL(url: string) : boolean { + const regex = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i + + return url.match(regex) == null ? false : true; +} + /** * Fix text encoding * diff --git a/src/utils/VirtualDirsUtils.ts b/src/utils/VirtualDirsUtils.ts new file mode 100644 index 0000000..441c290 --- /dev/null +++ b/src/utils/VirtualDirsUtils.ts @@ -0,0 +1,49 @@ +import { AccountHelper } from "../helpers/AccountHelper"; +import { GroupsHelper } from "../helpers/GroupsHelper"; + +/** + * Virtual directories utilities + * + * @author Pierre HUBERT + */ + +export enum VirtualDirType { + USER, + GROUP +} + +/** + * Check out whether a virtual directory is valid or not + * + * @param dir The virtual directory to check + */ +export function checkVirtualDirectory(dir: string) : boolean { + if(dir.length < 4) return false; + + for(let el of [".html", ".txt", ".php", "à", "â", "é", "ê", "@", "/", "\"", "'", '"', "<", ">", "?", "&", "#"]) + if(dir.includes(el)) + return false; + + return true; +} + +/** + * Check the availability of a virtual directory + * + * @param dir Directory to check + * @param id The ID of the element to check + * @param type The type of the element + */ +export async function checkVirtualDirectoryAvailability(dir: string, id: number, type: VirtualDirType) : Promise { + if(!checkVirtualDirectory(dir)) return false; + + if(type == VirtualDirType.USER) { + return await AccountHelper.CheckUserDirectoryAvailability(dir, id) + && await GroupsHelper.CheckDirectoryAvailability(dir); + } + + else { + return await AccountHelper.CheckUserDirectoryAvailability(dir) + && await GroupsHelper.CheckDirectoryAvailability(dir, id); + } +} \ No newline at end of file