diff --git a/source/server/routes/api/v1/scenes/scene/history/post.ts b/source/server/routes/api/v1/scenes/scene/history/post.ts index 971d93c7..864523f2 100644 --- a/source/server/routes/api/v1/scenes/scene/history/post.ts +++ b/source/server/routes/api/v1/scenes/scene/history/post.ts @@ -11,8 +11,11 @@ import { ItemEntry } from "../../../../../../vfs/index.js"; * What is "before" or "after" is defined by the reverse of what is returned by `GET /api/v1/scenes/:scene/history` * That is the algorithm will remove everything in indices : * history[0] .. history[indexOf(:id)] + * + * It uses a file's name and generation or id and type to find the point to restore to. * - * @see {getSceneHistory} + * @see {@link getSceneHistory} for content ordering + * @see {@link getScene} for another example */ export async function postSceneHistory(req :Request, res :Response){ let requester = getUser(req); diff --git a/source/server/utils/locals.test.ts b/source/server/utils/locals.test.ts index b808f678..3aeff58b 100644 --- a/source/server/utils/locals.test.ts +++ b/source/server/utils/locals.test.ts @@ -1,7 +1,7 @@ import express, { Express, NextFunction, Request, RequestHandler, Response } from "express"; import request from "supertest"; import { InternalError, UnauthorizedError } from "./errors.js"; -import { either } from "./locals.js"; +import { either, getSceneParams } from "./locals.js"; //Dummy middlewares function pass(req :Request, res :Response, next :NextFunction){ @@ -52,4 +52,11 @@ describe("either() middleware", function(){ app.get("/", either(fail, fail), h); await request(app).get("/").expect(401); }); -}); \ No newline at end of file +}); + +describe("getSceneParams()", function(){ + + it("parses a request's scene parameter", function(){ + expect(getSceneParams({params:{scene:"foo"}} as any)).to.deep.equal({scene:"foo", revision: undefined}); + }) +}) \ No newline at end of file diff --git a/source/server/utils/locals.ts b/source/server/utils/locals.ts index cf0d0350..ee85597a 100644 --- a/source/server/utils/locals.ts +++ b/source/server/utils/locals.ts @@ -3,7 +3,7 @@ import { NextFunction, Request, RequestHandler, Response } from "express"; import {basename, dirname} from "path"; import User, { SafeUser } from "../auth/User.js"; import UserManager, { AccessType, AccessTypes } from "../auth/UserManager.js"; -import Vfs, { GetFileParams } from "../vfs/index.js"; +import Vfs, { GetFileParams, GetSceneParams } from "../vfs/index.js"; import { BadRequestError, ForbiddenError, HTTPError, InternalError, NotFoundError, UnauthorizedError } from "./errors.js"; import Templates from "./templates.js"; @@ -136,12 +136,21 @@ export function getUserId(req :Request){ return getUser(req).uid; } +export function getSceneParams(req :Request) :GetSceneParams{ + const {scene:sceneSlug} = req.params; + if(!sceneSlug) throw new BadRequestError(`Scene parameter not provided`); + const [scene, revision] = sceneSlug.split("$"); + if(revision && !/^(?:file|document)\.\d+$/.test(revision)) throw new BadRequestError(`Invalid revision parameter "${revision}"`); + return {scene, revision}; +} + export function getFileParams(req :Request):GetFileParams{ - let {scene, name} = req.params; + const {scene, revision} = getSceneParams(req); + let {name} = req.params; if(!scene) throw new BadRequestError(`Scene parameter not provided`); if(!name) throw new BadRequestError(`File parameter not provided`); - return {scene, name}; + return {scene, revision, name}; } export function getVfs(req :Request){ diff --git a/source/server/vfs/Scenes.ts b/source/server/vfs/Scenes.ts index 97f40e0d..ad5dab9e 100644 --- a/source/server/vfs/Scenes.ts +++ b/source/server/vfs/Scenes.ts @@ -9,10 +9,25 @@ import { ItemEntry, Scene, SceneQuery } from "./types.js"; export default abstract class ScenesVfs extends BaseVfs{ + /** + * Create a new scene + * Will check if name is a valid scene name. ie: + * - not empty + * - not too long (< 255 bytes) + * - not containing a "$" (reserved for generation selection) + */ async createScene(name :string):Promise async createScene(name :string, author_id :number):Promise async createScene(name :string, permissions:Record):Promise async createScene(name :string, perms ?:Record|number) :Promise{ + + //Check name validity + if(typeof name !== "string") throw new BadRequestError(`Scene name must be a string. Received "${typeof name}"`); + if(name.length == 0) throw new BadRequestError("Scene name cannot be empty"); + if(Buffer.byteLength(name) >= 255) throw new BadRequestError("Scene name is too long (max 254 bytes)"); + if(name.includes("$")) throw new BadRequestError("Scene name cannot contain '$'"); + + let permissions :Record = (typeof perms === "object")? perms : {}; //Always provide permissions for default user permissions['0'] ??= (config.public?"read":"none"); diff --git a/source/server/vfs/types.ts b/source/server/vfs/types.ts index 2b8c752a..adb2dec8 100644 --- a/source/server/vfs/types.ts +++ b/source/server/vfs/types.ts @@ -6,11 +6,13 @@ import { AccessType } from "../auth/UserManager.js"; export type DataStream = ReadStream|AsyncGenerator|Request; - - -export interface GetFileParams { +export interface GetSceneParams { /** Scene name or scene id */ scene :string|number; + revision ?:string; +} + +export interface GetFileParams extends GetSceneParams{ name :string; /**Also return deleted files */ archive ?:boolean; diff --git a/source/server/vfs/vfs.test.ts b/source/server/vfs/vfs.test.ts index adb1fcb3..716f7c0c 100644 --- a/source/server/vfs/vfs.test.ts +++ b/source/server/vfs/vfs.test.ts @@ -102,6 +102,17 @@ describe("Vfs", function(){ Uid.make = old; } }); + + it("requires valid names", async function(){ + await Promise.all(([ + [null, "Scene name must be a string. Received \"object\""], + ["", "Scene name cannot be empty"], + ["a".repeat(255), "Scene name is too long (max 254 bytes)"], + ["$", "Scene name cannot contain '$'"], + ] as Array<[string, string]>).map(async ([name, error])=>{ + await expect(vfs.createScene(name)).to.be.rejectedWith(error); + })); + }) }); describe("getScenes()", function(){