diff --git a/.env b/.env index 25331ca93..26e468c96 100755 --- a/.env +++ b/.env @@ -33,6 +33,7 @@ BOOL_SEND_EMAILS=false #URLS_STATIC= #URLS_ENGINES= +#URLS_H5P= #BOOL_ADMIN_UPLOADER_ENABLE=true #ASSET_STORAGE_DRIVER=file #ASSET_STORAGE_S3_REGION=us-east-1 @@ -88,3 +89,8 @@ LTI_KEY="materia-production-lti-key" #BOOL_LTI_USE_LAUNCH_ROLES=true #BOOL_LTI_GRACEFUL_CONFIG_FALLBACK=true #BOOL_LTI_LOG_FOR_DEBUGGING=false + +# THIRD PARTY OAUTH =================== + +OAUTH_KEY="materia-third-party-oauth-key" +OAUTH_SECRET="third-party-oauth-secret" \ No newline at end of file diff --git a/docker/config/nginx/nginx-dev.conf b/docker/config/nginx/nginx-dev.conf index cb866e2bd..4096eb91e 100644 --- a/docker/config/nginx/nginx-dev.conf +++ b/docker/config/nginx/nginx-dev.conf @@ -164,4 +164,17 @@ http { } + upstream h5p { + server h5p:3333; + } + + server { + listen *:3000 ssl; + listen [::]:3000 ssl; + + location / { + proxy_pass https://h5p$request_uri; + } + } + } diff --git a/docker/docker-compose.override.yml b/docker/docker-compose.override.yml index a705ea366..c639c9e82 100644 --- a/docker/docker-compose.override.yml +++ b/docker/docker-compose.override.yml @@ -30,6 +30,11 @@ services: fakes3: volumes: - uploaded_media:/s3mnt/fakes3_root/fakes3_uploads/media/ + + h5p: + volumes: + - ./config/nginx/key.pem:/etc/nginx/conf.d/key.pem:ro + - ./config/nginx/cert.pem:/etc/nginx/conf.d/cert.pem:ro volumes: # static_files: {} # compiled js/css and uploaded widgets diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e44f90704..8f65f3f80 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.5' +version: "3.5" services: webserver: @@ -10,6 +10,7 @@ services: - "80:80" # main materia - "443:443" # main materia - "8008:8008" # static files (simulates a different domain sandbox & cdn) + - "3000:3000" # h5p networks: - frontend depends_on: @@ -45,6 +46,7 @@ services: - THEME_PACKAGE=materia-theme-ucf - URLS_ENGINES=https://localhost:8008/widget/ - URLS_STATIC=https://localhost:8008/ + - URLS_H5P=https://localhost:3000 - USER_INSTRUCTOR_PASSWORD=${DEV_ONLY_USER_PASSWORD} - USER_STUDENT_PASSWORD=${DEV_ONLY_USER_PASSWORD} - USER_SYSTEM_PASSWORD=${DEV_ONLY_USER_PASSWORD} @@ -81,9 +83,22 @@ services: - frontend - backend + h5p: + build: + context: ../ + dockerfile: ./h5p-server/materia-h5p.Dockerfile + args: + ENVIRONMENT: dev + ports: + - "3333:3333" # port should match what's being provided in URLS_H5P above + networks: + - frontend + environment: + - MATERIA_WEBSERVER_NETWORK=webserver + - MATERIA_URL=https://localhost:8008 # should be same as static URL above + networks: frontend: name: materia_frontend backend: name: materia_backend - diff --git a/docker/run_first.sh b/docker/run_first.sh index ed52d00c5..3f87acf8f 100755 --- a/docker/run_first.sh +++ b/docker/run_first.sh @@ -44,6 +44,10 @@ source run_build_assets.sh # create a dev user based on your current shell user (password will be 'kogneato') MATERIA_DEV_PASS=whatever can be used to set a custom pw source run_create_me.sh +# run setup for the h5p server +cd ../h5p-server/ +source setup.sh + echo -e "Materia will be hosted on \033[32m$DOCKER_IP\033[0m" echo -e "\033[1mRun an oil comand:\033[0m ./run.sh php oil r widget:show_engines" echo -e "\033[1mRun the web app:\033[0m docker-compose up" diff --git a/fuel/app/classes/controller/media.php b/fuel/app/classes/controller/media.php index ed3506046..f61b5e346 100644 --- a/fuel/app/classes/controller/media.php +++ b/fuel/app/classes/controller/media.php @@ -5,6 +5,7 @@ */ use \Materia\Widget_Asset_Manager; use \Materia\Widget_Asset; +use \Thirdparty\Oauth; class Controller_Media extends Controller { @@ -64,8 +65,11 @@ public function get_import() // This currently assumes a single uploaded file at a time public function action_upload() { - // Validate Logged in - if (\Service_User::verify_session() !== true) throw new HttpNotFoundException; + // Either Validate Logged in + // or validate a third party server thru Oauth + if (\Service_User::verify_session() !== true) + if (Oauth::validate_post() !== true) + throw new HttpNotFoundException; $res = new Response(); // Make sure file is not cached (as it happens for example on iOS devices) diff --git a/fuel/app/classes/thirdparty/oauth.php b/fuel/app/classes/thirdparty/oauth.php new file mode 100644 index 000000000..146b36444 --- /dev/null +++ b/fuel/app/classes/thirdparty/oauth.php @@ -0,0 +1,39 @@ +getMessage(), \Uri::current(), print_r(\Input::post(), 1)], 'lti-error-dump'); + } + + return false; + } +} \ No newline at end of file diff --git a/fuel/app/classes/trait/commoncontrollertemplate.php b/fuel/app/classes/trait/commoncontrollertemplate.php index c32f090a6..7f8e8ec95 100644 --- a/fuel/app/classes/trait/commoncontrollertemplate.php +++ b/fuel/app/classes/trait/commoncontrollertemplate.php @@ -50,6 +50,7 @@ public function inject_common_js_constants() 'BASE_URL' => Uri::base(), 'WIDGET_URL' => Config::get('materia.urls.engines'), 'MEDIA_URL' => Config::get('materia.urls.media'), + 'H5P_URL' => Config::get('materia.urls.h5p'), 'MEDIA_UPLOAD_URL' => Config::get('materia.urls.media_upload'), 'STATIC_CROSSDOMAIN' => Config::get('materia.urls.static'), ]; diff --git a/fuel/app/config/config.php b/fuel/app/config/config.php index a50151d06..6e88b5549 100644 --- a/fuel/app/config/config.php +++ b/fuel/app/config/config.php @@ -333,13 +333,13 @@ ], [ 'id' => 2, - 'package' => 'https://github.com/ucfopen/hangman-materia-widget/releases/latest/download/hangman.wigt', - 'checksum' => 'https://github.com/ucfopen/hangman-materia-widget/releases/latest/download/hangman-build-info.yml', + 'package' => 'https://github.com/ucfopen/guess-the-phrase-materia-widget/releases/latest/download/guess-the-phrase.wigt', + 'checksum' => 'https://github.com/ucfopen/guess-the-phrase-materia-widget/releases/latest/download/guess-the-phrase-build-info.yml', ], [ 'id' => 3, - 'package' => 'https://github.com/ucfopen/matching-materia-widget/releases/latest/download/matching.wigt', - 'checksum' => 'https://github.com/ucfopen/matching-materia-widget/releases/latest/download/matching-build-info.yml', + 'package' => 'https://github.com/ucfopen/guess-the-phrase-materia-widget/releases/latest/download/guess-the-phrase.wigt', + 'checksum' => 'https://github.com/ucfopen/guess-the-phrase-materia-widget/releases/latest/download/guess-the-phrase-build-info.yml', ], [ 'id' => 4, diff --git a/fuel/app/config/materia.php b/fuel/app/config/materia.php index ed7fd531c..9f19700ae 100644 --- a/fuel/app/config/materia.php +++ b/fuel/app/config/materia.php @@ -26,7 +26,8 @@ 'embed' => \Uri::create('embed/'), // game embed urls http://siteurl.com/embed/3434 'preview' => \Uri::create('preview/'), // game preview urls http://siteurl.com/preview/3443 'static' => $_ENV['URLS_STATIC'] ?? \Uri::create(), // allows you to host another domain for static assets http://static.siteurl.com/ - 'engines' => $_ENV['URLS_ENGINES'] ?? \Uri::create('widget/'), // widget file locations + 'engines' => $_ENV['URLS_ENGINES'] ?? \Uri::create('widget/'), // widget file locations, + 'h5p' => $_ENV['URLS_H5P'] ?? null ], diff --git a/h5p-server/.dockerignore b/h5p-server/.dockerignore new file mode 100644 index 000000000..5171c5408 --- /dev/null +++ b/h5p-server/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log \ No newline at end of file diff --git a/h5p-server/.gitignore b/h5p-server/.gitignore new file mode 100644 index 000000000..927d27ca1 --- /dev/null +++ b/h5p-server/.gitignore @@ -0,0 +1,5 @@ +node_modules +h5p/libraries/* +h5p/temporary_storage/* +config/*.pem +.env* \ No newline at end of file diff --git a/h5p-server/Dockerfile b/h5p-server/Dockerfile new file mode 100644 index 000000000..d101699ac --- /dev/null +++ b/h5p-server/Dockerfile @@ -0,0 +1,31 @@ +# This dockerfile is meant to spin up the h5p-server alone + +FROM node:12 + +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN yarn install + +# Bundle app source +COPY . . + +# for local development, don't copy existing h5p info into +# fresh docker container +# RUN rm -r h5p/core h5p/editor h5p/libraries h5p/temporary-storage +# RUN mkdir h5p/core h5p/editor h5p/libraries h5p/temporary-storage + +RUN ./setup.sh + +EXPOSE 3333 + +# connect to the localhost of our machine from inside the docker container +# used to make requests to local materia server +ENV MATERIA_WEBSERVER_NETWORK=host.docker.internal + +CMD ["yarn", "start"] \ No newline at end of file diff --git a/h5p-server/README.md b/h5p-server/README.md new file mode 100644 index 000000000..6f9b8fa3f --- /dev/null +++ b/h5p-server/README.md @@ -0,0 +1,183 @@ +# H5P in Materia + +This is a node server built to serve [H5P](h5p.org) widgets specifically for integration with [Materia](https://github.com/ucfopen/Materia). It is based on LumiEducation’s open source [H5P NodeJS Library](https://github.com/Lumieducation/H5P-Nodejs-library) implementation. + +Although the server is set up to handle any number of h5p widgets, we currently support the following: + +- [Interactive Video](https://h5p.org/interactive-video) +- [Multiple Choice](https://h5p.org/multichoice) +- [Quiz (Question Set)](https://h5p.org/question-set) +- [Advanced Fill in the Blanks](https://h5p.org/advanced-fill-the-blanks) +- [Mark the Words](https://h5p.org/mark-the-words) +- [Drag the Words](https://h5p.org/drag-the-words) + +## Installation + +Note: This project is under active development. If you're making code changes, we advise running it manually with the yarn commands below. Running it with docker will require destroying the h5p containers and images each time you want code changes to take effect. + +### Docker + +``` +docker build --tag h5p-server:1.0 . +docker run --env ENVIRONMENT=mwdk --publish 3000:3000 --name h5p h5p-server:1.0 +``` + +The server is currently configured to run at localhost:3000 in the browser. The publish flag exposes it to your local machine’s port 3000. + +### Manual (No Docker) + +Install the modules required: +`yarn install && ./setup.sh` +Start the server with +`yarn start:{mwdk/dev/prod}` +The server will be running at localhost:3000. If not in prod, view the h5p hub client at localhost:3000/admin. + +### Configure .env + +If you are running this server in the Materia stack, it will pull the .env file from there. However if you are running it alone with the steps above, you will have to create a `.env.local` file at the root directory, and add the following contents: + +``` +OAUTH_KEY="materia-third-party-oauth-key" +OAUTH_SECRET="third-party-oauth-secret" +MATERIA_URL="https://localhost:8008" # whatever your local static materia url is set to +``` + +Where the `key` and `secret` values have to match those on your local materia server. These will allow you to upload any media when creating an H5P widget. + +## Development + +### Selecting a Materia Environment Variable + +This server utilizes postMessages to facilitate communication between H5P and Materia. It is configured to use the specified environment variable to determine which domain to send and receive postMessages. + +| Env Var | Description | +| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `mwdk` | runs with the [Materia Widget Development Kit](https://ucfopen.github.io/Materia-Docs/develop/materia-widget-development-kit.html), a local server spun up quickly for developing individual Materia widgets | +| `dev` | runs with your local instance of [Materia](https://github.com/ucfopen/Materia). An h5p widget must be built with the mwdk and exported to your local instance of Materia in order to use this. | +| `prod` | Used in production, points to your local instance of Materia, unless otherwise specified by the environment variable MATERIA_WEBSERVER_NETWORK. This env variable is used when integrated to the Materia docker stack to point to the Materia webserver directly (see docker-compose.yml in the Materia repo). Additionally, prod sets the env variable NODE_ENV=production which optimizes performance. | + +### Porting New H5P Widgets + +We would suggest using a simple H5P widget such as [Multiple Choice](https://github.com/ucfcdl/h5p-multichoice-materia-widget) as a reference point to port a new one. See the [Materia widget developer guide](https://ucfopen.github.io/Materia-Docs/develop/widget-developer-guide.html) for an in-depth understanding of Materia widgets. + +1. Navigate to the /admin page and install the new library you want to port +2. Make a copy of the h5p multichoice repo as a template for your new widget repo. + 1. Rename all instances of `multichoice` in the repo to an appropriate name for your new widget + 2. In creator.js: the `paramsToQset()` function, remove code that modifies `qset.items` as this is content-specific to multichoice and will cause errors when you try to save an instance of your new widget. + 3. In player.js: update the `/play/1` url to the number created in step 3.b. below. +3. Update the h5p server to be able to handle loading this new widget: + 1. First you have to find the main library name/version for the widget you just installed. There are a couple ways to do so, one is to go into your h5p/libraries directory and look for the corresponding name. (For example if you just installed the Crossword widget, the library would look like `H5P.Crossword 0.4`). + 2. Next, in the h5p/content directory, we have an incrementing list of folders that correspond to h5p widget config files. It’s suggested to copy/paste the multichoice one as a template, increment the directory number, then + 1. rename the values as appropriate (`title`, `mainLibrary`, etc) . + 2. replace the `preloadedDependency` for `H5P.Multichoice` with the values for your main library. (For example for Crossword, replace `machineName` with `H5P.Crossword`, `majorVersion: 0`, `minorVersion: 4`). + 3. In `renderers/editor.js`: find the switch case that sets the selected library in `rendererInit()`. Add a case for your new library. For example in the case of crossword, the new case would look like + ``` + case “h5p-crossword”: + window.selectedLibrary = “H5P.Crossword 0.4”; + break; + ``` +4. At this point you should be able to run `yarn start` in your new repo and see your new widget running at `localhost:8118`. +5. The next step would be to implement scoring for the widget (see [Implementing Scoring](#implementing-scoring) below) +6. You’ll want to create a demo for your widget, which can be used by users that want to try out functionality in Materia. Simply copy a working qset that you’d like to use into `demo.json` +7. Finally, the last step would be to port a compiled version of the widget from your MWDK (which you have been working in) to your local Materia installation. + 1. At the top right corner of your MWDK page, click `Download Package`. This will bring up a prompt where you can either + - Download a .wigt file and manually install it to your local Materia + - Click Install to Docker Materia where the MWDK will attempt to upload the widget automatically. Wait a few seconds to see a success or failure message pop up. + +If you’re porting a widget that uses libraries not currently installed by one of the other widgets, you may need to add them yourself. While Interactive Video covers a large amount of the libraries available, it’s still possible you are porting a widget that requires others. +In development, this can be done by navigating to localhost:3000/admin and clicking `Install` on the widget you are porting. (same as step 1) +However if you want this widget to be supported by default when installing H5P in the future, you will need to update the `setup.sh` script. Use the only list of available library download links we currently know of [here](https://h5p.org/comment/18115#comment-18115) to grab a download link and copy an existing `curl` command to add its required libraries to the ones that get installed by default. + +### Implementing Scoring + +H5P widgets in Materia implement scoring using the data captured from H5P events (see [Integration with Materia](#integration-with-materia) below). Because each H5P widget is created differently, a standard scoring mechanism cannot be guaranteed for every H5P content type. +In your H5P widget’s `player.js`, you can start by printing out the events received to the console to see at which interactions you receive scoring data. In some cases it might be after every interaction, or it could not be until the very end of play. Typically, the best way to monitor for scoring events is to filter for events with the `event.data.result` property present. +Once you find when and where to get your score data, you can choose to utilize + +- Materia’s default scoring module, which is written in php and found in `src/_score`. An example of this can be found in H5P widget repositories such as Interactive Video or Quiz Question Set. +- Write your own custom score module in javascript. An example of custom scoring can be found in quite a few H5P widget repositories such as Drag the Words. + +To use a custom score module with the widget, you will have to add it to the `install.yaml` file config. Under `score:`, add the name of your custom score screen html file like so: + +``` +score: + is_scorable: Yes + score_module: H5PMultiChoice + score_screen: scoreScreen.html +``` + +### H5P Server Customization + +The behavior of h5p can be customized in a few different ways, see Lumi docs on [Customization](https://docs.lumi.education/advanced-usage/customization) for more details. When writing your own customization, remember to append a reference/URL of your script to the options parameter of the H5PEditor object in `express.js`, as defined in the docs. +We have implemented a few scripts that customize behavior in the `custom/` directory: + +| Script | Description | How it Works | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Fullscreen.js` | Forces the h5p editor to be fullscreen within the iframe of the Materia widget, and removes a button that is unnecessary for Materia users. | Overrides h5p/editor/scripts/h5peditor-fullscreen-bar.js to grab a reference to the fullscreen html button and call `.click()` on it while the page is loading. | +| `interactiveVideoUpload.js` | Prevents users from uploading videos specifically for the H5P Interactive Video Widget, since Materia cannot currently support video uploads. | Overrides h5p/editor/scripts/h5peditor-av.js to specifically remove the video file upload html that is inserted in the `C.createAdd()` function | +| `Hooks.js` | This hook is used to prevent users from uploading videos to any other widgets besides Interactive Video, since Materia currently can’t support video uploads. | utilizing [this](https://docs.lumi.education/advanced-usage/customization#changing-the-semantics-of-individual-libraries) provided `alterLibrarySemanticsHook` to intercept libraries as they are being loaded for a widget, removing the `H5P.Video` library if found. | + +Though not the cleanest solution, the most effective way we’ve found to customize behavior is copying the scripts that power the editor (mostly found in h5p/editor/scripts/) to a custom script, and overwriting the parts you need to. + +### Image Uploading to Materia + +By default, the H5P server saves uploaded media to a file on the server in the `h5p/temporary-storage` directory. This implementation is built to be able to be overridden with a custom interface, which is what we have done in order to pass all media uploaded directly to the Materia server. The following resources were referenced to create our custom interface + +- some sparse documentation of an implementation written for S3 upload [here](https://docs.lumi.education/npm-packages/h5p-mongos3/s3-temporary-file-storage) +- The full file for that S3 implementation [here](https://github.com/Lumieducation/H5P-Nodejs-library/blob/master/packages/h5p-mongos3/src/S3TemporaryFileStorage.ts) +- the actual implementation of the class from the original server found [here](https://github.com/Lumieducation/H5P-Nodejs-library/blob/master/packages/h5p-server/src/implementation/fs/DirectoryTemporaryFileStorage.ts) + +You can find this custom built interface in `interfaces/MateriaTempFileStorage.js`. The high-level overview of this media uploading flow is + +1. Create the file in a temporary directory on the server +2. Create a readstream using the temporary file +3. Upload this readstream directly to the Materia server with a POST request, which requires some OAuth steps. +4. If the upload was successful, Materia will send back a 5 digit hash that represents the ID of the file on the Materia server. We rename the temporary file with this 5 digit hash so that it matches up, and can be inserted into the qset when the user saves the widget they’re editing. + +There’s also a cron job that can be found in `express.js` that is used to clean out this temporary directory daily at 3:00am. + +## Setup and Docker Install Overview + +The setup script + +- Downloads the [H5P Core](https://github.com/h5p/h5p-php-library/archive/1.24.0.zip) and [H5P Editor](https://github.com/h5p/h5p-editor-php-library/archive/1.24.0.zip) files and unzips them into their requisite directories: h5p/core and h5p/editor respectively. +- It then downloads the library files needed for the h5p widgets we currently offer and unzips them into h5p/libraries. This is done for two reasons: + - The install is automated and they do not have to be manually installed from the h5p hub client. + - In prod, we do not have a reason to expose the `/admin` page, so no auth needs to be built into the server. + +The Dockerfile that builds the server is incredibly simple, it + +1. Copies the package.json to the container and installs the necessary dependencies +2. Copies the application source code to the container +3. Optionally can remove h5p/ directories in the container to ensure a fresh install of the server for development purposes +4. Runs the setup script +5. Exposes the port to your local machine +6. Starts the server + +### Integrating into the Materia Stack + +When integrated into the Materia Stack, this server sits in its own directory called `h5p-server/`. It is spun up with the rest of the Materia stack in Materia’s docker-compose file. There, a few configs need to be specified in order for the integration to be successful: + +- An `arg` is added to the build config to specify what [Materia environment variable](#selecting-a-materia-environment-variable) we want to use. Don’t forget to update this to `prod` once you are ready to deploy to production alongside Materia! +- The correct ports are exposed between your local machine and the container (3000 for both, by default) +- The `frontend` network is specified, so that this container is capable of communicating with Materia’s `webserver` container, which is on the same network. +- The environment variable `MATERIA_WEBSERVER_NETWORK` is set to the name of Materia’s server container (`webserver`) so that h5p can send the upload requests to the corresponding URL + +## Architecture and Integration + +### H5P Server + +Lumi provides a short overview of the architecture for their template server [here](https://docs.lumi.education/usage/architecture). There they provide the following visual diagram: +![H5P Node Server Architevture](docs/H5P_node_architecture.svg) +This application implements our own `Express Adapter` and `Temporary File Storage`, as well as customizations on various pieces described above, such as the `editor client` and `LibraryManager`. The `player client` is not shown but works similarly to the editor, just more simply with fewer endpoints. + +### Integration with Materia + +![Materia H5P Integration Architecture Diagram](docs/Materia_H5P_Architecture_Diagram.png) +The above figure demonstrates the relationship between the Materia server, iFrame, and the H5P Server. H5P only talks to Materia to upload images that users upload in the creator. +The iFrames send postMessages back and forth to communicate events with each other: +| H5P iFrame to Materia Parent iFrame | Materia iFrame to H5P child iFrame | +| ----------------------------------- | ---------------------------------- | +| `ready_for_qset`: when editing or playing an existing widget, H5P requests the qset from Materia | `params_send`: response to `ready_for_qset`. When editing or playing an existing widget, Materia provides H5P the qset to load. | +| `save`: response to `widget_save`. Sends the library and h5p widget parameters to Materia to store in the qset when saving a widget | `widget_save`: Tells H5P that the widget is ready to be created. H5P will respond with `save` postMessage along with params to be encoded in the qset. | +| H5P xAPI Events: when H5P dispatches its own events from the user interacting with a widget, we capture them and send them to the Materia iFrame in case they are useful for scoring, etc. See H5P xAPI Docs (https://h5p.org/documentation/x-api) for light documentation. | diff --git a/h5p-server/config/h5p-config.json b/h5p-server/config/h5p-config.json new file mode 100644 index 000000000..d0ffce76e --- /dev/null +++ b/h5p-server/config/h5p-config.json @@ -0,0 +1,12 @@ +{ + "fetchingDisabled": 0, + "uuid": "8de62c47-f335-42f6-909d-2d8f4b7fb7f5", + "siteType": "local", + "sendUsageStatistics": false, + "hubRegistrationEndpoint": "https://api.h5p.org/v1/sites", + "hubContentTypesEndpoint": "https://api.h5p.org/v1/content-types/", + "contentTypeCacheRefreshInterval": 86400000, + "enableLrsContentTypes": true, + "ajaxUrl": "/ajax?action=", + "baseUrl": "/h5p" +} diff --git a/h5p-server/custom/fullscreen.js b/h5p-server/custom/fullscreen.js new file mode 100644 index 000000000..649ce844f --- /dev/null +++ b/h5p-server/custom/fullscreen.js @@ -0,0 +1,111 @@ +H5PEditor.FullscreenBar = (function($) { + function FullscreenBar($mainForm, library) { + const title = H5PEditor.libraryCache[library] + ? H5PEditor.libraryCache[library].title + : library; + const iconId = library + .split(" ")[0] + .split(".")[1] + .toLowerCase(); + + let isInFullscreen = false; + let exitSemiFullscreen; + + $mainForm.addClass("h5peditor-form-manager"); + + // Add fullscreen bar + const $bar = ns.$("
", { + class: "h5peditor-form-manager-head" + }); + + const $breadcrumb = ns.$("
", { + class: "h5peditor-form-manager-breadcrumb", + appendTo: $bar + }); + + const $title = ns.$("
", { + class: "h5peditor-form-manager-title " + iconId, + text: title, + appendTo: $breadcrumb + }); + + const fullscreenButton = createButton( + "fullscreen", + "", + function() { + if (isInFullscreen) { + // Trigger semi-fullscreen exit + exitSemiFullscreen(); + } else { + // Trigger semi-fullscreen enter + exitSemiFullscreen = H5PEditor.semiFullscreen( + $mainForm, + function() { + fullscreenButton.setAttribute( + "aria-label", + H5PEditor.t("core", "exitFullscreenButtonLabel") + ); + isInFullscreen = true; + }, + function() { + fullscreenButton.setAttribute( + "aria-label", + H5PEditor.t("core", "enterFullscreenButtonLabel") + ); + isInFullscreen = false; + } + ); + } + }, + H5PEditor.t("core", "enterFullscreenButtonLabel") + ); + + // Create 'Proceed to save' button + // const proceedButton = createButton( + // "proceed", + // H5PEditor.t("core", "proceedButtonLabel"), + // function() { + // exitSemiFullscreen(); + // } + // ); + + // $bar.append(proceedButton); + $bar.append(fullscreenButton); + $mainForm.prepend($bar); + fullscreenButton.click(); + } + + /** + * Helper for creating buttons. + * + * @private + * @param {string} id + * @param {string} text + * @param {function} clickHandler + * @param {string} ariaLabel + * @return {Element} + */ + const createButton = function(id, text, clickHandler, ariaLabel) { + if (ariaLabel === undefined) { + ariaLabel = text; + } + + const button = document.createElement("button"); + button.setAttribute("type", "button"); + button.classList.add("h5peditor-form-manager-button"); + button.classList.add("h5peditor-form-manager-" + id); + button.setAttribute("aria-label", ariaLabel); + button.addEventListener("click", clickHandler); + + // Create special inner filler to avoid focus from pointer devices. + const content = document.createElement("span"); + content.classList.add("h5peditor-form-manager-button-inner"); + content.innerText = text; + content.tabIndex = -1; + button.appendChild(content); + + return button; + }; + + return FullscreenBar; +})(ns.jQuery); diff --git a/h5p-server/custom/hooks.js b/h5p-server/custom/hooks.js new file mode 100644 index 000000000..a776852ab --- /dev/null +++ b/h5p-server/custom/hooks.js @@ -0,0 +1,33 @@ +module.exports = { + /** + * This hook is called when the editor retrieves the semantics of a + * library. + * Note: This function should be immutable, so it shouldn't change the + * semantics parameter but return a clone! + * @param library the library that is currently being loaded + * @param semantics the original semantic structure + * @returns the changed semantic structure + */ + alterLibrarySemanticsHook: (library, semantics) => { + // Updating the Library.fields.options array to remove + // "H5P.Video" from the allowed options, so users cannot upload + // videos to any h5p widgets, as materia cannot support video files + if (semantics[0].hasOwnProperty("fields")) { + if (semantics[0].fields[0].hasOwnProperty("options")) { + semantics[0].fields[0].options = semantics[0].fields[0].options.filter( + library => { + // messy but have to check if it is a string array + // because of course all h5p widgets have to be different + if (typeof library == "string") { + return !library.startsWith("H5P.Video"); + } + // if not a string array then just return `true` to the filter + // to not remove anything + return true; + } + ); + } + } + return semantics; + } +}; diff --git a/h5p-server/custom/interactiveVideoUpload.js b/h5p-server/custom/interactiveVideoUpload.js new file mode 100644 index 000000000..8c6f08fe5 --- /dev/null +++ b/h5p-server/custom/interactiveVideoUpload.js @@ -0,0 +1,842 @@ +/* +this custom script overrides h5p/editor/scripts/h5peditor-av.js to specifically +remove the video file upload html that is inserted in the C.createAdd() function +*/ + +/* global ns */ +/** + * Audio/Video module. + * Makes it possible to add audio or video through file uploads and urls. + * + */ +H5PEditor.widgets.video = H5PEditor.widgets.audio = H5PEditor.AV = (function( + $ +) { + /** + * Constructor. + * + * @param {mixed} parent + * @param {object} field + * @param {mixed} params + * @param {function} setValue + * @returns {_L3.C} + */ + function C(parent, field, params, setValue) { + var self = this; + + // Initialize inheritance + H5PEditor.FileUploader.call(self, field); + + this.parent = parent; + this.field = field; + this.params = params; + this.setValue = setValue; + this.changes = []; + + if (params !== undefined && params[0] !== undefined) { + this.setCopyright(params[0].copyright); + } + + // When uploading starts + self.on("upload", function() { + // Insert throbber + self.$uploading = $( + '
' + + H5PEditor.t("core", "uploading") + + "
" + ).insertAfter(self.$add.hide()); + + // Clear old error messages + self.$errors.html(""); + + // Close dialog + self.closeDialog(); + }); + + // Monitor upload progress + self.on("uploadProgress", function(e) { + self.$uploading.html( + H5PEditor.t("core", "uploading") + " " + Math.round(e.data * 100) + " %" + ); + }); + + // Handle upload complete + self.on("uploadComplete", function(event) { + var result = event.data; + + // Clear out add dialog + this.$addDialog.find(".h5p-file-url").val(""); + + try { + if (result.error) { + throw result.error; + } + + // Set params if none is set + if (self.params === undefined) { + self.params = []; + self.setValue(self.field, self.params); + } + + // Add a new file/source + var file = { + path: result.data.path, + mime: result.data.mime, + copyright: self.copyright + }; + var index = + self.updateIndex !== undefined + ? self.updateIndex + : self.params.length; + self.params[index] = file; + self.addFile(index); + + // Trigger change callbacks (old event system) + for (var i = 0; i < self.changes.length; i++) { + self.changes[i](file); + } + } catch (error) { + // Display errors + self.$errors.append(H5PEditor.createError(error)); + } + + if (self.$uploading !== undefined && self.$uploading.length !== 0) { + // Hide throbber and show add button + self.$uploading.remove(); + self.$add.show(); + } + }); + } + + C.prototype = Object.create(ns.FileUploader.prototype); + C.prototype.constructor = C; + + /** + * Append widget to given wrapper. + * + * @param {jQuery} $wrapper + */ + C.prototype.appendTo = function($wrapper) { + var self = this; + const id = ns.getNextFieldId(this.field); + + var imageHtml = + '
    ' + + (self.field.widgetExtensions + ? C.createTabbedAdd( + self.field.type, + self.field.widgetExtensions, + id, + self.field.description !== undefined + ) + : C.createAdd( + self.field.type, + id, + self.field.description !== undefined + )); + + if (!this.field.disableCopyright) { + imageHtml += + '' + + H5PEditor.t("core", "editCopyright") + + ""; + } + + imageHtml += + '
    ' + + '' + + "
    "; + + var html = H5PEditor.createFieldMarkup(this.field, imageHtml, id); + + var $container = $(html).appendTo($wrapper); + this.$files = $container.children(".file"); + this.$add = $container.children(".h5p-add-file").click(function() { + self.$addDialog.addClass("h5p-open"); + }); + + // Tabs that are hard-coded into this widget. Any other tab must be an extension. + const TABS = { + UPLOAD: 0, + INPUT: 1 + }; + + // The current active tab + let activeTab = TABS.UPLOAD; + + /** + * @param {number} tab + * @return {boolean} + */ + const isExtension = function(tab) { + return tab > TABS.INPUT; // Always last tab + }; + + /** + * Toggle the currently active tab. + */ + const toggleTab = function() { + // Pause the last active tab + if (isExtension(activeTab)) { + tabInstances[activeTab].pause(); + } + + // Update tab + this.parentElement + .querySelector(".selected") + .classList.remove("selected"); + this.classList.add("selected"); + + // Update tab panel + const el = document.getElementById(this.getAttribute("aria-controls")); + el.parentElement + .querySelector(".av-tabpanel:not([hidden])") + .setAttribute("hidden", ""); + el.removeAttribute("hidden"); + + // Set active tab index + for (let i = 0; i < el.parentElement.children.length; i++) { + if (el.parentElement.children[i] === el) { + activeTab = i - 1; // Compensate for .av-tablist in the same wrapper + break; + } + } + + // Toggle insert button disabled + if (activeTab === TABS.UPLOAD) { + self.$insertButton[0].disabled = true; + } else if (activeTab === TABS.INPUT) { + self.$insertButton[0].disabled = false; + } else { + self.$insertButton[0].disabled = !tabInstances[activeTab].hasMedia(); + } + }; + + /** + * Switch focus between the buttons in the tablist + */ + const moveFocus = function(el) { + if (el) { + this.setAttribute("tabindex", "-1"); + el.setAttribute("tabindex", "0"); + el.focus(); + } + }; + + // Register event listeners to tab DOM elements + $container + .find(".av-tab") + .click(toggleTab) + .keydown(function(e) { + if (e.which === 13 || e.which === 32) { + // Enter or Space + toggleTab.call(this, e); + e.preventDefault(); + } else if (e.which === 37 || e.which === 38) { + // Left or Up + moveFocus.call(this, this.previousSibling); + e.preventDefault(); + } else if (e.which === 39 || e.which === 40) { + // Right or Down + moveFocus.call(this, this.nextSibling); + e.preventDefault(); + } + }); + + this.$addDialog = this.$add + .next() + .children() + .first(); + + // Prepare to add the extra tab instances + const tabInstances = [null, null]; // Add nulls for hard-coded tabs + self.tabInstances = tabInstances; + + if (self.field.widgetExtensions) { + /** + * @param {string} type Constructor name scoped inside this widget + * @param {number} index + */ + const createTabInstance = function(type, index) { + const tabInstance = new H5PEditor.AV[type](); + tabInstance.appendTo( + self.$addDialog[0].children[0].children[index + 1] + ); // Compensate for .av-tablist in the same wrapper + tabInstance.on("hasMedia", function(e) { + if (index === activeTab) { + self.$insertButton[0].disabled = !e.data; + } + }); + tabInstances.push(tabInstance); + }; + + // Append extra tabs + for (let i = 0; i < self.field.widgetExtensions.length; i++) { + if (H5PEditor.AV[self.field.widgetExtensions[i]]) { + createTabInstance(self.field.widgetExtensions[i], i + 2); // Compensate for the number of hard-coded tabs + } + } + } + + var $url = (this.$url = this.$addDialog.find(".h5p-file-url")); + this.$addDialog.find(".h5p-cancel").click(function() { + self.updateIndex = undefined; + self.closeDialog(); + }); + + this.$addDialog + .find(".h5p-file-drop-upload") + .addClass("has-advanced-upload") + .on("drag dragstart dragend dragover dragenter dragleave drop", function( + e + ) { + e.preventDefault(); + e.stopPropagation(); + }) + .on("dragover dragenter", function(e) { + $(this).addClass("over"); + e.originalEvent.dataTransfer.dropEffect = "copy"; + }) + .on("dragleave", function() { + $(this).removeClass("over"); + }) + .on("drop", function(e) { + self.uploadFiles(e.originalEvent.dataTransfer.files); + }) + .click(function() { + self.openFileSelector(); + }); + + this.$insertButton = this.$addDialog.find(".h5p-insert").click(function() { + if (isExtension(activeTab)) { + const media = tabInstances[activeTab].getMedia(); + if (media) { + self.upload(media.data, media.name); + } + } else { + const url = $url.val().trim(); + if (url) { + self.useUrl(url); + } + } + + self.closeDialog(); + }); + + this.$errors = $container.children(".h5p-errors"); + + if (this.params !== undefined) { + for (var i = 0; i < this.params.length; i++) { + this.addFile(i); + } + } else { + $container.find(".h5p-copyright-button").addClass("hidden"); + } + + var $dialog = $container.find(".h5p-editor-dialog"); + $container + .find(".h5p-copyright-button") + .add($dialog.find(".h5p-close")) + .click(function() { + $dialog.toggleClass("h5p-open"); + return false; + }); + + ns.File.addCopyright(self, $dialog, function(field, value) { + self.setCopyright(value); + }); + }; + + /** + * Add file icon with actions. + * + * @param {Number} index + */ + C.prototype.addFile = function(index) { + var that = this; + var fileHtml; + var file = this.params[index]; + var rowInputId = "h5p-av-" + C.getNextId(); + var defaultQualityName = H5PEditor.t("core", "videoQualityDefaultLabel", { + ":index": index + 1 + }); + var qualityName = + file.metadata && file.metadata.qualityName + ? file.metadata.qualityName + : defaultQualityName; + + // Check if source is YouTube + var youtubeRegex = C.providers.filter(function(provider) { + return provider.name === "YouTube"; + })[0].regexp; + var isYoutube = file.path && file.path.match(youtubeRegex); + + // Only allow single source if YouTube + if (isYoutube) { + // Remove all other files except this one + that.$files.children().each(function(i) { + if (i !== that.updateIndex) { + that.removeFileWithElement($(this)); + } + }); + // Remove old element if updating + that.$files.children().each(function() { + $(this).remove(); + }); + // This is now the first and only file + index = 0; + } + this.$add.toggleClass("hidden", !!isYoutube); + + // If updating remove and recreate element + if (that.updateIndex !== undefined) { + var $oldFile = this.$files.children(":eq(" + index + ")"); + $oldFile.remove(); + this.updateIndex = undefined; + } + + // Create file with customizable quality if enabled and not youtube + if (this.field.enableCustomQualityLabel === true && !isYoutube) { + fileHtml = + '
  • ' + + '
    ' + + '
    ' + + file.mime.split("/")[1] + + "
    " + + '
    ' + + "
    " + + "
    " + + '
    ' + + '
    ' + + H5PEditor.t("core", "videoQuality") + + "
    " + + '" + + '' + + "
    " + + "
  • "; + } else { + fileHtml = + '
  • ' + + '
    ' + + '
    ' + + file.mime.split("/")[1] + + "
    " + + '
    ' + + "
    " + + "
  • "; + } + + // Insert file element in appropriate order + var $file = $(fileHtml); + if (index >= that.$files.children().length) { + $file.appendTo(that.$files); + } else { + $file.insertBefore(that.$files.children().eq(index)); + } + + this.$add + .parent() + .find(".h5p-copyright-button") + .removeClass("hidden"); + + // Handle thumbnail click + $file.children(".h5p-thumbnail").click(function() { + if (!that.$add.is(":visible")) { + return; // Do not allow editing of file while uploading + } + that.$addDialog + .addClass("h5p-open") + .find(".h5p-file-url") + .val(that.params[index].path); + that.updateIndex = index; + }); + + // Handle remove button click + $file.find(".h5p-remove").click(function() { + if (that.$add.is(":visible")) { + confirmRemovalDialog.show($file.offset().top); + } + + return false; + }); + + // on input update + $file.find("input").change(function() { + file.metadata = { qualityName: $(this).val() }; + }); + + // Create remove file dialog + var confirmRemovalDialog = new H5P.ConfirmationDialog({ + headerText: H5PEditor.t("core", "removeFile"), + dialogText: H5PEditor.t("core", "confirmRemoval", { ":type": "file" }) + }).appendTo(document.body); + + // Remove file on confirmation + confirmRemovalDialog.on("confirmed", function() { + that.removeFileWithElement($file); + if (that.$files.children().length === 0) { + that.$add + .parent() + .find(".h5p-copyright-button") + .addClass("hidden"); + } + }); + }; + + /** + * Remove file at index + * + * @param {number} $file File element + */ + C.prototype.removeFileWithElement = function($file) { + var index = $file.index(); + + // Remove from params. + if (this.params.length === 1) { + delete this.params; + this.setValue(this.field); + } else { + this.params.splice(index, 1); + } + + $file.remove(); + this.$add.removeClass("hidden"); + + // Notify change listeners + for (var i = 0; i < this.changes.length; i++) { + this.changes[i](); + } + }; + + C.prototype.useUrl = function(url) { + if (this.params === undefined) { + this.params = []; + this.setValue(this.field, this.params); + } + + var mime; + var aspectRatio; + var i; + var matches = url.match(/\.(webm|mp4|ogv|m4a|mp3|ogg|oga|wav)/i); + if (matches !== null) { + mime = matches[matches.length - 1]; + } else { + // Try to find a provider + for (i = 0; i < C.providers.length; i++) { + if (C.providers[i].regexp.test(url)) { + mime = C.providers[i].name; + aspectRatio = C.providers[i].aspectRatio; + break; + } + } + } + + var file = { + path: url, + mime: this.field.type + "/" + (mime ? mime : "unknown"), + copyright: this.copyright, + aspectRatio: aspectRatio ? aspectRatio : undefined + }; + var index = + this.updateIndex !== undefined ? this.updateIndex : this.params.length; + this.params[index] = file; + this.addFile(index); + + for (i = 0; i < this.changes.length; i++) { + this.changes[i](file); + } + }; + + /** + * Validate the field/widget. + * + * @returns {Boolean} + */ + C.prototype.validate = function() { + return true; + }; + + /** + * Remove this field/widget. + */ + C.prototype.remove = function() { + this.$errors.parent().remove(); + }; + + /** + * Sync copyright between all video files. + * + * @returns {undefined} + */ + C.prototype.setCopyright = function(value) { + this.copyright = value; + if (this.params !== undefined) { + for (var i = 0; i < this.params.length; i++) { + this.params[i].copyright = value; + } + } + }; + + /** + * Collect functions to execute once the tree is complete. + * + * @param {function} ready + * @returns {undefined} + */ + C.prototype.ready = function(ready) { + if (this.passReadies) { + this.parent.ready(ready); + } else { + ready(); + } + }; + + /** + * Close the add media dialog + */ + C.prototype.closeDialog = function() { + this.$addDialog.removeClass("h5p-open"); + + // Reset URL input + this.$url.val(""); + + // Reset all of the tabs + for (let i = 0; i < this.tabInstances.length; i++) { + if (this.tabInstances[i]) { + this.tabInstances[i].reset(); + } + } + }; + + /** + * Create the HTML for the dialog itself. + * + * @param {string} content HTML + * @param {boolean} disableInsert + * @param {string} id + * @param {boolean} hasDescription + * @returns {string} HTML + */ + C.createInsertDialog = function(content, disableInsert, id, hasDescription) { + return ( + '
    ' + + '
    ' + + '
    ' + + content + + "
    " + + '
    ' + + '" + + '" + + "
    " + + "
    " + ); + }; + + /** + * Creates the HTML needed for the given tab. + * + * @param {string} tab Tab Identifier + * @param {string} type 'video' or 'audio' + * @returns {string} HTML + */ + C.createTabContent = function(tab, type) { + switch (tab) { + case "BasicFileUpload": + const id = "av-upload-" + C.getNextId(); + return ( + '

    ' + + H5PEditor.t( + "core", + type === "audio" ? "uploadAudioTitle" : "uploadVideoTitle" + ) + + "

    " + + '
    ' + + '
    ' + + "
    " + ); + + case "InputLinkURL": + return ( + "

    " + + H5PEditor.t( + "core", + type === "audio" ? "enterAudioTitle" : "enterVideoTitle" + ) + + "

    " + + '
    ' + + '' + + "
    " + + (type === "audio" + ? "" + : '
    ' + + H5PEditor.t("core", "addVideoDescription") + + "
    ") + ); + + default: + return ""; + } + }; + + /** + * Creates the HTML for the tabbed insert media dialog. Only used when there + * are extra tabs. + * + * @param {string} type 'video' or 'audio' + * @param {Array} extraTabs + * @returns {string} HTML + */ + C.createTabbedAdd = function(type, extraTabs, id, hasDescription) { + let i; + + const tabs = ["BasicFileUpload", "InputLinkURL"]; + for (i = 0; i < extraTabs.length; i++) { + tabs.push(extraTabs[i]); + } + + let tabsHTML = ""; + let tabpanelsHTML = ""; + + for (i = 0; i < tabs.length; i++) { + const tab = tabs[i]; + const tabId = C.getNextId(); + const tabindex = i === 0 ? 0 : -1; + const selected = i === 0 ? "true" : "false"; + const title = + i > 1 + ? H5PEditor.t("H5PEditor." + tab, "title") + : H5PEditor.t("core", "tabTitle" + tab); + + tabsHTML += + '"; + tabpanelsHTML += + '"; + } + + return C.createInsertDialog( + '
    ' + + tabsHTML + + "
    " + + tabpanelsHTML, + true, + id, + hasDescription + ); + }; + + /** + * Creates the HTML for the basic 'Upload or URL' dialog. + * + * @param {string} type 'video' or 'audio' + * @param {string} id + * @param {boolean} hasDescription + * @returns {string} HTML + */ + C.createAdd = function(type, id, hasDescription) { + return C.createInsertDialog( + // '
    ' + + // C.createTabContent('BasicFileUpload', type) + + // '
    ' + + // '
    ' + + // '
    ' + + // '
    ' + + // '
    ' + H5PEditor.t('core', 'or') + '
    ' + + // '
    ' + + // '
    ' + + '
    ' + + C.createTabContent("InputLinkURL", type) + + "
    ", + false, + id, + hasDescription + ); + }; + + /** + * Providers incase mime type is unknown. + * @public + */ + C.providers = [ + { + name: "YouTube", + regexp: /(?:https?:\/\/)?(?:www\.)?(?:(?:youtube.com\/(?:attribution_link\?(?:\S+))?(?:v\/|embed\/|watch\/|(?:user\/(?:\S+)\/)?watch(?:\S+)v\=))|(?:youtu.be\/|y2u.be\/))([A-Za-z0-9_-]{11})/i, + aspectRatio: "16:9" + } + ]; + + // Avoid ID attribute collisions + let idCounter = 0; + + /** + * Grab the next available ID to avoid collisions on the page. + * @public + */ + C.getNextId = function() { + return idCounter++; + }; + + return C; +})(H5P.jQuery); diff --git a/h5p-server/docs/H5P_node_architecture.svg b/h5p-server/docs/H5P_node_architecture.svg new file mode 100644 index 000000000..37f88cdb5 --- /dev/null +++ b/h5p-server/docs/H5P_node_architecture.svg @@ -0,0 +1,3 @@ + + +
    @lumieducation/h5p-express
    @lumieducation/h5p-express
    Express Adapter
    Express Adapter
    H5P standard editor client
    (downloaded from Joubel's 
    PHP implementation)
    H5P standard editor client...
    your server implementation
    your server implementation
    @lumieducation/h5p-server
    @lumieducation/h5p-server
    Library Storage
    (e.g. out-of-the box FileLibraryStorage or class from h5p-mongos3)
    Library Storage...
    Content Storage
    (e.g. out-of-the-box FileContentStorage or class from h5p-mongos3)
    Content Storage...
    Temporary File Storage
    (e.g. out-of-the-box DirectoryTemporaryFileStorage or class from h5p-mongos3)
    Temporary File Stora...
    initialize with configuration
    values
    initialize with configuration...
    Configuration Management
    (e.g. out-of-the-box H5PConfig)
    Configuration Manage...
    User and Permission Management
    User and Permission...
    Cache
    (e.g. out-of-the-box InMemoryStorage or a cache using cache-manager)
    Cache...
    HTTP endpoints
    HTTP endpoints
    your web client
    your web client
    User
    User
    upload image
    upload image
    H5PEditor
    H5PEditor
    saveContentFile
    saveContentFile
    get user objects and permission
    for calls of H5PEditor
    get user objects and permission...
    ContentManager
    ContentManager
    saveH5P
    saveH5P
    get parameters
    get parameters
    save content
    save content
    upload h5p package
    upload h5p package
    uploadPackage
    uploadPackage
    LibraryManager
    LibraryManager
    select library to create new content
    select library to cr...
    getLibraryData
    getLibraryData
    display content
    display content
    GET endpoints
    GET endpoints
    render
    render
    POST endpoints
    POST endpoints
    ...
    ...
    ...
    ...
    ...
    ...
    ... many  classes ...
    ... ma...
    ...
    ...
    POST AJAX endpoint
    POST AJAX endpoint
    GET AJAX endpoint
    GET AJAX endpoint
    ...
    ...
    Viewer does not support full SVG 1.1
    \ No newline at end of file diff --git a/h5p-server/docs/Materia_H5P_Architecture_Diagram.png b/h5p-server/docs/Materia_H5P_Architecture_Diagram.png new file mode 100644 index 000000000..04cd7c59a Binary files /dev/null and b/h5p-server/docs/Materia_H5P_Architecture_Diagram.png differ diff --git a/h5p-server/express.js b/h5p-server/express.js new file mode 100644 index 000000000..7f8f4db3e --- /dev/null +++ b/h5p-server/express.js @@ -0,0 +1,295 @@ +const express = require("express"); +const https = require("https"); +const fs = require("fs"); +const path = require("path"); +const bodyParser = require("body-parser"); +const upload = require("express-fileupload"); +const cors = require("cors"); +const H5PServer = require("@lumieducation/h5p-server"); +const { h5pAjaxExpressRouter } = require("@lumieducation/h5p-express"); +const dotenv = require("dotenv"); +const cron = require("node-cron"); +const morgan = require("morgan"); + +const { editorRenderer } = require("./renderers/editor"); +const { playerRenderer } = require("./renderers/player"); +const { adminRenderer } = require("./renderers/admin"); +const hooks = require("./custom/hooks"); +import MateriaTempFileStorage from "./interfaces/MateriaTempFileStorage.js"; + +const app = express(); + +const port = 3333; + +// if in development use '.env.local' file, else use '.env' file +const envFile = process.env.ENVIRONMENT == "prod" ? `.env` : ".env.local"; +dotenv.config({ path: path.resolve(envFile) }); + +function User() { + this.id = "1"; + this.name = "Firstname Surname"; + this.canInstallRecommended = true; + this.canUpdateAndInstallLibraries = true; + this.canCreateRestricted = true; + this.type = "local"; + this.email = "test@example.com"; +} + +let h5pEditor = {}; +let h5pPlayer = {}; + +const setupConfig = (resolve, reject) => { + new H5PServer.H5PConfig( + new H5PServer.fsImplementations.JsonStorage( + path.resolve("config/h5p-config.json") + ) + ) + .load() + .then(config => { + resolve(config); + }); +}; + +const setupPlayerAndEditor = config => { + // using cached library storage to cache most common calls to the libraries, improving performance + // also using custom tempfilestorage to automate uploading media to materia from h5p server + const editor = new H5PServer.H5PEditor( + new H5PServer.fsImplementations.InMemoryStorage(), + config, + // new H5PServer.fsImplementations.FileLibraryStorage( + // path.resolve("h5p/libraries") + // ), + new H5PServer.cacheImplementations.CachedLibraryStorage( + new H5PServer.fsImplementations.FileLibraryStorage( + path.resolve("h5p/libraries") + ) + ), + new H5PServer.fsImplementations.FileContentStorage( + path.resolve("h5p/content") + ), + // new H5PServer.fsImplementations.DirectoryTemporaryFileStorage( + // path.resolve("h5p/temporary-storage") + // ) + new MateriaTempFileStorage(), + undefined, + undefined, + { + customization: { + global: { + scripts: [ + "/custom/fullscreen.js", + "/custom/interactiveVideoUpload.js" + ] + }, + alterLibrarySemantics: hooks.alterLibrarySemanticsHook + } + } + ); + + const player = new H5PServer.H5PPlayer( + editor.libraryStorage, + editor.contentStorage, + config + ); + + return [editor, player]; +}; + +const setupServer = ([editor, player]) => { + h5pEditor = editor; + h5pPlayer = player; + + const setupServerPromise = (resolve, reject) => { + // TODO replace whitelist with hosts from context env var + + const whitelist = + process.env.ENVIRONMENT == "prod" || process.env.ENVIRONMENT == "dev" + ? [process.env.MATERIA_URL] + : ["http://localhost:8118"]; + + app.use( + cors({ + origin: whitelist + }) + ); + + app.use(morgan("combined")); + + app.use(bodyParser.json({ limit: "500mb" })); + app.use( + bodyParser.urlencoded({ + extended: true + }) + ); + + app.use( + upload({ + limits: { fileSize: h5pEditor.config.maxFileSize } + }) + ); + + // inject user data into request + // TODO don't just make an arbitrary user object! + app.use((req, res, next) => { + req.user = new User(); + next(); + }); + + // load custom styles and js + app.use("/styles", express.static("styles")); + app.use("/custom", express.static("custom")); + + // RENDERER OVERRIDES + // ASSUMING DIRECT CONTROL + h5pEditor.setRenderer(editorRenderer); + h5pPlayer.setRenderer(playerRenderer); + + app.use( + h5pEditor.config.baseUrl, + h5pAjaxExpressRouter( + h5pEditor, + path.resolve("h5p/core"), // the path on the local disc where the files of the JavaScript client of the player are stored + path.resolve("h5p/editor") // the path on the local disc where the files of the JavaScript client of the editor are stored + // undefined, + // "auto" // You can change the language of the editor here by setting + // the language code you need here. 'auto' means the route will try + // to use the language detected by the i18next language detector. + ) + ); + + // cron job runs once daily at 3:00am to delete temporary files that have expired. + // expired time being 24 hours after initial upload + cron.schedule("0 3 * * *", () => { + h5pEditor.temporaryFileManager.cleanUp(); + }); + + resolve(); + }; + return new Promise(setupServerPromise); +}; + +new Promise(setupConfig) + .then(setupPlayerAndEditor) + .then(setupServer) + .then(page => { + app.get("/status", (req, res) => { + res.status(200).end(); + }); + + // only expose /admin route if in development + if (process.env.ENVIRONMENT != "prod") { + app.get("/admin", (req, res) => { + h5pEditor.setRenderer(adminRenderer); + h5pEditor + .render(undefined, "en", req.user) + .then(page => { + res.send(page); + res.status(200).end(); + }) + .catch(error => { + console.error("Error GET /admin:"); + console.error(error); + res.status(500).end(); + }); + }); + } + + // create new h5p content of a given type + app.get("/new/:type", (req, res) => { + h5pEditor.setRenderer(editorRenderer); + + h5pEditor + .render(undefined, "en", req.user) + .then(page => { + res.send(page); + res.status(200).end(); + }) + .catch(error => { + console.error("Error GET /new/:type :"); + console.error(error); + res.status(500).end(); + }); + }); + + app.get("/edit/:type", (req, res) => { + h5pEditor.setRenderer(editorRenderer); + + h5pEditor + .render(undefined, "en", req.user) + .then(page => { + res.send(page); + res.status(200).end(); + }) + .catch(error => { + console.error("Error GET /edit/:type :"); + console.error(error); + res.status(500).end(); + }); + }); + + app.get(`${h5pEditor.config.playUrl}/:type`, (req, res) => { + // TODO provide h5pPlayer with content id depending on h5P | materia toggle? Is that needed? + // otherwise, we're looking for req.params.contentId + if (req.params.type == "undefined") { + // TODO: probly need to display a 404 for this + console.error("Error GET /play/:type : type undefined"); + res.status(404).end(); + } + + // type directs to render configs located at h5p/content/:type + h5pPlayer + .render(req.params.type, "en", req.user) + .then(h5pPage => { + res.send(h5pPage); + res.status(200).end(); + }) + .catch(error => { + console.error("Error GET /play/:type :"); + console.error(error); + res.status(500).end(); + }); + }); + + // return a new h5p widget + // used for materia to check for specific library + // should be deprecated as there is no need to save this information on the server + app.post("/new/:type", (req, res) => { + h5pEditor + .saveOrUpdateContent( + undefined, + req.body.params.params, + req.body.params.metadata, + req.body.library, + req.user + ) + .then(contentId => { + //returns contentID of widget for materia to put in the qset + res.send(JSON.stringify({ contentId })); + res.status(200).end(); + }) + .catch(error => { + console.error("Error POST /new/:type :"); + console.error(error); + res.status(500).end(); + }); + }); + + var key = fs.readFileSync("config/key.pem"); + var cert = fs.readFileSync("config/cert.pem"); + var options = { + key: key, + cert: cert + }; + + const server = https.createServer(options, app); + + server.listen(port, () => { + console.log( + `Server is running at http(s)://localhost:${port}. Current Materia context: ${process.env.ENVIRONMENT}.` + ); + }); + }) + .catch(error => { + console.error("Error setting up the h5p node express server:"); + console.error(error); + }); diff --git a/h5p-server/h5p/content/0/content.json b/h5p-server/h5p/content/0/content.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/h5p-server/h5p/content/0/content.json @@ -0,0 +1 @@ +{} diff --git a/h5p-server/h5p/content/0/h5p.json b/h5p-server/h5p/content/0/h5p.json new file mode 100644 index 000000000..97b4dbef6 --- /dev/null +++ b/h5p-server/h5p/content/0/h5p.json @@ -0,0 +1,70 @@ +{ + "embedTypes": ["iframe"], + "language": "en", + "license": "U", + "extraTitle": "Materia h5p question set", + "title": "Materia h5p question set", + "mainLibrary": "H5P.QuestionSet", + "preloadedDependencies": [ + { + "machineName": "H5P.Image", + "majorVersion": 1, + "minorVersion": 1 + }, + { + "machineName": "FontAwesome", + "majorVersion": 4, + "minorVersion": 5 + }, + { + "machineName": "EmbeddedJS", + "majorVersion": 1, + "minorVersion": 0 + }, + { + "machineName": "H5P.JoubelUI", + "majorVersion": 1, + "minorVersion": 3 + }, + { + "machineName": "H5P.Question", + "majorVersion": 1, + "minorVersion": 4 + }, + { + "machineName": "H5P.MultiChoice", + "majorVersion": 1, + "minorVersion": 14 + }, + { + "machineName": "H5P.QuestionSet", + "majorVersion": 1, + "minorVersion": 17 + }, + { + "machineName": "H5P.DragQuestion", + "majorVersion": 1, + "minorVersion": 13 + }, + { + "machineName": "H5P.TrueFalse", + "majorVersion": 1, + "minorVersion": 6 + }, + { + "machineName": "H5P.Blanks", + "majorVersion": 1, + "minorVersion": 12 + }, + { + "machineName": "H5P.MarkTheWords", + "majorVersion": 1, + "minorVersion": 9 + }, + { + "machineName": "H5P.DragText", + "majorVersion": 1, + "minorVersion": 8 + } + ] +} diff --git a/h5p-server/h5p/content/1/content.json b/h5p-server/h5p/content/1/content.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/h5p-server/h5p/content/1/content.json @@ -0,0 +1 @@ +{} diff --git a/h5p-server/h5p/content/1/h5p.json b/h5p-server/h5p/content/1/h5p.json new file mode 100644 index 000000000..db73060c8 --- /dev/null +++ b/h5p-server/h5p/content/1/h5p.json @@ -0,0 +1,40 @@ +{ + "embedTypes": ["iframe"], + "language": "en", + "license": "U", + "extraTitle": "Materia h5p multiple choice", + "title": "Materia h5p multiple choice", + "mainLibrary": "H5P.MultiChoice", + "preloadedDependencies": [ + { + "machineName": "H5P.Image", + "majorVersion": 1, + "minorVersion": 1 + }, + { + "machineName": "FontAwesome", + "majorVersion": 4, + "minorVersion": 5 + }, + { + "machineName": "EmbeddedJS", + "majorVersion": 1, + "minorVersion": 0 + }, + { + "machineName": "H5P.JoubelUI", + "majorVersion": 1, + "minorVersion": 3 + }, + { + "machineName": "H5P.MultiChoice", + "majorVersion": 1, + "minorVersion": 14 + }, + { + "machineName": "H5P.Video", + "majorVersion": 1, + "minorVersion": 5 + } + ] +} diff --git a/h5p-server/h5p/content/2/content.json b/h5p-server/h5p/content/2/content.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/h5p-server/h5p/content/2/content.json @@ -0,0 +1 @@ +{} diff --git a/h5p-server/h5p/content/2/h5p.json b/h5p-server/h5p/content/2/h5p.json new file mode 100644 index 000000000..18a24353b --- /dev/null +++ b/h5p-server/h5p/content/2/h5p.json @@ -0,0 +1,40 @@ +{ + "embedTypes": ["iframe"], + "language": "en", + "license": "U", + "extraTitle": "Materia h5p mark the words", + "title": "Materia h5p mark the words", + "mainLibrary": "H5P.MarkTheWords", + "preloadedDependencies": [ + { + "machineName": "H5P.Image", + "majorVersion": 1, + "minorVersion": 1 + }, + { + "machineName": "FontAwesome", + "majorVersion": 4, + "minorVersion": 5 + }, + { + "machineName": "EmbeddedJS", + "majorVersion": 1, + "minorVersion": 0 + }, + { + "machineName": "H5P.JoubelUI", + "majorVersion": 1, + "minorVersion": 3 + }, + { + "machineName": "H5P.MarkTheWords", + "majorVersion": 1, + "minorVersion": 9 + }, + { + "machineName": "H5P.Video", + "majorVersion": 1, + "minorVersion": 5 + } + ] +} diff --git a/h5p-server/h5p/content/3/content.json b/h5p-server/h5p/content/3/content.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/h5p-server/h5p/content/3/content.json @@ -0,0 +1 @@ +{} diff --git a/h5p-server/h5p/content/3/h5p.json b/h5p-server/h5p/content/3/h5p.json new file mode 100644 index 000000000..16629491d --- /dev/null +++ b/h5p-server/h5p/content/3/h5p.json @@ -0,0 +1,40 @@ +{ + "embedTypes": ["iframe"], + "language": "en", + "license": "U", + "extraTitle": "Materia h5p advanced blanks", + "title": "Materia h5p advanced blanks", + "mainLibrary": "H5P.Blanks", + "preloadedDependencies": [ + { + "machineName": "H5P.Image", + "majorVersion": 1, + "minorVersion": 1 + }, + { + "machineName": "FontAwesome", + "majorVersion": 4, + "minorVersion": 5 + }, + { + "machineName": "EmbeddedJS", + "majorVersion": 1, + "minorVersion": 0 + }, + { + "machineName": "H5P.JoubelUI", + "majorVersion": 1, + "minorVersion": 3 + }, + { + "machineName": "H5P.Blanks", + "majorVersion": 1, + "minorVersion": 12 + }, + { + "machineName": "H5P.Video", + "majorVersion": 1, + "minorVersion": 5 + } + ] +} diff --git a/h5p-server/h5p/content/4/content.json b/h5p-server/h5p/content/4/content.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/h5p-server/h5p/content/4/content.json @@ -0,0 +1 @@ +{} diff --git a/h5p-server/h5p/content/4/h5p.json b/h5p-server/h5p/content/4/h5p.json new file mode 100644 index 000000000..97f8940d7 --- /dev/null +++ b/h5p-server/h5p/content/4/h5p.json @@ -0,0 +1,40 @@ +{ + "embedTypes": ["iframe"], + "language": "en", + "license": "U", + "extraTitle": "Materia h5p drag the words", + "title": "Materia h5p drag the words", + "mainLibrary": "H5P.DragText", + "preloadedDependencies": [ + { + "machineName": "H5P.Image", + "majorVersion": 1, + "minorVersion": 1 + }, + { + "machineName": "FontAwesome", + "majorVersion": 4, + "minorVersion": 5 + }, + { + "machineName": "EmbeddedJS", + "majorVersion": 1, + "minorVersion": 0 + }, + { + "machineName": "H5P.JoubelUI", + "majorVersion": 1, + "minorVersion": 3 + }, + { + "machineName": "H5P.DragText", + "majorVersion": 1, + "minorVersion": 8 + }, + { + "machineName": "H5P.Video", + "majorVersion": 1, + "minorVersion": 5 + } + ] +} diff --git a/h5p-server/h5p/content/5/content.json b/h5p-server/h5p/content/5/content.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/h5p-server/h5p/content/5/content.json @@ -0,0 +1 @@ +{} diff --git a/h5p-server/h5p/content/5/h5p.json b/h5p-server/h5p/content/5/h5p.json new file mode 100644 index 000000000..f92942d22 --- /dev/null +++ b/h5p-server/h5p/content/5/h5p.json @@ -0,0 +1,110 @@ +{ + "embedTypes": ["iframe"], + "language": "en", + "license": "U", + "extraTitle": "Materia h5p interactive video", + "title": "Materia h5p interactive video", + "mainLibrary": "H5P.InteractiveVideo", + "preloadedDependencies": [ + { + "machineName": "H5P.Image", + "majorVersion": 1, + "minorVersion": 1 + }, + { + "machineName": "FontAwesome", + "majorVersion": 4, + "minorVersion": 5 + }, + { + "machineName": "EmbeddedJS", + "majorVersion": 1, + "minorVersion": 0 + }, + { + "machineName": "H5P.JoubelUI", + "majorVersion": 1, + "minorVersion": 3 + }, + { + "machineName": "H5P.GoToQuestion", + "majorVersion": 1, + "minorVersion": 3 + }, + { + "machineName": "H5P.Table", + "majorVersion": 1, + "minorVersion": 1 + }, + { + "machineName": "H5P.Link", + "majorVersion": 1, + "minorVersion": 3 + }, + { + "machineName": "H5P.SingleChoiceSet", + "majorVersion": 1, + "minorVersion": 11 + }, + { + "machineName": "H5P.IVHotspot", + "majorVersion": 1, + "minorVersion": 2 + }, + { + "machineName": "H5P.DragText", + "majorVersion": 1, + "minorVersion": 8 + }, + { + "machineName": "H5P.InteractiveVideo", + "majorVersion": 1, + "minorVersion": 22 + }, + { + "machineName": "H5P.Blanks", + "majorVersion": 1, + "minorVersion": 12 + }, + { + "machineName": "H5P.MarkTheWords", + "majorVersion": 1, + "minorVersion": 9 + }, + { + "machineName": "H5P.DragQuestion", + "majorVersion": 1, + "minorVersion": 13 + }, + { + "machineName": "H5P.MultiChoice", + "majorVersion": 1, + "minorVersion": 14 + }, + { + "machineName": "H5P.Summary", + "majorVersion": 1, + "minorVersion": 10 + }, + { + "machineName": "H5P.TrueFalse", + "majorVersion": 1, + "minorVersion": 6 + }, + { + "machineName": "H5P.Video", + "majorVersion": 1, + "minorVersion": 5 + }, + { + "machineName": "H5P.Text", + "majorVersion": 1, + "minorVersion": 1 + }, + { + "machineName": "H5P.FreeTextQuestion", + "majorVersion": 1, + "minorVersion": 0 + } + ] +} diff --git a/h5p-server/h5p/content/999999999/content.json b/h5p-server/h5p/content/999999999/content.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/h5p-server/h5p/content/999999999/content.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/h5p-server/h5p/content/999999999/h5p.json b/h5p-server/h5p/content/999999999/h5p.json new file mode 100644 index 000000000..338aa933f --- /dev/null +++ b/h5p-server/h5p/content/999999999/h5p.json @@ -0,0 +1,50 @@ +{ + "embedTypes": ["iframe"], + "language": "en", + "license": "U", + "extraTitle": "MATERIA DUMMY H5P FILE", + "title": "MATERIA DUMMY H5P FILE", + "mainLibrary": "H5P.QuestionSet", + "preloadedDependencies": [ + { + "machineName": "H5P.Image", + "majorVersion": 1, + "minorVersion": 1 + }, + { + "machineName": "FontAwesome", + "majorVersion": 4, + "minorVersion": 5 + }, + { + "machineName": "EmbeddedJS", + "majorVersion": 1, + "minorVersion": 0 + }, + { + "machineName": "H5P.JoubelUI", + "majorVersion": 1, + "minorVersion": 3 + }, + { + "machineName": "H5P.Question", + "majorVersion": 1, + "minorVersion": 4 + }, + { + "machineName": "H5P.MultiChoice", + "majorVersion": 1, + "minorVersion": 14 + }, + { + "machineName": "H5P.QuestionSet", + "majorVersion": 1, + "minorVersion": 17 + }, + { + "machineName": "H5P.TrueFalse", + "majorVersion": 1, + "minorVersion": 6 + } + ] +} diff --git a/h5p-server/h5p/core/.gitignore b/h5p-server/h5p/core/.gitignore new file mode 100644 index 000000000..b303012e5 --- /dev/null +++ b/h5p-server/h5p/core/.gitignore @@ -0,0 +1,6 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore + +# required to push empty directory to github \ No newline at end of file diff --git a/h5p-server/h5p/editor/.gitignore b/h5p-server/h5p/editor/.gitignore new file mode 100644 index 000000000..b303012e5 --- /dev/null +++ b/h5p-server/h5p/editor/.gitignore @@ -0,0 +1,6 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore + +# required to push empty directory to github \ No newline at end of file diff --git a/h5p-server/h5p/libraries/.gitignore b/h5p-server/h5p/libraries/.gitignore new file mode 100644 index 000000000..86d0cb272 --- /dev/null +++ b/h5p-server/h5p/libraries/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/h5p-server/h5p/temporary-storage/.gitignore b/h5p-server/h5p/temporary-storage/.gitignore new file mode 100644 index 000000000..86d0cb272 --- /dev/null +++ b/h5p-server/h5p/temporary-storage/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/h5p-server/interfaces/MateriaTempFileStorage.js b/h5p-server/interfaces/MateriaTempFileStorage.js new file mode 100644 index 000000000..1ee7f4161 --- /dev/null +++ b/h5p-server/interfaces/MateriaTempFileStorage.js @@ -0,0 +1,275 @@ +import { H5pError, Logger } from "@lumieducation/h5p-server"; + +const fsextra = require("fs-extra"); +const fs = require("fs"); +const request = require("request-promise"); +const promisepipe = require("promisepipe"); +const path = require("path"); +const crypto = require("crypto"); + +const log = new Logger("MateriaTempFileStorage"); + +// instructs node to allow untrusted certificates +// when uploading images to materia +// only for use in development +if (process.env.ENVIRONMENT != "prod") { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +} + +export default class MateriaTempFileStorage { + constructor() {} + + /** + * this gets called when a user uploads a new media file in an h5p widget creator + * + * Saves the file to temp directory then uploads to Materia + * @param filename: string auto generated by h5p + * @param dataStream: PassThrough (Read/Write stream) generated by h5p. not compatible with materia + * @param user: h5p user object, user.id for materia should always be 1 + * @param expiration time: set for an hour after upload + */ + async saveFile(filename, dataStream, user, expirationTime) { + if (!filename) { + log.error(`Filename empty!`); + throw new H5pError("illegal-filename", {}, 400); + } + + // create the file in a temporary directory + // use this file to upload to materia + await fsextra.ensureDir(this.getAbsoluteUserDirectoryPath(user.id)); + const filePath = this.getAbsoluteFilePath(user.id, filename); + await fsextra.ensureDir(path.dirname(filePath)); + const writeStream = fsextra.createWriteStream(filePath); + await promisepipe(dataStream, writeStream); + await fsextra.writeJSON(`${filePath}.metadata`, { + expiresAt: expirationTime.getTime() + }); + + // upload to materia process + try { + // create a readstream with the temp file + const stream = fs.createReadStream(filePath); + + //oauth token + const timestamp = Math.floor(new Date().getTime() / 1000).toString(); + const nonce = crypto.randomBytes(16).toString("hex"); + const token = crypto + .createHmac("sha256", process.env.OAUTH_SECRET + timestamp + nonce) + .update(process.env.OAUTH_KEY) + .digest("hex"); + const materiaHost = process.env.MATERIA_WEBSERVER_NETWORK || "localhost"; + + // set up post request with multiform-data + var options = { + method: "POST", + url: `https://${materiaHost}/media/upload`, + formData: { + "Content-Type": "image/png", //todo get actual content type + file: { + value: stream, + options: { + filename: filename, + contentType: null + } + }, + name: filename, + oauth_consumer_key: process.env.OAUTH_KEY, + oauth_timestamp: timestamp, + oauth_nonce: nonce, + oauth_signature: token + } + }; + + // create a filename using the materia media ID that is + // returned to us from the upload request + var materiaFilename = ""; + + await request(options, function(error, response) { + if (error) throw new Error(error); + if (response.statusCode == 404) + throw new Error("Unable to upload file to Materia"); + // response should look like {"success": "true", "id": "5-digit-random-hash"} + response.body = JSON.parse(response.body); + + // rename the temp file to materia ID hash so + // we can insert it in the materia qset later + const dir = filename.match(/[^\/]*\//); // something like "images/" + const filetype = filename.match(/\.[0-9a-z]+$/i); // something like ".png" + materiaFilename = dir + response.body.id + filetype; + fs.rename( + path.join("./h5p/temporary-storage/", user.id, filename), + path.join("./h5p/temporary-storage/", user.id, materiaFilename), + error => { + if (error) { + console.log("error renaming files"); + console.log(error); + } else { + // also need to rename the metadata file so + // h5p knows where to find the file stats + fs.rename( + path.join( + "./h5p/temporary-storage/", + user.id, + filename + ".metadata" + ), + path.join( + "./h5p/temporary-storage/", + user.id, + materiaFilename + ".metadata" + ), + error => { + if (error) { + console.log("error renaming metadata files"); + console.log(error); + } + } + ); + } + } + ); + }); + + // return the file object so h5p creator can retrieve it + return { + expiresAt: expirationTime, + filename: materiaFilename, + ownedByUserId: user.id + }; + } catch (error) { + log.error( + `Error while uploading file "${filename}" to Materia storage: ${error.message}` + ); + throw new H5pError( + `temporary-storage:materia-upload-error`, + { filename }, + 500 + ); + } + } + + // helper function simply generates temp file path + // for h5p creator to find media + getAbsoluteFilePath(userId, filename) { + return path.join("./h5p/temporary-storage/", userId, filename); + } + + // generate users directory for temp file storage + // in materia's case userId will always be 1 + getAbsoluteUserDirectoryPath(userId) { + return path.join("./h5p/temporary-storage/", userId); + } + + // returns an object representing a file using metadata to get + // its expire time + async getTempFileInfo(userId, filename) { + const metadata = await fsextra.readJSON( + `${this.getAbsoluteFilePath(userId, filename)}.metadata` + ); + return { + expiresAt: new Date(metadata.expiresAt), + filename, + ownedByUserId: userId + }; + } + + /** + * Checks if a file exists in temporary storage. + * @param filename the file to check + * @param user the user who wants to access the file + */ + async fileExists(filename, user) { + const filePath = this.getAbsoluteFilePath(user.id, filename); + return fsextra.pathExists(filePath); + } + + /** + * Removes a file from temporary storage. Will silently do nothing if the file does not + * exist or is not accessible. + * @param filename + * @param user + */ + async deleteFile(filename, userId) { + const filePath = this.getAbsoluteFilePath(userId, filename); + fsextra.remove(filePath); + fsextra.remove(`${filePath}.metadata`); + } + + /** + * List all files in temporary storage, from both the images/ and videos/ directories, + * and append them together. removes each corresponding .metadata file from the list + * @returns an object for each file of the form + * { + * expiresAt: DateTime, + * filename, + * ownedByUserId: userID + * } + */ + async listFiles() { + // get list of files from images/ directory + let result = await fsextra.readdir("./h5p/temporary-storage/1/images"); + // prepend images/ to each file + result = result.map(file => "images/" + file); + + // get list of files from /videos direcotry + let videoResults = await fsextra.readdir( + "./h5p/temporary-storage/1/videos" + ); + // prepend videos/ to each file + videoResults = videoResults.map(file => "videos/" + file); + + // concatenate the two arrays to get all temporary files in one array + result = result.concat(videoResults); + + // remove all metadata files from the array + result = result.filter(file => !file.endsWith(".metadata")); + // create object for each file, which includes expire time and user owner + result = await Promise.all( + result.map(file => this.getTempFileInfo("1", file)) + ); + return result; + } + + /** + * Returns a information about a temporary file. + * Throws an exception if the file does not exist. + * @param filename the relative path inside the library + * @param user the user who wants to access the file + * @returns the file stats + */ + async getFileStats(filename, user) { + if (!(await this.fileExists(filename, user))) { + throw new H5pError( + "storage-file-implementations:temporary-file-not-found", + { + filename, + userId: user.id + }, + 404 + ); + } + const filePath = this.getAbsoluteFilePath(user.id, filename); + return fsextra.stat(filePath); + } + + /** + * Returns a readable for a file. + * + * Note: Make sure to handle the 'error' event of the Readable! This method + * does not check if the file exists in storage to avoid the extra request. + * However, this means that there will be an error when piping the Readable + * to the response if the file doesn't exist! + * @param filename + * @param user + */ + async getFileStream(filename, user) { + const filePath = this.getAbsoluteFilePath(user.id, filename); + if (!(await fsextra.pathExists(filePath))) { + throw new H5pError( + "storage-file-implementations:temporary-file-not-found", + { filename, userId: user.id }, + 404 + ); + } + return fsextra.createReadStream(filePath); + } +} diff --git a/h5p-server/materia-h5p.Dockerfile b/h5p-server/materia-h5p.Dockerfile new file mode 100644 index 000000000..3fdf8068c --- /dev/null +++ b/h5p-server/materia-h5p.Dockerfile @@ -0,0 +1,37 @@ +# This dockerfile is meant to be run by ../docker/docker-compose.yml file, +# which spins up the entire Materia stack + +# To run the h5p-server alone, use ./Dockerfile + +FROM node:12 + +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY ./h5p-server/package*.json ./ + +# Copy Materia's env file(s) to the current working directory +COPY .env* ./ + +RUN yarn install + +# Bundle app source +COPY ./h5p-server . + +# for local development, don't copy existing h5p info into +# fresh docker container +# RUN rm -r h5p/core h5p/editor h5p/libraries h5p/temporary-storage +# RUN mkdir h5p/core h5p/editor h5p/libraries h5p/temporary-storage + +RUN ./setup.sh + +EXPOSE 3000 + +# set in docker-compose +ARG ENVIRONMENT +ENV ENVIRONMENT $ENVIRONMENT + +CMD yarn start:${ENVIRONMENT} \ No newline at end of file diff --git a/h5p-server/materia-h5p.Dockerfile.dockerignore b/h5p-server/materia-h5p.Dockerfile.dockerignore new file mode 100644 index 000000000..5171c5408 --- /dev/null +++ b/h5p-server/materia-h5p.Dockerfile.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log \ No newline at end of file diff --git a/h5p-server/package.json b/h5p-server/package.json new file mode 100644 index 000000000..988b7c6c9 --- /dev/null +++ b/h5p-server/package.json @@ -0,0 +1,27 @@ +{ + "name": "materia-h5p-server", + "license": "GPL-3.0-or-later", + "main": "express.js", + "scripts": { + "start": "node -r esm express.js", + "start:dev": "ENVIRONMENT=dev node -r esm express.js", + "start:mwdk": "ENVIRONMENT=mwdk node -r esm express.js", + "start:prod": "NODE_ENV=production ENVIRONMENT=prod node -r esm express.js" + }, + "dependencies": { + "@lumieducation/h5p-express": "^9.0.3", + "@lumieducation/h5p-server": "^9.0.3", + "body-parser": "^1.19.0", + "cors": "^2.8.5", + "dotenv": "^10.0.0", + "esm": "^3.2.25", + "express": "^4.17.1", + "express-fileupload": "^1.1.7-alpha.4", + "fs-extra": "^10.0.0", + "got": "^11.3.0", + "morgan": "^1.10.0", + "node-cron": "^3.0.0", + "request": "^2.88.2", + "request-promise": "^4.2.6" + } +} diff --git a/h5p-server/renderers/admin.js b/h5p-server/renderers/admin.js new file mode 100644 index 000000000..caf888e65 --- /dev/null +++ b/h5p-server/renderers/admin.js @@ -0,0 +1,133 @@ +import { H5PEditor } from "@lumieducation/h5p-server"; + +export function adminRenderer(model) { + function rendererInit() { + var ns = H5PEditor; + + (function($) { + H5PEditor.init = function() { + H5PEditor.$ = H5P.jQuery; + H5PEditor.basePath = H5PIntegration.editor.libraryUrl; + H5PEditor.fileIcon = H5PIntegration.editor.fileIcon; + H5PEditor.ajaxPath = H5PIntegration.editor.ajaxPath; + H5PEditor.filesPath = H5PIntegration.editor.filesPath; + H5PEditor.apiVersion = H5PIntegration.editor.apiVersion; + H5PEditor.contentLanguage = H5PIntegration.editor.language; + + // Semantics describing what copyright information can be stored for media. + H5PEditor.copyrightSemantics = H5PIntegration.editor.copyrightSemantics; + H5PEditor.metadataSemantics = H5PIntegration.editor.metadataSemantics; + + // Required styles and scripts for the editor + H5PEditor.assets = H5PIntegration.editor.assets; + + // Required for assets + H5PEditor.baseUrl = ""; + + var $editorElement = $(".h5p-editor"); + var $type = $('input[name="action"]'); + var $upload = $(".h5p-upload"); + var $create = $(".h5p-create").hide(); + var $editor = $(".h5p-editor"); + var $library = $('input[name="library"]'); + var $params = $('input[name="parameters"]'); + var library = $library.val(); + + var $goBackElement = $(".go-back-warning").hide(); + $("#go-back").click(function(event) { + $create.html('
    '); + + H5PEditor.init(); + $goBackElement.hide(); + $create.show(); + }); + + var h5peditor = new ns.Editor(undefined, undefined, $editorElement[0]); + $create.show(); + + H5P.externalDispatcher.on("editorloaded", function(event) { + $create.hide(); + $goBackElement.show(); + }); + }; + + H5PEditor.getAjaxUrl = function(action, parameters) { + var url = H5PIntegration.editor.ajaxPath + action; + + if (parameters !== undefined) { + for (var property in parameters) { + if (parameters.hasOwnProperty(property)) { + url += "&" + property + "=" + parameters[property]; + } + } + } + + url += window.location.search.replace(/\\?/g, "&"); + return url; + }; + + $(document).ready(H5PEditor.init); + })(H5P.jQuery); + } + + var initAsString = new String(rendererInit); + + return ` + + + + ${model.styles + .map(style => ``) + .join("\n ")} + ${model.scripts + .map(script => ``) + .join("\n ")} + + + + + +
    +

    Sorry! Picking H5P Libraries won't work here. Go back.

    + +
    +
    +
    +
    + + + `; +} diff --git a/h5p-server/renderers/editor.js b/h5p-server/renderers/editor.js new file mode 100644 index 000000000..44809f9c3 --- /dev/null +++ b/h5p-server/renderers/editor.js @@ -0,0 +1,282 @@ +export function editorRenderer(model) { + // TODO conditionally apply when visiting an h5p library - e.g., selectedLibrary != undefined + // currently obscures the h5p-hub when you need to visit the picker + // uncomment this if you need to visit the h5p hub - at least for the moment + model.integration.editor.assets.css.push("/styles/creator.css"); + + // TODO override styles from model: + /* + styles: [ + '/h5p/core/styles/h5p.css', + '/h5p/core/styles/h5p-confirmation-dialog.css', + '/h5p/core/styles/h5p-core-button.css', + '/h5p/editor/libs/darkroom.css', + '/h5p/editor/styles/css/h5p-hub-client.css', + '/h5p/editor/styles/css/fonts.css', + '/h5p/editor/styles/css/application.css', + '/h5p/editor/styles/css/libs/zebra_datepicker.min.css' + ], + */ + // placeholder to prevent errors at runtime + var modelParams = ""; + var context = ""; + + // this function is defined here to properly allow for syntax highlighting in editors + // as opposed to being entirely defined within the return string + // we use some seriously hackish js magic to convert the entire function to a string and insert it into the return string down below + function rendererInit() { + // this switch case looks at the URL to determine which editor to load + // it will have to eventually include every H5P library we want to support + // TODO see if there's a better way to do this + // TODO determine if it's possible to use the library string directly instead of a clean name, or transform it into a safe string + // this URL will be provided by the Materia widget + window.selectedLibrary = undefined; + + const pattern = /^\/(new|edit){1}\/([A-Za-z0-9\-]+)$/; + let editTypeMatch = window.location.pathname.match(pattern)[1]; // check if url is "new" or "edit" + let libraryMatch = window.location.pathname.match(pattern)[2]; // grab library title that follows + + if (libraryMatch) { + switch (libraryMatch) { + case "h5p-multichoice": + window.selectedLibrary = "H5P.MultiChoice 1.14"; + break; + case "h5p-questionset": + window.selectedLibrary = "H5P.QuestionSet 1.17"; + break; + case "h5p-advancedblanks": + window.selectedLibrary = "H5P.Blanks 1.12"; + break; + case "h5p-markthewords": + window.selectedLibrary = "H5P.MarkTheWords 1.9"; + break; + case "h5p-dragtext": + window.selectedLibrary = "H5P.DragText 1.8"; + break; + case "h5p-interactivevideo": + window.selectedLibrary = "H5P.InteractiveVideo 1.22"; + break; + default: + window.selectedLibrary = undefined; + } + } + + let materiaPath = ""; + switch (context) { + case "prod": + case "dev": + materiaPath = materiaUrl; + break; + case "mwdk": + default: + materiaPath = "http://localhost:8118"; // this is the default mwdk URL + break; + } + + var ns = H5PEditor; + + (function($) { + H5PEditor.init = function() { + H5PEditor.$ = H5P.jQuery; + H5PEditor.basePath = H5PIntegration.editor.libraryUrl; + H5PEditor.fileIcon = H5PIntegration.editor.fileIcon; + H5PEditor.ajaxPath = H5PIntegration.editor.ajaxPath; + H5PEditor.filesPath = H5PIntegration.editor.filesPath; + H5PEditor.apiVersion = H5PIntegration.editor.apiVersion; + H5PEditor.contentLanguage = H5PIntegration.editor.language; + + // Semantics describing what copyright information can be stored for media. + H5PEditor.copyrightSemantics = H5PIntegration.editor.copyrightSemantics; + H5PEditor.metadataSemantics = H5PIntegration.editor.metadataSemantics; + + // Required styles and scripts for the editor + H5PEditor.assets = H5PIntegration.editor.assets; + + // Required for assets + H5PEditor.baseUrl = ""; + + if (H5PIntegration.editor.nodeVersionId !== undefined) { + H5PEditor.contentId = H5PIntegration.editor.nodeVersionId; + } + + var h5peditor; + var $type = $('input[name="action"]'); + var $upload = $(".h5p-upload"); + var $create = $(".h5p-create").hide(); + var $editor = $(".h5p-editor"); + var $library = $('input[name="library"]'); + var $params = $('input[name="parameters"]'); + var library = $library.val(); + + $upload.hide(); + + if (h5peditor === undefined) { + // contentId is present in search query (existing content) + if (editTypeMatch == "edit") { + window.parent.postMessage( + { message: "ready_for_qset" }, + materiaPath + ); // TODO add this url to a config somewhere? + } + // no contentId passed in, initialize empty editor + else if (editTypeMatch == "new") { + h5peditor = new ns.Editor( + window.selectedLibrary, + undefined, + $editor[0] + ); + $create.show(); + } else { + console.error("H5P URL malformed!"); + } + } else { + $create.show(); + } + + // this is for uploading H5P content - which we won't support? + // if ($type.filter(':checked').val() === 'upload') { + // $type.change(); + // } else { + // $type + // .filter('input[value="create"]') + // .attr('checked', true) + // .change(); + // } + + // Adds listener to talk to the widget frame above us + window.addEventListener("message", receiveMessage, false); + + // postMessage handler for talking to Materia + function receiveMessage(event) { + switch (event.data.message) { + // widget wants to save, send it the params + case "widget_save": + handlePublish(); + break; + // widget has sent params to initialize existing content + case "params_send": + h5peditor = new ns.Editor( + window.selectedLibrary, + JSON.stringify(event.data.params), + $editor[0] + ); + $create.show(); + break; + default: + return false; + } + + return event.preventDefault(); + } + + function handlePublish() { + var params = h5peditor.getParams(); + + if (params.params !== undefined) { + // Validate mandatory main title. Prevent submitting if that's not set. + // Deliberately doing it after getParams(), so that any other validation + // problems are also revealed + + if (!h5peditor.isMainTitleSet()) { + console.warn("Main title must be set in order to publish!"); + // TODO send postMessage to alert Materia that main title is not set + // This is in-line with what other widgets do + return false; + } + + // Set main library + $library.val(h5peditor.getLibrary()); + + // Set params + $params.val(JSON.stringify(params)); + + window.parent.postMessage( + { + message: "save", + library: h5peditor.getLibrary(), + params + }, + materiaPath + ); + + return event.preventDefault(); + // TODO - Calculate & set max score + // $maxscore.val(h5peditor.getMaxScore(params.params)); + } + } + + // Title label + var $title = $("#h5p-content-form #title"); + var $label = $title.prev(); + $title + .focus(function() { + $label.addClass("screen-reader-text"); + }) + .blur(function() { + if ($title.val() === "") { + $label.removeClass("screen-reader-text"); + } + }) + .focus(); + + // Delete confirm + $(".submitdelete").click(function() { + return confirm(H5PIntegration.editor.deleteMessage); + }); + }; + + H5PEditor.getAjaxUrl = function(action, parameters) { + var url = H5PIntegration.editor.ajaxPath + action; + + if (parameters !== undefined) { + for (var property in parameters) { + if (parameters.hasOwnProperty(property)) { + url += "&" + property + "=" + parameters[property]; + } + } + } + + // url += window.location.search.replace(/\\?/g, '&'); + return url; + }; + + $(document).ready(H5PEditor.init); + })(H5P.jQuery); + } + + // converts the entire function into a string - allows us to inject it as part of the returned page + // instead of writing js within the returned string directly + var initAsString = new String(rendererInit); + + // TODO redo all this with something cleaner + return ` + + + + + ${model.styles + .map(style => ``) + .join("\n ")} + ${model.scripts + .map(script => ``) + .join("\n ")} + + +
    +
    +
    + + + `; +} diff --git a/h5p-server/renderers/player.js b/h5p-server/renderers/player.js new file mode 100644 index 000000000..806d846eb --- /dev/null +++ b/h5p-server/renderers/player.js @@ -0,0 +1,70 @@ +export function playerRenderer(model) { + var context = ""; + + function playerInit(contentId = "") { + H5P.preventInit = true; + + if (H5PIntegration) { + let materiaPath = ""; + switch (context) { + case "prod": + case "dev": + materiaPath = materiaUrl; + break; + case "mwdk": + default: + materiaPath = "http://localhost:8118"; // this is the default mwdk url + break; + } + + // perform a postMessage to the widget to ask for the qset + window.parent.postMessage({ message: "ready_for_qset" }, materiaPath); + + // Adds listener to talk to the widget frame above us + window.addEventListener("message", receiveMessage, false); + + // postMessage handler for talking to Materia + function receiveMessage(event) { + let params = event.data.params.params; + H5PIntegration.contents[ + `cid-${contentId}` + ].jsonContent = JSON.stringify(params); + + // this actually inits the player when we're ready + H5P.init(document.getElementById("h5p-player")); + return event.preventDefault(); + } + + H5P.externalDispatcher.on("xAPI", function(event) { + window.parent.postMessage(event.data.statement, materiaPath); + }); + } + } + + var initAsString = new String(playerInit); + + return ` + + + + ${model.scripts + .map(script => ``) + .join("\n ")} + ${model.styles + .map(style => ``) + .join("\n ")} + + +
    +
    + + + `; +} diff --git a/h5p-server/setup.sh b/h5p-server/setup.sh new file mode 100755 index 000000000..bb7b3ceb7 --- /dev/null +++ b/h5p-server/setup.sh @@ -0,0 +1,24 @@ +curl -o h5p.zip https://codeload.github.com/h5p/h5p-php-library/zip/1.24.0 +unzip h5p.zip +(cd h5p-php-library-1.24.0 && tar c .) | (cd h5p/core && tar xfk -) +rm -rf h5p.zip h5p-php-library-1.24.0 + +curl -o editor.zip https://codeload.github.com/h5p/h5p-editor-php-library/zip/1.24.0 +unzip editor.zip +(cd h5p-editor-php-library-1.24.0 && tar c .) | (cd h5p/editor && tar xfk -) +rm -rf editor.zip h5p-editor-php-library-1.24.0 + +curl -o libraries.zip https://h5p.org/sites/default/files/h5p/exports/interactive-video-2-618.h5p +unzip -n libraries.zip -d h5p/libraries/ +rm -rf libraries.zip + +curl -o libraries.zip https://h5p.org/sites/default/files/h5p/exports/question-set-616.h5p +unzip -n libraries.zip -d h5p/libraries/ +rm -rf libraries.zip + +# clear out any existing certs +rm -rf ./config/key.pem +rm -rf ./config/cert.pem + +# generate a self-signed ssl cert +openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ./config/key.pem -out ./config/cert.pem -days 365 \ No newline at end of file diff --git a/h5p-server/styles/creator.css b/h5p-server/styles/creator.css new file mode 100644 index 000000000..2f058b80e --- /dev/null +++ b/h5p-server/styles/creator.css @@ -0,0 +1,5 @@ +/* creator.css */ + +section.h5p-hub { + display: none; +} \ No newline at end of file diff --git a/h5p-server/yarn.lock b/h5p-server/yarn.lock new file mode 100644 index 000000000..525a1735d --- /dev/null +++ b/h5p-server/yarn.lock @@ -0,0 +1,1615 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@lumieducation/h5p-express@^9.0.3": + version "9.0.3" + resolved "https://registry.npmjs.org/@lumieducation/h5p-express/-/h5p-express-9.0.3.tgz" + integrity sha512-QaFmISJMVyhbkyM6aG6SqWm+fOJgcy7FHjk5dCE0LSH6cEaccCy+Pjbp3n5362o3jOlvuIqSsIfnXcI6zA0Uww== + dependencies: + "@lumieducation/h5p-server" "^9.0.3" + express "4.17.1" + +"@lumieducation/h5p-server@^9.0.3": + version "9.0.3" + resolved "https://registry.npmjs.org/@lumieducation/h5p-server/-/h5p-server-9.0.3.tgz" + integrity sha512-UB7zK8YPwBIICi8ZihHjmPqslDJf7rCY4csIbrtRsIaLTJOwWdoZ17zTQR8FYBuDPPwfRYBvFQe+MA3FTHOUAw== + dependencies: + ajv "^8.6.3" + ajv-keywords "^5.0.0" + axios "^0.21.4" + cache-manager "^3.4.4" + crc "^3.8.0" + debug "^4.3.2" + flat "^5.0.2" + fs-extra "^10.0.0" + get-all-files "^3.0.0" + https-proxy-agent "^5.0.0" + image-size "^1.0.0" + jsonpath "^1.1.1" + merge "^2.1.1" + mime-types "^2.1.32" + nanoid "^3.1.25" + promisepipe "^3.0.0" + qs "^6.10.1" + sanitize-html "^2.5.1" + stream-buffers "^3.0.2" + tmp-promise "^3.0.2" + upath "^2.0.1" + yauzl-promise "^2.1.3" + yazl "^2.5.1" + +"@sindresorhus/is@^2.1.1": + version "2.1.1" + resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz" + integrity sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg== + +"@szmarczak/http-timer@^4.0.5": + version "4.0.5" + resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz" + integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== + dependencies: + defer-to-connect "^2.0.0" + +"@types/cacheable-request@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz" + integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "*" + "@types/node" "*" + "@types/responselike" "*" + +"@types/http-cache-semantics@*": + version "4.0.0" + resolved "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz" + integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== + +"@types/keyv@*": + version "3.1.1" + resolved "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz" + integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "14.0.9" + resolved "https://registry.npmjs.org/@types/node/-/node-14.0.9.tgz" + integrity sha512-0sCTiXKXELOBxvZLN4krQ0FPOAA7ij+6WwvD0k/PHd9/KAkr4dXel5J9fh6F4x1FwAQILqAWkmpeuS6mjf1iKA== + +"@types/responselike@*", "@types/responselike@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +agent-base@6: + version "6.0.2" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ajv-keywords@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.0.0.tgz" + integrity sha512-ULd1QMjRoH6JDNUQIfDLrlE+OgZlFaxyYCjzt58uNuUQtKXt8/U+vK/8Ql0gyn/C5mqZzUWtKMqr/4YquvTrWA== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.6.3: + version "8.6.3" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz" + integrity sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +async@3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/async/-/async-3.2.0.tgz" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +axios@^0.21.4: + version "0.21.4" + resolved "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +bluebird@^3.5.0: + version "3.7.2" + resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@1.19.0, body-parser@^1.19.0: + version "1.19.0" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + +buffer@^5.1.0: + version "5.6.0" + resolved "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz" + integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + +busboy@^0.3.1: + version "0.3.1" + resolved "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz" + integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw== + dependencies: + dicer "0.3.0" + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cache-manager@^3.4.4: + version "3.4.4" + resolved "https://registry.npmjs.org/cache-manager/-/cache-manager-3.4.4.tgz" + integrity sha512-oayy7ukJqNlRUYNUfQBwGOLilL0X5q7GpuaF19Yqwo6qdx49OoTZKRIF5qbbr+Ru8mlTvOpvnMvVq6vw72pOPg== + dependencies: + async "3.2.0" + lodash "^4.17.21" + lru-cache "6.0.0" + +cacheable-lookup@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz" + integrity sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w== + +cacheable-request@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz" + integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^2.0.0" + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +clone-response@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz" + integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= + dependencies: + mimic-response "^1.0.0" + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +crc@^3.8.0: + version "3.8.0" + resolved "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz" + integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ== + dependencies: + buffer "^5.1.0" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.3.2: + version "4.3.2" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + dependencies: + ms "2.1.2" + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +defer-to-connect@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz" + integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +depd@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +dicer@0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz" + integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== + dependencies: + streamsearch "0.1.2" + +dom-serializer@^1.0.1: + version "1.3.2" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz" + integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + +domhandler@^4.0.0, domhandler@^4.2.0: + version "4.2.2" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz" + integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.5.2: + version "2.8.0" + resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +dotenv@^10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz" + integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escodegen@^1.8.1: + version "1.14.1" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz" + integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +esm@^3.2.25: + version "3.2.25" + resolved "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + +esprima@1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz" + integrity sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs= + +esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +events-intercept@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/events-intercept/-/events-intercept-2.0.0.tgz" + integrity sha1-rb84aBxaSyARxB7kH2GjTLpEiJc= + +express-fileupload@^1.1.7-alpha.4: + version "1.1.7-alpha.4" + resolved "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.1.7-alpha.4.tgz" + integrity sha512-uNl/TB3adUH25cDRp1gDoXQ38SdIZXOAVzC54G/xnOAa4M3maBWiZTVz39cnoQ7TXhmYXYpnOfMDMbqSelXFmQ== + dependencies: + busboy "^0.3.1" + +express@4.17.1, express@^4.17.1: + version "4.17.1" + resolved "https://registry.npmjs.org/express/-/express-4.17.1.tgz" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@1.3.0, extsprintf@^1.2.0: + version "1.3.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +follow-redirects@^1.14.0: + version "1.14.7" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz" + integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +fs-extra@^10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-all-files@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/get-all-files/-/get-all-files-3.0.0.tgz" + integrity sha512-WkhplR2a8uALIGXCbV/bAyihkBU7r2jSCtQIph6roCXJmMEVvyhxuoHAizNd5MUSrHaSLN2JoUEv5yro0SLaKQ== + +get-intrinsic@^1.0.2: + version "1.1.1" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + +get-stream@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz" + integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== + dependencies: + pump "^3.0.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@^7.1.3: + version "7.1.6" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +got@^11.3.0: + version "11.3.0" + resolved "https://registry.npmjs.org/got/-/got-11.3.0.tgz" + integrity sha512-yi/kiZY2tNMtt5IfbfX8UL3hAZWb2gZruxYZ72AY28pU5p0TZjZdl0uRsuaFbnC0JopdUi3I+Mh1F3dPQ9Dh0Q== + dependencies: + "@sindresorhus/is" "^2.1.1" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.1" + decompress-response "^6.0.0" + get-stream "^5.1.0" + http2-wrapper "^1.0.0-beta.4.5" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.4" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +has-symbols@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +htmlparser2@^6.0.0: + version "6.1.0" + resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +http2-wrapper@^1.0.0-beta.4.5: + version "1.0.0-beta.4.6" + resolved "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.4.6.tgz" + integrity sha512-9oB4BiGDTI1FmIBlOF9OJ5hwJvcBEmPCqk/hy314Uhy2uq5TjekUZM8w8SPLLlUEM+mxNhXdPAXfrJN2Zbb/GQ== + dependencies: + quick-lru "^5.0.0" + resolve-alpn "^1.0.0" + +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +image-size@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/image-size/-/image-size-1.0.0.tgz" + integrity sha512-JLJ6OwBfO1KcA+TvJT+v8gbE6iWbj24LyDNFgFEN0lzegn6cC6a/p3NIDaepMsJjQjlUWqIC7wJv8lBFxPNjcw== + dependencies: + queue "6.0.2" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsonfile@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz" + integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== + dependencies: + universalify "^1.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonpath@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz" + integrity sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w== + dependencies: + esprima "1.2.2" + static-eval "2.0.2" + underscore "1.12.1" + +jsprim@^1.2.2: + version "1.4.2" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +keyv@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.0.1.tgz" + integrity sha512-xz6Jv6oNkbhrFCvCP7HQa8AaII8y8LRpoSm661NOKLr4uHuBwhX4epXrPQgF3+xdJnN4Esm5X0xwY4bOlALOtw== + dependencies: + json-buffer "3.0.1" + +klona@^2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz" + integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lodash@^4.17.19, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lru-cache@6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +merge@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz" + integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@1.49.0: + version "1.49.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz" + integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== + +mime-types@^2.1.12, mime-types@^2.1.32, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.32" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz" + integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== + dependencies: + mime-db "1.49.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-response@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +moment-timezone@^0.5.31: + version "0.5.34" + resolved "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz" + integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0": + version "2.29.1" + resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nanocolors@^0.2.2: + version "0.2.12" + resolved "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.12.tgz" + integrity sha512-SFNdALvzW+rVlzqexid6epYdt8H9Zol7xDoQarioEFcFN0JHo4CYNztAxmtfgGTVRCmFlEOqqhBpoFGKqSAMug== + +nanoid@^3.1.25: + version "3.1.28" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.1.28.tgz" + integrity sha512-gSu9VZ2HtmoKYe/lmyPFES5nknFrHa+/DT9muUFWFMi6Jh9E1I7bkvlQ8xxf1Kos9pi9o8lBnIOkatMhKX/YUw== + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +node-cron@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz" + integrity sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA== + dependencies: + moment-timezone "^0.5.31" + +normalize-url@^4.1.0: + version "4.5.1" + resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-inspect@^1.9.0: + version "1.11.0" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz" + integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +p-cancelable@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz" + integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== + +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz" + integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE= + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +postcss@^8.0.2: + version "8.3.8" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.3.8.tgz" + integrity sha512-GT5bTjjZnwDifajzczOC+r3FI3Cu+PgPvrsjhQdRqa2kTJ4968/X9CUce9xttIB0xOs5c6xf0TCWZo/y9lF6bA== + dependencies: + nanocolors "^0.2.2" + nanoid "^3.1.25" + source-map-js "^0.6.2" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +promisepipe@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/promisepipe/-/promisepipe-3.0.0.tgz" + integrity sha512-V6TbZDJ/ZswevgkDNpGt/YqNCiZP9ASfgU+p83uJE6NrGtvSGoOcHLiDCqkMs2+yg7F5qHdLV8d0aS8O26G/KA== + +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@^6.10.1: + version "6.10.1" + resolved "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz" + integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== + dependencies: + side-channel "^1.0.4" + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +queue@6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz" + integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== + dependencies: + inherits "~2.0.3" + +quick-lru@^5.0.0: + version "5.1.1" + resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +request-promise-core@1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz" + integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== + dependencies: + lodash "^4.17.19" + +request-promise@^4.2.6: + version "4.2.6" + resolved "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz" + integrity sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ== + dependencies: + bluebird "^3.5.0" + request-promise-core "1.1.4" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@^2.88.2: + version "2.88.2" + resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve-alpn@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz" + integrity sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA== + +responselike@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz" + integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== + dependencies: + lowercase-keys "^2.0.0" + +rimraf@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sanitize-html@^2.5.1: + version "2.5.1" + resolved "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.5.1.tgz" + integrity sha512-hUITPitQk+eFNLtr4dEkaaiAJndG2YE87IOpcfBSL1XdklWgwcNDJdr9Ppe8QKL/C3jFt1xH/Mbj20e0GZQOfg== + dependencies: + deepmerge "^4.2.2" + escape-string-regexp "^4.0.0" + htmlparser2 "^6.0.0" + is-plain-object "^5.0.0" + klona "^2.0.3" + parse-srcset "^1.0.2" + postcss "^8.0.2" + +send@0.17.1: + version "0.17.1" + resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +source-map-js@^0.6.2: + version "0.6.2" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz" + integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== + +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +static-eval@2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz" + integrity sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg== + dependencies: + escodegen "^1.8.1" + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +stream-buffers@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz" + integrity sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ== + +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + +tmp-promise@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.2.tgz" + integrity sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA== + dependencies: + tmp "^0.2.0" + +tmp@^0.2.0: + version "0.2.1" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +tough-cookie@^2.3.3, tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +underscore@1.12.1: + version "1.12.1" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz" + integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== + +universalify@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz" + integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +upath@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz" + integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yauzl-clone@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/yauzl-clone/-/yauzl-clone-1.0.4.tgz" + integrity sha1-i8bSk7F8yYgCu77S4onRjnaXyWw= + dependencies: + events-intercept "^2.0.0" + +yauzl-promise@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/yauzl-promise/-/yauzl-promise-2.1.3.tgz" + integrity sha1-F0Z4RduJ/GWSyph8ouz+6MOBrj0= + dependencies: + yauzl "^2.9.1" + yauzl-clone "^1.0.4" + +yauzl@^2.9.1: + version "2.10.0" + resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + +yazl@^2.5.1: + version "2.5.1" + resolved "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz" + integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== + dependencies: + buffer-crc32 "~0.2.3"