import { Writable, Readable } from 'stream'; import { IUser, ILibraryDetailedDataForClient, IHubInfo, ContentId, IContentMetadata, ContentParameters, ILibraryOverviewForClient, IFileStats, IAjaxResponse } from './types'; import H5PEditor from './H5PEditor'; import AjaxSuccessResponse from './helpers/AjaxSuccessResponse'; /** * Each method in this class corresponds to a route that is called by the H5P * client. Normally, the implementation must call them and send back the * return values as a JSON body in the HTTP response. * * If something goes wrong, the methods throw H5pErrors, which include HTTP * status codes. Send back these status codes in your HTTP response. * * Remarks: * - Most of the route names can be configured in the IH5PConfig object and thus * might have different names in your case! * - The GET /content// and GET /temp-files/ routes * should support partials for the Interactive Video editor to work. This * means that the response with HTTP 200 must send back the header * 'Accept-Ranges: bytes' and must send back status code 206 for partials. * Check out the documentation of the methods `getContentFile` and * `getTemporaryFile` for more details. */ export default class H5PAjaxEndpoint { private h5pEditor; constructor(h5pEditor: H5PEditor); private semanticsEnforcer; /** * This method must be called by the GET route at the Ajax URL, e.g. GET * /ajax. This route must be implemented for editor to work. * @param action This is the sub action that should be executed. It is part * of the query like this: GET /ajax?action=xyz Possible values: * - content-type-cache: Requests information about available content * types from the server. The user parameter must be set, as the * accessible content types and possible actions (update, etc.) can vary * from user to user * - content-hub-metadata-cache: Requests information about the metadata * currently in use by the H5P Content Hub. * - libraries: Requests detailed data about a single library. The * parameters machineName, majorVersion, minorVersion and language must * be set in this case. Queries look like this: GET * /ajax?action=libraries?machineName=&majorVersion=&minorVersion= * @param machineName (need if action == 'libraries') The machine name of * the library about which information is requested, e.g. 'H5P.Example'. It * is part of the query, e.g. +machineName=H5P.Example * @param majorVersion (need if action == 'libraries') The major version of * the library about which information is requested, e.g. '1'. * @param minorVersion (need if action == 'libraries') The minor version of * the library about which information is requested, e.g. '0'. * @param language (can be set if action == 'libraries') The language in * which the editor is currently displayed, e.g. 'en'. Will default to * English if unset. * @param user (needed if action == 'content-type-cache') The user who is * displaying the H5P Content Type Hub. It is the job of the implementation * to inject this object. * @returns an object which must be sent back in the response as JSON with * HTTP status code 200 * @throws H5pErrors with HTTP status codes, which you must catch and then * send back in the response */ getAjax: (action: "content-type-cache" | "libraries" | string, machineName?: string, majorVersion?: number | string, minorVersion?: number | string, language?: string, user?: IUser) => Promise; /** * The method must be called by the GET /content// * route. * As it is necessary for the method to know about a possible range start * and end, you need to pass in a callback function that returns the range * specified in the http request. The start and end of the range cannot be * passed to this method directly, as you need to know the file size to * calculate the correct range. That's why we use the callback. * * Note: You can also use a status route to serve content files, but then * you lose the rights and permission check. * * @param contentId the contentId of the resource * @param filename the filename of the resource * @param user the user who is requesting the resource * @param getRangeCallback a function that can be called to retrieve the * start and end of a range; if the request doesn't specify a range, simply * return undefined; if you do not specify getRangeCallback, the method * will simply always use the whole file. * @returns mimetype: the mimetype of the file, the range (undefined if * unused), stats about the file and a readable. Do this with the response: * - Pipe back the readable as the response with status code 200. * - Set this HTTP header in the response: "Content-Type: " * - If range is undefined: * - Return status code 200 * - Set this HTTP header in the response: 'Accept-Ranges: bytes' * - If range is defined: * - Return status code 206 * - Set these HTTP headers in the response: 'Content-Length: ' * 'Content-Range': `bytes /` * **IMPORTANT:** You must subscribe to the error event of the readable and * send back a 404 status code if an error occurs. This is necessary as the * AWS S3 library doesn't throw proper 404 errors. * @throws H5pErrors with HTTP status codes, which you must catch and then * send back in the response */ getContentFile: (contentId: ContentId, filename: string, user: IUser, getRangeCallback?: (fileSize: number) => { end: number; start: number; } | undefined) => Promise<{ mimetype: string; range: { end: number; start: number; }; stats: IFileStats; stream: Readable; }>; /** * This method must be called when the editor requests the parameters * (= content.json) of a piece of content, which is done with * GET /params/ * This route must be implemented for the editor to work. * @param contentId the content id of the piece of content; it is part of * the requests URL (see above) * @param user the user who is using the editor. It is the job of the * implementation to inject this object. * @returns an object which must be sent back in the response as JSON with * HTTP status code 200 * @throws H5pErrors with HTTP status codes, which you must catch and then * send back in the response */ getContentParameters: (contentId: ContentId, user: IUser) => Promise<{ h5p: IContentMetadata; library: string; params: { metadata: IContentMetadata; params: ContentParameters; }; }>; /** * This method must be called when the user downloads a H5P package (the * .h5p file containing all libraries and content files). The route is * GET /download/ * @param contentId the id of the content object. It is part of the request * URL (see above). * @param user the user who wants to download the package. It is the job of * the implementation to inject this object. * @param outputWritable a Writable to which the file contents are piped. * @returns no return value; the result is directly piped to the Writable, * which is the Response object in Express, for instance. Note that you must * set the HTTP Header 'Content-disposition: attachment; filename=xyz.h5p' * in your response! * @throws H5pErrors with HTTP status codes, which you must catch and then * send back in the response */ getDownload: (contentId: ContentId, user: IUser, outputWritable: Writable) => Promise; /** * This method must be called when the client requests a library file with * GET /libraries//. * * Note: You can also use a static route to serve library files. * * @param ubername the ubername of the library (e.g. H5P.Example-1.0). This * is the first component of the path after /libraries/ * @param filename the filename of the requested file, e.g. xyz.js. This is the * rest of the path after the uberName. * @returns all the values that must be send back with HTTP status code 200. * You should send back: * - status code 200 * - the mimetype * - the file size (in stats) * @throws H5pErrors with HTTP status codes, which you must catch and then * send back in the response */ getLibraryFile: (ubername: string, filename: string) => Promise<{ mimetype: string; stats: IFileStats; stream: Readable; }>; /** * The method must be called by the GET /temp-files/ route. * As it is necessary for the method to know about a possible range start * and end, you need to pass in a callback function that returns the range * specified in the http request. The start and end of the range cannot be * passed to this method directly, as you need to know the file size to * calculate the correct range. That's why we use the callback. * @param filename the filename of the resource * @param user the user who is requesting the resource * @param getRangeCallback a function that can be called to retrieve the * start and end of a range; if the request doesn't specify a range, simply * return undefined; if you do not specify getRangeCallback, the method * will simply always use the whole file. * @returns mimetype: the mimetype of the file, the range (undefined if * unused), stats about the file and a readable. Do this with the response: * - Pipe back the readable as the response with status code 200. * - Set this HTTP header in the response: "Content-Type: " * - If range is undefined: * - Return status code 200 * - Set this HTTP header in the response: 'Accept-Ranges': 'bytes' * - If range is defined: * - Return status code 206 * - Set these HTTP headers in the response: 'Content-Length: ' * 'Content-Range': `bytes /` * **IMPORTANT:** You must subscribe to the error event of the readable and * send back a 404 status code if an error occurs. This is necessary as the * AWS S3 library doesn't throw proper 404 errors. * @throws H5pErrors with HTTP status codes, which you must catch and then * send back in the response */ getTemporaryFile: (filename: string, user: IUser, getRangeCallback?: (fileSize: number) => { end: number; start: number; }) => Promise<{ mimetype: string; range: { end: number; start: number; }; stats: IFileStats; stream: Readable; }>; /** * Implements the POST /ajax route. Performs various actions. Don't be * confused by the fact that many of the requests dealt with here are not * really POST requests, but look more like GET requests. This is simply how * the H5P client works and we can't change it. * @param action This is the sub action that should be executed. It is part * of the query like this: POST /ajax?action=xyz Possible values: * - libraries: returns basic information about a list of libraries * - translations: returns translation data about a list of libraries * in a specific language * - files: uploads a resource file (image, video, ...) into * temporary storage * - filter: cleans the parameters passed to it to avoid XSS * attacks and schema violations (currently only * implemented as a stub) * - library-install: downloads an installs content types from the H5P Hub * - library-upload: uploads a h5p package from the user's computer and * installs the libraries in it; returns the parameters * and metadata in it * @param body the parsed JSON content of the request body * @param language (needed for 'translations' and optionally possible for * 'libraries') the language code for which the translations should be * retrieved, e.g. 'en'. This paramter is part of the query URL, e.g. POST * /ajax?action=translations&language=en * @param user (needed for 'files', 'library-install' and 'get-content') the * user who is performing the action. It is the job of the implementation to * inject this object. * @param filesFile (needed for 'files') the file uploaded to the server; * this file is part of the HTTP request and has the name 'file'. * @param id (needed for 'library-install') the machine name of the library * to install. The id is part of the query URL, e.g. POST * /ajax?action=library-install&id=H5P.Example * @param translate (needed for 'library-install' and 'library-upload') a * translation function used to localize messages * @param filesFile (needed for 'library-upload') the file uploaded to the * server; this file is part of the HTTP request and has the name 'h5p'. * @param hubId (need for 'get-content') the id of a content object on the * H5P Content Hub * @returns an object which must be sent back in the response as JSON with * HTTP status code 200 * @throws H5pErrors with HTTP status codes, which you must catch and then * send back in the response */ postAjax: (action: "libraries" | "translations" | "files" | "filter" | "library-install" | "library-upload" | "content-hub-metadata-cache" | "get-content" | string, body?: { libraries: string[]; } | { contentId: string; field: string; } | { libraryParameters: string; }, language?: string, user?: IUser, filesFile?: { data?: Buffer; mimetype: string; name: string; size: number; tempFilePath?: string; }, id?: string, translate?: (stringId: string, replacements: { [key: string]: any; }) => string, libraryUploadFile?: { data?: Buffer; mimetype: string; name: string; size: number; tempFilePath?: string; }, hubId?: string) => Promise; }