diff --git a/.drone.yml b/.drone.yml index a51b535..dbf8000 100644 --- a/.drone.yml +++ b/.drone.yml @@ -58,6 +58,36 @@ steps: event: push branch: master +--- +kind: pipeline +type: docker +name: Build Docker Image (s2oj-remote-judger) + +trigger: + branch: + - master + +steps: + - name: tags + image: alpine + commands: + - echo -n "latest, $DRONE_BRANCH, ${DRONE_COMMIT_SHA:0:8}" > .tags + + - name: docker + image: plugins/docker + settings: + registry: git.m.ac + repo: git.m.ac/baoshuo/s2oj-remote-judger + context: remote_judger + dockerfile: remote_judger/Dockerfile + username: baoshuo + password: + from_secret: GITMAC_SECRET + cache_from: git.m.ac/baoshuo/s2oj-remote-judger:latest + when: + event: push + branch: master + --- kind: pipeline type: docker diff --git a/.editorconfig b/.editorconfig index a318561..002128d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,7 +3,12 @@ root = true [*] indent_style = tab indent_size = 4 +end_of_line = lf [*.y{,a}ml] indent_style = space indent_size = 2 + +[remote_judger/**.{js,ts}] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8034ef..682fe8e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,9 +3,10 @@ name: Build & Push Docker Images on: push: branches: - - 'master' - tags: - - 'v*' + - master + pull_request: + branches: + - master workflow_dispatch: env: @@ -13,88 +14,31 @@ env: IMAGE_BASENAME: ${{ github.repository }} jobs: - build-db: - name: Build Database Image + build: + name: Build Image runs-on: ubuntu-latest + permissions: contents: read packages: write - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - name: Log in to the Container registry - uses: docker/login-action@v2.0.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + strategy: + matrix: + include: + - image_name: db + context: db + dockerfile: db/Dockerfile + - image_name: judger + context: judger + dockerfile: judger/Dockerfile + - image_name: remote-judger + context: remote_judger + dockerfile: remote_judger/Dockerfile + - image_name: web + context: . + dockerfile: web/Dockerfile + fail-fast: false - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4.0.1 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_BASENAME }}-db - tags: | - latest - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha,prefix= - - - name: Build and push Docker image - uses: docker/build-push-action@v3.1.1 - with: - context: db - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - build-judger: - name: Build Judger Image - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v2.0.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4.0.1 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_BASENAME }}-judger - tags: | - latest - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha,prefix= - - - name: Build and push Docker image - uses: docker/build-push-action@v3.1.1 - with: - context: judger - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - build-web: - name: Build Web Image - runs-on: ubuntu-latest - permissions: - contents: read - packages: write steps: - name: Checkout repository uses: actions/checkout@v3 @@ -104,7 +48,7 @@ jobs: sed -i "s/'s2oj-version' => 'dev'/'s2oj-version' => '$(echo "${{ github.sha }}" | cut -c1-7)'/g" web/app/.default-config.php - name: Log in to the Container registry - uses: docker/login-action@v2.0.0 + uses: docker/login-action@v2.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -112,9 +56,9 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v4.0.1 + uses: docker/metadata-action@v4.3.0 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_BASENAME }}-web + images: ${{ env.REGISTRY }}/${{ env.IMAGE_BASENAME }}-${{ matrix.image_name }} tags: | latest type=ref,event=branch @@ -124,10 +68,10 @@ jobs: type=sha,prefix= - name: Build and push Docker image - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v3.3.0 with: - context: . - file: web/Dockerfile - push: true + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} + push: ${{ github.event_name == 'push' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index e4d53e4..c89a998 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ docker-compose.local.yml .config.php .config.development.php .config.local.php +*.development.env +*.local.env diff --git a/db/app_uoj233.sql b/db/app_uoj233.sql index 12b362a..f7c8b6c 100644 --- a/db/app_uoj233.sql +++ b/db/app_uoj233.sql @@ -622,8 +622,10 @@ CREATE TABLE `problems` ( `ac_num` int NOT NULL DEFAULT '0', `submit_num` int NOT NULL DEFAULT '0', `difficulty` int NOT NULL DEFAULT '-1', + `type` varchar(20) NOT NULL DEFAULT 'local', `assigned_to_judger` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'any', PRIMARY KEY (`id`), + KEY `type` (`type`), KEY `assigned_to_judger` (`assigned_to_judger`), KEY `uploader` (`uploader`), KEY `difficulty` (`difficulty`), @@ -648,6 +650,7 @@ UNLOCK TABLES; /*!40101 SET character_set_client = utf8mb4 */; CREATE TABLE `problems_contents` ( `id` int NOT NULL, + `remote_content` longtext COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `statement` longtext COLLATE utf8mb4_unicode_ci NOT NULL, `statement_md` longtext COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) @@ -989,7 +992,9 @@ INSERT INTO `upgrades` (`name`, `status`, `updated_at`) VALUES ('16_list_v3', 'up', now()), ('18_user_permissions', 'up', now()), ('20_problem_difficulty', 'up', now()), - ('21_problem_difficulty', 'up', now()); + ('21_problem_difficulty', 'up', now()), + ('28_remote_judge', 'up', now()), + ('31_problem_resources', 'up', now()); /*!40000 ALTER TABLE `upgrades` ENABLE KEYS */; UNLOCK TABLES; diff --git a/docker-compose.development.yml b/docker-compose.development.yml index 31daa8d..3b694da 100644 --- a/docker-compose.development.yml +++ b/docker-compose.development.yml @@ -42,6 +42,23 @@ services: - SOCKET_PORT=2333 - SOCKET_PASSWORD=_judger_socket_password_ + uoj-remote-judger: + build: + context: ./remote_judger/ + dockerfile: Dockerfile + args: + - USE_MIRROR=1 + container_name: uoj-remote-judger + restart: always + env_file: + - remote-judger.development.env + environment: + - DEV=true + - UOJ_PROTOCOL=http + - UOJ_HOST=uoj-web + - UOJ_JUDGER_NAME=remote_judger + - UOJ_JUDGER_PASSWORD=_judger_password_ + uoj-web: build: context: ./ diff --git a/docker-compose.yml b/docker-compose.yml index 1da0339..57c96ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,16 @@ services: - SOCKET_PORT=2333 - SOCKET_PASSWORD=_judger_socket_password_ + uoj-remote-judger: + image: git.m.ac/baoshuo/s2oj-remote-judger + container_name: uoj-remote-judger + restart: always + environment: + - UOJ_PROTOCOL=http + - UOJ_HOST=uoj-web + - UOJ_JUDGER_NAME=remote_judger + - UOJ_JUDGER_PASSWORD=_judger_password_ + uoj-web: image: git.m.ac/baoshuo/s2oj-web container_name: uoj-web diff --git a/judger/add_judger.sql b/judger/add_judger.sql index 164db47..ab0cad3 100644 --- a/judger/add_judger.sql +++ b/judger/add_judger.sql @@ -1,2 +1,2 @@ USE `app_uoj233`; -insert into judger_info (judger_name, password, ip) values ('compose_judger', '_judger_password_', 'uoj-judger'); +insert into judger_info (judger_name, password, ip, display_name, description) values ('compose_judger', '_judger_password_', 'uoj-judger', '内置评测机', '用于评测本地题目的评测机。'); diff --git a/judger/uoj_judger/include/uoj_judger.h b/judger/uoj_judger/include/uoj_judger.h index ac4c6f0..daf825e 100644 --- a/judger/uoj_judger/include/uoj_judger.h +++ b/judger/uoj_judger/include/uoj_judger.h @@ -133,6 +133,9 @@ string file_preview(const string &name, const int &len = 100) { return ""; } + struct stat stat_buf; + stat(name.c_str(), &stat_buf); + string res = ""; if (len == -1) { int c; @@ -145,8 +148,9 @@ string file_preview(const string &name, const int &len = 100) { res += c; } if ((int)res.size() > len + 3) { + int omitted = (int)stat_buf.st_size - len; res.resize(len); - res += "..."; + res += "\n\n(" + to_string(omitted) + " bytes omitted)"; } } fclose(f); diff --git a/judger/uoj_judger/include/uoj_judger_v2.h b/judger/uoj_judger/include/uoj_judger_v2.h index cc1222b..d804a4f 100644 --- a/judger/uoj_judger/include/uoj_judger_v2.h +++ b/judger/uoj_judger/include/uoj_judger_v2.h @@ -130,6 +130,9 @@ string file_preview(const string &name, const int &len = 100) { return ""; } + struct stat stat_buf; + stat(name.c_str(), &stat_buf); + string res = ""; if (len == -1) { int c; @@ -142,8 +145,9 @@ string file_preview(const string &name, const int &len = 100) { res += c; } if ((int)res.size() > len + 3) { + int omitted = (int)stat_buf.st_size - len; res.resize(len); - res += "..."; + res += "\n\n(" + to_string(omitted) + " bytes omitted)"; } } fclose(f); diff --git a/remote_judger/.gitignore b/remote_judger/.gitignore new file mode 100644 index 0000000..123b7be --- /dev/null +++ b/remote_judger/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +*-error.log diff --git a/remote_judger/.prettierrc b/remote_judger/.prettierrc new file mode 100644 index 0000000..fb15891 --- /dev/null +++ b/remote_judger/.prettierrc @@ -0,0 +1,14 @@ +{ + "arrowParens": "avoid", + "bracketSpacing": true, + "endOfLine": "lf", + "jsxSingleQuote": false, + "printWidth": 80, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false, + "organizeImportsSkipDestructiveCodeActions": true +} diff --git a/remote_judger/Dockerfile b/remote_judger/Dockerfile new file mode 100644 index 0000000..5587f77 --- /dev/null +++ b/remote_judger/Dockerfile @@ -0,0 +1,12 @@ +FROM node:18.13.0 + +WORKDIR /opt/s2oj_remote_judger +COPY package*.json ./ + +RUN npm ci + +COPY . . + +RUN npm run build + +CMD [ "node", "--experimental-specifier-resolution=node", "dist/entrypoint.js" ] diff --git a/remote_judger/README b/remote_judger/README new file mode 100644 index 0000000..ef3ed2b --- /dev/null +++ b/remote_judger/README @@ -0,0 +1,5 @@ +本模块借鉴了以下项目的源码: + +- https://github.com/hydro-dev/Hydro/blob/feb51804766e35dbd13f7cb74fda95c0b783c49d/packages/vjudge/ + +在此表示感谢。 diff --git a/remote_judger/add_judger.sql b/remote_judger/add_judger.sql new file mode 100644 index 0000000..0d764e3 --- /dev/null +++ b/remote_judger/add_judger.sql @@ -0,0 +1,2 @@ +USE `app_uoj233`; +insert into judger_info (judger_name, password, ip, display_name, description) values ('remote_judger', '_judger_password_', 'uoj-remote-judger', '远端评测机', '用于桥接远端 OJ 评测机的虚拟评测机。'); diff --git a/remote_judger/package-lock.json b/remote_judger/package-lock.json new file mode 100644 index 0000000..d66645f --- /dev/null +++ b/remote_judger/package-lock.json @@ -0,0 +1,2660 @@ +{ + "name": "s2oj-remote-judger", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "s2oj-remote-judger", + "version": "0.0.0", + "license": "AGPL-3.0", + "dependencies": { + "crlf-normalize": "^1.0.18", + "fs-extra": "^11.1.0", + "jsdom": "^21.0.0", + "math-sum": "^2.0.0", + "reggol": "^1.3.4", + "superagent": "^8.0.6", + "superagent-proxy": "^3.0.0" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.1", + "@types/js-yaml": "^4.0.5", + "@types/jsdom": "^20.0.1", + "@types/node": "^18.11.18", + "@types/superagent": "^4.1.16", + "@types/superagent-proxy": "^3.0.0", + "js-yaml": "^4.1.0", + "typescript": "^4.9.4" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", + "dev": true + }, + "node_modules/@types/fs-extra": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz", + "integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==", + "dev": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", + "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==", + "dev": true + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/jsonfile": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz", + "integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + }, + "node_modules/@types/superagent": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.16.tgz", + "integrity": "sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ==", + "dev": true, + "dependencies": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "node_modules/@types/superagent-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/superagent-proxy/-/superagent-proxy-3.0.0.tgz", + "integrity": "sha512-gLNLONQrB8p9WQh4NFzeITZxDqv086q4xwCYFCaoQdH+4WQXmrGrXCJDn4cO3wNh+Sgsjb78lSVnjTDuST2Yxg==", + "dev": true, + "dependencies": { + "@types/superagent": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + }, + "node_modules/acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "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" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "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" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cosmokit": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cosmokit/-/cosmokit-1.4.0.tgz", + "integrity": "sha512-9Y5epwkPxnWDSjweuWoFATY8GKg9N1/r/3wL32Cjs7FIvo0S9syyY39xmNKq7+SZjbw+9bZUSbeQSbJaqufV3Q==" + }, + "node_modules/crlf-normalize": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/crlf-normalize/-/crlf-normalize-1.0.18.tgz", + "integrity": "sha512-bBPJTekqhw/yUgHvHrOT2QBb6gJt/gNDx++GKkSYaBIepeEiLIezouV8xgDFTL3yRMpK7wOYC9a8xvY7bFQCLg==", + "dependencies": { + "ts-type": ">=2" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + }, + "node_modules/data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "node_modules/degenerator": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.2.tgz", + "integrity": "sha512-c0mef3SNQo56t6urUU6tdQAs+ThoD0o9B9MJ8HEt7NQcGEILCRFqQb7ZbP9JAv+QF1Ky5plydhMR/IrqWDm+TQ==", + "dependencies": { + "ast-types": "^0.13.2", + "escodegen": "^1.8.1", + "esprima": "^4.0.0", + "vm2": "^3.9.8" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/degenerator/node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/degenerator/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escodegen": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "node_modules/file-uri-to-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", + "integrity": "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/fs-extra": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz", + "integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==", + "dependencies": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", + "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", + "dependencies": { + "@tootallnate/once": "1", + "data-uri-to-buffer": "3", + "debug": "4", + "file-uri-to-path": "2", + "fs-extra": "^8.1.0", + "ftp": "^0.3.10" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/get-uri/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/get-uri/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/get-uri/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/get-uri/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "node_modules/has": { + "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" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "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" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-21.0.0.tgz", + "integrity": "sha512-AIw+3ZakSUtDYvhwPwWHiZsUi3zHugpMEKlNPaurviseYoBqo0zBd3zqoUi3LPCNtPFlEP8FiW9MqCZdjb2IYA==", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lru-cache": { + "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" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/math-sum/-/math-sum-2.0.0.tgz", + "integrity": "sha512-pt3L7X8npPNZzYCCOZSxgmRBc091gZ8aPAxqJwq4c7Zf2Gw8K+fjF6gWepZZEkY6fDadlTxYdltDuT/dq894Hw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", + "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==" + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pac-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz", + "integrity": "sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4", + "get-uri": "3", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "5", + "pac-resolver": "^5.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "5" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/pac-proxy-agent/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pac-resolver": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-5.0.1.tgz", + "integrity": "sha512-cy7u00ko2KVgBAjuhevqpPeHIkCIqPe1v24cydhWjmeuzaBfmUWFCZJ1iAh5TuVzVZoUzXIW7K8sMYOZ84uZ9Q==", + "dependencies": { + "degenerator": "^3.0.2", + "ip": "^1.1.5", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", + "integrity": "sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g==", + "dependencies": { + "agent-base": "^6.0.0", + "debug": "4", + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^5.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/proxy-agent/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-agent/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/proxy-agent/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.2.0.tgz", + "integrity": "sha512-LN6QV1IJ9ZhxWTNdktaPClrNfp8xdSAYS0Zk2ddX7XsXZAxckMHPCBcHRo0cTcEIgYPRiGEkmji3Idkh2yFtYw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/reggol": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/reggol/-/reggol-1.3.4.tgz", + "integrity": "sha512-E/sL4WovP0kR5EHQx3YF4ZDsI5D5baTROXaELCN1mOCB8tEOVOR3PHr807yVcGit2BfmXMSW5KyRaUAV5K8Vzw==", + "dependencies": { + "cosmokit": "^1.3.6", + "object-inspect": "^1.12.2", + "supports-color": "^8.1.1" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "4", + "socks": "^2.3.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/socks/node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, + "node_modules/superagent": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.6.tgz", + "integrity": "sha512-HqSe6DSIh3hEn6cJvCkaM1BLi466f1LHi4yubR0tpewlMpk4RUFFy35bKz8SsPBwYfIIJy5eclp+3tCYAuX0bw==", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/superagent-proxy/-/superagent-proxy-3.0.0.tgz", + "integrity": "sha512-wAlRInOeDFyd9pyonrkJspdRAxdLrcsZ6aSnS+8+nu4x1aXbz6FWSTT9M6Ibze+eG60szlL7JA8wEIV7bPWuyQ==", + "dependencies": { + "debug": "^4.3.2", + "proxy-agent": "^5.0.0" + }, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "superagent": ">= 0.15.4 || 1 || 2 || 3" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", + "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ts-toolbelt": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "peer": true + }, + "node_modules/ts-type": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ts-type/-/ts-type-3.0.1.tgz", + "integrity": "sha512-cleRydCkBGBFQ4KAvLH0ARIkciduS745prkGVVxPGvcRGhMMoSJUB7gNR1ByKhFTEYrYRg2CsMRGYnqp+6op+g==", + "dependencies": { + "@types/node": "*", + "tslib": ">=2", + "typedarray-dts": "^1.0.0" + }, + "peerDependencies": { + "ts-toolbelt": "^9.6.0" + } + }, + "node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typedarray-dts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typedarray-dts/-/typedarray-dts-1.0.0.tgz", + "integrity": "sha512-Ka0DBegjuV9IPYFT1h0Qqk5U4pccebNIJCGl8C5uU7xtOs+jpJvKGAY4fHGK25hTmXZOEUl9Cnsg5cS6K/b5DA==" + }, + "node_modules/typescript": { + "version": "4.9.4", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/vm2": { + "version": "3.9.13", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.13.tgz", + "integrity": "sha512-0rvxpB8P8Shm4wX2EKOiMp7H2zq+HUE/UwodY0pCZXs9IffIKZq6vUti5OgkVCTakKo9e/fgO4X1fkwfjWxE3Q==", + "dependencies": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, + "bin": { + "vm2": "bin/vm2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/word-wrap": { + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz", + "integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "node_modules/xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==", + "engines": { + "node": "*" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + }, + "dependencies": { + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" + }, + "@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", + "dev": true + }, + "@types/fs-extra": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz", + "integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==", + "dev": true, + "requires": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "@types/js-yaml": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", + "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==", + "dev": true + }, + "@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "@types/jsonfile": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz", + "integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + }, + "@types/superagent": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.16.tgz", + "integrity": "sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@types/superagent-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/superagent-proxy/-/superagent-proxy-3.0.0.tgz", + "integrity": "sha512-gLNLONQrB8p9WQh4NFzeITZxDqv086q4xwCYFCaoQdH+4WQXmrGrXCJDn4cO3wNh+Sgsjb78lSVnjTDuST2Yxg==", + "dev": true, + "requires": { + "@types/superagent": "*" + } + }, + "@types/tough-cookie": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", + "dev": true + }, + "abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + }, + "acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==" + }, + "acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "requires": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "requires": { + "tslib": "^2.0.1" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "cosmokit": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cosmokit/-/cosmokit-1.4.0.tgz", + "integrity": "sha512-9Y5epwkPxnWDSjweuWoFATY8GKg9N1/r/3wL32Cjs7FIvo0S9syyY39xmNKq7+SZjbw+9bZUSbeQSbJaqufV3Q==" + }, + "crlf-normalize": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/crlf-normalize/-/crlf-normalize-1.0.18.tgz", + "integrity": "sha512-bBPJTekqhw/yUgHvHrOT2QBb6gJt/gNDx++GKkSYaBIepeEiLIezouV8xgDFTL3yRMpK7wOYC9a8xvY7bFQCLg==", + "requires": { + "ts-type": ">=2" + } + }, + "cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + } + } + }, + "data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" + }, + "data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "requires": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "degenerator": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.2.tgz", + "integrity": "sha512-c0mef3SNQo56t6urUU6tdQAs+ThoD0o9B9MJ8HEt7NQcGEILCRFqQb7ZbP9JAv+QF1Ky5plydhMR/IrqWDm+TQ==", + "requires": { + "ast-types": "^0.13.2", + "escodegen": "^1.8.1", + "esprima": "^4.0.0", + "vm2": "^3.9.8" + }, + "dependencies": { + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "requires": { + "webidl-conversions": "^7.0.0" + } + }, + "entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==" + }, + "escodegen": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "file-uri-to-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", + "integrity": "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", + "requires": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + } + }, + "fs-extra": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz", + "integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==", + "requires": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", + "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", + "requires": { + "@tootallnate/once": "1", + "data-uri-to-buffer": "3", + "debug": "4", + "file-uri-to-path": "2", + "fs-extra": "^8.1.0", + "ftp": "^0.3.10" + }, + "dependencies": { + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + } + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==" + }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" + }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsdom": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-21.0.0.tgz", + "integrity": "sha512-AIw+3ZakSUtDYvhwPwWHiZsUi3zHugpMEKlNPaurviseYoBqo0zBd3zqoUi3LPCNtPFlEP8FiW9MqCZdjb2IYA==", + "requires": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "math-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/math-sum/-/math-sum-2.0.0.tgz", + "integrity": "sha512-pt3L7X8npPNZzYCCOZSxgmRBc091gZ8aPAxqJwq4c7Zf2Gw8K+fjF6gWepZZEkY6fDadlTxYdltDuT/dq894Hw==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==" + }, + "nwsapi": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", + "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==" + }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "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" + } + }, + "pac-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz", + "integrity": "sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4", + "get-uri": "3", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "5", + "pac-resolver": "^5.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "5" + }, + "dependencies": { + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + } + } + }, + "pac-resolver": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-5.0.1.tgz", + "integrity": "sha512-cy7u00ko2KVgBAjuhevqpPeHIkCIqPe1v24cydhWjmeuzaBfmUWFCZJ1iAh5TuVzVZoUzXIW7K8sMYOZ84uZ9Q==", + "requires": { + "degenerator": "^3.0.2", + "ip": "^1.1.5", + "netmask": "^2.0.2" + } + }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" + }, + "proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", + "integrity": "sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g==", + "requires": { + "agent-base": "^6.0.0", + "debug": "4", + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^5.0.0" + }, + "dependencies": { + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "punycode": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.2.0.tgz", + "integrity": "sha512-LN6QV1IJ9ZhxWTNdktaPClrNfp8xdSAYS0Zk2ddX7XsXZAxckMHPCBcHRo0cTcEIgYPRiGEkmji3Idkh2yFtYw==" + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "reggol": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/reggol/-/reggol-1.3.4.tgz", + "integrity": "sha512-E/sL4WovP0kR5EHQx3YF4ZDsI5D5baTROXaELCN1mOCB8tEOVOR3PHr807yVcGit2BfmXMSW5KyRaUAV5K8Vzw==", + "requires": { + "cosmokit": "^1.3.6", + "object-inspect": "^1.12.2", + "supports-color": "^8.1.1" + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "requires": { + "xmlchars": "^2.2.0" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" + }, + "socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "requires": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "dependencies": { + "ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + } + } + }, + "socks-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", + "requires": { + "agent-base": "^6.0.2", + "debug": "4", + "socks": "^2.3.3" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, + "superagent": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.6.tgz", + "integrity": "sha512-HqSe6DSIh3hEn6cJvCkaM1BLi466f1LHi4yubR0tpewlMpk4RUFFy35bKz8SsPBwYfIIJy5eclp+3tCYAuX0bw==", + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + } + }, + "superagent-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/superagent-proxy/-/superagent-proxy-3.0.0.tgz", + "integrity": "sha512-wAlRInOeDFyd9pyonrkJspdRAxdLrcsZ6aSnS+8+nu4x1aXbz6FWSTT9M6Ibze+eG60szlL7JA8wEIV7bPWuyQ==", + "requires": { + "debug": "^4.3.2", + "proxy-agent": "^5.0.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tough-cookie": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", + "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "dependencies": { + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" + } + } + }, + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "requires": { + "punycode": "^2.1.1" + } + }, + "ts-toolbelt": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "peer": true + }, + "ts-type": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ts-type/-/ts-type-3.0.1.tgz", + "integrity": "sha512-cleRydCkBGBFQ4KAvLH0ARIkciduS745prkGVVxPGvcRGhMMoSJUB7gNR1ByKhFTEYrYRg2CsMRGYnqp+6op+g==", + "requires": { + "@types/node": "*", + "tslib": ">=2", + "typedarray-dts": "^1.0.0" + } + }, + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "typedarray-dts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typedarray-dts/-/typedarray-dts-1.0.0.tgz", + "integrity": "sha512-Ka0DBegjuV9IPYFT1h0Qqk5U4pccebNIJCGl8C5uU7xtOs+jpJvKGAY4fHGK25hTmXZOEUl9Cnsg5cS6K/b5DA==" + }, + "typescript": { + "version": "4.9.4", + "dev": true + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "vm2": { + "version": "3.9.13", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.13.tgz", + "integrity": "sha512-0rvxpB8P8Shm4wX2EKOiMp7H2zq+HUE/UwodY0pCZXs9IffIKZq6vUti5OgkVCTakKo9e/fgO4X1fkwfjWxE3Q==", + "requires": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + } + }, + "w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "requires": { + "xml-name-validator": "^4.0.0" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "requires": { + "iconv-lite": "0.6.3" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==" + }, + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + }, + "word-wrap": { + "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": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "ws": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz", + "integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==", + "requires": {} + }, + "xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==" + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/remote_judger/package.json b/remote_judger/package.json new file mode 100644 index 0000000..86ff0e9 --- /dev/null +++ b/remote_judger/package.json @@ -0,0 +1,33 @@ +{ + "name": "s2oj-remote-judger", + "version": "0.0.0", + "description": "Remote judger of S2OJ.", + "scripts": { + "build": "tsc -p .", + "start": "node dist/entrypoint.js" + }, + "type": "module", + "repository": "https://github.com/renbaoshuo/S2OJ", + "author": "Baoshuo ", + "license": "AGPL-3.0", + "private": true, + "dependencies": { + "crlf-normalize": "^1.0.18", + "fs-extra": "^11.1.0", + "jsdom": "^21.0.0", + "math-sum": "^2.0.0", + "reggol": "^1.3.4", + "superagent": "^8.0.6", + "superagent-proxy": "^3.0.0" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.1", + "@types/js-yaml": "^4.0.5", + "@types/jsdom": "^20.0.1", + "@types/node": "^18.11.18", + "@types/superagent": "^4.1.16", + "@types/superagent-proxy": "^3.0.0", + "js-yaml": "^4.1.0", + "typescript": "^4.9.4" + } +} diff --git a/remote_judger/src/daemon.ts b/remote_judger/src/daemon.ts new file mode 100644 index 0000000..c38e01e --- /dev/null +++ b/remote_judger/src/daemon.ts @@ -0,0 +1,194 @@ +import fs from 'fs-extra'; +import superagent from 'superagent'; +import proxy from 'superagent-proxy'; +import Logger from './utils/logger'; +import sleep from './utils/sleep'; +import * as TIME from './utils/time'; +import htmlspecialchars from './utils/htmlspecialchars'; +import { apply } from './vjudge'; +import path from 'path'; +import child from 'child_process'; + +proxy(superagent); + +const logger = new Logger('daemon'); + +interface UOJConfig { + server_url: string; + judger_name: string; + password: string; +} + +interface UOJSubmission { + id: number; + problem_id: number; + problem_mtime: number; + content: any; + status: string; + judge_time: string; +} + +export default async function daemon(config: UOJConfig) { + const request = (url: string, data = {}) => + superagent + .post(`${config.server_url}/judge${url}`) + .set('Content-Type', 'application/x-www-form-urlencoded') + .send( + Object.entries({ + judger_name: config.judger_name, + password: config.password, + ...data, + }) + .map( + ([k, v]) => + `${k}=${encodeURIComponent( + typeof v === 'string' ? v : JSON.stringify(v) + )}` + ) + .join('&') + ); + const vjudge = await apply(request); + + while (true) { + try { + const { text, error } = await request('/submit'); + + if (error) { + logger.error('/submit', error.message); + + await sleep(3 * TIME.second); + } else if (text.startsWith('Nothing to judge')) { + await sleep(3 * TIME.second); + } else { + const data: UOJSubmission = JSON.parse(text); + const { id, content, judge_time } = data; + const config = Object.fromEntries(content.config); + const tmpdir = `/tmp/s2oj_rmj/${id}/`; + + if (config.test_sample_only === 'on') { + await request('/submit', { + submit: true, + fetch_new: false, + id, + result: JSON.stringify({ + status: 'Judged', + score: 100, + time: 0, + memory: 0, + details: 'Sample test is not available.', + }), + judge_time, + }); + + continue; + } + + fs.ensureDirSync(tmpdir); + + const reportError = async (error: string, details: string) => { + await request('/submit', { + submit: true, + fetch_new: false, + id, + result: JSON.stringify({ + status: 'Judged', + score: 0, + error, + details: `${htmlspecialchars(details)}`, + }), + judge_time, + }); + }; + + // Download source code + logger.debug('Downloading source code.', id); + const zipFilePath = path.resolve(tmpdir, 'all.zip'); + const res = request(`/download${content.file_name}`); + const stream = fs.createWriteStream(zipFilePath); + res.pipe(stream); + + try { + await new Promise((resolve, reject) => { + stream.on('finish', resolve); + stream.on('error', reject); + }); + } catch (e) { + await reportError( + 'Judgment Failed', + `Failed to download source code.` + ); + logger.error('Failed to download source code.', id, e.message); + + fs.removeSync(tmpdir); + + continue; + } + + // Unzip source code + logger.debug('Unzipping source code.', id); + const extractedPath = path.resolve(tmpdir, 'all'); + + try { + await new Promise((resolve, reject) => { + child.exec(`unzip ${zipFilePath} -d ${extractedPath}`, e => { + if (e) reject(e); + else resolve(true); + }); + }); + } catch (e) { + await reportError('Judgment Failed', `Failed to unzip source code.`); + logger.error('Failed to unzip source code.', id, e.message); + + fs.removeSync(tmpdir); + + continue; + } + + // Read source code + logger.debug('Reading source code.', id); + const sourceCodePath = path.resolve(extractedPath, 'answer.code'); + let code = ''; + + try { + code = fs.readFileSync(sourceCodePath, 'utf-8'); + } catch (e) { + await reportError('Judgment Failed', `Failed to read source code.`); + logger.error('Failed to read source code.', id, e.message); + + fs.removeSync(tmpdir); + + continue; + } + + // Start judging + logger.info('Start judging', id, `(problem ${data.problem_id})`); + try { + await vjudge.judge( + id, + config.remote_online_judge, + config.remote_problem_id, + config.answer_language, + code, + judge_time + ); + } catch (err) { + await reportError( + 'Judgment Failed', + 'No details, please contact admin!' + ); + logger.error('Judgment Failed.', id, err.message); + + fs.removeSync(tmpdir); + + continue; + } + + fs.removeSync(tmpdir); + } + } catch (err) { + logger.error(err.message); + + await sleep(3 * TIME.second); + } + } +} diff --git a/remote_judger/src/entrypoint.ts b/remote_judger/src/entrypoint.ts new file mode 100644 index 0000000..97bf1d6 --- /dev/null +++ b/remote_judger/src/entrypoint.ts @@ -0,0 +1,15 @@ +import daemon from './daemon'; + +const { + UOJ_PROTOCOL = 'http', + UOJ_HOST = 'uoj-web', + UOJ_JUDGER_NAME = 'remote_judger', + UOJ_JUDGER_PASSWORD = '', +} = process.env; +const UOJ_BASEURL = `${UOJ_PROTOCOL}://${UOJ_HOST}`; + +daemon({ + server_url: UOJ_BASEURL, + judger_name: UOJ_JUDGER_NAME, + password: UOJ_JUDGER_PASSWORD, +}); diff --git a/remote_judger/src/interface.ts b/remote_judger/src/interface.ts new file mode 100644 index 0000000..77af4c5 --- /dev/null +++ b/remote_judger/src/interface.ts @@ -0,0 +1,35 @@ +export interface RemoteAccount { + type: string; + cookie?: string[]; + handle: string; + password: string; + endpoint?: string; + proxy?: string; +} + +export type NextFunction = (body: Partial) => void; + +export interface IBasicProvider { + ensureLogin(): Promise; + submitProblem( + id: string, + lang: string, + code: string, + submissionId: number, + next: NextFunction, + end: NextFunction + ): Promise; + waitForSubmission( + problem_id: string, + id: string, + next: NextFunction, + end: NextFunction + ): Promise; +} + +export interface BasicProvider { + new (account: RemoteAccount): IBasicProvider; +} + +export const USER_AGENT = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36 S2OJ/3.1.0'; diff --git a/remote_judger/src/providers/atcoder.ts b/remote_judger/src/providers/atcoder.ts new file mode 100644 index 0000000..97d05ad --- /dev/null +++ b/remote_judger/src/providers/atcoder.ts @@ -0,0 +1,326 @@ +import { JSDOM } from 'jsdom'; +import superagent from 'superagent'; +import proxy from 'superagent-proxy'; +import sleep from '../utils/sleep'; +import { IBasicProvider, RemoteAccount, USER_AGENT } from '../interface'; +import Logger from '../utils/logger'; + +proxy(superagent); +const logger = new Logger('remote/atcoder'); + +const langs_map = { + C: { + name: 'C (GCC 9.2.1)', + id: 4001, + comment: '//', + }, + 'C++': { + name: 'C++ (GCC 9.2.1)', + id: 4003, + comment: '//', + }, + Pascal: { + name: 'Pascal (FPC 3.0.4)', + id: 4041, + comment: '//', + }, + Python3: { + name: 'Python (3.8.2)', + id: 4006, + comment: '#', + }, +}; + +export function getAccountInfoFromEnv(): RemoteAccount | null { + const { + ATCODER_HANDLE, + ATCODER_PASSWORD, + ATCODER_ENDPOINT = 'https://atcoder.jp', + ATCODER_PROXY, + } = process.env; + + if (!ATCODER_HANDLE || !ATCODER_PASSWORD) return null; + + const account: RemoteAccount = { + type: 'atcoder', + handle: ATCODER_HANDLE, + password: ATCODER_PASSWORD, + endpoint: ATCODER_ENDPOINT, + }; + + if (ATCODER_PROXY) account.proxy = ATCODER_PROXY; + + return account; +} + +function parseProblemId(id: string) { + let [, contestId, problemId] = /^(\w+)([a-z][1-9]?)$/.exec(id); + + if (contestId.endsWith('_')) { + problemId = `${contestId}${problemId}`; + } else { + problemId = `${contestId}_${problemId}`; + } + + contestId = contestId.replace(/_/g, ''); + + return [contestId, problemId]; +} + +export default class AtcoderProvider implements IBasicProvider { + constructor(public account: RemoteAccount) { + if (account.cookie) this.cookie = account.cookie; + this.account.endpoint ||= 'https://atcoder.jp'; + } + + cookie: string[] = ['language=en']; + csrf: string; + + get(url: string) { + logger.debug('get', url); + if (!url.includes('//')) url = `${this.account.endpoint}${url}`; + const req = superagent + .get(url) + .redirects(0) + .ok(res => res.status < 400) + .set('Cookie', this.cookie) + .set('User-Agent', USER_AGENT); + if (this.account.proxy) return req.proxy(this.account.proxy); + return req; + } + + post(url: string) { + logger.debug('post', url, this.cookie); + if (!url.includes('//')) url = `${this.account.endpoint}${url}`; + const req = superagent + .post(url) + .type('form') + .redirects(0) + .ok(res => res.status < 400) + .set('Cookie', this.cookie) + .set('User-Agent', USER_AGENT); + if (this.account.proxy) return req.proxy(this.account.proxy); + return req; + } + + getCookie(target: string) { + return this.cookie + .find(i => i.startsWith(`${target}=`)) + ?.split('=')[1] + ?.split(';')[0]; + } + + setCookie(target: string, value: string) { + this.cookie = this.cookie.filter(i => !i.startsWith(`${target}=`)); + this.cookie.push(`${target}=${value}`); + } + + async getCsrfToken(url: string) { + const { text: html, header } = await this.get(url); + const { + window: { document }, + } = new JSDOM(html); + + if (header['set-cookie']) { + this.cookie = header['set-cookie']; + } + + if (document.body.children.length < 2 && html.length < 512) { + throw new Error(document.body.textContent!); + } + + return document + .querySelector('input[name="csrf_token"]') + ?.getAttribute('value'); + } + + get loggedIn() { + return this.get('/login').then(res => { + const html = res.text; + + if (res.header['set-cookie']) { + this.cookie = res.header['set-cookie']; + } + + if (html.includes('Sign In')) return false; + return true; + }); + } + + async ensureLogin() { + if (await this.loggedIn) return true; + logger.info('retry normal login'); + const csrf = await this.getCsrfToken('/login'); + const res = await this.post('/login').send({ + csrf_token: csrf, + username: this.account.handle, + password: this.account.password, + }); + const cookie = res.header['set-cookie']; + if (cookie) { + this.cookie = cookie; + } + if (await this.loggedIn) { + logger.success('Logged in'); + return true; + } + return false; + } + + async submitProblem( + id: string, + lang: string, + code: string, + submissionId: number, + next, + end + ) { + const programType = langs_map[lang] || langs_map['C++']; + const comment = programType.comment; + + if (comment) { + const msg = `S2OJ Submission #${submissionId} @ ${new Date().getTime()}`; + if (typeof comment === 'string') code = `${comment} ${msg}\n${code}`; + else if (comment instanceof Array) + code = `${comment[0]} ${msg} ${comment[1]}\n${code}`; + } + + const [contestId, problemId] = parseProblemId(id); + const csrf = await this.getCsrfToken( + `/contests/${contestId}/tasks/${problemId}` + ); + + logger.debug( + 'Submitting', + id, + programType, + lang, + `(S2OJ Submission #${submissionId})` + ); + + // TODO: check submit time to ensure submission + const res = await this.post(`/contests/${contestId}/submit`).send({ + csrf_token: csrf, + 'data.TaskScreenName': problemId, + 'data.LanguageId': programType.id, + sourceCode: code, + }); + + if (res.error) { + await end({ + error: true, + status: 'Judgment Failed', + message: 'Failed to submit code.', + }); + + return null; + } + + if (res.header['set-cookie']) { + this.cookie = res.header['set-cookie']; + } + + const { text: status, header: status_header } = await this.get( + `/contests/${contestId}/submissions/me` + ).retry(3); + + if (status_header['set-cookie']) { + this.cookie = status_header['set-cookie']; + } + + const { + window: { document }, + } = new JSDOM(status); + + return document + .querySelector('.submission-score[data-id]') + .getAttribute('data-id'); + } + + async waitForSubmission(problem_id: string, id: string, next, end) { + let i = 0; + + const [contestId] = parseProblemId(problem_id); + const status_url = `/contests/${contestId}/submissions/me/status/json?reload=true&sids[]=${id}`; + + while (true) { + if (++i > 60) { + return await end({ + id, + error: true, + status: 'Judgment Failed', + message: 'Failed to fetch submission details.', + }); + } + + await sleep(2000); + const { body, error, header } = await this.get(status_url).retry(3); + + if (header['set-cookie']) { + this.cookie = header['set-cookie']; + } + + if (error) continue; + + const result = body.Result[id]; + const { + window: { document }, + } = new JSDOM(`${result.Html}
`); + + const elements = document.querySelectorAll('td'); + const statusTd = elements[0]; + const statusElem = statusTd.querySelector('span'); + + if ( + statusElem.title === 'Waiting for Judging' || + statusElem.title === 'Waiting for Re-judging' || + ['WJ', 'WR'].includes(statusElem.innerHTML.trim()) + ) { + await next({ test_id: 0 }); + + continue; + } + + if ( + statusElem.title === 'Judging' || + (statusTd.colSpan == 3 && statusTd.className.includes('waiting-judge')) + ) { + await next({ test_id: /(\d+)/.exec(statusElem.innerHTML)[1] || 0 }); + + continue; + } + + if (statusElem.title === 'Compilation Error') { + return await end({ + id, + error: true, + status: 'Compile Error', + message: '', + }); + } + + if (statusElem.title === 'Internal Error') { + return await end({ + error: true, + status: 'Judgment Failed', + message: 'AtCoder Internal Error.', + }); + } + + const time = parseInt(elements[1].innerHTML.trim()); + const memory = parseInt(elements[2].innerHTML.trim()); + + return await end({ + id, + status: statusElem.title || 'None', + score: + statusElem.title === 'Accepted' || + statusElem.innerHTML.trim() === 'AC' + ? 100 + : 0, + time, + memory, + }); + } + } +} diff --git a/remote_judger/src/providers/codeforces.ts b/remote_judger/src/providers/codeforces.ts new file mode 100644 index 0000000..6a5e006 --- /dev/null +++ b/remote_judger/src/providers/codeforces.ts @@ -0,0 +1,384 @@ +import { JSDOM } from 'jsdom'; +import superagent from 'superagent'; +import proxy from 'superagent-proxy'; +import { crlf, LF } from 'crlf-normalize'; +import sleep from '../utils/sleep'; +import mathSum from 'math-sum'; +import { IBasicProvider, RemoteAccount, USER_AGENT } from '../interface'; +import { normalize, VERDICT } from '../verdict'; +import Logger from '../utils/logger'; + +proxy(superagent); +const logger = new Logger('remote/codeforces'); + +const langs_map = { + C: { + name: 'GNU GCC C11 5.1.0', + id: 43, + comment: '//', + }, + 'C++': { + name: 'GNU G++14 6.4.0', + id: 50, + comment: '//', + }, + 'C++17': { + name: 'GNU G++17 7.3.0', + id: 54, + comment: '//', + }, + 'C++20': { + name: 'GNU G++20 11.2.0 (64 bit, winlibs)', + id: 73, + comment: '//', + }, + Pascal: { + name: 'Free Pascal 3.0.2', + id: 4, + comment: '//', + }, + 'Python2.7': { + name: 'Python 2.7.18', + id: 7, + comment: '#', + }, + Python3: { + name: 'Python 3.9.1', + id: 31, + comment: '#', + }, +}; + +export function getAccountInfoFromEnv(): RemoteAccount | null { + const { + CODEFORCES_HANDLE, + CODEFORCES_PASSWORD, + CODEFORCES_ENDPOINT = 'https://codeforces.com', + CODEFORCES_PROXY, + } = process.env; + + if (!CODEFORCES_HANDLE || !CODEFORCES_PASSWORD) return null; + + const account: RemoteAccount = { + type: 'codeforces', + handle: CODEFORCES_HANDLE, + password: CODEFORCES_PASSWORD, + endpoint: CODEFORCES_ENDPOINT, + }; + + if (CODEFORCES_PROXY) account.proxy = CODEFORCES_PROXY; + + return account; +} + +function parseProblemId(id: string) { + const [, type, contestId, problemId] = id.startsWith('921') + ? ['', '921', '01'] + : /^(|GYM)(\d+)([A-Z]+[0-9]*)$/.exec(id); + if (type === 'GYM' && +contestId < 100000) { + return [type, (+contestId + 100000).toString(), problemId]; + } + return [type, contestId, problemId]; +} + +export default class CodeforcesProvider implements IBasicProvider { + constructor(public account: RemoteAccount) { + if (account.cookie) this.cookie = account.cookie; + this.account.endpoint ||= 'https://codeforces.com'; + } + + cookie: string[] = []; + csrf: string; + + get(url: string) { + logger.debug('get', url); + if (!url.includes('//')) url = `${this.account.endpoint}${url}`; + const req = superagent + .get(url) + .set('Cookie', this.cookie) + .set('User-Agent', USER_AGENT); + if (this.account.proxy) return req.proxy(this.account.proxy); + return req; + } + + post(url: string) { + logger.debug('post', url, this.cookie); + if (!url.includes('//')) url = `${this.account.endpoint}${url}`; + const req = superagent + .post(url) + .type('form') + .set('Cookie', this.cookie) + .set('User-Agent', USER_AGENT); + if (this.account.proxy) return req.proxy(this.account.proxy); + return req; + } + + getCookie(target: string) { + return this.cookie + .find(i => i.startsWith(`${target}=`)) + ?.split('=')[1] + ?.split(';')[0]; + } + + setCookie(target: string, value: string) { + this.cookie = this.cookie.filter(i => !i.startsWith(`${target}=`)); + this.cookie.push(`${target}=${value}`); + } + + tta(_39ce7: string) { + let _tta = 0; + for (let c = 0; c < _39ce7.length; c++) { + _tta = (_tta + (c + 1) * (c + 2) * _39ce7.charCodeAt(c)) % 1009; + if (c % 3 === 0) _tta++; + if (c % 2 === 0) _tta *= 2; + if (c > 0) + _tta -= + Math.floor(_39ce7.charCodeAt(Math.floor(c / 2)) / 2) * (_tta % 5); + _tta = ((_tta % 1009) + 1009) % 1009; + } + return _tta; + } + + async getCsrfToken(url: string) { + const { text: html } = await this.get(url); + const { + window: { document }, + } = new JSDOM(html); + if (document.body.children.length < 2 && html.length < 512) { + throw new Error(document.body.textContent!); + } + const ftaa = this.getCookie('70a7c28f3de') || 'n/a'; + const bfaa = this.getCookie('raa') || this.getCookie('bfaa') || 'n/a'; + return [ + ( + document.querySelector('meta[name="X-Csrf-Token"]') || + document.querySelector('input[name="csrf_token"]') + )?.getAttribute('content'), + ftaa, + bfaa, + ]; + } + + get loggedIn() { + return this.get('/enter').then(res => { + const html = res.text; + if (html.includes('Login into Codeforces')) return false; + if (html.length < 1000 && html.includes('Redirecting...')) { + logger.debug('Got a redirect', html); + return false; + } + return true; + }); + } + + async ensureLogin() { + if (await this.loggedIn) return true; + logger.info('retry normal login'); + const [csrf, ftaa, bfaa] = await this.getCsrfToken('/enter'); + const { header } = await this.get('/enter'); + if (header['set-cookie']) { + this.cookie = header['set-cookie']; + } + const res = await this.post('/enter').send({ + csrf_token: csrf, + action: 'enter', + ftaa, + bfaa, + handleOrEmail: this.account.handle, + password: this.account.password, + remember: 'on', + _tta: this.tta(this.getCookie('39ce7')), + }); + const cookie = res.header['set-cookie']; + if (cookie) { + this.cookie = cookie; + } + if (await this.loggedIn) { + logger.success('Logged in'); + return true; + } + return false; + } + + async submitProblem( + id: string, + lang: string, + code: string, + submissionId: number, + next, + end + ) { + const programType = langs_map[lang] || langs_map['C++']; + const comment = programType.comment; + if (comment) { + const msg = `S2OJ Submission #${submissionId} @ ${new Date().getTime()}`; + if (typeof comment === 'string') code = `${comment} ${msg}\n${code}`; + else if (comment instanceof Array) + code = `${comment[0]} ${msg} ${comment[1]}\n${code}`; + } + const [type, contestId, problemId] = parseProblemId(id); + const [csrf, ftaa, bfaa] = await this.getCsrfToken( + type !== 'GYM' ? '/problemset/submit' : `/gym/${contestId}/submit` + ); + logger.debug( + 'Submitting', + id, + programType, + lang, + `(S2OJ Submission #${submissionId})` + ); + // TODO: check submit time to ensure submission + const { text: submit, error } = await this.post( + `/${ + type !== 'GYM' ? 'problemset' : `gym/${contestId}` + }/submit?csrf_token=${csrf}` + ).send({ + csrf_token: csrf, + action: 'submitSolutionFormSubmitted', + programTypeId: programType.id, + source: code, + tabsize: 4, + sourceFile: '', + ftaa, + bfaa, + _tta: this.tta(this.getCookie('39ce7')), + ...(type !== 'GYM' + ? { + submittedProblemCode: contestId + problemId, + sourceCodeConfirmed: true, + } + : { + submittedProblemIndex: problemId, + }), + }); + + if (error) { + end({ + error: true, + status: 'Judgment Failed', + message: 'Failed to submit code.', + }); + + return null; + } + + const { + window: { document: statusDocument }, + } = new JSDOM(submit); + const message = Array.from(statusDocument.querySelectorAll('.error')) + .map(i => i.textContent) + .join('') + .replace(/ /g, ' ') + .trim(); + + if (message) { + end({ error: true, status: 'Compile Error', message }); + return null; + } + + const { text: status } = await this.get( + type !== 'GYM' ? '/problemset/status?my=on' : `/gym/${contestId}/my` + ).retry(3); + const { + window: { document }, + } = new JSDOM(status); + this.csrf = document + .querySelector('meta[name="X-Csrf-Token"]') + .getAttribute('content'); + return document + .querySelector('[data-submission-id]') + .getAttribute('data-submission-id'); + } + + async waitForSubmission(problem_id: string, id: string, next, end) { + let i = 0; + + while (true) { + if (++i > 60) { + return await end({ + id, + error: true, + status: 'Judgment Failed', + message: 'Failed to fetch submission details.', + }); + } + + await sleep(3000); + const { body, error } = await this.post('/data/submitSource') + .send({ + csrf_token: this.csrf, + submissionId: id, + }) + .retry(3); + if (error) continue; + if (body.compilationError === 'true') { + return await end({ + id, + error: true, + status: 'Compile Error', + message: crlf(body['checkerStdoutAndStderr#1'], LF), + }); + } + const time = mathSum( + Object.keys(body) + .filter(k => k.startsWith('timeConsumed#')) + .map(k => +body[k]) + ); + const memory = + Math.max( + ...Object.keys(body) + .filter(k => k.startsWith('memoryConsumed#')) + .map(k => +body[k]) + ) / 1024; + await next({ test_id: body.testCount }); + if (body.waiting === 'true') continue; + + const testCount = +body.testCount; + const status = + VERDICT[ + Object.keys(VERDICT).find(k => normalize(body.verdict).includes(k)) + ]; + let tests: string[] = []; + + for (let i = 1; i <= testCount; i++) { + let test_info = ''; + let info_text = + VERDICT[ + Object.keys(VERDICT).find(k => + normalize(body[`verdict#${i}`]).includes(k) + ) + ]; + + test_info += ``; + + const parse = (id: string) => crlf(body[id], LF); + + test_info += `${parse(`input#${i}`)}\n`; + test_info += `${parse(`output#${i}`)}\n`; + test_info += `${parse(`answer#${i}`)}\n`; + test_info += `${parse(`checkerStdoutAndStderr#${i}`)}\n`; + + test_info += ''; + + tests.push(test_info); + } + + const details = + '
' + + `REMOTE_SUBMISSION_ID = ${id}\nVERDICT = ${status}` + + `${tests.join('\n')}` + + '
'; + + return await end({ + id, + status, + score: status === 'Accepted' ? 100 : 0, + time, + memory, + details, + }); + } + } +} diff --git a/remote_judger/src/providers/loj.ts b/remote_judger/src/providers/loj.ts new file mode 100644 index 0000000..c80cf84 --- /dev/null +++ b/remote_judger/src/providers/loj.ts @@ -0,0 +1,459 @@ +import superagent from 'superagent'; +import proxy from 'superagent-proxy'; +import { stripVTControlCharacters } from 'util'; +import sleep from '../utils/sleep'; +import { IBasicProvider, RemoteAccount, USER_AGENT } from '../interface'; +import Logger from '../utils/logger'; +import { normalize, VERDICT } from '../verdict'; +import { crlf, LF } from 'crlf-normalize'; + +proxy(superagent); +const logger = new Logger('remote/loj'); + +const langs_map = { + C: { + name: 'C (gcc, c11, O2, m64)', + info: { + language: 'c', + compileAndRunOptions: { + compiler: 'gcc', + O: '2', + m: '64', + std: 'c11', + }, + }, + comment: '//', + }, + 'C++03': { + name: 'C++ (g++, c++03, O2, m64)', + info: { + language: 'cpp', + compileAndRunOptions: { + compiler: 'g++', + std: 'c++03', + O: '2', + m: '64', + }, + }, + comment: '//', + }, + 'C++11': { + name: 'C++ (g++, c++11, O2, m64)', + info: { + language: 'cpp', + compileAndRunOptions: { + compiler: 'g++', + std: 'c++11', + O: '2', + m: '64', + }, + }, + comment: '//', + }, + 'C++': { + name: 'C++ (g++, c++14, O2, m64)', + info: { + language: 'cpp', + compileAndRunOptions: { + compiler: 'g++', + std: 'c++14', + O: '2', + m: '64', + }, + }, + comment: '//', + }, + 'C++17': { + name: 'C++ (g++, c++17, O2, m64)', + info: { + language: 'cpp', + compileAndRunOptions: { + compiler: 'g++', + std: 'c++17', + O: '2', + m: '64', + }, + }, + comment: '//', + }, + 'C++20': { + name: 'C++ (g++, c++20, O2, m64)', + info: { + language: 'cpp', + compileAndRunOptions: { + compiler: 'g++', + std: 'c++20', + O: '2', + m: '64', + }, + }, + comment: '//', + }, + 'Python2.7': { + name: 'Python (2.7)', + info: { + language: 'python', + compileAndRunOptions: { + version: '2.7', + }, + }, + comment: '#', + }, + Python3: { + name: 'Python (3.10)', + info: { + language: 'python', + compileAndRunOptions: { + version: '3.10', + }, + }, + comment: '#', + }, + Java17: { + name: 'Java', + info: { + language: 'java', + compileAndRunOptions: {}, + }, + comment: '//', + }, + Pascal: { + name: 'Pascal', + info: { + language: 'pascal', + compileAndRunOptions: { + O: '2', + }, + }, + comment: '//', + }, +}; + +export function getAccountInfoFromEnv(): RemoteAccount | null { + const { + LOJ_HANDLE, + LOJ_TOKEN, + LOJ_ENDPOINT = 'https://api.loj.ac.cn/api', + LOJ_PROXY, + } = process.env; + + if (!LOJ_TOKEN) return null; + + const account: RemoteAccount = { + type: 'loj', + handle: LOJ_HANDLE, + password: LOJ_TOKEN, + endpoint: LOJ_ENDPOINT, + }; + + if (LOJ_PROXY) account.proxy = LOJ_PROXY; + + return account; +} + +export default class LibreojProvider implements IBasicProvider { + constructor(public account: RemoteAccount) { + this.account.endpoint ||= 'https://api.loj.ac.cn/api'; + } + + get(url: string) { + logger.debug('get', url); + if (!url.includes('//')) url = `${this.account.endpoint}${url}`; + const req = superagent + .get(url) + .auth(this.account.password, { type: 'bearer' }) + .set('User-Agent', USER_AGENT); + if (this.account.proxy) return req.proxy(this.account.proxy); + return req; + } + + post(url: string) { + logger.debug('post', url); + if (!url.includes('//')) url = `${this.account.endpoint}${url}`; + const req = superagent + .post(url) + .type('json') + .auth(this.account.password, { type: 'bearer' }) + .set('User-Agent', USER_AGENT) + .set('x-recaptcha-token', 'skip'); + if (this.account.proxy) return req.proxy(this.account.proxy); + return req; + } + + get loggedIn() { + return this.get('/auth/getSessionInfo?token=' + this.account.password).then( + res => + res.body.userMeta && res.body.userMeta.username === this.account.handle + ); + } + + async ensureLogin() { + if (await this.loggedIn) return true; + logger.info('retry login'); + // TODO: login + return false; + } + + async getProblemId(displayId: number) { + const { body } = await this.post('/problem/getProblem').send({ displayId }); + + return body.meta.id; + } + + async submitProblem( + displayId: string, + lang: string, + code: string, + submissionId: number, + next, + end + ) { + const programType = langs_map[lang] || langs_map['C++']; + const comment = programType.comment; + + if (comment) { + const msg = `S2OJ Submission #${submissionId} @ ${new Date().getTime()}`; + if (typeof comment === 'string') code = `${comment} ${msg}\n${code}`; + else if (comment instanceof Array) + code = `${comment[0]} ${msg} ${comment[1]}\n${code}`; + } + + const id = await this.getProblemId(parseInt(displayId)); + + logger.debug( + 'Submitting', + id, + `(displayId: ${displayId})`, + programType, + lang, + `(S2OJ Submission #${submissionId})` + ); + + const { body, error } = await this.post('/submission/submit').send({ + problemId: id, + content: { + code, + ...programType.info, + }, + uploadInfo: null, + }); + + if (error) { + await end({ + error: true, + status: 'Judgment Failed', + message: 'Failed to submit code.', + }); + + return null; + } + + return body.submissionId; + } + + async waitForSubmission(problem_id: string, id: string, next, end) { + let i = 0; + + while (true) { + if (++i > 60) { + return await end({ + id, + error: true, + status: 'Judgment Failed', + message: 'Failed to fetch submission details.', + }); + } + + await sleep(1000); + const { body, error } = await this.post('/submission/getSubmissionDetail') + .send({ submissionId: String(id), locale: 'zh_CN' }) + .retry(3); + + if (error) continue; + + if (!body.progress) { + await next({ status: 'Waiting for Remote Judge' }); + + continue; + } + + await next({ + status: `${body.progress.progressType}`, + }); + + if (body.progress.progressType !== 'Finished') { + continue; + } + + const status = + VERDICT[ + Object.keys(VERDICT).find(k => + normalize(body.meta.status).includes(k) + ) + ]; + + if (status === 'Compile Error') { + await end({ + error: true, + id, + status: 'Compile Error', + message: stripVTControlCharacters(body.progress.compile.message), + }); + } + + if (status === 'Judgment Failed') { + await end({ + error: true, + id, + status: 'Judgment Failed', + message: 'Error occurred on remote online judge.', + }); + } + + const parse = (str: string) => crlf(str, LF); + + const getSubtaskStatusDisplayText = (testcases: any): string => { + let result: string = null; + + for (const testcase of testcases) { + if (!testcase.testcaseHash) { + result = 'Skipped'; + + break; + } else if ( + body.progress.testcaseResult[testcase.testcaseHash].status !== + 'Accepted' + ) { + result = body.progress.testcaseResult[testcase.testcaseHash].status; + + break; + } + } + + result ||= 'Accepted'; + result = + VERDICT[ + Object.keys(VERDICT).find(k => normalize(result).includes(k)) + ]; + + return result; + }; + + const getTestcaseBlock = (id: string, index: number): string => { + const testcase = body.progress.testcaseResult[id]; + + if (!testcase) return ''; + + const status = + VERDICT[ + Object.keys(VERDICT).find(k => + normalize(testcase.status).includes(k) + ) + ]; + let test_info = ''; + + test_info += ``; + + if (testcase.input) { + if (typeof testcase.input === 'string') { + test_info += `${parse(testcase.input)}\n`; + } else { + test_info += `${parse(testcase.input.data)}\n\n(${ + testcase.input.omittedLength + } bytes omitted)\n`; + } + } + + if (testcase.userOutput) { + if (typeof testcase.userOutput === 'string') { + test_info += `${parse(testcase.userOutput)}\n`; + } else { + test_info += `${parse(testcase.userOutput.data)}\n\n(${ + testcase.userOutput.omittedLength + } bytes omitted)\n`; + } + } + + if (testcase.output) { + if (typeof testcase.output === 'string') { + test_info += `${parse(testcase.output)}\n`; + } else { + test_info += `${parse(testcase.output.data)}\n\n(${ + testcase.output.omittedLength + } bytes omitted)\n`; + } + } + + if (testcase.checkerMessage) { + if (typeof testcase.checkerMessage === 'string') { + test_info += `${parse(testcase.checkerMessage)}\n`; + } else { + test_info += `${parse(testcase.checkerMessage.data)}\n\n(${ + testcase.checkerMessage.omittedLength + } bytes omitted)\n`; + } + } + + test_info += ''; + + return test_info; + }; + + let details = ''; + + details += `REMOTE_SUBMISSION_ID = ${id}\nVERDICT = ${status}`; + + // Samples + if (body.progress.samples) { + details += `${body.progress.samples + .map((item, index) => + item.testcaseHash + ? getTestcaseBlock(item.testcaseHash, index) + : `` + ) + .join('\n')}`; + } + + // Tests + if (body.progress.subtasks.length === 1) { + details += `${body.progress.subtasks[0].testcases + .map((item, index) => + item.testcaseHash + ? getTestcaseBlock(item.testcaseHash, index) + : `` + ) + .join('\n')}`; + } else { + details += `${body.progress.subtasks + .map( + (subtask, index) => + `${subtask.testcases + .map((item, index) => + item.testcaseHash + ? getTestcaseBlock(item.testcaseHash, index) + : `` + ) + .join('\n')}` + ) + .join('\n')}`; + } + + return await end({ + id, + status: body.meta.status, + score: body.meta.score, + time: body.meta.timeUsed, + memory: body.meta.memoryUsed, + details: `
${details}
`, + }); + } + } +} diff --git a/remote_judger/src/providers/uoj.ts b/remote_judger/src/providers/uoj.ts new file mode 100644 index 0000000..9268df9 --- /dev/null +++ b/remote_judger/src/providers/uoj.ts @@ -0,0 +1,266 @@ +import { JSDOM } from 'jsdom'; +import superagent from 'superagent'; +import proxy from 'superagent-proxy'; +import Logger from '../utils/logger'; +import { IBasicProvider, RemoteAccount, USER_AGENT } from '../interface'; +import { parseTimeMS, parseMemoryMB } from '../utils/parse'; +import sleep from '../utils/sleep'; + +proxy(superagent); +const logger = new Logger('remote/uoj'); + +const langs_map = { + C: { + name: 'C', + id: 'C', + comment: '//', + }, + 'C++03': { + name: 'C++ 03', + id: 'C++', + comment: '//', + }, + 'C++11': { + name: 'C++ 11', + id: 'C++11', + comment: '//', + }, + 'C++': { + name: 'C++ 14', + id: 'C++14', + comment: '//', + }, + 'C++17': { + name: 'C++ 17', + id: 'C++17', + comment: '//', + }, + 'C++20': { + name: 'C++ 20', + id: 'C++20', + comment: '//', + }, + 'Python2.7': { + name: 'Python 2.7', + id: 'Python2.7', + comment: '#', + }, + Python3: { + name: 'Python 3', + id: 'Python3', + comment: '#', + }, + Java8: { + name: 'Java 8', + id: 'Java8', + comment: '//', + }, + Java11: { + name: 'Java 11', + id: 'Java11', + comment: '//', + }, + Java17: { + name: 'Java 17', + id: 'Java17', + comment: '//', + }, + Pascal: { + name: 'Pascal', + id: 'Pascal', + comment: '//', + }, +}; + +export function getAccountInfoFromEnv(): RemoteAccount | null { + const { + UOJ_HANDLE, + UOJ_PASSWORD, + UOJ_ENDPOINT = 'https://uoj.ac', + UOJ_PROXY, + } = process.env; + + if (!UOJ_HANDLE || !UOJ_PASSWORD) return null; + + const account: RemoteAccount = { + type: 'uoj', + handle: UOJ_HANDLE, + password: UOJ_PASSWORD, + endpoint: UOJ_ENDPOINT, + }; + + if (UOJ_PROXY) account.proxy = UOJ_PROXY; + + return account; +} + +export default class UOJProvider implements IBasicProvider { + constructor(public account: RemoteAccount) { + if (account.cookie) this.cookie = account.cookie; + } + + cookie: string[] = []; + csrf: string; + + get(url: string) { + logger.debug('get', url); + if (!url.includes('//')) + url = `${this.account.endpoint || 'https://uoj.ac'}${url}`; + const req = superagent + .get(url) + .set('Cookie', this.cookie) + .set('User-Agent', USER_AGENT); + if (this.account.proxy) return req.proxy(this.account.proxy); + return req; + } + + post(url: string) { + logger.debug('post', url, this.cookie); + if (!url.includes('//')) + url = `${this.account.endpoint || 'https://uoj.ac'}${url}`; + const req = superagent + .post(url) + .set('Cookie', this.cookie) + .set('User-Agent', USER_AGENT) + .type('form'); + if (this.account.proxy) return req.proxy(this.account.proxy); + return req; + } + + async getCsrfToken(url: string) { + const { text: html, header } = await this.get(url); + if (header['set-cookie']) { + this.cookie = header['set-cookie']; + } + let value = /_token *: *"(.+?)"/g.exec(html); + if (value) return value[1]; + value = /_token" value="(.+?)"/g.exec(html); + return value?.[1]; + } + + get loggedIn() { + return this.get('/login').then( + ({ text: html }) => !html.includes('登录') + ); + } + + async ensureLogin() { + if (await this.loggedIn) return true; + logger.info('retry login'); + const _token = await this.getCsrfToken('/login'); + const { header, text } = await this.post('/login').send({ + _token, + login: '', + username: this.account.handle, + // NOTE: you should pass a pre-hashed key! + password: this.account.password, + }); + if (header['set-cookie'] && this.cookie.length === 1) { + header['set-cookie'].push(...this.cookie); + this.cookie = header['set-cookie']; + } + if (text === 'ok') return true; + return text; + } + + async submitProblem( + id: string, + lang: string, + code: string, + submissionId: number, + next, + end + ) { + const programType = langs_map[lang] || langs_map['C++']; + const comment = programType.comment; + + if (comment) { + const msg = `S2OJ Submission #${submissionId} @ ${new Date().getTime()}`; + if (typeof comment === 'string') code = `${comment} ${msg}\n${code}`; + else if (comment instanceof Array) + code = `${comment[0]} ${msg} ${comment[1]}\n${code}`; + } + + const _token = await this.getCsrfToken(`/problem/${id}`); + const { text } = await this.post(`/problem/${id}`).send({ + _token, + answer_answer_language: programType.id, + answer_answer_upload_type: 'editor', + answer_answer_editor: code, + 'submit-answer': 'answer', + }); + + if (!text.includes('我的提交记录')) throw new Error('Submit failed'); + + const { text: status } = await this.get( + `/submissions?problem_id=${id}&submitter=${this.account.handle}` + ); + + const $dom = new JSDOM(status); + return $dom.window.document + .querySelector('tbody>tr>td>a') + .innerHTML.split('#')[1]; + } + + async waitForSubmission(problem_id: string, id: string, next, end) { + let i = 0; + + while (true) { + if (++i > 60) { + return await end({ + id, + error: true, + status: 'Judgment Failed', + message: 'Failed to fetch submission details.', + }); + } + + await sleep(2000); + const { text } = await this.get(`/submission/${id}`); + const { + window: { document }, + } = new JSDOM(text); + const find = (content: string) => + Array.from( + document.querySelectorAll('.panel-heading>.panel-title') + ).find(n => n.innerHTML === content).parentElement.parentElement + .children[1]; + if (text.includes('Compile Error')) { + return await end({ + error: true, + id, + status: 'Compile Error', + message: find('详细').children[0].innerHTML, + }); + } + + await next({}); + + const summary = document.querySelector('tbody>tr'); + if (!summary) continue; + const time = parseTimeMS(summary.children[4].innerHTML); + const memory = parseMemoryMB(summary.children[5].innerHTML) * 1024; + let panel = document.getElementById( + 'details_details_accordion_collapse_subtask_1' + ); + if (!panel) { + panel = document.getElementById('details_details_accordion'); + if (!panel) continue; + } + + if (document.querySelector('tbody').innerHTML.includes('Judging')) + continue; + + const score = +summary.children[3]?.children[0]?.innerHTML || 0; + const status = score === 100 ? 'Accepted' : 'Unaccepted'; + + return await end({ + id, + status, + score, + time, + memory, + }); + } + } +} diff --git a/remote_judger/src/proxy.ts b/remote_judger/src/proxy.ts new file mode 100644 index 0000000..aeb9f60 --- /dev/null +++ b/remote_judger/src/proxy.ts @@ -0,0 +1,7 @@ +declare module 'superagent' { + interface Request { + proxy(url: string): this; + } +} + +export default {}; diff --git a/remote_judger/src/utils/htmlspecialchars.ts b/remote_judger/src/utils/htmlspecialchars.ts new file mode 100644 index 0000000..d300a3e --- /dev/null +++ b/remote_judger/src/utils/htmlspecialchars.ts @@ -0,0 +1,11 @@ +export default function htmlspecialchars(text: string) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + + return text.replace(/[&<>"']/g, m => map[m]); +} diff --git a/remote_judger/src/utils/logger.ts b/remote_judger/src/utils/logger.ts new file mode 100644 index 0000000..a72027f --- /dev/null +++ b/remote_judger/src/utils/logger.ts @@ -0,0 +1,11 @@ +import Logger from 'reggol'; + +Logger.levels.base = process.env.DEV ? 3 : 2; +Logger.targets[0].showTime = 'dd hh:mm:ss'; +Logger.targets[0].label = { + align: 'right', + width: 9, + margin: 1, +}; + +export default Logger; diff --git a/remote_judger/src/utils/parse.ts b/remote_judger/src/utils/parse.ts new file mode 100644 index 0000000..e79e3c9 --- /dev/null +++ b/remote_judger/src/utils/parse.ts @@ -0,0 +1,20 @@ +const TIME_RE = /^([0-9]+(?:\.[0-9]*)?)([mu]?)s?$/i; +const TIME_UNITS = { '': 1000, m: 1, u: 0.001 }; +const MEMORY_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i; +const MEMORY_UNITS = { k: 1 / 1024, m: 1, g: 1024 }; + +export function parseTimeMS(str: string | number, throwOnError = true) { + if (typeof str === 'number' || Number.isSafeInteger(+str)) return +str; + const match = TIME_RE.exec(str); + if (!match && throwOnError) throw new Error(`${str} error parsing time`); + if (!match) return 1000; + return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2].toLowerCase()]); +} + +export function parseMemoryMB(str: string | number, throwOnError = true) { + if (typeof str === 'number' || Number.isSafeInteger(+str)) return +str; + const match = MEMORY_RE.exec(str); + if (!match && throwOnError) throw new Error(`${str} error parsing memory`); + if (!match) return 256; + return Math.ceil(parseFloat(match[1]) * MEMORY_UNITS[match[2].toLowerCase()]); +} diff --git a/remote_judger/src/utils/sleep.ts b/remote_judger/src/utils/sleep.ts new file mode 100644 index 0000000..96d32bb --- /dev/null +++ b/remote_judger/src/utils/sleep.ts @@ -0,0 +1,5 @@ +export default function sleep(timeout: number) { + return new Promise(resolve => { + setTimeout(() => resolve(true), timeout); + }); +} diff --git a/remote_judger/src/utils/time.ts b/remote_judger/src/utils/time.ts new file mode 100644 index 0000000..2a4fcd7 --- /dev/null +++ b/remote_judger/src/utils/time.ts @@ -0,0 +1,5 @@ +export const second = 1000; +export const minute = second * 60; +export const hour = minute * 60; +export const day = hour * 24; +export const week = day * 7; diff --git a/remote_judger/src/verdict.ts b/remote_judger/src/verdict.ts new file mode 100644 index 0000000..9f30271 --- /dev/null +++ b/remote_judger/src/verdict.ts @@ -0,0 +1,46 @@ +export function normalize(key: string) { + return key.toUpperCase().replace(/ /g, '_'); +} + +export const VERDICT = new Proxy<Record<string, string>>( + { + RUNTIME_ERROR: 'Runtime Error', + WRONG_ANSWER: 'Wrong Answer', + OK: 'Accepted', + COMPILING: 'Compiling', + TIME_LIMIT_EXCEEDED: 'Time Limit Exceeded', + MEMORY_LIMIT_EXCEEDED: 'Memory Limit Exceeded', + IDLENESS_LIMIT_EXCEEDED: 'Idleness Limit Exceeded', + ACCEPTED: 'Accepted', + PRESENTATION_ERROR: 'Wrong Answer', + OUTPUT_LIMIT_EXCEEDED: 'Output Limit Exceeded', + EXTRA_TEST_PASSED: 'Accepted', + COMPILE_ERROR: 'Compile Error', + 'RUNNING_&_JUDGING': 'Judging', + + // Codeforces + 'HAPPY_NEW_YEAR!': 'Accepted', + + // LibreOJ + COMPILATIONERROR: 'Compile Error', + COMPILEERROR: 'Compile Error', + FILEERROR: 'File Error', + RUNTIMEERROR: 'Runtime Error', + TIMELIMITEXCEEDED: 'Time Limit Exceeded', + MEMORYLIMITEXCEEDED: 'Memory Limit Exceeded', + OUTPUTLIMITEXCEEDED: 'Output Limit Exceeded', + WRONGANSWER: 'Wrong Answer', + PARTIALLYCORRECT: 'Partially Correct', + JUDGEMENTFAILED: 'Judgment Failed', + SYSTEMERROR: 'Judgment Failed', + CONFIGURATIONERROR: 'Judgment Failed', + }, + { + get(self, key) { + if (typeof key === 'symbol') return null; + key = normalize(key); + if (self[key]) return self[key]; + return null; + }, + } +); diff --git a/remote_judger/src/vjudge.ts b/remote_judger/src/vjudge.ts new file mode 100644 index 0000000..ce0035a --- /dev/null +++ b/remote_judger/src/vjudge.ts @@ -0,0 +1,161 @@ +import type { BasicProvider, IBasicProvider, RemoteAccount } from './interface'; +import * as Time from './utils/time'; +import Logger from './utils/logger'; +import htmlspecialchars from './utils/htmlspecialchars'; + +const logger = new Logger('vjudge'); + +class AccountService { + api: IBasicProvider; + + constructor( + public Provider: BasicProvider, + public account: RemoteAccount, + private request: any + ) { + this.api = new Provider(account); + this.main().catch(e => + logger.error(`Error occured in ${account.type}/${account.handle}`, e) + ); + } + + async judge( + id: number, + problem_id: string, + language: string, + code: string, + judge_time: string + ) { + const next = async payload => { + return await this.request('/submit', { + 'update-status': true, + fetch_new: false, + id, + status: + payload.status || + (payload.test_id ? `Judging Test #${payload.test_id}` : 'Judging'), + }); + }; + + const end = async payload => { + if (payload.error) { + return await this.request('/submit', { + submit: true, + fetch_new: false, + id, + result: JSON.stringify({ + status: 'Judged', + score: 0, + error: payload.status, + details: + '<div>' + + `<info-block>ID = ${payload.id || 'None'}</info-block>` + + `<error>${htmlspecialchars(payload.message)}</error>` + + '</div>', + }), + judge_time, + }); + } + + return await this.request('/submit', { + submit: true, + fetch_new: false, + id, + result: JSON.stringify({ + status: 'Judged', + score: payload.score, + time: payload.time, + memory: payload.memory, + details: + payload.details || + '<div>' + + `<info-block>ID = ${payload.id || 'None'}</info-block>` + + `<info-block>VERDICT = ${payload.status}</info-block>` + + '</div>', + }), + judge_time, + }); + }; + + try { + const rid = await this.api.submitProblem( + problem_id, + language, + code, + id, + next, + end + ); + + if (!rid) return; + + await this.api.waitForSubmission(problem_id, rid, next, end); + } catch (e) { + logger.error(e); + await end({ error: true, message: e.message }); + } + } + + async login() { + const login = await this.api.ensureLogin(); + if (login === true) { + logger.info(`${this.account.type}/${this.account.handle}: logged in`); + return true; + } + logger.warn( + `${this.account.type}/${this.account.handle}: login fail`, + login || '' + ); + return false; + } + + async main() { + const res = await this.login(); + if (!res) return; + setInterval(() => this.login(), Time.hour); + } +} + +class VJudge { + private providers: Record<string, AccountService> = {}; + + constructor(private request: any) {} + + async addProvider(type: string) { + if (this.providers[type]) throw new Error(`duplicate provider ${type}`); + const provider = await import(`./providers/${type}`); + const account = provider.getAccountInfoFromEnv(); + + if (!account) throw new Error(`no account info for ${type}`); + + this.providers[type] = new AccountService( + provider.default, + account, + this.request + ); + } + + async judge( + id: number, + type: string, + problem_id: string, + language: string, + code: string, + judge_time: string + ) { + if (!this.providers[type]) throw new Error(`no provider ${type}`); + + this.providers[type].judge(id, problem_id, language, code, judge_time); + } +} + +export async function apply(request: any) { + const vjudge = new VJudge(request); + + await vjudge.addProvider('codeforces'); + await vjudge.addProvider('atcoder'); + await vjudge.addProvider('uoj'); + await vjudge.addProvider('loj'); + + return vjudge; +} diff --git a/remote_judger/tsconfig.json b/remote_judger/tsconfig.json new file mode 100644 index 0000000..48b6c8c --- /dev/null +++ b/remote_judger/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["esnext"], + "allowJs": true, + "skipLibCheck": true, + // "strict": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "incremental": true, + "baseUrl": ".", + "outDir": "./dist" + }, + "include": ["**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/s2oj-backup.sh b/s2oj-backup.sh new file mode 100755 index 0000000..ad2a8f8 --- /dev/null +++ b/s2oj-backup.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +## 0 3 * * * /data/s2oj/s2oj-backup.sh >/dev/null 2>&1 + +if pidof -o %PPID -x "/data/s2oj/s2oj-backup.sh"; then + echo "Already running" + exit 1 +fi + +rclone sync /data/s2oj baoshuo-s3-de-fra2:s2oj --include "/docker-compose.local.yml" --include "/.config.local.php" --include "/uoj_data/web/data/*.zip" --include "/uoj_data/web/storage/**" --transfers=20 --buffer-size=500M --checkers=20 +/usr/local/bin/docker-compose -f /data/s2oj/docker-compose.local.yml exec uoj-db mysqldump -uroot --password="${MYSQL_ROOT_PASSWORD:-root}" app_uoj233 > "/data/s2oj-db-backup/s2oj-database_$(date +'%F-%H%M%S').sql" +find /data/s2oj-db-backup/ -mtime +7 -name "*.sql" -exec rm -rf {} \; +rclone sync /data/s2oj-db-backup/ baoshuo-s3-de-fra2:s2oj-db-backup --buffer-size=500M diff --git a/web/.htaccess b/web/.htaccess index bab8ebe..a99eac7 100644 --- a/web/.htaccess +++ b/web/.htaccess @@ -2,10 +2,10 @@ Options -Indexes php_value session.save_path /var/lib/php/uoj_sessions php_value session.gc_maxlifetime 172800 -php_value session.cookie_lifetime 31536000 +php_value session.cookie_lifetime 604800 -php_value post_max_size 1000M -php_value upload_max_filesize 1000M +php_value post_max_size 1024M +php_value upload_max_filesize 1024M php_value session.gc_probability 1 php_value session.gc_divisor 1000 @@ -16,6 +16,26 @@ DirectorySlash Off DirectoryIndex +<filesMatch "\.(ico|gif|jpg|png)$"> + ExpiresActive On + ExpiresDefault "access plus 6 month" + Header append Cache-Control "public" +</filesMatch> + +<filesMatch "\.(css|js)$"> + ExpiresActive On + ExpiresDefault "access plus 1 week" + Header append Cache-Control "public" +</filesMatch> + +<filesMatch "\.(pdf)$"> + ExpiresActive On + ExpiresDefault "access plus 1 month" + Header append Cache-Control "public" +</filesMatch> + +RequestHeader append X-Author "Baoshuo ( https://baoshuo.ren )" + RewriteEngine On RewriteCond %{QUERY_STRING} ^$ diff --git a/web/Dockerfile b/web/Dockerfile index 94d5602..e9f118a 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -6,7 +6,7 @@ ENV USE_MIRROR $USE_MIRROR SHELL ["/bin/bash", "-c"] ENV DEBIAN_FRONTEND=noninteractive -ENV PKGS="php7.4 php7.4-yaml php7.4-xml php7.4-dev php7.4-zip php7.4-mysql php7.4-mbstring php7.4-gd php7.4-imagick libseccomp-dev git vim ntp zip unzip curl wget apache2 libapache2-mod-xsendfile php-pear mysql-client build-essential fp-compiler re2c libseccomp-dev libyaml-dev python2.7 python3.10 python3-requests openjdk-8-jdk openjdk-11-jdk openjdk-17-jdk" +ENV PKGS="php7.4 php7.4-yaml php7.4-xml php7.4-dev php7.4-zip php7.4-mysql php7.4-mbstring php7.4-gd php7.4-curl php7.4-imagick libseccomp-dev git vim ntp zip unzip curl wget apache2 libapache2-mod-xsendfile php-pear mysql-client build-essential fp-compiler re2c libseccomp-dev libyaml-dev python2.7 python3.10 python3-requests openjdk-8-jdk openjdk-11-jdk openjdk-17-jdk" RUN if [[ "$USE_MIRROR" == "1" ]]; then\ sed -i "s@http://.*archive.ubuntu.com@https://mirrors.aliyun.com@g" /etc/apt/sources.list &&\ sed -i "s@http://.*security.ubuntu.com@https://mirrors.aliyun.com@g" /etc/apt/sources.list ;\ diff --git a/web/app/composer.json b/web/app/composer.json index 8ef8663..c857c4d 100644 --- a/web/app/composer.json +++ b/web/app/composer.json @@ -3,7 +3,10 @@ "gregwar/captcha": "^1.1", "phpmailer/phpmailer": "^6.6", "ezyang/htmlpurifier": "^4.16", - "erusev/parsedown": "^1.7" + "erusev/parsedown": "^1.7", + "php-curl-class/php-curl-class": "^9.13", + "ext-dom": "20031129", + "ivopetkov/html5-dom-document-php": "2.*" }, "autoload": { "classmap": [ diff --git a/web/app/controllers/app/image_hosting/get_image.php b/web/app/controllers/app/image_hosting/get_image.php deleted file mode 100644 index 589663c..0000000 --- a/web/app/controllers/app/image_hosting/get_image.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php - -if (!Auth::check()) { - become403Page(UOJLocale::get('need login')); -} - -$name = $_GET['image_name']; -if (!validateString($name)) { - become404Page(); -} - -$file_name = UOJContext::storagePath() . "/image_hosting/$name.png"; - -$finfo = finfo_open(FILEINFO_MIME); -$mimetype = finfo_file($finfo, $file_name); -if ($mimetype === false) { - become404Page(); -} -finfo_close($finfo); - -header("X-Sendfile: $file_name"); -header("Content-type: $mimetype"); -header("Cache-Control: max-age=604800", true); diff --git a/web/app/controllers/app/html2markdown.php b/web/app/controllers/apps/html2markdown.php similarity index 100% rename from web/app/controllers/app/html2markdown.php rename to web/app/controllers/apps/html2markdown.php diff --git a/web/app/controllers/app/image_hosting/index.php b/web/app/controllers/apps/image_hosting.php similarity index 94% rename from web/app/controllers/app/image_hosting/index.php rename to web/app/controllers/apps/image_hosting.php index c6c60dd..fdc5cca 100644 --- a/web/app/controllers/app/image_hosting/index.php +++ b/web/app/controllers/apps/image_hosting.php @@ -36,9 +36,13 @@ if ($_POST['image_upload_file_submit'] == 'submit') { } if (!isset($_SESSION['phrase']) || !PhraseBuilder::comparePhrases($_SESSION['phrase'], $_POST['captcha'])) { + unset($_SESSION['phrase']); + throwError("bad_captcha"); } + unset($_SESSION['phrase']); + if ($_FILES["image_upload_file"]["error"] > 0) { throwError($_FILES["image_upload_file"]["error"]); } @@ -408,7 +412,7 @@ $pag = new Paginator($pag_config); <?php foreach ($pag->get() as $idx => $row) : ?> <div class="col"> <div class="card"> - <img src="<?= $row['path'] ?>" class="card-img-top" height="200" style="object-fit: contain"> + <img src="<?= $row['path'] ?>" class="card-img-top" height="200" style="object-fit: contain" loading="lazy" decoding="async"> <div class="card-footer bg-transparent small px-2"> <div class="d-flex flex-wrap justify-content-between"> <time><?= $row['upload_time'] ?></time> @@ -454,36 +458,40 @@ $pag = new Paginator($pag_config); <?= $pag->pagination() ?> </div> -<div class="toast-container position-fixed bottom-0 start-0 ms-3 mb-4"> - <div id="copy-url-toast" class="toast text-bg-success align-items-center border-0" role="alert" aria-live="assertive" aria-atomic="true"> - <div class="d-flex"> - <div class="toast-body"> - 复制成功! - </div> - <button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button> - </div> - </div> -</div> - <script> - $(document).ready(function() { - [...document.querySelectorAll('[data-bs-toggle="tooltip"]')].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); - }); - - var copy_url_toast = new bootstrap.Toast('#copy-url-toast', { - delay: 2000 - }); - $('.image-copy-url-button').click(function() { + var _this = this; var url = new URL($(this).data('image-path'), location.origin); - navigator.clipboard.writeText(url); - copy_url_toast.show(); + + navigator.clipboard.writeText(url).then(function() { + $(_this).addClass('btn-success'); + $(_this).removeClass('btn-outline-secondary'); + $(_this).html('<i class="bi bi-check2"></i>'); + + setTimeout(function() { + $(_this).addClass('btn-outline-secondary'); + $(_this).removeClass('btn-success'); + $(_this).html('<i class="bi bi-clipboard"></i>'); + }, 1000); + }); }); $('.image-copy-md-button').click(function() { + var _this = this; var url = new URL($(this).data('image-path'), location.origin); - navigator.clipboard.writeText('![](' + url + ')'); - copy_url_toast.show(); + + navigator.clipboard.writeText('![](' + url + ')').then(function() { + $(_this).addClass('btn-success'); + $(_this).removeClass('btn-outline-secondary'); + $(_this).html('<i class="bi bi-check2"></i>'); + + setTimeout(function() { + $(_this).addClass('btn-outline-secondary'); + $(_this).removeClass('btn-success'); + $(_this).html('<i class="bi bi-markdown"></i>'); + }, 1000); + }); + }); </script> diff --git a/web/app/controllers/contest_manage.php b/web/app/controllers/contest_manage.php index ac6aef0..340a090 100644 --- a/web/app/controllers/contest_manage.php +++ b/web/app/controllers/contest_manage.php @@ -588,11 +588,11 @@ EOD); </tr> EOD, function ($row) { - $problem = UOJProblem::query($row['problem_id']); + $problem = UOJContestProblem::query($row['problem_id'], UOJContest::cur()); echo '<tr>'; echo '<td>', $row['problem_id'], '</td>'; echo '<td>', $problem->getLink(['with' => 'none']), '</td>'; - echo '<td>', isset($contest['extra_config']["problem_{$problem->info['id']}"]) ? $contest['extra_config']["problem_{$problem->info['id']}"] : 'default', '</td>'; + echo '<td>', $problem->getJudgeTypeInContest(), '</td>'; echo '<td>'; echo '<form class="d-inline-block" method="POST" target="_self" onsubmit=\'return confirm("你确定要将题目 #', $problem->info['id'], ' 从比赛中移除吗?")\'>'; echo '<input type="hidden" name="_token" value="', crsf_token(), '">'; diff --git a/web/app/controllers/forgot_pw.php b/web/app/controllers/forgot_pw.php index 71fc4e4..6521434 100644 --- a/web/app/controllers/forgot_pw.php +++ b/web/app/controllers/forgot_pw.php @@ -38,9 +38,13 @@ $forgot_form->handle = function (&$vdata) { $password = $user["password"]; if (!isset($_SESSION['phrase']) || !PhraseBuilder::comparePhrases($_SESSION['phrase'], $_POST['captcha'])) { + unset($_SESSION['phrase']); + becomeMsgPage('验证码错误!'); } + unset($_SESSION['phrase']); + if (!$user['email']) { becomeMsgPage('用户未填写邮件地址,请联系管理员重置!'); } @@ -99,7 +103,6 @@ EOD; } }; $forgot_form->submit_button_config['align'] = 'offset'; - $forgot_form->runAtServer(); ?> <?php echoUOJPageHeader('找回密码') ?> diff --git a/web/app/controllers/group.php b/web/app/controllers/group.php index 82e4eed..26a40b4 100644 --- a/web/app/controllers/group.php +++ b/web/app/controllers/group.php @@ -134,16 +134,15 @@ UOJGroup::cur()->userCanView(Auth::user(), ['ensure' => true]); </div> </div> - <div class="card card-default mb-3"> - <div class="card-body"> - <h2 class="card-title h3"> - <?= UOJLocale::get('top solver') ?> - </h2> - <?php UOJRanklist::printHTML([ - 'page_len' => 15, - 'group_id' => UOJGroup::info('id'), - ]) ?> + <div class="card mb-3"> + <div class="card-header bg-transparent"> + <h2 class="h3 mb-0"><?= UOJLocale::get('top solver') ?></h2> </div> + <?php UOJRanklist::printHTML([ + 'page_len' => 15, + 'group_id' => UOJGroup::info('id'), + 'flush' => true, + ]) ?> </div> <!-- end left col --> </div> diff --git a/web/app/controllers/index.php b/web/app/controllers/index.php index 10d91f6..19ce86d 100644 --- a/web/app/controllers/index.php +++ b/web/app/controllers/index.php @@ -60,15 +60,15 @@ $friend_links = DB::selectAll([ </div> </div> <?php if (Auth::check()) : ?> - <div class="mt-4 card"> - <div class="card-body"> - <h4 class="card-title mb-2"><?= UOJLocale::get('top solver') ?></h4> - <?php UOJRanklist::printHTML(['top10' => true]) ?> - <div class="text-center mt-2"> - <a href="/solverlist" class="text-decoration-none"> - <?= UOJLocale::get('view all') ?> - </a> - </div> + <div class="card mt-4"> + <div class="card-header bg-transparent"> + <h4 class="mb-0"><?= UOJLocale::get('top solver') ?></h4> + </div> + <?php UOJRanklist::printHTML(['top10' => true, 'flush' => true]) ?> + <div class="card-footer bg-transparent text-center"> + <a href="/solverlist"> + <?= UOJLocale::get('view all') ?> + </a> </div> </div> <?php else : ?> diff --git a/web/app/controllers/judge/submit.php b/web/app/controllers/judge/submit.php index d6c7913..59e25c0 100644 --- a/web/app/controllers/judge/submit.php +++ b/web/app/controllers/judge/submit.php @@ -130,17 +130,39 @@ if (isset($_POST['update-status'])) { die(); } -$problem_ban_list = DB::selectAll([ +$assignCond = []; + +$problem_ban_list = array_map(fn ($x) => $x['id'], DB::selectAll([ "select id from problems", "where", [ ["assigned_to_judger", "!=", "any"], ["assigned_to_judger", "!=", $_POST['judger_name']] ] -]); -foreach ($problem_ban_list as &$val) { - $val = $val['id']; +])); + +if ($problem_ban_list) { + $assignCond[] = ["problem_id", "not in", DB::rawtuple($problem_ban_list)]; +} + +if ($_POST['judger_name'] == "remote_judger") { + $problem_ban_list = array_map(fn ($x) => $x['id'], DB::selectAll([ + "select id from problems", + "where", [ + ["type", "!=", "remote"], + ], + ])); +} else { + $problem_ban_list = array_map(fn ($x) => $x['id'], DB::selectAll([ + "select id from problems", + "where", [ + ["type", "!=", "local"], + ], + ])); +} + +if ($problem_ban_list) { + $assignCond[] = ["problem_id", "not in", DB::rawtuple($problem_ban_list)]; } -$assignCond = $problem_ban_list ? [["problem_id", "not in", DB::rawtuple($problem_ban_list)]] : []; $submission = null; $hack = null; diff --git a/web/app/controllers/list.php b/web/app/controllers/list.php index ad1fcf7..8060b9e 100644 --- a/web/app/controllers/list.php +++ b/web/app/controllers/list.php @@ -20,6 +20,9 @@ function getProblemTR($info) { if ($problem->isUserOwnProblem(Auth::user())) { $html .= ' <span class="badge text-white bg-info">' . UOJLocale::get('problems::my problem') . '</span> '; } + if ($info['type'] == 'remote') { + HTML::tag('a', ['class' => 'badge text-bg-success', 'href' => '/problems/remote'], '远端评测题'); + } if ($info['is_hidden']) { $html .= ' <span class="badge text-bg-danger"><i class="bi bi-eye-slash-fill"></i> ' . UOJLocale::get('hidden') . '</span> '; } @@ -74,15 +77,7 @@ $pag_config = [ 'page_len' => 20, 'col_names' => [ 'best_ac_submissions.submission_id as submission_id', - 'problems.id as id', - 'problems.is_hidden as is_hidden', - 'problems.title as title', - 'problems.submit_num as submit_num', - 'problems.ac_num as ac_num', - 'problems.zan as zan', - 'problems.difficulty as difficulty', - 'problems.extra_config as extra_config', - 'problems.uploader as uploader', + 'problems.*', ], 'table_name' => [ "problems", diff --git a/web/app/controllers/new_remote_problem.php b/web/app/controllers/new_remote_problem.php new file mode 100644 index 0000000..63cf369 --- /dev/null +++ b/web/app/controllers/new_remote_problem.php @@ -0,0 +1,167 @@ +<?php +requireLib('bootstrap5'); +requirePHPLib('form'); +requirePHPLib('data'); + +Auth::check() || redirectToLogin(); +UOJProblem::userCanCreateProblem(Auth::user()) || UOJResponse::page403(); + +$new_remote_problem_form = new UOJForm('new_remote_problem'); +$new_remote_problem_form->addSelect('remote_online_judge', [ + 'label' => '远程 OJ', + 'options' => array_map(fn ($provider) => $provider['name'], UOJRemoteProblem::$providers), +]); +$new_remote_problem_form->addInput('remote_problem_id', [ + 'div_class' => 'mt-3', + 'label' => '远程 OJ 上的题目 ID', + 'validator_php' => function ($id, &$vdata) { + $remote_oj = $_POST['remote_online_judge']; + if ($remote_oj === 'codeforces') { + $id = trim(strtoupper($id)); + + if (!validateCodeforcesProblemId($id)) { + return '不合法的题目 ID'; + } + + $vdata['remote_problem_id'] = $id; + + return ''; + } else if ($remote_oj === 'atcoder') { + $id = trim(strtolower($id)); + + if (!validateString($id)) { + return '不合法的题目 ID'; + } + + $vdata['remote_problem_id'] = $id; + + return ''; + } else if ($remote_oj === 'uoj') { + if (!validateUInt($id)) { + return '不合法的题目 ID'; + } + + $vdata['remote_problem_id'] = $id; + + return ''; + } else if ($remote_oj === 'loj') { + if (!validateUInt($id)) { + return '不合法的题目 ID'; + } + + $vdata['remote_problem_id'] = $id; + + return ''; + } + + return '不合法的远程 OJ 类型'; + }, +]); +$new_remote_problem_form->handle = function (&$vdata) { + $remote_online_judge = $_POST['remote_online_judge']; + $remote_problem_id = $vdata['remote_problem_id']; + $remote_provider = UOJRemoteProblem::$providers[$remote_online_judge]; + + try { + $data = UOJRemoteProblem::getProblemBasicInfo($remote_online_judge, $remote_problem_id); + } catch (Exception $e) { + $data = null; + UOJLog::error($e->getMessage()); + } + + if ($data === null) { + UOJResponse::page500('题目抓取失败,可能是题目不存在或者没有题面!如果题目没有问题,请稍后再试。<a href="">返回</a>'); + } + + $submission_requirement = [ + [ + "name" => "answer", + "type" => "source code", + "file_name" => "answer.code", + "languages" => $remote_provider['languages'], + ] + ]; + $enc_submission_requirement = json_encode($submission_requirement); + + $extra_config = [ + 'remote_online_judge' => $remote_online_judge, + 'remote_problem_id' => $remote_problem_id, + 'time_limit' => $data['time_limit'], + 'memory_limit' => $data['memory_limit'], + ]; + $enc_extra_config = json_encode($extra_config); + + DB::insert([ + "insert into problems", + "(title, uploader, is_hidden, submission_requirement, extra_config, difficulty, type)", + "values", DB::tuple([$data['title'], Auth::id(), 1, $enc_submission_requirement, $enc_extra_config, $data['difficulty'] ?: -1, "remote"]) + ]); + + $id = DB::insert_id(); + dataNewProblem($id); + + if ($data['type'] == 'pdf') { + file_put_contents(UOJContext::storagePath(), "/problem_resources/$id/statement.pdf", $data['pdf_data']); + $data['statement'] = "<div data-pdf data-src=\"/problem/$id/resources/statement.pdf\"></div>\n" . $data['statement']; + } + + DB::insert([ + "insert into problems_contents", + "(id, remote_content, statement, statement_md)", + "values", + DB::tuple([$id, HTML::purifier(['a' => ['target' => 'Enum#_blank']])->purify($data['statement']), '', '']) + ]); + + DB::insert([ + "insert into problems_tags", + "(problem_id, tag)", + "values", + DB::tuple([$id, $remote_provider['name']]), + ]); + + UOJRemoteProblem::downloadImagesInRemoteContent(strval($id)); + + redirectTo("/problem/{$id}"); + die(); +}; +$new_remote_problem_form->runAtServer(); +?> + +<?php echoUOJPageHeader('导入远程题库') ?> + +<h1>导入远程题库</h1> + +<div class="row"> + <div class="col-md-9"> + <div class="card"> + <div class="card-body"> + <div class="row"> + <div class="col-md-6"> + <?php $new_remote_problem_form->printHTML() ?> + </div> + <div class="col-md-6 mt-3 mt-md-0"> + <h4>使用帮助</h4> + <ul> + <li> + <p>目前支持导入以下题库的题目作为远端评测题:</p> + <ul class="mb-3"> + <li><a href="https://codeforces.com/problemset">Codeforces</a></li> + <li><a href="https://codeforces.com/gyms">Codeforces::Gym</a>(题号前加 <code>GYM</code>)</li> + <li><a href="https://atcoder.jp/contests/archive">AtCoder</a></li> + <li><a href="https://uoj.ac/problems">UniversalOJ</a></li> + <li><a href="https://loj.ac/p">LibreOJ</a></li> + </ul> + </li> + <li>在导入题目前请先搜索题库中是否已经存在相应题目,避免重复添加。</li> + </ul> + </div> + </div> + </div> + </div> + </div> + <div class="col-md-3"> + <?php uojIncludeView('sidebar') ?> + </div> +</div> + +<?php echoUOJPageFooter() ?> diff --git a/web/app/controllers/problem.php b/web/app/controllers/problem.php index 40757b3..1575b6d 100644 --- a/web/app/controllers/problem.php +++ b/web/app/controllers/problem.php @@ -2,6 +2,7 @@ requireLib('bootstrap5'); requireLib('hljs'); requireLib('mathjax'); +requireLib('pdf.js'); requirePHPLib('form'); requirePHPLib('judger'); @@ -223,7 +224,6 @@ if (UOJContest::cur()) { <div class="row"> <!-- Left col --> <div class="col-lg-9"> - <?php if (isset($tabs_info)) : ?> <!-- 比赛导航 --> <div class="mb-2"> @@ -233,7 +233,6 @@ if (UOJContest::cur()) { <div class="card card-default mb-2"> <div class="card-body"> - <h1 class="card-title text-center"> <?php if (UOJContest::cur()) : ?> <?= UOJProblem::cur()->getTitle(['with' => 'letter', 'simplify' => true]) ?> @@ -243,8 +242,13 @@ if (UOJContest::cur()) { </h1> <?php - $time_limit = $conf instanceof UOJProblemConf ? $conf->getVal('time_limit', 1) : null; - $memory_limit = $conf instanceof UOJProblemConf ? $conf->getVal('memory_limit', 256) : null; + if (UOJProblem::info('type') == 'local') { + $time_limit = $conf instanceof UOJProblemConf ? $conf->getVal('time_limit', 1) : null; + $memory_limit = $conf instanceof UOJProblemConf ? $conf->getVal('memory_limit', 256) : null; + } else if (UOJProblem::info('type') == 'remote') { + $time_limit = UOJProblem::cur()->getExtraConfig('time_limit'); + $memory_limit = UOJProblem::cur()->getExtraConfig('memory_limit'); + } ?> <div class="text-center small"> 时间限制: <?= $time_limit ? "$time_limit s" : "N/A" ?> @@ -259,6 +263,14 @@ if (UOJContest::cur()) { <article class="mt-3 markdown-body"> <?= $problem_content['statement'] ?> </article> + + <?php if (UOJProblem::info('type') == 'remote') : ?> + <hr> + + <article class="mt-3 markdown-body remote-content"> + <?= UOJProblem::cur()->queryContent()['remote_content'] ?> + </article> + <?php endif ?> </div> <div class="tab-pane" id="submit"> <?php if ($pre_submit_check_ret !== true) : ?> @@ -392,6 +404,14 @@ if (UOJContest::cur()) { <?= UOJProblem::cur()->getUploaderLink() ?> </span> </li> + <li class="list-group-item d-flex justify-content-between align-items-center"> + <span class="flex-shrink-0"> + 题目来源 + </span> + <span> + <?= UOJProblem::cur()->getProviderLink() ?> + </span> + </li> <?php if (!UOJContest::cur() || UOJContest::cur()->progress() >= CONTEST_FINISHED) : ?> <li class="list-group-item d-flex justify-content-between align-items-center"> <span class="flex-shrink-0"> @@ -448,23 +468,24 @@ if (UOJContest::cur()) { </div> <!-- 附件 --> - <div class="card card-default mb-2"> - <ul class="nav nav-fill flex-column"> + <div class="card mb-2"> + <div class="card-header fw-bold">附件</div> + <div class="list-group list-group-flush"> <?php if (UOJProblem::cur()->userCanDownloadTestData(Auth::user())) : ?> - <li class="nav-item text-start"> - <a class="nav-link" href="<?= HTML::url("/download/problem/{$problem['id']}/data.zip") ?>"> - <i class="bi bi-hdd-stack"></i> - 测试数据 - </a> - </li> - <?php endif ?> - <li class="nav-item text-start"> - <a class="nav-link" href="<?= HTML::url("/download/problem/{$problem['id']}/attachment.zip") ?>"> - <i class="bi bi-download"></i> - 附件下载 + <a class="list-group-item list-group-item-action" href="<?= HTML::url(UOJProblem::cur()->getMainDataUri()) ?>"> + <i class="bi bi-hdd-stack"></i> + 测试数据 </a> - </li> - </ul> + <?php endif ?> + <a class="list-group-item list-group-item-action" href="<?= HTML::url(UOJProblem::cur()->getAttachmentUri()) ?>"> + <i class="bi bi-download"></i> + 附件下载 + </a> + <a class="list-group-item list-group-item-action" href="<?= HTML::url(UOJProblem::cur()->getResourcesBaseUri()) ?>"> + <i class="bi bi-folder2-open"></i> + 相关资源 + </a> + </div> </div> <?php diff --git a/web/app/controllers/problem_data_manage.php b/web/app/controllers/problem_data_manage.php index 65deeb9..91112c7 100644 --- a/web/app/controllers/problem_data_manage.php +++ b/web/app/controllers/problem_data_manage.php @@ -10,10 +10,11 @@ requirePHPLib('data'); UOJProblem::init(UOJRequest::get('id')) || UOJResponse::page404(); UOJProblem::cur()->userCanManage(Auth::user()) || UOJResponse::page403(); +UOJProblem::info('type') === 'local' || UOJResponse::page404(); $problem = UOJProblem::info(); $problem_extra_config = UOJProblem::cur()->getExtraConfig(); -$data_dir = "/var/uoj_data/${problem['id']}"; +$data_dir = "/var/uoj_data/{$problem['id']}"; function echoFileNotFound($file_name) { echo '<h5>', htmlspecialchars($file_name), '</h5>'; @@ -466,93 +467,6 @@ $rejudgege97_form->submit_button_config['class_str'] = 'btn btn-danger d-block w $rejudgege97_form->submit_button_config['text'] = '重测 >=97 的程序'; $rejudgege97_form->submit_button_config['smart_confirm'] = ''; -$view_type_form = new UOJBs4Form('view_type'); -$view_type_form->addVSelect( - 'view_content_type', - array( - 'NONE' => '禁止', - 'ALL_AFTER_AC' => 'AC后', - 'ALL' => '所有人' - ), - '查看提交文件:', - $problem_extra_config['view_content_type'] -); -$view_type_form->addVSelect( - 'view_all_details_type', - array( - 'NONE' => '禁止', - 'SELF' => '仅自己', - 'ALL_AFTER_AC' => 'AC后', - 'ALL' => '所有人' - ), - '查看全部详细信息:', - $problem_extra_config['view_all_details_type'] -); -$view_type_form->addVSelect( - 'view_details_type', - array( - 'NONE' => '禁止', - 'SELF' => '仅自己', - 'ALL_AFTER_AC' => 'AC后', - 'ALL' => '所有人' - ), - '查看测试点详细信息:', - $problem_extra_config['view_details_type'] -); -$view_type_form->handle = function () { - global $problem, $problem_extra_config; - - $config = $problem_extra_config; - $config['view_content_type'] = $_POST['view_content_type']; - $config['view_all_details_type'] = $_POST['view_all_details_type']; - $config['view_details_type'] = $_POST['view_details_type']; - $esc_config = json_encode($config); - - DB::update([ - "update problems", - "set", ["extra_config" => $esc_config], - "where", ["id" => $problem['id']] - ]); -}; -$view_type_form->submit_button_config['class_str'] = 'btn btn-warning d-block w-100 mt-2'; - -$solution_view_type_form = new UOJBs4Form('solution_view_type'); -$solution_view_type_form->addVSelect( - 'view_solution_type', - array( - 'NONE' => '禁止', - 'ALL_AFTER_AC' => 'AC后', - 'ALL' => '所有人' - ), - '查看题解:', - $problem_extra_config['view_solution_type'] -); -$solution_view_type_form->addVSelect( - 'submit_solution_type', - array( - 'NONE' => '禁止', - 'ALL_AFTER_AC' => 'AC后', - 'ALL' => '所有人' - ), - '提交题解:', - $problem_extra_config['submit_solution_type'] -); -$solution_view_type_form->handle = function () { - global $problem, $problem_extra_config; - - $config = $problem_extra_config; - $config['view_solution_type'] = $_POST['view_solution_type']; - $config['submit_solution_type'] = $_POST['submit_solution_type']; - $esc_config = json_encode($config); - - DB::update([ - "update problems", - "set", ["extra_config" => $esc_config], - "where", ["id" => $problem['id']] - ]); -}; -$solution_view_type_form->submit_button_config['class_str'] = 'btn btn-warning d-block w-100 mt-2'; - if ($problem['hackable']) { $test_std_form = new UOJBs4Form('test_std'); $test_std_form->handle = function () use ($problem, $data_disp) { @@ -618,8 +532,6 @@ if ($problem['hackable']) { } $hackable_form->runAtServer(); -$view_type_form->runAtServer(); -$solution_view_type_form->runAtServer(); $data_form->runAtServer(); $clear_data_form->runAtServer(); $rejudge_form->runAtServer(); @@ -753,18 +665,6 @@ $info_form->runAtServer(); <?php $test_std_form->printHTML() ?> </div> <?php endif ?> - <div class="mt-2"> - <button id="button-display_view_type" type="button" class="btn btn-primary d-block w-100" onclick="$('#div-view_type').toggle('fast');">提交记录可视权限</button> - <div class="mt-2" id="div-view_type" style="display:none; padding-left:5px; padding-right:5px;"> - <?php $view_type_form->printHTML(); ?> - </div> - </div> - <div class="mt-2"> - <button id="button-solution_view_type" type="button" class="btn btn-primary d-block w-100" onclick="$('#div-solution_view_type').toggle('fast');">题解可视权限</button> - <div class="mt-2" id="div-solution_view_type" style="display:none; padding-left:5px; padding-right:5px;"> - <?php $solution_view_type_form->printHTML(); ?> - </div> - </div> <div class="mt-2"> <?php $data_form->printHTML(); ?> </div> diff --git a/web/app/controllers/problem_managers_manage.php b/web/app/controllers/problem_managers_manage.php index 23bba87..9e6dd9a 100644 --- a/web/app/controllers/problem_managers_manage.php +++ b/web/app/controllers/problem_managers_manage.php @@ -91,11 +91,13 @@ if (isSuperUser(Auth::user())) { 管理者 </a> </li> - <li class="nav-item"> - <a class="nav-link" href="/problem/<?= UOJProblem::info('id') ?>/manage/data" role="tab"> - 数据 - </a> - </li> + <?php if (UOJProblem::info('type') === 'local') : ?> + <li class="nav-item"> + <a class="nav-link" href="/problem/<?= UOJProblem::info('id') ?>/manage/data" role="tab"> + 数据 + </a> + </li> + <?php endif ?> </ul> <div class="card card-default"> diff --git a/web/app/controllers/problem_resources.php b/web/app/controllers/problem_resources.php new file mode 100644 index 0000000..fb59db5 --- /dev/null +++ b/web/app/controllers/problem_resources.php @@ -0,0 +1,53 @@ +<?php +requireLib('bootstrap5'); +requireLib('hljs'); + +Auth::check() || redirectToLogin(); +UOJProblem::init(UOJRequest::get('id')) || UOJResponse::page404(); + +$problem = UOJProblem::cur()->info; +$problem_content = UOJProblem::cur()->queryContent(); +$user_can_view = UOJProblem::cur()->userCanView(Auth::user()); + +if (!$user_can_view) { + foreach (UOJProblem::cur()->findInContests() as $cp) { + if ($cp->contest->progress() >= CONTEST_IN_PROGRESS && $cp->contest->userHasRegistered(Auth::user())) { + $user_can_view = true; + + break; + } + } +} + +if (!$user_can_view) { + UOJResponse::page403(); +} + +// Create directory if not exists +if (!is_dir(UOJProblem::cur()->getResourcesPath())) { + mkdir(UOJProblem::cur()->getResourcesPath(), 0755, true); +} + +define('APP_TITLE', '题目资源 - ' . UOJProblem::cur()->getTitle(['with' => false])); +define('FM_EMBED', true); +define('FM_DISABLE_COLS', true); +define('FM_DATETIME_FORMAT', UOJTime::FORMAT); +define('FM_ROOT_PATH', UOJProblem::cur()->getResourcesFolderPath()); +define('FM_ROOT_URL', UOJProblem::cur()->getResourcesBaseUri()); + +$sub_path = UOJRequest::get('sub_path', 'is_string', ''); + +if ($sub_path) { + $filepath = realpath(UOJProblem::cur()->getResourcesPath($sub_path)); + $realbasepath = realpath(UOJProblem::cur()->getResourcesPath()); + + if (!str_starts_with($filepath, $realbasepath)) { + UOJResponse::page406(); + } + + UOJResponse::xsendfile($filepath); +} + +$global_readonly = !UOJProblem::cur()->userCanManage(Auth::user()); + +include(__DIR__ . '/tinyfilemanager/tinyfilemanager.php'); diff --git a/web/app/controllers/problem_set.php b/web/app/controllers/problem_set.php index 474a3d2..284e779 100644 --- a/web/app/controllers/problem_set.php +++ b/web/app/controllers/problem_set.php @@ -77,10 +77,10 @@ EOD; redirectTo("/problem/{$id}/manage/statement"); die(); }; - $new_problem_form->config['submit_container']['class'] = 'text-end'; - $new_problem_form->config['submit_button']['class'] = 'btn btn-primary'; - $new_problem_form->config['submit_button']['text'] = UOJLocale::get('problems::add new'); - $new_problem_form->config['confirm']['smart'] = true; + $new_problem_form->config['submit_container']['class'] = ''; + $new_problem_form->config['submit_button']['class'] = 'bg-transparent border-0 d-block w-100 px-3 py-2 text-start'; + $new_problem_form->config['submit_button']['text'] = '<i class="bi bi-plus-lg"></i> 新建本地题目'; + $new_problem_form->config['confirm']['text'] = '你真的要添加新题吗?'; $new_problem_form->runAtServer(); } @@ -94,6 +94,9 @@ function getProblemTR($info) { if ($problem->isUserOwnProblem(Auth::user())) { $html .= ' <a href="/problems?my=on"><span class="badge text-white bg-info">' . UOJLocale::get('problems::my problem') . '</span></a> '; } + if ($info['type'] == 'remote') { + $html .= ' ' . HTML::tag('a', ['class' => 'badge text-bg-success', 'href' => '/problems/remote'], '远端评测题'); + } if ($info['is_hidden']) { $html .= ' <a href="/problems?is_hidden=on"><span class="badge text-bg-danger"><i class="bi bi-eye-slash-fill"></i> ' . UOJLocale::get('hidden') . '</span></a> '; } @@ -141,6 +144,8 @@ $search_is_effective = false; $cur_tab = UOJRequest::get('tab', 'is_string', 'all'); if ($cur_tab == 'template') { $search_tag = "模板题"; +} else if ($cur_tab == 'remote') { + $cond['type'] = 'remote'; } if (is_string($search_tag)) { $cond[] = [ @@ -199,16 +204,20 @@ $header .= '<th class="text-center" style="width:4em;">' . UOJLocale::get('probl $header .= '<th class="text-center" style="width:50px;">' . UOJLocale::get('appraisal') . '</th>'; $header .= '</tr>'; -$tabs_info = array( - 'all' => array( +$tabs_info = [ + 'all' => [ 'name' => UOJLocale::get('problems::all problems'), 'url' => "/problems" - ), - 'template' => array( + ], + 'template' => [ 'name' => UOJLocale::get('problems::template problems'), 'url' => "/problems/template" - ) -); + ], + 'remote' => [ + 'name' => UOJLocale::get('problems::remote problems'), + 'url' => "/problems/remote" + ], +]; $pag = new Paginator([ 'col_names' => ['*'], @@ -253,43 +262,35 @@ $pag = new Paginator([ <!-- left col --> <div class="col-md-9"> <!-- title --> - <div class="d-flex justify-content-between"> + <div class="d-flex justify-content-between flex-wrap"> <h1> <?= UOJLocale::get('problems') ?> </h1> - <?php if (isset($new_problem_form)) : ?> - <div class="text-end"> - <?php $new_problem_form->printHTML(); ?> - </div> - <?php endif ?> - + <div> + <?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills') ?> + </div> </div> <!-- end title --> - <div class="row"> - <div class="col-sm-4 col-12"> - <?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills') ?> - </div> - <div class="text-end p-2 col-12 col-sm-8"> - <div class="form-check d-inline-block me-2"> - <input type="checkbox" id="input-show_tags_mode" class="form-check-input" <?= isset($_COOKIE['show_tags_mode']) ? 'checked="checked" ' : '' ?> /> - <label class="form-check-label" for="input-show_tags_mode"> - <?= UOJLocale::get('problems::show tags') ?> - </label> - </div> + <?= $pag->pagination() ?> - <div class="form-check d-inline-block"> - <input type="checkbox" id="input-show_submit_mode" class="form-check-input" <?= isset($_COOKIE['show_submit_mode']) ? 'checked="checked" ' : '' ?> /> - <label class="form-check-label" for="input-show_submit_mode"> - <?= UOJLocale::get('problems::show statistics') ?> - </label> - </div> + <div class="text-end"> + <div class="form-check d-inline-block me-2"> + <input type="checkbox" id="input-show_tags_mode" class="form-check-input" <?= isset($_COOKIE['show_tags_mode']) ? 'checked="checked" ' : '' ?> /> + <label class="form-check-label" for="input-show_tags_mode"> + <?= UOJLocale::get('problems::show tags') ?> + </label> + </div> + + <div class="form-check d-inline-block"> + <input type="checkbox" id="input-show_submit_mode" class="form-check-input" <?= isset($_COOKIE['show_submit_mode']) ? 'checked="checked" ' : '' ?> /> + <label class="form-check-label" for="input-show_submit_mode"> + <?= UOJLocale::get('problems::show statistics') ?> + </label> </div> </div> - <?= $pag->pagination() ?> - <script type="text/javascript"> $('#input-show_tags_mode').click(function() { if (this.checked) { @@ -389,6 +390,23 @@ $pag = new Paginator([ }); </script> + <?php if (UOJProblem::userCanCreateProblem(Auth::user())) : ?> + <div class="card mb-3"> + <div class="card-header fw-bold"> + 新建题目 + </div> + <div class="list-group list-group-flush"> + <div class="list-group-item list-group-item-action p-0"> + <?php $new_problem_form->printHTML() ?> + </div> + <a class="list-group-item list-group-item-action" href="/problems/remote/new"> + <i class="bi bi-cloud-plus"></i> + 新建远端评测题目 + </a> + </div> + </div> + <?php endif ?> + <!-- sidebar --> <?php uojIncludeView('sidebar') ?> </aside> diff --git a/web/app/controllers/problem_solutions.php b/web/app/controllers/problem_solutions.php index 143c6f0..59d9747 100644 --- a/web/app/controllers/problem_solutions.php +++ b/web/app/controllers/problem_solutions.php @@ -2,6 +2,7 @@ requireLib('bootstrap5'); requireLib('hljs'); requireLib('mathjax'); +requireLib('pdf.js'); requirePHPLib('form'); requirePHPLib('judger'); @@ -119,6 +120,12 @@ if (UOJProblem::cur()->userCanManage(Auth::user()) || UOJProblem::cur()->userPer $blog_id = DB::insert_id(); + DB::insert([ + "insert into problems_solutions", + DB::bracketed_fields(["problem_id", "blog_id"]), + "values", DB::tuple([UOJProblem::info('id'), $blog_id]), + ]); + redirectTo(HTML::blog_url(Auth::id(), "/post/{$blog_id}/write")); die(); }; diff --git a/web/app/controllers/problem_statement_manage.php b/web/app/controllers/problem_statement_manage.php index 3aa2e63..55dcb6a 100644 --- a/web/app/controllers/problem_statement_manage.php +++ b/web/app/controllers/problem_statement_manage.php @@ -65,9 +65,12 @@ $problem_editor->runAtServer(); $difficulty_form = new UOJForm('difficulty'); $difficulty_form->addSelect('difficulty', [ + 'div_class' => 'flex-grow-1', 'options' => [-1 => '暂无评定'] + array_combine(UOJProblem::$difficulty, UOJProblem::$difficulty), 'default_value' => UOJProblem::info('difficulty'), ]); +$difficulty_form->config['form']['class'] = 'd-flex'; +$difficulty_form->config['submit_container']['class'] = 'ms-2'; $difficulty_form->handle = function () { DB::update([ "update problems", @@ -80,6 +83,211 @@ $difficulty_form->handle = function () { ]); }; $difficulty_form->runAtServer(); + +if (UOJProblem::info('type') == 'remote') { + $remote_online_judge = UOJProblem::cur()->getExtraConfig('remote_online_judge'); + $remote_problem_id = UOJProblem::cur()->getExtraConfig('remote_problem_id'); + $remote_provider = UOJRemoteProblem::$providers[$remote_online_judge]; + + $re_crawl_form = new UOJForm('re_crawl'); + $re_crawl_form->appendHTML(<<<EOD + <ul> + <li>远程题库:{$remote_provider['name']}</li> + <li>远程题号:{$remote_problem_id}</li> + </ul> + EOD); + $re_crawl_form->config['submit_button']['text'] = '重新爬取'; + $re_crawl_form->handle = function () use ($remote_online_judge, $remote_problem_id, $remote_provider) { + try { + $data = UOJRemoteProblem::getProblemBasicInfo($remote_online_judge, $remote_problem_id); + } catch (Exception $e) { + $data = null; + UOJLog::error($e->getMessage()); + } + + if ($data === null) { + UOJResponse::page500('题目抓取失败,可能是题目不存在或者没有题面!如果题目没有问题,请稍后再试。<a href="">返回</a>'); + } + + if ($data['difficulty'] == -1) { + $data['difficulty'] = UOJProblem::info('difficulty'); + } + + $submission_requirement = [ + [ + "name" => "answer", + "type" => "source code", + "file_name" => "answer.code", + "languages" => $remote_provider['languages'], + ] + ]; + $enc_submission_requirement = json_encode($submission_requirement); + + $extra_config = [ + 'remote_online_judge' => $remote_online_judge, + 'remote_problem_id' => $remote_problem_id, + 'time_limit' => $data['time_limit'], + 'memory_limit' => $data['memory_limit'], + ]; + $enc_extra_config = json_encode($extra_config); + + DB::update([ + "update problems", + "set", [ + "title" => $data['title'], + "submission_requirement" => $enc_submission_requirement, + "extra_config" => $enc_extra_config, + "difficulty" => $data['difficulty'] ?: -1, + ], + "where", [ + "id" => UOJProblem::info('id'), + ], + ]); + + if ($data['type'] == 'pdf') { + file_put_contents(UOJContext::storagePath() . "/problem_resources/" . UOJProblem::info('id') . "/statement.pdf", $data['pdf_data']); + $data['statement'] = '<div data-pdf data-src="/problem/' . UOJProblem::info('id') . '/resources/statement.pdf"></div>' . "\n" . $data['statement']; + } + + DB::update([ + "update problems_contents", + "set", [ + "remote_content" => HTML::purifier(['a' => ['target' => 'Enum#_blank']])->purify($data['statement']), + ], + "where", [ + "id" => UOJProblem::info('id'), + ], + ]); + + UOJRemoteProblem::downloadImagesInRemoteContent(UOJProblem::info('id')); + + redirectTo(UOJProblem::cur()->getUri()); + }; + $re_crawl_form->runAtServer(); + + $convert_local_form = new UOJForm('convert_local'); + $convert_local_form->handle = function () { + DB::update([ + "update problems", + "set", [ + "type" => 'local', + "submission_requirement" => "{}", + "extra_config" => "{}", + ], + "where", [ + "id" => UOJProblem::info('id'), + ], + ]); + + DB::update([ + "update problems_contents", + "set", [ + "remote_content" => '', + ], + "where", [ + "id" => UOJProblem::info('id'), + ], + ]); + }; + $convert_local_form->config['submit_container']['class'] = ''; + $convert_local_form->config['submit_button']['class'] = 'btn btn-danger'; + $convert_local_form->config['submit_button']['text'] = '将本题转换为本地题目(不可逆)'; + $convert_local_form->config['confirm']['text'] = '您真的要*不可逆*地将本题转换为本地题目吗?'; + $convert_local_form->runAtServer(); +} + +$view_type_form = new UOJForm('view_type'); +$view_type_form->addSelect('view_content_type', [ + 'div_class' => 'row align-items-center g-0', + 'label_class' => 'form-label col-auto m-0 flex-grow-1', + 'select_class' => 'col-auto form-select w-auto', + 'label' => '查看提交文件', + 'options' => [ + 'NONE' => '禁止', + 'ALL_AFTER_AC' => 'AC 后', + 'ALL' => '所有人', + ], + 'default_value' => UOJProblem::cur()->getExtraConfig('view_content_type'), +]); +$view_type_form->addSelect('view_all_details_type', [ + 'div_class' => 'row align-items-center g-0 mt-3', + 'label_class' => 'form-label col-auto m-0 flex-grow-1', + 'select_class' => 'col-auto form-select w-auto', + 'label' => '查看全部详细信息', + 'options' => [ + 'NONE' => '禁止', + 'SELF' => '仅自己', + 'ALL_AFTER_AC' => 'AC 后', + 'ALL' => '所有人' + ], + 'default_value' => UOJProblem::cur()->getExtraConfig('view_all_details_type'), +]); +$view_type_form->addSelect('view_details_type', [ + 'div_class' => 'row align-items-center g-0 mt-3', + 'label_class' => 'form-label col-auto m-0 flex-grow-1', + 'select_class' => 'col-auto form-select w-auto', + 'label' => '查看测试点详细信息', + 'options' => [ + 'NONE' => '禁止', + 'SELF' => '仅自己', + 'ALL_AFTER_AC' => 'AC 后', + 'ALL' => '所有人', + ], + 'default_value' => UOJProblem::cur()->getExtraConfig('view_details_type'), +]); +$view_type_form->handle = function () { + $config = UOJProblem::cur()->getExtraConfig(); + $config['view_content_type'] = $_POST['view_content_type']; + $config['view_all_details_type'] = $_POST['view_all_details_type']; + $config['view_details_type'] = $_POST['view_details_type']; + $esc_config = json_encode($config); + + DB::update([ + "update problems", + "set", ["extra_config" => $esc_config], + "where", ["id" => UOJProblem::info('id')] + ]); +}; +$view_type_form->runAtServer(); + +$solution_view_type_form = new UOJForm('solution_view_type'); +$solution_view_type_form->addSelect('view_solution_type', [ + 'div_class' => 'row align-items-center g-0', + 'label_class' => 'form-label col-auto m-0 flex-grow-1', + 'select_class' => 'col-auto form-select w-auto', + 'label' => '查看题解', + 'options' => [ + 'NONE' => '禁止', + 'ALL_AFTER_AC' => 'AC 后', + 'ALL' => '所有人', + ], + 'default_value' => UOJProblem::cur()->getExtraConfig('view_solution_type'), +]); +$solution_view_type_form->addSelect('submit_solution_type', [ + 'div_class' => 'row align-items-center g-0 mt-3', + 'label_class' => 'form-label col-auto m-0 flex-grow-1', + 'select_class' => 'col-auto form-select w-auto', + 'label' => '提交题解', + 'options' => [ + 'NONE' => '禁止', + 'ALL_AFTER_AC' => 'AC 后', + 'ALL' => '所有人', + ], + 'default_value' => UOJProblem::cur()->getExtraConfig('submit_solution_type'), +]); +$solution_view_type_form->handle = function () { + $config = UOJProblem::cur()->getExtraConfig(); + $config['view_solution_type'] = $_POST['view_solution_type']; + $config['submit_solution_type'] = $_POST['submit_solution_type']; + $esc_config = json_encode($config); + + DB::update([ + "update problems", + "set", ["extra_config" => $esc_config], + "where", ["id" => UOJProblem::info('id')] + ]); +}; +$solution_view_type_form->runAtServer(); ?> <?php echoUOJPageHeader('题面编辑 - ' . HTML::stripTags(UOJProblem::info('title'))) ?> @@ -102,11 +310,13 @@ $difficulty_form->runAtServer(); 管理者 </a> </li> - <li class="nav-item"> - <a class="nav-link" href="/problem/<?= UOJProblem::info('id') ?>/manage/data" role="tab"> - 数据 - </a> - </li> + <?php if (UOJProblem::info('type') == 'local') : ?> + <li class="nav-item"> + <a class="nav-link" href="/problem/<?= UOJProblem::info('id') ?>/manage/data" role="tab"> + 数据 + </a> + </li> + <?php endif ?> </ul> <div class="card card-default"> @@ -121,8 +331,8 @@ $difficulty_form->runAtServer(); <h2 class="h3 card-title">提示</h2> <ol> <li>请勿引用不稳定的外部资源(如来自个人服务器的图片或文档等),以便备份及后期维护;</li> - <li>请勿在题面中直接插入大段 HTML 代码,这可能会破坏页面的显示,可以考虑使用 <a class="text-decoration-none" href="/html2markdown" target="_blank">转换工具</a> 转换后再作修正;</li> - <li>图片上传推荐使用 <a class="text-decoration-none" href="/image_hosting" target="_blank">S2OJ 图床</a>,以免后续产生外链图片大量失效的情况。</li> + <li>请勿在题面中直接插入大段 HTML 代码,这可能会破坏页面的显示,可以考虑使用 <a class="text-decoration-none" href="/apps/html2markdown" target="_blank">转换工具</a> 转换后再作修正;</li> + <li>图片上传推荐使用 <a class="text-decoration-none" href="/apps/image_hosting" target="_blank">S2OJ 图床</a>,以免后续产生外链图片大量失效的情况。</li> </ol> <p class="card-text"> 更多内容请查看 S2OJ 用户手册中的「<a class="text-decoration-none" href="https://s2oj.github.io/#/manage/problem?id=%e4%bc%a0%e9%a2%98%e6%8c%87%e5%bc%95">传题指引</a>」部分。 @@ -261,6 +471,47 @@ $difficulty_form->runAtServer(); <?php $difficulty_form->printHTML() ?> </div> </div> + + <?php if (UOJProblem::info('type') == 'remote') : ?> + <div class="card mt-3"> + <div class="card-header fw-bold"> + 重新爬取题目信息 + </div> + <div class="card-body"> + <?php $re_crawl_form->printHTML() ?> + </div> + </div> + + <div class="card mt-3 border-danger"> + <div class="card-header fw-bold text-bg-danger border-danger"> + 转换为本地题目 + </div> + <div class="card-body border-danger"> + <?php $convert_local_form->printHTML() ?> + </div> + <div class="card-footer bg-transparent small text-muted border-danger"> + 转换为本地题目之后可以上传自行准备的测试数据进行评测。转换后不再将代码提交至远端 OJ 进行评测。该操作不可逆。 + </div> + </div> + <?php endif ?> + + <div class="card mt-3"> + <div class="card-header fw-bold"> + 提交记录可视权限 + </div> + <div class="card-body"> + <?php $view_type_form->printHTML() ?> + </div> + </div> + + <div class="card mt-3"> + <div class="card-header fw-bold"> + 题解可视权限 + </div> + <div class="card-body"> + <?php $solution_view_type_form->printHTML() ?> + </div> + </div> </aside> </div> diff --git a/web/app/controllers/subdomain/blog/blog.php b/web/app/controllers/subdomain/blog/blog.php index 9399add..4212b4f 100644 --- a/web/app/controllers/subdomain/blog/blog.php +++ b/web/app/controllers/subdomain/blog/blog.php @@ -2,6 +2,7 @@ requireLib('bootstrap5'); requireLib('mathjax'); requireLib('hljs'); +requireLib('pdf.js'); requirePHPLib('form'); Auth::check() || redirectToLogin(); diff --git a/web/app/controllers/subdomain/blog/blog_write.php b/web/app/controllers/subdomain/blog/blog_write.php index 13444d8..ca72c40 100644 --- a/web/app/controllers/subdomain/blog/blog_write.php +++ b/web/app/controllers/subdomain/blog/blog_write.php @@ -107,8 +107,8 @@ $blog_editor->runAtServer(); <ol> <li>题解发布后还需要返回对应题目的题解页面 <b>手动输入博客 ID</b> 来将本文添加到题目的题解列表中(博客 ID 可以在右上角找到);</li> <li>请勿引用不稳定的外部资源(如来自个人服务器的图片或文档等),以便备份及后期维护;</li> - <li>请勿在博文中直接插入大段 HTML 代码,这可能会破坏页面的显示,可以考虑使用 <a class="text-decoration-none" href="/html2markdown" target="_blank">转换工具</a> 转换后再作修正;</li> - <li>图片上传推荐使用 <a class="text-decoration-none" href="/image_hosting" target="_blank">S2OJ 图床</a>,以免后续产生外链图片大量失效的情况。</li> + <li>请勿在博文中直接插入大段 HTML 代码,这可能会破坏页面的显示,可以考虑使用 <a class="text-decoration-none" href="/apps/html2markdown" target="_blank">转换工具</a> 转换后再作修正;</li> + <li>图片上传推荐使用 <a class="text-decoration-none" href="/apps/image_hosting" target="_blank">S2OJ 图床</a>,以免后续产生外链图片大量失效的情况。</li> </ol> <p class="card-text"> 帮助:<a class="text-decoration-none" href="http://uoj.ac/blog/7">UOJ 博客使用教程</a>。 diff --git a/web/app/controllers/tinyfilemanager/config.php b/web/app/controllers/tinyfilemanager/config.php new file mode 100644 index 0000000..01b3614 --- /dev/null +++ b/web/app/controllers/tinyfilemanager/config.php @@ -0,0 +1,123 @@ +<?php + +/* +################################################################################################################# +This is an OPTIONAL configuration file. +The role of this file is to make updating of "tinyfilemanager.php" easier. +So you can: +-Feel free to remove completely this file and configure "tinyfilemanager.php" as a single file application. +or +-Put inside this file all the static configuration you want and forgot to configure "tinyfilemanager.php". +################################################################################################################# +*/ + + +// Auth with login/password +// set true/false to enable/disable it +// Is independent from IP white- and blacklisting +$use_auth = false; + +// Login user name and password +// Users: array('Username' => 'Password', 'Username2' => 'Password2', ...) +// Generate secure password hash - https://tinyfilemanager.github.io/docs/pwd.html +$auth_users = []; + +//set application theme +//options - 'light' and 'dark' +$theme = 'light'; + +// Readonly users +// e.g. array('users', 'guest', ...) +$readonly_users = []; + +// Enable highlight.js (https://highlightjs.org/) on view's page +$use_highlightjs = true; + +// highlight.js style +// for dark theme use 'ir-black' +$highlightjs_style = 'vs'; + +// Enable ace.js (https://ace.c9.io/) on view's page +$edit_files = true; + +// Default timezone for date() and time() +// Doc - http://php.net/manual/en/timezones.php +$default_timezone = 'Asia/Shanghai'; // UTC + +// Root path for file manager +// use absolute path of directory i.e: '/var/www/folder' or $_SERVER['DOCUMENT_ROOT'].'/folder' +$root_path = UOJContext::storagePath(); + +// Root url for links in file manager. Relative to $http_host. Variants: '', 'path/to/subfolder' +// Will not working if $root_path will be outside of server document root +$root_url = ''; + +// Server hostname. Can set manually if wrong +$http_host = UOJContext::httpHost(); + +// user specific directories +// array('Username' => 'Directory path', 'Username2' => 'Directory path', ...) +$directories_users = []; + +// input encoding for iconv +$iconv_input_encoding = 'UTF-8'; + +// date() format for file modification date +// Doc - https://www.php.net/manual/en/function.date.php +$datetime_format = UOJTime::FORMAT; + +// Allowed file extensions for create and rename files +// e.g. 'txt,html,css,js' +$allowed_file_extensions = ''; + +// Allowed file extensions for upload files +// e.g. 'gif,png,jpg,html,txt' +$allowed_upload_extensions = ''; + +// Favicon path. This can be either a full url to an .PNG image, or a path based on the document root. +// full path, e.g http://example.com/favicon.png +// local path, e.g images/icons/favicon.png +$favicon_path = '/images/favicon.ico'; + +// Files and folders to excluded from listing +// e.g. array('myfile.html', 'personal-folder', '*.php', ...) +$exclude_items = []; + +// Online office Docs Viewer +// Availabe rules are 'google', 'microsoft' or false +// google => View documents using Google Docs Viewer +// microsoft => View documents using Microsoft Web Apps Viewer +// false => disable online doc viewer +$online_viewer = 'false'; + +// Sticky Nav bar +// true => enable sticky header +// false => disable sticky header +$sticky_navbar = true; + + +// max upload file size +$max_upload_size_bytes = 40 * 1024 * 1024; + +// Possible rules are 'OFF', 'AND' or 'OR' +// OFF => Don't check connection IP, defaults to OFF +// AND => Connection must be on the whitelist, and not on the blacklist +// OR => Connection must be on the whitelist, or not on the blacklist +$ip_ruleset = 'OFF'; + +// Should users be notified of their block? +$ip_silent = true; + +// IP-addresses, both ipv4 and ipv6 +$ip_whitelist = array( + '127.0.0.1', // local ipv4 + '::1' // local ipv6 +); + +// IP-addresses, both ipv4 and ipv6 +$ip_blacklist = array( + '0.0.0.0', // non-routable meta ipv4 + '::' // non-routable meta ipv6 +); + +?> diff --git a/web/app/controllers/tinyfilemanager/tinyfilemanager.php b/web/app/controllers/tinyfilemanager/tinyfilemanager.php new file mode 100644 index 0000000..aa0bbd4 --- /dev/null +++ b/web/app/controllers/tinyfilemanager/tinyfilemanager.php @@ -0,0 +1,4078 @@ +<?php +//Default Configuration +$CONFIG = '{"lang":"zh-CN","show_hidden":true,"hide_Cols":true,"calc_folder":false,"theme":"light","error_reporting":false}'; + +/** + * H3K | Tiny File Manager V2.5.2 + * @author Prasath Mani | CCP Programmers + * @email ccpprogrammers@gmail.com + * @github https://github.com/prasathmani/tinyfilemanager + * @link https://tinyfilemanager.github.io + */ + +//TFM version +define('VERSION', '2.5.2'); + +//Application Title +define('APP_TITLE', 'Tiny File Manager'); + +// if User has the external config file, try to use it to override the default config above [config.php] +// sample config - https://tinyfilemanager.github.io/config-sample.txt +$config_file = __DIR__.'/config.php'; +if (is_readable($config_file)) { + @include($config_file); +} + +// --- EDIT BELOW CAREFULLY OR DO NOT EDIT AT ALL --- + +// max upload file size +define('MAX_UPLOAD_SIZE', $max_upload_size_bytes); + +// private key and session name to store to the session +if ( !defined( 'FM_SESSION_ID')) { + define('FM_SESSION_ID', 'filemanager'); +} + +// Configuration +$cfg = new FM_Config(); + +// Default language +$lang = isset($cfg->data['lang']) ? $cfg->data['lang'] : 'en'; + +// Show or hide files and folders that starts with a dot +$show_hidden_files = isset($cfg->data['show_hidden']) ? $cfg->data['show_hidden'] : true; + +// PHP error reporting - false = Turns off Errors, true = Turns on Errors +$report_errors = isset($cfg->data['error_reporting']) ? $cfg->data['error_reporting'] : true; + +// Hide Permissions and Owner cols in file-listing +$hide_Cols = isset($cfg->data['hide_Cols']) ? $cfg->data['hide_Cols'] : true; + +// Theme +$theme = isset($cfg->data['theme']) ? $cfg->data['theme'] : 'light'; + +define('FM_THEME', $theme); + +//available languages +$lang_list = array( + 'en' => 'English' +); + +if ($report_errors == true) { + @ini_set('error_reporting', E_ALL); + @ini_set('display_errors', 1); +} else { + @ini_set('error_reporting', E_ALL); + @ini_set('display_errors', 0); +} + +// if fm included +if (defined('FM_EMBED')) { + $use_auth = false; + $sticky_navbar = false; +} else { + @set_time_limit(600); + + date_default_timezone_set($default_timezone); + + ini_set('default_charset', 'UTF-8'); + if (version_compare(PHP_VERSION, '5.6.0', '<') && function_exists('mb_internal_encoding')) { + mb_internal_encoding('UTF-8'); + } + if (function_exists('mb_regex_encoding')) { + mb_regex_encoding('UTF-8'); + } + + session_cache_limiter(''); + session_name(FM_SESSION_ID ); + function session_error_handling_function($code, $msg, $file, $line) { + // Permission denied for default session, try to create a new one + if ($code == 2) { + session_abort(); + session_id(session_create_id()); + @session_start(); + } + } + set_error_handler('session_error_handling_function'); + session_start(); + restore_error_handler(); +} + +//Genrating CSRF Token +if (empty($_SESSION['token'])) { + $_SESSION['token'] = bin2hex(random_bytes(32)); +} + +if (empty($auth_users)) { + $use_auth = false; +} + +$is_https = UOJContext::isUsingHttps(); + +// update $root_url based on user specific directories +if (isset($_SESSION[FM_SESSION_ID]['logged']) && !empty($directories_users[$_SESSION[FM_SESSION_ID]['logged']])) { + $wd = fm_clean_path(dirname($_SERVER['PHP_SELF'])); + $root_url = $root_url.$wd.DIRECTORY_SEPARATOR.$directories_users[$_SESSION[FM_SESSION_ID]['logged']]; +} +// clean $root_url +$root_url = fm_clean_path($root_url); + +// abs path for site +defined('FM_ROOT_URL') || define('FM_ROOT_URL', HTML::url($root_url)); +defined('FM_SELF_URL') || define('FM_SELF_URL', HTML::url('?')); + +// logout +if (isset($_GET['logout'])) { + unset($_SESSION[FM_SESSION_ID]['logged']); + unset( $_SESSION['token']); + fm_redirect(FM_SELF_URL); +} + +// Validate connection IP +if ($ip_ruleset != 'OFF') { + function getClientIP() { + if (array_key_exists('HTTP_CF_CONNECTING_IP', $_SERVER)) { + return $_SERVER["HTTP_CF_CONNECTING_IP"]; + }else if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) { + return $_SERVER["HTTP_X_FORWARDED_FOR"]; + }else if (array_key_exists('REMOTE_ADDR', $_SERVER)) { + return $_SERVER['REMOTE_ADDR']; + }else if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) { + return $_SERVER['HTTP_CLIENT_IP']; + } + return ''; + } + + $clientIp = getClientIP(); + $proceed = false; + $whitelisted = in_array($clientIp, $ip_whitelist); + $blacklisted = in_array($clientIp, $ip_blacklist); + + if($ip_ruleset == 'AND'){ + if($whitelisted == true && $blacklisted == false){ + $proceed = true; + } + } else + if($ip_ruleset == 'OR'){ + if($whitelisted == true || $blacklisted == false){ + $proceed = true; + } + } + + if($proceed == false){ + trigger_error('User connection denied from: ' . $clientIp, E_USER_WARNING); + + if($ip_silent == false){ + UOJResponse::message('Access denied. IP restriction applicable', 'error'); + fm_show_header_login(); + fm_show_message(); + } + exit(); + } +} + +// Checking if the user is logged in or not. If not, it will show the login form. +if ($use_auth) { + if (isset($_SESSION[FM_SESSION_ID]['logged'], $auth_users[$_SESSION[FM_SESSION_ID]['logged']])) { + // Logged + } elseif (isset($_POST['fm_usr'], $_POST['fm_pwd'], $_POST['token'])) { + // Logging In + sleep(1); + if(function_exists('password_verify')) { + if (isset($auth_users[$_POST['fm_usr']]) && isset($_POST['fm_pwd']) && password_verify($_POST['fm_pwd'], $auth_users[$_POST['fm_usr']]) && verifyToken($_POST['token'])) { + $_SESSION[FM_SESSION_ID]['logged'] = $_POST['fm_usr']; + fm_set_msg(lng('You are logged in')); + fm_redirect(FM_ROOT_URL); + } else { + unset($_SESSION[FM_SESSION_ID]['logged']); + fm_set_msg(lng('Login failed. Invalid username or password'), 'error'); + fm_redirect(FM_ROOT_URL); + } + } else { + fm_set_msg(lng('password_hash not supported, Upgrade PHP version'), 'error');; + } + } else { + // Form + unset($_SESSION[FM_SESSION_ID]['logged']); + fm_show_header_login(); + ?> + <section class="h-100"> + <div class="container h-100"> + <div class="row justify-content-md-center h-100"> + <div class="card-wrapper"> + <div class="card fat <?php echo fm_get_theme(); ?>"> + <div class="card-body"> + <form class="form-signin" action="" method="post" autocomplete="off"> + <div class="mb-3"> + <div class="brand"> + <svg version="1.0" xmlns="http://www.w3.org/2000/svg" M1008 width="100%" height="80px" viewBox="0 0 238.000000 140.000000" aria-label="H3K Tiny File Manager"> + <g transform="translate(0.000000,140.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none"> + <path d="M160 700 l0 -600 110 0 110 0 0 260 0 260 70 0 70 0 0 -260 0 -260 110 0 110 0 0 600 0 600 -110 0 -110 0 0 -260 0 -260 -70 0 -70 0 0 260 0 260 -110 0 -110 0 0 -600z"/> + <path fill="#003500" d="M1008 1227 l-108 -72 0 -117 0 -118 110 0 110 0 0 110 0 110 70 0 70 0 0 -180 0 -180 -125 0 c-69 0 -125 -3 -125 -6 0 -3 23 -39 52 -80 l52 -74 73 0 73 0 0 -185 0 -185 -70 0 -70 0 0 115 0 115 -110 0 -110 0 0 -190 0 -190 181 0 181 0 109 73 108 72 1 181 0 181 -69 48 -68 49 68 50 69 49 0 249 0 248 -182 -1 -183 0 -107 -72z"/> + <path d="M1640 700 l0 -600 110 0 110 0 0 208 0 208 35 34 35 34 35 -34 35 -34 0 -208 0 -208 110 0 110 0 0 212 0 213 -87 87 -88 88 88 88 87 87 0 213 0 212 -110 0 -110 0 0 -208 0 -208 -70 -69 -70 -69 0 277 0 277 -110 0 -110 0 0 -600z"/></g> + </svg> + </div> + <div class="text-center"> + <h1 class="card-title"><?php echo APP_TITLE; ?></h1> + </div> + </div> + <hr /> + <div class="mb-3"> + <label for="fm_usr" class="pb-2"><?php echo lng('Username'); ?></label> + <input type="text" class="form-control" id="fm_usr" name="fm_usr" required autofocus> + </div> + + <div class="mb-3"> + <label for="fm_pwd" class="pb-2"><?php echo lng('Password'); ?></label> + <input type="password" class="form-control" id="fm_pwd" name="fm_pwd" required> + </div> + + <div class="mb-3"> + <?php fm_show_message(); ?> + </div> + <input type="hidden" name="token" value="<?php echo htmlentities($_SESSION['token']); ?>" /> + <div class="mb-3"> + <button type="submit" class="btn btn-success btn-block w-100 mt-4" role="button"> + <?php echo lng('Login'); ?> + </button> + </div> + </form> + </div> + </div> + <div class="footer text-center"> + —— © + <a href="https://tinyfilemanager.github.io/" target="_blank" class="text-decoration-none text-muted" data-version="<?php echo VERSION; ?>">CCP Programmers</a> —— + </div> + </div> + </div> + </div> + </section> + + <?php + fm_show_footer_login(); + exit; + } +} + +// update root path +if ($use_auth && isset($_SESSION[FM_SESSION_ID]['logged'])) { + $root_path = isset($directories_users[$_SESSION[FM_SESSION_ID]['logged']]) ? $directories_users[$_SESSION[FM_SESSION_ID]['logged']] : $root_path; +} + +// clean and check $root_path +$root_path = rtrim($root_path, '\\/'); +$root_path = str_replace('\\', '/', $root_path); +if (!@is_dir($root_path)) { + echo "<h1>".lng('Root path')." \"{$root_path}\" ".lng('not found!')." </h1>"; + exit; +} + +defined('FM_SHOW_HIDDEN') || define('FM_SHOW_HIDDEN', $show_hidden_files); +defined('FM_ROOT_PATH') || define('FM_ROOT_PATH', $root_path); +defined('FM_LANG') || define('FM_LANG', $lang); +defined('FM_FILE_EXTENSION') || define('FM_FILE_EXTENSION', $allowed_file_extensions); +defined('FM_UPLOAD_EXTENSION') || define('FM_UPLOAD_EXTENSION', $allowed_upload_extensions); +defined('FM_EXCLUDE_ITEMS') || define('FM_EXCLUDE_ITEMS', (version_compare(PHP_VERSION, '7.0.0', '<') ? serialize($exclude_items) : $exclude_items)); +defined('FM_DOC_VIEWER') || define('FM_DOC_VIEWER', $online_viewer); +define('FM_READONLY', $global_readonly || ($use_auth && !empty($readonly_users) && isset($_SESSION[FM_SESSION_ID]['logged']) && in_array($_SESSION[FM_SESSION_ID]['logged'], $readonly_users))); +define('FM_IS_WIN', DIRECTORY_SEPARATOR == '\\'); + +// always use ?p= +if (!isset($_GET['p']) && empty($_FILES)) { + fm_redirect(FM_SELF_URL . '?p='); +} + +// get path +$p = isset($_GET['p']) ? $_GET['p'] : (isset($_POST['p']) ? $_POST['p'] : ''); + +// clean path +$p = fm_clean_path($p); + +// for ajax request - save +$input = file_get_contents('php://input'); +$_POST = (strpos($input, 'ajax') != FALSE && strpos($input, 'save') != FALSE) ? json_decode($input, true) : $_POST; + +// instead globals vars +define('FM_PATH', $p); +define('FM_USE_AUTH', $use_auth); +define('FM_EDIT_FILE', $edit_files); +defined('FM_ICONV_INPUT_ENC') || define('FM_ICONV_INPUT_ENC', $iconv_input_encoding); +defined('FM_USE_HIGHLIGHTJS') || define('FM_USE_HIGHLIGHTJS', $use_highlightjs); +defined('FM_HIGHLIGHTJS_STYLE') || define('FM_HIGHLIGHTJS_STYLE', $highlightjs_style); +defined('FM_DATETIME_FORMAT') || define('FM_DATETIME_FORMAT', $datetime_format); + +unset($p, $use_auth, $iconv_input_encoding, $use_highlightjs, $highlightjs_style); + +/*************************** ACTIONS ***************************/ + +// Handle all AJAX Request +if ((isset($_SESSION[FM_SESSION_ID]['logged'], $auth_users[$_SESSION[FM_SESSION_ID]['logged']]) || !FM_USE_AUTH) && isset($_POST['ajax'], $_POST['token']) && !FM_READONLY) { + if(!verifyToken($_POST['token'])) { + header('HTTP/1.0 401 Unauthorized'); + die("Invalid Token."); + } + + //search : get list of files from the current folder + if(isset($_POST['type']) && $_POST['type']=="search") { + $dir = $_POST['path'] == "." ? '': $_POST['path']; + $response = scan(fm_clean_path($dir), $_POST['content']); + echo json_encode($response); + exit(); + } + + // save editor file + if (isset($_POST['type']) && $_POST['type'] == "save") { + // get current path + $path = FM_ROOT_PATH; + if (FM_PATH != '') { + $path .= '/' . FM_PATH; + } + // check path + if (!is_dir($path)) { + fm_redirect(FM_SELF_URL . '?p='); + } + $file = $_GET['edit']; + $file = fm_clean_path($file); + $file = str_replace('/', '', $file); + if ($file == '' || !is_file($path . '/' . $file)) { + fm_set_msg(lng('File not found'), 'error'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + } + header('X-XSS-Protection:0'); + $file_path = $path . '/' . $file; + + $writedata = $_POST['content']; + $fd = fopen($file_path, "w"); + $write_results = @fwrite($fd, $writedata); + fclose($fd); + if ($write_results === false){ + header("HTTP/1.1 500 Internal Server Error"); + die("Could Not Write File! - Check Permissions / Ownership"); + } + die(true); + } + + // backup files + if (isset($_POST['type']) && $_POST['type'] == "backup" && !empty($_POST['file'])) { + $fileName = fm_clean_path($_POST['file']); + $fullPath = FM_ROOT_PATH . '/'; + if (!empty($_POST['path'])) { + $relativeDirPath = fm_clean_path($_POST['path']); + $fullPath .= "{$relativeDirPath}/"; + } + $date = date("dMy-His"); + $newFileName = "{$fileName}-{$date}.bak"; + $fullyQualifiedFileName = $fullPath . $fileName; + try { + if (!file_exists($fullyQualifiedFileName)) { + throw new Exception("File {$fileName} not found"); + } + if (copy($fullyQualifiedFileName, $fullPath . $newFileName)) { + echo "Backup {$newFileName} created"; + } else { + throw new Exception("Could not copy file {$fileName}"); + } + } catch (Exception $e) { + echo $e->getMessage(); + } + } + + // Save Config + if (isset($_POST['type']) && $_POST['type'] == "settings") { + global $cfg, $lang, $report_errors, $show_hidden_files, $lang_list, $hide_Cols, $theme; + $newLng = $_POST['js-language']; + fm_get_translations([]); + if (!array_key_exists($newLng, $lang_list)) { + $newLng = 'en'; + } + + $erp = isset($_POST['js-error-report']) && $_POST['js-error-report'] == "true" ? true : false; + $shf = isset($_POST['js-show-hidden']) && $_POST['js-show-hidden'] == "true" ? true : false; + $hco = isset($_POST['js-hide-cols']) && $_POST['js-hide-cols'] == "true" ? true : false; + $te3 = $_POST['js-theme-3']; + + if ($cfg->data['lang'] != $newLng) { + $cfg->data['lang'] = $newLng; + $lang = $newLng; + } + if ($cfg->data['error_reporting'] != $erp) { + $cfg->data['error_reporting'] = $erp; + $report_errors = $erp; + } + if ($cfg->data['show_hidden'] != $shf) { + $cfg->data['show_hidden'] = $shf; + $show_hidden_files = $shf; + } + if ($cfg->data['show_hidden'] != $shf) { + $cfg->data['show_hidden'] = $shf; + $show_hidden_files = $shf; + } + if ($cfg->data['hide_Cols'] != $hco) { + $cfg->data['hide_Cols'] = $hco; + $hide_Cols = $hco; + } + if ($cfg->data['theme'] != $te3) { + $cfg->data['theme'] = $te3; + $theme = $te3; + } + $cfg->save(); + echo true; + } + + // new password hash + if (isset($_POST['type']) && $_POST['type'] == "pwdhash") { + $res = isset($_POST['inputPassword2']) && !empty($_POST['inputPassword2']) ? password_hash($_POST['inputPassword2'], PASSWORD_DEFAULT) : ''; + echo $res; + } + + //upload using url + if(isset($_POST['type']) && $_POST['type'] == "upload" && !empty($_REQUEST["uploadurl"])) { + $path = FM_ROOT_PATH; + if (FM_PATH != '') { + $path .= '/' . FM_PATH; + } + + function event_callback ($message) { + global $callback; + echo json_encode($message); + } + + function get_file_path () { + global $path, $fileinfo, $temp_file; + return $path."/".basename($fileinfo->name); + } + + $url = !empty($_REQUEST["uploadurl"]) && preg_match("|^http(s)?://.+$|", stripslashes($_REQUEST["uploadurl"])) ? stripslashes($_REQUEST["uploadurl"]) : null; + + //prevent 127.* domain and known ports + $domain = parse_url($url, PHP_URL_HOST); + $port = parse_url($url, PHP_URL_PORT); + $knownPorts = [22, 23, 25, 3306]; + + if (preg_match("/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/i", $domain) || in_array($port, $knownPorts)) { + $err = array("message" => "URL is not allowed"); + event_callback(array("fail" => $err)); + exit(); + } + + $use_curl = false; + $temp_file = tempnam(sys_get_temp_dir(), "upload-"); + $fileinfo = new stdClass(); + $fileinfo->name = trim(basename($url), ".\x00..\x20"); + + $allowed = (FM_UPLOAD_EXTENSION) ? explode(',', FM_UPLOAD_EXTENSION) : false; + $ext = strtolower(pathinfo($fileinfo->name, PATHINFO_EXTENSION)); + $isFileAllowed = ($allowed) ? in_array($ext, $allowed) : true; + + $err = false; + + if(!$isFileAllowed) { + $err = array("message" => "File extension is not allowed"); + event_callback(array("fail" => $err)); + exit(); + } + + if (!$url) { + $success = false; + } else if ($use_curl) { + @$fp = fopen($temp_file, "w"); + @$ch = curl_init($url); + curl_setopt($ch, CURLOPT_NOPROGRESS, false ); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_FILE, $fp); + @$success = curl_exec($ch); + $curl_info = curl_getinfo($ch); + if (!$success) { + $err = array("message" => curl_error($ch)); + } + @curl_close($ch); + fclose($fp); + $fileinfo->size = $curl_info["size_download"]; + $fileinfo->type = $curl_info["content_type"]; + } else { + $ctx = stream_context_create(); + @$success = copy($url, $temp_file, $ctx); + if (!$success) { + $err = error_get_last(); + } + } + + if ($success) { + $success = rename($temp_file, strtok(get_file_path(), '?')); + } + + if ($success) { + event_callback(array("done" => $fileinfo)); + } else { + unlink($temp_file); + if (!$err) { + $err = array("message" => "Invalid url parameter"); + } + event_callback(array("fail" => $err)); + } + } + exit(); +} + +// Delete file / folder +if (isset($_GET['del'], $_POST['token']) && !FM_READONLY) { + $del = str_replace( '/', '', fm_clean_path( $_GET['del'] ) ); + if ($del != '' && $del != '..' && $del != '.' && verifyToken($_POST['token'])) { + $path = FM_ROOT_PATH; + if (FM_PATH != '') { + $path .= '/' . FM_PATH; + } + $is_dir = is_dir($path . '/' . $del); + if (fm_rdelete($path . '/' . $del)) { + $msg = $is_dir ? lng('Folder').' <b>%s</b> '.lng('Deleted') : lng('File').' <b>%s</b> '.lng('Deleted'); + fm_set_msg(sprintf($msg, fm_enc($del))); + } else { + $msg = $is_dir ? lng('Folder').' <b>%s</b> '.lng('not deleted') : lng('File').' <b>%s</b> '.lng('not deleted'); + fm_set_msg(sprintf($msg, fm_enc($del)), 'error'); + } + } else { + fm_set_msg(lng('Invalid file or folder name'), 'error'); + } + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); +} + +// Create a new file/folder +if (isset($_POST['newfilename'], $_POST['newfile'], $_POST['token']) && !FM_READONLY) { + $type = urldecode($_POST['newfile']); + $new = str_replace( '/', '', fm_clean_path( strip_tags( $_POST['newfilename'] ) ) ); + if (fm_isvalid_filename($new) && $new != '' && $new != '..' && $new != '.' && verifyToken($_POST['token'])) { + $path = FM_ROOT_PATH; + if (FM_PATH != '') { + $path .= '/' . FM_PATH; + } + if ($type == "file") { + if (!file_exists($path . '/' . $new)) { + if(fm_is_valid_ext($new)) { + @fopen($path . '/' . $new, 'w') or die('Cannot open file: ' . $new); + fm_set_msg(sprintf(lng('File').' <b>%s</b> '.lng('Created'), fm_enc($new))); + } else { + fm_set_msg(lng('File extension is not allowed'), 'error'); + } + } else { + fm_set_msg(sprintf(lng('File').' <b>%s</b> '.lng('already exists'), fm_enc($new)), 'alert'); + } + } else { + if (fm_mkdir($path . '/' . $new, false) === true) { + fm_set_msg(sprintf(lng('Folder').' <b>%s</b> '.lng('Created'), $new)); + } elseif (fm_mkdir($path . '/' . $new, false) === $path . '/' . $new) { + fm_set_msg(sprintf(lng('Folder').' <b>%s</b> '.lng('already exists'), fm_enc($new)), 'alert'); + } else { + fm_set_msg(sprintf(lng('Folder').' <b>%s</b> '.lng('not created'), fm_enc($new)), 'error'); + } + } + } else { + fm_set_msg(lng('Invalid characters in file or folder name'), 'error'); + } + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); +} + +// Copy folder / file +if (isset($_GET['copy'], $_GET['finish']) && !FM_READONLY) { + // from + $copy = urldecode($_GET['copy']); + $copy = fm_clean_path($copy); + // empty path + if ($copy == '') { + fm_set_msg(lng('Source path not defined'), 'error'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + } + // abs path from + $from = FM_ROOT_PATH . '/' . $copy; + // abs path to + $dest = FM_ROOT_PATH; + if (FM_PATH != '') { + $dest .= '/' . FM_PATH; + } + $dest .= '/' . basename($from); + // move? + $move = isset($_GET['move']); + $move = fm_clean_path(urldecode($move)); + // copy/move/duplicate + if ($from != $dest) { + $msg_from = trim(FM_PATH . '/' . basename($from), '/'); + if ($move) { // Move and to != from so just perform move + $rename = fm_rename($from, $dest); + if ($rename) { + fm_set_msg(sprintf(lng('Moved from').' <b>%s</b> '.lng('to').' <b>%s</b>', fm_enc($copy), fm_enc($msg_from))); + } elseif ($rename === null) { + fm_set_msg(lng('File or folder with this path already exists'), 'alert'); + } else { + fm_set_msg(sprintf(lng('Error while moving from').' <b>%s</b> '.lng('to').' <b>%s</b>', fm_enc($copy), fm_enc($msg_from)), 'error'); + } + } else { // Not move and to != from so copy with original name + if (fm_rcopy($from, $dest)) { + fm_set_msg(sprintf(lng('Copied from').' <b>%s</b> '.lng('to').' <b>%s</b>', fm_enc($copy), fm_enc($msg_from))); + } else { + fm_set_msg(sprintf(lng('Error while copying from').' <b>%s</b> '.lng('to').' <b>%s</b>', fm_enc($copy), fm_enc($msg_from)), 'error'); + } + } + } else { + if (!$move){ //Not move and to = from so duplicate + $msg_from = trim(FM_PATH . '/' . basename($from), '/'); + $fn_parts = pathinfo($from); + $extension_suffix = ''; + if(!is_dir($from)){ + $extension_suffix = '.'.$fn_parts['extension']; + } + //Create new name for duplicate + $fn_duplicate = $fn_parts['dirname'].'/'.$fn_parts['filename'].'-'.date('YmdHis').$extension_suffix; + $loop_count = 0; + $max_loop = 1000; + // Check if a file with the duplicate name already exists, if so, make new name (edge case...) + while(file_exists($fn_duplicate) & $loop_count < $max_loop){ + $fn_parts = pathinfo($fn_duplicate); + $fn_duplicate = $fn_parts['dirname'].'/'.$fn_parts['filename'].'-copy'.$extension_suffix; + $loop_count++; + } + if (fm_rcopy($from, $fn_duplicate, False)) { + fm_set_msg(sprintf('Copyied from <b>%s</b> to <b>%s</b>', fm_enc($copy), fm_enc($fn_duplicate))); + } else { + fm_set_msg(sprintf('Error while copying from <b>%s</b> to <b>%s</b>', fm_enc($copy), fm_enc($fn_duplicate)), 'error'); + } + } + else{ + fm_set_msg(lng('Paths must be not equal'), 'alert'); + } + } + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); +} + +// Mass copy files/ folders +if (isset($_POST['file'], $_POST['copy_to'], $_POST['finish'], $_POST['token']) && !FM_READONLY) { + + if(!verifyToken($_POST['token'])) { + fm_set_msg(lng('Invalid Token.'), 'error'); + } + + // from + $path = FM_ROOT_PATH; + if (FM_PATH != '') { + $path .= '/' . FM_PATH; + } + // to + $copy_to_path = FM_ROOT_PATH; + $copy_to = fm_clean_path($_POST['copy_to']); + if ($copy_to != '') { + $copy_to_path .= '/' . $copy_to; + } + if ($path == $copy_to_path) { + fm_set_msg(lng('Paths must be not equal'), 'alert'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + } + if (!is_dir($copy_to_path)) { + if (!fm_mkdir($copy_to_path, true)) { + fm_set_msg('Unable to create destination folder', 'error'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + } + } + // move? + $move = isset($_POST['move']); + // copy/move + $errors = 0; + $files = $_POST['file']; + if (is_array($files) && count($files)) { + foreach ($files as $f) { + if ($f != '') { + $f = fm_clean_path($f); + // abs path from + $from = $path . '/' . $f; + // abs path to + $dest = $copy_to_path . '/' . $f; + // do + if ($move) { + $rename = fm_rename($from, $dest); + if ($rename === false) { + $errors++; + } + } else { + if (!fm_rcopy($from, $dest)) { + $errors++; + } + } + } + } + if ($errors == 0) { + $msg = $move ? 'Selected files and folders moved' : 'Selected files and folders copied'; + fm_set_msg($msg); + } else { + $msg = $move ? 'Error while moving items' : 'Error while copying items'; + fm_set_msg($msg, 'error'); + } + } else { + fm_set_msg(lng('Nothing selected'), 'alert'); + } + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); +} + +// Rename +if (isset($_POST['rename_from'], $_POST['rename_to'], $_POST['token']) && !FM_READONLY) { + if(!verifyToken($_POST['token'])) { + fm_set_msg("Invalid Token.", 'error'); + } + // old name + $old = urldecode($_POST['rename_from']); + $old = fm_clean_path($old); + $old = str_replace('/', '', $old); + // new name + $new = urldecode($_POST['rename_to']); + $new = fm_clean_path(strip_tags($new)); + $new = str_replace('/', '', $new); + // path + $path = FM_ROOT_PATH; + if (FM_PATH != '') { + $path .= '/' . FM_PATH; + } + // rename + if (fm_isvalid_filename($new) && $old != '' && $new != '') { + if (fm_rename($path . '/' . $old, $path . '/' . $new)) { + fm_set_msg(sprintf(lng('Renamed from').' <b>%s</b> '. lng('to').' <b>%s</b>', fm_enc($old), fm_enc($new))); + } else { + fm_set_msg(sprintf(lng('Error while renaming from').' <b>%s</b> '. lng('to').' <b>%s</b>', fm_enc($old), fm_enc($new)), 'error'); + } + } else { + fm_set_msg(lng('Invalid characters in file name'), 'error'); + } + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); +} + +// Download +if (isset($_GET['dl'], $_POST['token'])) { + if(!verifyToken($_POST['token'])) { + fm_set_msg("Invalid Token.", 'error'); + } + + $dl = urldecode($_GET['dl']); + $dl = fm_clean_path($dl); + $dl = str_replace('/', '', $dl); + $path = FM_ROOT_PATH; + if (FM_PATH != '') { + $path .= '/' . FM_PATH; + } + if ($dl != '' && is_file($path . '/' . $dl)) { + fm_download_file($path . '/' . $dl, $dl, 1024); + exit; + } else { + fm_set_msg(lng('File not found'), 'error'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + } +} + +// Upload +if (!empty($_FILES) && !FM_READONLY) { + if(isset($_POST['token'])) { + if(!verifyToken($_POST['token'])) { + $response = array ('status' => 'error','info' => "Invalid Token."); + echo json_encode($response); exit(); + } + } else { + $response = array ('status' => 'error','info' => "Token Missing."); + echo json_encode($response); exit(); + } + + $override_file_name = false; + $chunkIndex = $_POST['dzchunkindex']; + $chunkTotal = $_POST['dztotalchunkcount']; + $fullPathInput = fm_clean_path($_REQUEST['fullpath']); + + $f = $_FILES; + $path = FM_ROOT_PATH; + $ds = DIRECTORY_SEPARATOR; + if (FM_PATH != '') { + $path .= '/' . FM_PATH; + } + + $errors = 0; + $uploads = 0; + $allowed = (FM_UPLOAD_EXTENSION) ? explode(',', FM_UPLOAD_EXTENSION) : false; + $response = array ( + 'status' => 'error', + 'info' => 'Oops! Try again' + ); + + $filename = $f['file']['name']; + $tmp_name = $f['file']['tmp_name']; + $ext = pathinfo($filename, PATHINFO_FILENAME) != '' ? strtolower(pathinfo($filename, PATHINFO_EXTENSION)) : ''; + $isFileAllowed = ($allowed) ? in_array($ext, $allowed) : true; + + if(!fm_isvalid_filename($filename) && !fm_isvalid_filename($fullPathInput)) { + $response = array ( + 'status' => 'error', + 'info' => "Invalid File name!", + ); + echo json_encode($response); exit(); + } + + $targetPath = $path . $ds; + if ( is_writable($targetPath) ) { + $fullPath = $path . '/' . basename($fullPathInput); + $folder = substr($fullPath, 0, strrpos($fullPath, "/")); + + if(file_exists ($fullPath) && !$override_file_name && !$chunks) { + $ext_1 = $ext ? '.'.$ext : ''; + $fullPath = $path . '/' . basename($fullPathInput, $ext_1) .'_'. date('ymdHis'). $ext_1; + } + + if (!is_dir($folder)) { + $old = umask(0); + mkdir($folder, 0777, true); + umask($old); + } + + if (empty($f['file']['error']) && !empty($tmp_name) && $tmp_name != 'none' && $isFileAllowed) { + if ($chunkTotal){ + $out = @fopen("{$fullPath}.part", $chunkIndex == 0 ? "wb" : "ab"); + if ($out) { + $in = @fopen($tmp_name, "rb"); + if ($in) { + while ($buff = fread($in, 4096)) { fwrite($out, $buff); } + $response = array ( + 'status' => 'success', + 'info' => "file upload successful" + ); + } else { + $response = array ( + 'status' => 'error', + 'info' => "failed to open output stream", + 'errorDetails' => error_get_last() + ); + } + @fclose($in); + @fclose($out); + @unlink($tmp_name); + + $response = array ( + 'status' => 'success', + 'info' => "file upload successful" + ); + } else { + $response = array ( + 'status' => 'error', + 'info' => "failed to open output stream" + ); + } + + if ($chunkIndex == $chunkTotal - 1) { + rename("{$fullPath}.part", $fullPath); + } + + } else if (move_uploaded_file($tmp_name, $fullPath)) { + // Be sure that the file has been uploaded + if ( file_exists($fullPath) ) { + $response = array ( + 'status' => 'success', + 'info' => "file upload successful" + ); + } else { + $response = array ( + 'status' => 'error', + 'info' => 'Couldn\'t upload the requested file.' + ); + } + } else { + $response = array ( + 'status' => 'error', + 'info' => "Error while uploading files. Uploaded files $uploads", + ); + } + } + } else { + $response = array ( + 'status' => 'error', + 'info' => 'The specified folder for upload isn\'t writeable.' + ); + } + // Return the response + echo json_encode($response); + exit(); +} + +// Mass deleting +if (isset($_POST['group'], $_POST['delete'], $_POST['token']) && !FM_READONLY) { + + if(!verifyToken($_POST['token'])) { + fm_set_msg(lng("Invalid Token."), 'error'); + } + + $path = FM_ROOT_PATH; + if (FM_PATH != '') { + $path .= '/' . FM_PATH; + } + + $errors = 0; + $files = $_POST['file']; + if (is_array($files) && count($files)) { + foreach ($files as $f) { + if ($f != '') { + $new_path = $path . '/' . $f; + if (!fm_rdelete($new_path)) { + $errors++; + } + } + } + if ($errors == 0) { + fm_set_msg(lng('Selected files and folder deleted')); + } else { + fm_set_msg(lng('Error while deleting items'), 'error'); + } + } else { + fm_set_msg(lng('Nothing selected'), 'alert'); + } + + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); +} + +// Pack files zip, tar +if (isset($_POST['group'], $_POST['token']) && (isset($_POST['zip']) || isset($_POST['tar'])) && !FM_READONLY) { + + if(!verifyToken($_POST['token'])) { + fm_set_msg(lng("Invalid Token."), 'error'); + } + + $path = FM_ROOT_PATH; + $ext = 'zip'; + if (FM_PATH != '') { + $path .= '/' . FM_PATH; + } + + //set pack type + $ext = isset($_POST['tar']) ? 'tar' : 'zip'; + + if (($ext == "zip" && !class_exists('ZipArchive')) || ($ext == "tar" && !class_exists('PharData'))) { + fm_set_msg(lng('Operations with archives are not available'), 'error'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + } + + $files = $_POST['file']; + $sanitized_files = array(); + + // clean path + foreach($files as $file){ + array_push($sanitized_files, fm_clean_path($file)); + } + + $files = $sanitized_files; + + if (!empty($files)) { + chdir($path); + + if (count($files) == 1) { + $one_file = reset($files); + $one_file = basename($one_file); + $zipname = $one_file . '_' . date('ymd_His') . '.'.$ext; + } else { + $zipname = 'archive_' . date('ymd_His') . '.'.$ext; + } + + if($ext == 'zip') { + $zipper = new FM_Zipper(); + $res = $zipper->create($zipname, $files); + } elseif ($ext == 'tar') { + $tar = new FM_Zipper_Tar(); + $res = $tar->create($zipname, $files); + } + + if ($res) { + fm_set_msg(sprintf(lng('Archive').' <b>%s</b> '.lng('Created'), fm_enc($zipname))); + } else { + fm_set_msg(lng('Archive not created'), 'error'); + } + } else { + fm_set_msg(lng('Nothing selected'), 'alert'); + } + + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); +} + +// Unpack zip, tar +if (isset($_POST['unzip'], $_POST['token']) && !FM_READONLY) { + + if(!verifyToken($_POST['token'])) { + fm_set_msg(lng("Invalid Token."), 'error'); + } + + $unzip = urldecode($_POST['unzip']); + $unzip = fm_clean_path($unzip); + $unzip = str_replace('/', '', $unzip); + $isValid = false; + + $path = FM_ROOT_PATH; + if (FM_PATH != '') { + $path .= '/' . FM_PATH; + } + + if ($unzip != '' && is_file($path . '/' . $unzip)) { + $zip_path = $path . '/' . $unzip; + $ext = pathinfo($zip_path, PATHINFO_EXTENSION); + $isValid = true; + } else { + fm_set_msg(lng('File not found'), 'error'); + } + + if (($ext == "zip" && !class_exists('ZipArchive')) || ($ext == "tar" && !class_exists('PharData'))) { + fm_set_msg(lng('Operations with archives are not available'), 'error'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + } + + if ($isValid) { + //to folder + $tofolder = ''; + if (isset($_POST['tofolder'])) { + $tofolder = pathinfo($zip_path, PATHINFO_FILENAME); + if (fm_mkdir($path . '/' . $tofolder, true)) { + $path .= '/' . $tofolder; + } + } + + if($ext == "zip") { + $zipper = new FM_Zipper(); + $res = $zipper->unzip($zip_path, $path); + } elseif ($ext == "tar") { + try { + $gzipper = new PharData($zip_path); + if (@$gzipper->extractTo($path,null, true)) { + $res = true; + } else { + $res = false; + } + } catch (Exception $e) { + //TODO:: need to handle the error + $res = true; + } + } + + if ($res) { + fm_set_msg(lng('Archive unpacked')); + } else { + fm_set_msg(lng('Archive not unpacked'), 'error'); + } + } else { + fm_set_msg(lng('File not found'), 'error'); + } + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); +} + +// Change Perms (not for Windows) +if (isset($_POST['chmod'], $_POST['token']) && !FM_READONLY && !FM_IS_WIN) { + + if(!verifyToken($_POST['token'])) { + fm_set_msg(lng("Invalid Token."), 'error'); + } + + $path = FM_ROOT_PATH; + if (FM_PATH != '') { + $path .= '/' . FM_PATH; + } + + $file = $_POST['chmod']; + $file = fm_clean_path($file); + $file = str_replace('/', '', $file); + if ($file == '' || (!is_file($path . '/' . $file) && !is_dir($path . '/' . $file))) { + fm_set_msg(lng('File not found'), 'error'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + } + + $mode = 0; + if (!empty($_POST['ur'])) { + $mode |= 0400; + } + if (!empty($_POST['uw'])) { + $mode |= 0200; + } + if (!empty($_POST['ux'])) { + $mode |= 0100; + } + if (!empty($_POST['gr'])) { + $mode |= 0040; + } + if (!empty($_POST['gw'])) { + $mode |= 0020; + } + if (!empty($_POST['gx'])) { + $mode |= 0010; + } + if (!empty($_POST['or'])) { + $mode |= 0004; + } + if (!empty($_POST['ow'])) { + $mode |= 0002; + } + if (!empty($_POST['ox'])) { + $mode |= 0001; + } + + if (@chmod($path . '/' . $file, $mode)) { + fm_set_msg(lng('Permissions changed')); + } else { + fm_set_msg(lng('Permissions not changed'), 'error'); + } + + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); +} + +/*************************** ACTIONS ***************************/ + +// get current path +$path = FM_ROOT_PATH; +if (FM_PATH != '') { + $path .= '/' . FM_PATH; +} + +// check path +if (!is_dir($path)) { + fm_redirect(FM_SELF_URL . '?p='); +} + +// get parent folder +$parent = fm_get_parent_path(FM_PATH); + +$objects = is_readable($path) ? scandir($path) : array(); +$folders = array(); +$files = array(); +$current_path = array_slice(explode("/",$path), -1)[0]; +if (is_array($objects) && fm_is_exclude_items($current_path)) { + foreach ($objects as $file) { + if ($file == '.' || $file == '..') { + continue; + } + if (!FM_SHOW_HIDDEN && substr($file, 0, 1) === '.') { + continue; + } + $new_path = $path . '/' . $file; + if (@is_file($new_path) && fm_is_exclude_items($file)) { + $files[] = $file; + } elseif (@is_dir($new_path) && $file != '.' && $file != '..' && fm_is_exclude_items($file)) { + $folders[] = $file; + } + } +} + +if (!empty($files)) { + natcasesort($files); +} +if (!empty($folders)) { + natcasesort($folders); +} + +// upload form +if (isset($_GET['upload']) && !FM_READONLY) { + fm_show_header(); // HEADER + fm_show_nav_path(FM_PATH); // current path + //get the allowed file extensions + function getUploadExt() { + $extArr = explode(',', FM_UPLOAD_EXTENSION); + if(FM_UPLOAD_EXTENSION && $extArr) { + array_walk($extArr, function(&$x) {$x = ".$x";}); + return implode(',', $extArr); + } + return ''; + } + ?> + <?= HTML::css_link('/css/dropzone.min.css') ?> + <div class="path"> + + <div class="card mb-2 fm-upload-wrapper <?php echo fm_get_theme(); ?>"> + <div class="card-header"> + <ul class="nav nav-tabs card-header-tabs"> + <li class="nav-item"> + <a class="nav-link active" href="#fileUploader" data-target="#fileUploader"><i class="fa fa-arrow-circle-o-up"></i> <?php echo lng('UploadingFiles') ?></a> + </li> + <li class="nav-item"> + <a class="nav-link" href="#urlUploader" class="js-url-upload" data-target="#urlUploader"><i class="fa fa-link"></i> <?php echo lng('Upload from URL') ?></a> + </li> + </ul> + </div> + <div class="card-body"> + <p class="card-text"> + <a href="?p=<?php echo FM_PATH ?>" class="float-right"><i class="fa fa-chevron-circle-left go-back"></i> <?php echo lng('Back')?></a> + <strong><?php echo lng('DestinationFolder') ?></strong>: <?php echo fm_enc(fm_convert_win(FM_PATH)) ?> + </p> + + <form action="<?php echo htmlspecialchars(FM_SELF_URL) . '?p=' . fm_enc(FM_PATH) ?>" class="dropzone card-tabs-container" id="fileUploader" enctype="multipart/form-data"> + <input type="hidden" name="p" value="<?php echo fm_enc(FM_PATH) ?>"> + <input type="hidden" name="fullpath" id="fullpath" value="<?php echo fm_enc(FM_PATH) ?>"> + <input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>"> + <div class="fallback"> + <input name="file" type="file" multiple/> + </div> + </form> + + <div class="upload-url-wrapper card-tabs-container hidden" id="urlUploader"> + <form id="js-form-url-upload" class="row row-cols-lg-auto g-3 align-items-center" onsubmit="return upload_from_url(this);" method="POST" action=""> + <input type="hidden" name="type" value="upload" aria-label="hidden" aria-hidden="true"> + <input type="url" placeholder="URL" name="uploadurl" required class="form-control" style="width: 80%"> + <input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>"> + <button type="submit" class="btn btn-primary ms-3"><?php echo lng('Upload') ?></button> + <div class="lds-facebook"><div></div><div></div><div></div></div> + </form> + <div id="js-url-upload__list" class="col-9 mt-3"></div> + </div> + </div> + </div> + </div> + <?= HTML::js_src('/js/dropzone.min.js') ?> + <script> + Dropzone.options.fileUploader = { + chunking: true, + chunkSize: 2000000, // chunk size 2,000,000 bytes (~2MB) + forceChunking: true, + retryChunks: true, + retryChunksLimit: 3, + parallelUploads: 1, + parallelChunkUploads: false, + timeout: 120000, + maxFilesize: "<?php echo MAX_UPLOAD_SIZE; ?>", + acceptedFiles : "<?php echo getUploadExt() ?>", + init: function () { + this.on("sending", function (file, xhr, formData) { + let _path = (file.fullPath) ? file.fullPath : file.name; + document.getElementById("fullpath").value = _path; + xhr.ontimeout = (function() { + toast('Error: Server Timeout'); + }); + }).on("success", function (res) { + let _response = JSON.parse(res.xhr.response); + + if(_response.status == "error") { + toast(_response.info); + } + }).on("error", function(file, response) { + toast(response); + }); + } + } + </script> + <?php + fm_show_footer(); + exit; +} + +// copy form POST +if (isset($_POST['copy']) && !FM_READONLY) { + $copy_files = isset($_POST['file']) ? $_POST['file'] : null; + if (!is_array($copy_files) || empty($copy_files)) { + fm_set_msg(lng('Nothing selected'), 'alert'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + } + + fm_show_header(); // HEADER + fm_show_nav_path(FM_PATH); // current path + ?> + <div class="path"> + <div class="card <?php echo fm_get_theme(); ?>"> + <div class="card-header"> + <h6><?php echo lng('Copying') ?></h6> + </div> + <div class="card-body"> + <form action="" method="post"> + <input type="hidden" name="p" value="<?php echo fm_enc(FM_PATH) ?>"> + <input type="hidden" name="finish" value="1"> + <?php + foreach ($copy_files as $cf) { + echo '<input type="hidden" name="file[]" value="' . fm_enc($cf) . '">' . PHP_EOL; + } + ?> + <p class="break-word"><strong><?php echo lng('Files') ?></strong>: <b><?php echo implode('</b>, <b>', $copy_files) ?></b></p> + <p class="break-word"><strong><?php echo lng('SourceFolder') ?></strong>: <?php echo fm_enc(fm_convert_win(FM_ROOT_PATH . '/' . FM_PATH)) ?><br> + <label for="inp_copy_to"><strong><?php echo lng('DestinationFolder') ?></strong>:</label> + <?php echo FM_ROOT_PATH ?>/<input type="text" name="copy_to" id="inp_copy_to" value="<?php echo fm_enc(FM_PATH) ?>"> + </p> + <p class="custom-checkbox custom-control"><input type="checkbox" name="move" value="1" id="js-move-files" class="custom-control-input"><label for="js-move-files" class="custom-control-label ms-2"> <?php echo lng('Move') ?></label></p> + <p> + <b><a href="?p=<?php echo urlencode(FM_PATH) ?>" class="btn btn-outline-danger"><i class="fa fa-times-circle"></i> <?php echo lng('Cancel') ?></a></b>  + <input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>"> + <button type="submit" class="btn btn-success"><i class="fa fa-check-circle"></i> <?php echo lng('Copy') ?></button> + </p> + </form> + </div> + </div> + </div> + <?php + fm_show_footer(); + exit; +} + +// copy form +if (isset($_GET['copy']) && !isset($_GET['finish']) && !FM_READONLY) { + $copy = $_GET['copy']; + $copy = fm_clean_path($copy); + if ($copy == '' || !file_exists(FM_ROOT_PATH . '/' . $copy)) { + fm_set_msg(lng('File not found'), 'error'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + } + + fm_show_header(); // HEADER + fm_show_nav_path(FM_PATH); // current path + ?> + <div class="path"> + <p><b>Copying</b></p> + <p class="break-word"> + <strong>Source path:</strong> <?php echo fm_enc(fm_convert_win(FM_ROOT_PATH . '/' . $copy)) ?><br> + <strong>Destination folder:</strong> <?php echo fm_enc(fm_convert_win(FM_ROOT_PATH . '/' . FM_PATH)) ?> + </p> + <p> + <b><a href="?p=<?php echo urlencode(FM_PATH) ?>&copy=<?php echo urlencode($copy) ?>&finish=1"><i class="fa fa-check-circle"></i> Copy</a></b>   + <b><a href="?p=<?php echo urlencode(FM_PATH) ?>&copy=<?php echo urlencode($copy) ?>&finish=1&move=1"><i class="fa fa-check-circle"></i> Move</a></b>   + <b><a href="?p=<?php echo urlencode(FM_PATH) ?>" class="text-danger"><i class="fa fa-times-circle"></i> Cancel</a></b> + </p> + <p><i><?php echo lng('Select folder') ?></i></p> + <ul class="folders break-word"> + <?php + if ($parent !== false) { + ?> + <li><a href="?p=<?php echo urlencode($parent) ?>&copy=<?php echo urlencode($copy) ?>"><i class="fa fa-chevron-circle-left"></i> ..</a></li> + <?php + } + foreach ($folders as $f) { + ?> + <li> + <a href="?p=<?php echo urlencode(trim(FM_PATH . '/' . $f, '/')) ?>&copy=<?php echo urlencode($copy) ?>"><i class="fa fa-folder-o"></i> <?php echo fm_convert_win($f) ?></a></li> + <?php + } + ?> + </ul> + </div> + <?php + fm_show_footer(); + exit; +} + +if (isset($_GET['settings']) && !FM_READONLY) { + fm_show_header(); // HEADER + fm_show_nav_path(FM_PATH); // current path + global $cfg, $lang, $lang_list; + ?> + + <div class="col-md-8 offset-md-2 pt-3"> + <div class="card mb-2 <?php echo fm_get_theme(); ?>"> + <h6 class="card-header d-flex justify-content-between"> + <span><i class="fa fa-cog"></i> <?php echo lng('Settings') ?></span> + <a href="?p=<?php echo FM_PATH ?>" class="text-danger"><i class="fa fa-times-circle-o"></i> <?php echo lng('Cancel')?></a> + </h6> + <div class="card-body"> + <form id="js-settings-form" action="" method="post" data-type="ajax" onsubmit="return save_settings(this)"> + <input type="hidden" name="type" value="settings" aria-label="hidden" aria-hidden="true"> + <div class="form-group row"> + <label for="js-language" class="col-sm-3 col-form-label"><?php echo lng('Language') ?></label> + <div class="col-sm-5"> + <select class="form-select" id="js-language" name="js-language"> + <?php + function getSelected($l) { + global $lang; + return ($lang == $l) ? 'selected' : ''; + } + foreach ($lang_list as $k => $v) { + echo "<option value='$k' ".getSelected($k).">$v</option>"; + } + ?> + </select> + </div> + </div> + <div class="mt-3 mb-3 row "> + <label for="js-error-report" class="col-sm-3 col-form-label"><?php echo lng('ErrorReporting') ?></label> + <div class="col-sm-9"> + <div class="form-check form-switch"> + <input class="form-check-input" type="checkbox" role="switch" id="js-error-report" name="js-error-report" value="true" <?php echo $report_errors ? 'checked' : ''; ?> /> + </div> + </div> + </div> + + <div class="mb-3 row"> + <label for="js-show-hidden" class="col-sm-3 col-form-label"><?php echo lng('ShowHiddenFiles') ?></label> + <div class="col-sm-9"> + <div class="form-check form-switch"> + <input class="form-check-input" type="checkbox" role="switch" id="js-show-hidden" name="js-show-hidden" value="true" <?php echo $show_hidden_files ? 'checked' : ''; ?> /> + </div> + </div> + </div> + + <div class="mb-3 row"> + <label for="js-hide-cols" class="col-sm-3 col-form-label"><?php echo lng('HideColumns') ?></label> + <div class="col-sm-9"> + <div class="form-check form-switch"> + <input class="form-check-input" type="checkbox" role="switch" id="js-hide-cols" name="js-hide-cols" value="true" <?php echo $hide_Cols ? 'checked' : ''; ?> /> + </div> + </div> + </div> + + <div class="mb-3 row"> + <label for="js-3-1" class="col-sm-3 col-form-label"><?php echo lng('Theme') ?></label> + <div class="col-sm-5"> + <select class="form-select w-100" id="js-3-0" name="js-theme-3"> + <option value='light' <?php if($theme == "light"){echo "selected";} ?>><?php echo lng('light') ?></option> + <option value='dark' <?php if($theme == "dark"){echo "selected";} ?>><?php echo lng('dark') ?></option> + </select> + </div> + </div> + + <div class="mb-3 row"> + <div class="col-sm-10"> + <button type="submit" class="btn btn-success"> <i class="fa fa-check-circle"></i> <?php echo lng('Save'); ?></button> + </div> + </div> + + </form> + </div> + </div> + </div> + <?php + fm_show_footer(); + exit; +} + +if (isset($_GET['help'])) { + fm_show_header(); // HEADER + fm_show_nav_path(FM_PATH); // current path + global $cfg, $lang; + ?> + + <div class="col-md-8 offset-md-2 pt-3"> + <div class="card mb-2 <?php echo fm_get_theme(); ?>"> + <h6 class="card-header d-flex justify-content-between"> + <span><i class="fa fa-exclamation-circle"></i> <?php echo lng('Help') ?></span> + <a href="?p=<?php echo FM_PATH ?>" class="text-danger"><i class="fa fa-times-circle-o"></i> <?php echo lng('Cancel')?></a> + </h6> + <div class="card-body"> + <div class="row"> + <div class="col-xs-12 col-sm-6"> + <p><h3><a href="https://github.com/prasathmani/tinyfilemanager" target="_blank" class="app-v-title"> Tiny File Manager <?php echo VERSION; ?></a></h3></p> + <p>Author: Prasath Mani</p> + <p>Mail Us: <a href="mailto:ccpprogrammers@gmail.com">ccpprogrammers[at]gmail.com</a> </p> + </div> + <div class="col-xs-12 col-sm-6"> + <div class="card"> + <ul class="list-group list-group-flush"> + <li class="list-group-item"><a href="https://github.com/prasathmani/tinyfilemanager/wiki" target="_blank"><i class="fa fa-question-circle"></i> <?php echo lng('Help Documents') ?> </a> </li> + <li class="list-group-item"><a href="https://github.com/prasathmani/tinyfilemanager/issues" target="_blank"><i class="fa fa-bug"></i> <?php echo lng('Report Issue') ?></a></li> + <?php if(!FM_READONLY) { ?> + <li class="list-group-item"><a href="javascript:show_new_pwd();"><i class="fa fa-lock"></i> <?php echo lng('Generate new password hash') ?></a></li> + <?php } ?> + </ul> + </div> + </div> + </div> + <div class="row js-new-pwd hidden mt-2"> + <div class="col-12"> + <form class="form-inline" onsubmit="return new_password_hash(this)" method="POST" action=""> + <input type="hidden" name="type" value="pwdhash" aria-label="hidden" aria-hidden="true"> + <div class="form-group mb-2"> + <label for="staticEmail2"><?php echo lng('Generate new password hash') ?></label> + </div> + <div class="form-group mx-sm-3 mb-2"> + <label for="inputPassword2" class="sr-only"><?php echo lng('Password') ?></label> + <input type="text" class="form-control btn-sm" id="inputPassword2" name="inputPassword2" placeholder="<?php echo lng('Password') ?>" required> + </div> + <button type="submit" class="btn btn-success btn-sm mb-2"><?php echo lng('Generate') ?></button> + </form> + <textarea class="form-control" rows="2" readonly id="js-pwd-result"></textarea> + </div> + </div> + </div> + </div> + </div> + <?php + fm_show_footer(); + exit; +} + +// file viewer +if (isset($_GET['view'])) { + $file = $_GET['view']; + $file = fm_clean_path($file, false); + $file = str_replace('/', '', $file); + if ($file == '' || !is_file($path . '/' . $file) || in_array($file, $GLOBALS['exclude_items'])) { + fm_set_msg(lng('File not found'), 'error'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + } + + fm_show_header(); // HEADER + fm_show_nav_path(FM_PATH); // current path + + $file_url = FM_ROOT_URL . fm_convert_win((FM_PATH != '' ? '/' . FM_PATH : '') . '/' . $file); + $file_path = $path . '/' . $file; + + $ext = strtolower(pathinfo($file_path, PATHINFO_EXTENSION)); + $mime_type = fm_get_mime_type($file_path); + $filesize_raw = fm_get_size($file_path); + $filesize = fm_get_filesize($filesize_raw); + + $is_zip = false; + $is_gzip = false; + $is_image = false; + $is_audio = false; + $is_video = false; + $is_text = false; + $is_onlineViewer = false; + + $view_title = 'File'; + $filenames = false; // for zip + $content = ''; // for text + $online_viewer = strtolower(FM_DOC_VIEWER); + + if($online_viewer && $online_viewer !== 'false' && in_array($ext, fm_get_onlineViewer_exts())){ + $is_onlineViewer = true; + } + elseif ($ext == 'zip' || $ext == 'tar') { + $is_zip = true; + $view_title = 'Archive'; + $filenames = fm_get_zif_info($file_path, $ext); + } elseif (in_array($ext, fm_get_image_exts())) { + $is_image = true; + $view_title = 'Image'; + } elseif (in_array($ext, fm_get_audio_exts())) { + $is_audio = true; + $view_title = 'Audio'; + } elseif (in_array($ext, fm_get_video_exts())) { + $is_video = true; + $view_title = 'Video'; + } elseif (in_array($ext, fm_get_text_exts()) || substr($mime_type, 0, 4) == 'text' || in_array($mime_type, fm_get_text_mimes())) { + $is_text = true; + $content = file_get_contents($file_path); + } + + ?> + <div class="row"> + <div class="col-12"> + <p class="break-word"><b><?php echo lng($view_title) ?> "<?php echo fm_enc(fm_convert_win($file)) ?>"</b></p> + <p class="break-word"> + <strong>Full path:</strong> <?php echo fm_enc(fm_convert_win($file_path)) ?><br> + <strong>File size:</strong> <?php echo ($filesize_raw <= 1000) ? "$filesize_raw bytes" : $filesize; ?><br> + <strong>MIME-type:</strong> <?php echo $mime_type ?><br> + <?php + // ZIP info + if (($is_zip || $is_gzip) && $filenames !== false) { + $total_files = 0; + $total_comp = 0; + $total_uncomp = 0; + foreach ($filenames as $fn) { + if (!$fn['folder']) { + $total_files++; + } + $total_comp += $fn['compressed_size']; + $total_uncomp += $fn['filesize']; + } + ?> + <?php echo lng('Files in archive') ?>: <?php echo $total_files ?><br> + <?php echo lng('Total size') ?>: <?php echo fm_get_filesize($total_uncomp) ?><br> + <?php echo lng('Size in archive') ?>: <?php echo fm_get_filesize($total_comp) ?><br> + <?php echo lng('Compression') ?>: <?php echo round(($total_comp / max($total_uncomp, 1)) * 100) ?>%<br> + <?php + } + // Image info + if ($is_image) { + $image_size = getimagesize($file_path); + echo lng('Image sizes').': ' . (isset($image_size[0]) ? $image_size[0] : '0') . ' x ' . (isset($image_size[1]) ? $image_size[1] : '0') . '<br>'; + } + // Text info + if ($is_text) { + $is_utf8 = fm_is_utf8($content); + if (function_exists('iconv')) { + if (!$is_utf8) { + $content = iconv(FM_ICONV_INPUT_ENC, 'UTF-8//IGNORE', $content); + } + } + echo '<strong>'.lng('Charset').':</strong> ' . ($is_utf8 ? 'utf-8' : '8 bit') . '<br>'; + } + ?> + </p> + <div class="d-flex align-items-center mb-3"> + <form method="post" class="d-inline ms-2" action="?p=<?php echo urlencode(FM_PATH) ?>&dl=<?php echo urlencode($file) ?>"> + <input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>"> + <button type="submit" class="btn btn-link text-decoration-none fw-bold p-0"><i class="fa fa-cloud-download"></i> <?php echo lng('Download') ?></button>   + </form> + <b class="ms-2"><a href="<?php echo fm_enc($file_url) ?>" target="_blank"><i class="fa fa-external-link-square"></i> <?php echo lng('Open') ?></a></b> + <?php + // ZIP actions + if (!FM_READONLY && ($is_zip || $is_gzip) && $filenames !== false) { + $zip_name = pathinfo($file_path, PATHINFO_FILENAME); + ?> + <form method="post" class="d-inline ms-2"> + <input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>"> + <input type="hidden" name="unzip" value="<?php echo urlencode($file); ?>"> + <button type="submit" class="btn btn-link text-decoration-none fw-bold p-0" style="font-size: 14px;"><i class="fa fa-check-circle"></i> <?php echo lng('UnZip') ?></button> + </form>  + <form method="post" class="d-inline ms-2"> + <input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>"> + <input type="hidden" name="unzip" value="<?php echo urlencode($file); ?>"> + <input type="hidden" name="tofolder" value="1"> + <button type="submit" class="btn btn-link text-decoration-none fw-bold p-0" style="font-size: 14px;" data-bs-toggle="tooltip" data-bs-title="UnZip to <?php echo fm_enc($zip_name) ?>"><i class="fa fa-check-circle"></i> <?php echo lng('UnZipToFolder') ?></button> + </form>  + <?php + } + if ($is_text && !FM_READONLY) { + ?> + <b class="ms-2"><a href="?p=<?php echo urlencode(trim(FM_PATH)) ?>&edit=<?php echo urlencode($file) ?>" class="edit-file"><i class="fa fa-pencil-square"></i> <?php echo lng('Edit') ?> + </a></b>   + <b class="ms-2"><a href="?p=<?php echo urlencode(trim(FM_PATH)) ?>&edit=<?php echo urlencode($file) ?>&env=ace" + class="edit-file"><i class="fa fa-pencil-square-o"></i> <?php echo lng('AdvancedEditor') ?> + </a></b>   + <?php } ?> + <b class="ms-2"><a href="?p=<?php echo urlencode(FM_PATH) ?>"><i class="fa fa-chevron-circle-left go-back"></i> <?php echo lng('Back') ?></a></b> + </div> + <?php + if($is_onlineViewer) { + if($online_viewer == 'google') { + echo '<iframe src="https://docs.google.com/viewer?embedded=true&hl=en&url=' . fm_enc($file_url) . '" frameborder="no" style="width:100%;min-height:460px"></iframe>'; + } else if($online_viewer == 'microsoft') { + echo '<iframe src="https://view.officeapps.live.com/op/embed.aspx?src=' . fm_enc($file_url) . '" frameborder="no" style="width:100%;min-height:460px"></iframe>'; + } + } elseif ($is_zip) { + // ZIP content + if ($filenames !== false) { + echo '<code class="maxheight">'; + foreach ($filenames as $fn) { + if ($fn['folder']) { + echo '<b>' . fm_enc($fn['name']) . '</b><br>'; + } else { + echo $fn['name'] . ' (' . fm_get_filesize($fn['filesize']) . ')<br>'; + } + } + echo '</code>'; + } else { + echo '<p>'.lng('Error while fetching archive info').'</p>'; + } + } elseif ($is_image) { + // Image content + if (in_array($ext, array('gif', 'jpg', 'jpeg', 'png', 'bmp', 'ico', 'svg', 'webp', 'avif'))) { + echo '<p><img src="' . fm_enc($file_url) . '" alt="image" class="preview-img-container" class="preview-img"></p>'; + } + } elseif ($is_audio) { + // Audio content + echo '<p><audio src="' . fm_enc($file_url) . '" controls preload="metadata"></audio></p>'; + } elseif ($is_video) { + // Video content + echo '<div class="preview-video"><video src="' . fm_enc($file_url) . '" width="640" height="360" controls preload="metadata"></video></div>'; + } elseif ($is_text) { + if (FM_USE_HIGHLIGHTJS) { + // highlight + $hljs_classes = array( + 'shtml' => 'xml', + 'htaccess' => 'apache', + 'phtml' => 'php', + 'lock' => 'json', + 'svg' => 'xml', + ); + $hljs_class = isset($hljs_classes[$ext]) ? 'lang-' . $hljs_classes[$ext] : 'lang-' . $ext; + if (empty($ext) || in_array(strtolower($file), fm_get_text_names()) || preg_match('#\.min\.(css|js)$#i', $file)) { + $hljs_class = 'nohighlight'; + } + $content = '<pre class="with-hljs"><code class="' . $hljs_class . '">' . fm_enc($content) . '</code></pre>'; + } elseif (in_array($ext, array('php', 'php4', 'php5', 'phtml', 'phps'))) { + // php highlight + $content = highlight_string($content, true); + } else { + $content = '<pre>' . fm_enc($content) . '</pre>'; + } + echo $content; + } + ?> + </div> + </div> + <?php + fm_show_footer(); + exit; +} + +// file editor +if (isset($_GET['edit']) && !FM_READONLY) { + $file = $_GET['edit']; + $file = fm_clean_path($file, false); + $file = str_replace('/', '', $file); + if ($file == '' || !is_file($path . '/' . $file) || in_array($file, $GLOBALS['exclude_items'])) { + fm_set_msg(lng('File not found'), 'error'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + } + $editFile = ' : <i><b>'. $file. '</b></i>'; + header('X-XSS-Protection:0'); + fm_show_header(); // HEADER + fm_show_nav_path(FM_PATH); // current path + + $file_url = FM_ROOT_URL . fm_convert_win((FM_PATH != '' ? '/' . FM_PATH : '') . '/' . $file); + $file_path = $path . '/' . $file; + + // normal editer + $isNormalEditor = true; + if (isset($_GET['env'])) { + if ($_GET['env'] == "ace") { + $isNormalEditor = false; + } + } + + // Save File + if (isset($_POST['savedata'])) { + $writedata = $_POST['savedata']; + $fd = fopen($file_path, "w"); + @fwrite($fd, $writedata); + fclose($fd); + fm_set_msg(lng('File Saved Successfully')); + } + + $ext = strtolower(pathinfo($file_path, PATHINFO_EXTENSION)); + $mime_type = fm_get_mime_type($file_path); + $filesize = filesize($file_path); + $is_text = false; + $content = ''; // for text + + if (in_array($ext, fm_get_text_exts()) || substr($mime_type, 0, 4) == 'text' || in_array($mime_type, fm_get_text_mimes())) { + $is_text = true; + $content = file_get_contents($file_path); + } + + ?> + <div class="path"> + <div class="row"> + <div class="col-xs-12 col-sm-5 col-lg-6 pt-1"> + <div class="btn-toolbar" role="toolbar"> + <?php if (!$isNormalEditor) { ?> + <div class="btn-group js-ace-toolbar"> + <button data-cmd="none" data-option="fullscreen" class="btn btn-sm btn-outline-secondary" id="js-ace-fullscreen" data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Fullscreen') ?>"><i class="fa fa-expand" data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Fullscreen') ?>"></i></button> + <button data-cmd="find" class="btn btn-sm btn-outline-secondary" id="js-ace-search" data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Search') ?>"><i class="fa fa-search" data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Search') ?>"></i></button> + <button data-cmd="undo" class="btn btn-sm btn-outline-secondary" id="js-ace-undo" data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Undo') ?>"><i class="fa fa-undo" data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Undo') ?>"></i></button> + <button data-cmd="redo" class="btn btn-sm btn-outline-secondary" id="js-ace-redo" data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Redo') ?>"><i class="fa fa-repeat" data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Redo') ?>"></i></button> + <button data-cmd="none" data-option="wrap" class="btn btn-sm btn-outline-secondary" id="js-ace-wordWrap" data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Word Wrap') ?>"><i class="fa fa-text-width" data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Word Wrap') ?>"></i></button> + <select id="js-ace-mode" data-type="mode" title="<?php echo lng('Select Document Type') ?>" class="btn-outline-secondary border-start-0 d-none d-md-block"><option>-- <?php echo lng('Select Mode') ?> --</option></select> + <select id="js-ace-theme" data-type="theme" title="<?php echo lng('Select Theme') ?>" class="btn-outline-secondary border-start-0 d-none d-lg-block"><option>-- <?php echo lng('Select Theme') ?> --</option></select> + <select id="js-ace-fontSize" data-type="fontSize" title="<?php echo lng('Select Font Size') ?>" class="btn-outline-secondary border-start-0 d-none d-lg-block"><option>-- <?php echo lng('Select Font Size') ?> --</option></select> + </div> + <?php } ?> + </div> + </div> + <div class="edit-file-actions col-xs-12 col-sm-7 col-lg-6 text-end pt-1"> + <a title="<?php echo lng('Back') ?>" class="btn btn-sm btn-outline-primary" href="?p=<?php echo urlencode(trim(FM_PATH)) ?>&view=<?php echo urlencode($file) ?>"><i class="fa fa-reply-all"></i> <?php echo lng('Back') ?></a> + <a title="<?php echo lng('BackUp') ?>" class="btn btn-sm btn-outline-primary" href="javascript:void(0);" onclick="backup('<?php echo urlencode(trim(FM_PATH)) ?>','<?php echo urlencode($file) ?>')"><i class="fa fa-database"></i> <?php echo lng('BackUp') ?></a> + <?php if ($is_text) { ?> + <?php if ($isNormalEditor) { ?> + <a title="Advanced" class="btn btn-sm btn-outline-primary" href="?p=<?php echo urlencode(trim(FM_PATH)) ?>&edit=<?php echo urlencode($file) ?>&env=ace"><i class="fa fa-pencil-square-o"></i> <?php echo lng('AdvancedEditor') ?></a> + <button type="button" class="btn btn-sm btn-success" name="Save" data-url="<?php echo fm_enc($file_url) ?>" onclick="edit_save(this,'nrl')"><i class="fa fa-floppy-o"></i> Save + </button> + <?php } else { ?> + <a title="Plain Editor" class="btn btn-sm btn-outline-primary" href="?p=<?php echo urlencode(trim(FM_PATH)) ?>&edit=<?php echo urlencode($file) ?>"><i class="fa fa-text-height"></i> <?php echo lng('NormalEditor') ?></a> + <button type="button" class="btn btn-sm btn-success" name="Save" data-url="<?php echo fm_enc($file_url) ?>" onclick="edit_save(this,'ace')"><i class="fa fa-floppy-o"></i> <?php echo lng('Save') ?> + </button> + <?php } ?> + <?php } ?> + </div> + </div> + <?php + if ($is_text && $isNormalEditor) { + echo '<textarea class="mt-2" id="normal-editor" rows="33" cols="120" style="width: 99.5%;">' . htmlspecialchars($content) . '</textarea>'; + echo '<script>document.addEventListener("keydown", function(e) {if ((window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 83) { e.preventDefault();edit_save(this,"nrl");}}, false);</script>'; + } elseif ($is_text) { + echo '<div id="editor" contenteditable="true">' . htmlspecialchars($content) . '</div>'; + } else { + fm_set_msg(lng('FILE EXTENSION HAS NOT SUPPORTED'), 'error'); + } + ?> + </div> + <?php + fm_show_footer(); + exit; +} + +// chmod (not for Windows) +if (isset($_GET['chmod']) && !FM_READONLY && !FM_IS_WIN) { + $file = $_GET['chmod']; + $file = fm_clean_path($file); + $file = str_replace('/', '', $file); + if ($file == '' || (!is_file($path . '/' . $file) && !is_dir($path . '/' . $file))) { + fm_set_msg(lng('File not found'), 'error'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + } + + fm_show_header(); // HEADER + fm_show_nav_path(FM_PATH); // current path + + $file_url = FM_ROOT_URL . (FM_PATH != '' ? '/' . FM_PATH : '') . '/' . $file; + $file_path = $path . '/' . $file; + + $mode = fileperms($path . '/' . $file); + ?> + <div class="path"> + <div class="card mb-2 <?php echo fm_get_theme(); ?>"> + <h6 class="card-header"> + <?php echo lng('ChangePermissions') ?> + </h6> + <div class="card-body"> + <p class="card-text"> + Full path: <?php echo $file_path ?><br> + </p> + <form action="" method="post"> + <input type="hidden" name="p" value="<?php echo fm_enc(FM_PATH) ?>"> + <input type="hidden" name="chmod" value="<?php echo fm_enc($file) ?>"> + + <table class="table compact-table <?php echo fm_get_theme(); ?>"> + <tr> + <td></td> + <td><b><?php echo lng('Owner') ?></b></td> + <td><b><?php echo lng('Group') ?></b></td> + <td><b><?php echo lng('Other') ?></b></td> + </tr> + <tr> + <td style="text-align: right"><b><?php echo lng('Read') ?></b></td> + <td><label><input type="checkbox" name="ur" value="1"<?php echo ($mode & 00400) ? ' checked' : '' ?>></label></td> + <td><label><input type="checkbox" name="gr" value="1"<?php echo ($mode & 00040) ? ' checked' : '' ?>></label></td> + <td><label><input type="checkbox" name="or" value="1"<?php echo ($mode & 00004) ? ' checked' : '' ?>></label></td> + </tr> + <tr> + <td style="text-align: right"><b><?php echo lng('Write') ?></b></td> + <td><label><input type="checkbox" name="uw" value="1"<?php echo ($mode & 00200) ? ' checked' : '' ?>></label></td> + <td><label><input type="checkbox" name="gw" value="1"<?php echo ($mode & 00020) ? ' checked' : '' ?>></label></td> + <td><label><input type="checkbox" name="ow" value="1"<?php echo ($mode & 00002) ? ' checked' : '' ?>></label></td> + </tr> + <tr> + <td style="text-align: right"><b><?php echo lng('Execute') ?></b></td> + <td><label><input type="checkbox" name="ux" value="1"<?php echo ($mode & 00100) ? ' checked' : '' ?>></label></td> + <td><label><input type="checkbox" name="gx" value="1"<?php echo ($mode & 00010) ? ' checked' : '' ?>></label></td> + <td><label><input type="checkbox" name="ox" value="1"<?php echo ($mode & 00001) ? ' checked' : '' ?>></label></td> + </tr> + </table> + + <p> + <input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>"> + <b><a href="?p=<?php echo urlencode(FM_PATH) ?>" class="btn btn-outline-primary"><i class="fa fa-times-circle"></i> <?php echo lng('Cancel') ?></a></b>  + <button type="submit" class="btn btn-success"><i class="fa fa-check-circle"></i> <?php echo lng('Change') ?></button> + </p> + </form> + </div> + </div> + </div> + <?php + fm_show_footer(); + exit; +} + +// --- TINYFILEMANAGER MAIN --- +fm_show_header(); // HEADER +fm_show_nav_path(FM_PATH); // current path + +// show alert messages +fm_show_message(); + +$num_files = count($files); +$num_folders = count($folders); +$all_files_size = 0; +$tableTheme = (FM_THEME == "dark") ? "text-white bg-dark table-dark" : "bg-white"; +?> +<form action="" method="post" class="pt-3"> + <input type="hidden" name="p" value="<?php echo fm_enc(FM_PATH) ?>"> + <input type="hidden" name="group" value="1"> + <input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>"> + <div class="table-responsive"> + <table class="table table-bordered table-hover table-sm <?php echo $tableTheme; ?>" id="main-table"> + <thead class="thead-white"> + <tr> + <?php if (!FM_READONLY): ?> + <th style="width:3%" class="custom-checkbox-header"> + <div class="custom-control custom-checkbox"> + <input type="checkbox" class="custom-control-input" id="js-select-all-items" onclick="checkbox_toggle()"> + <label class="custom-control-label" for="js-select-all-items"></label> + </div> + </th><?php endif; ?> + <th><?php echo lng('Name') ?></th> + <th><?php echo lng('Size') ?></th> + <th><?php echo lng('Modified') ?></th> + <?php if (!FM_IS_WIN && !$hide_Cols): ?> + <th><?php echo lng('Perms') ?></th> + <th><?php echo lng('Owner') ?></th><?php endif; ?> + <th><?php echo lng('Actions') ?></th> + </tr> + </thead> + <?php + // link to parent folder + if ($parent !== false) { + ?> + <tr><?php if (!FM_READONLY): ?> + <td class="nosort"></td><?php endif; ?> + <td class="border-0" data-sort><a href="?p=<?php echo urlencode($parent) ?>"><i class="fa fa-chevron-circle-left go-back"></i> ..</a></td> + <td class="border-0" data-order></td> + <td class="border-0" data-order></td> + <td class="border-0"></td> + <?php if (!FM_IS_WIN && !$hide_Cols) { ?> + <td class="border-0"></td> + <td class="border-0"></td> + <?php } ?> + </tr> + <?php + } + $ii = 3399; + foreach ($folders as $f) { + $is_link = is_link($path . '/' . $f); + $img = $is_link ? 'icon-link_folder' : 'fa fa-folder-o'; + $modif_raw = filemtime($path . '/' . $f); + $modif = date(FM_DATETIME_FORMAT, $modif_raw); + $date_sorting = strtotime(date("F d Y H:i:s.", $modif_raw)); + $filesize_raw = ""; + $filesize = lng('Folder'); + $perms = substr(decoct(fileperms($path . '/' . $f)), -4); + if (function_exists('posix_getpwuid') && function_exists('posix_getgrgid')) { + $owner = posix_getpwuid(fileowner($path . '/' . $f)); + $group = posix_getgrgid(filegroup($path . '/' . $f)); + } else { + $owner = array('name' => '?'); + $group = array('name' => '?'); + } + ?> + <tr> + <?php if (!FM_READONLY): ?> + <td class="custom-checkbox-td"> + <div class="custom-control custom-checkbox"> + <input type="checkbox" class="custom-control-input" id="<?php echo $ii ?>" name="file[]" value="<?php echo fm_enc($f) ?>"> + <label class="custom-control-label" for="<?php echo $ii ?>"></label> + </div> + </td><?php endif; ?> + <td data-sort=<?php echo fm_convert_win(fm_enc($f)) ?>> + <div class="filename"><a href="?p=<?php echo urlencode(trim(FM_PATH . '/' . $f, '/')) ?>"><i class="<?php echo $img ?>"></i> <?php echo fm_convert_win(fm_enc($f)) ?> + </a><?php echo($is_link ? ' → <i>' . readlink($path . '/' . $f) . '</i>' : '') ?></div> + </td> + <td data-order="a-<?php echo str_pad($filesize_raw, 18, "0", STR_PAD_LEFT);?>"> + <?php echo $filesize; ?> + </td> + <td data-order="a-<?php echo $date_sorting;?>"><?php echo $modif ?></td> + <?php if (!FM_IS_WIN && !$hide_Cols): ?> + <td><?php if (!FM_READONLY): ?><a data-bs-toggle="tooltip" data-bs-title="Change Permissions" href="?p=<?php echo urlencode(FM_PATH) ?>&chmod=<?php echo urlencode($f) ?>"><?php echo $perms ?></a><?php else: ?><?php echo $perms ?><?php endif; ?> + </td> + <td><?php echo $owner['name'] . ':' . $group['name'] ?></td> + <?php endif; ?> + <td class="inline-actions"><?php if (!FM_READONLY): ?> + <a data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Delete')?>" href="?p=<?php echo urlencode(FM_PATH) ?>&del=<?php echo urlencode($f) ?>" onclick="confirmDailog(event, '1028','<?php echo lng('Delete').' '.lng('Folder'); ?>','<?php echo urlencode($f) ?>', this.href);"> <i class="fa fa-trash-o" aria-hidden="true"></i></a> + <a data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Rename')?>" href="#" onclick="rename('<?php echo fm_enc(addslashes(FM_PATH)) ?>', '<?php echo fm_enc(addslashes($f)) ?>');return false;"><i class="fa fa-pencil-square-o" aria-hidden="true"></i></a> + <a data-bs-toggle="tooltip" data-bs-title="<?php echo lng('CopyTo')?>..." href="?p=&copy=<?php echo urlencode(trim(FM_PATH . '/' . $f, '/')) ?>"><i class="fa fa-files-o" aria-hidden="true"></i></a> + <?php endif; ?> + <a data-bs-toggle="tooltip" data-bs-title="<?php echo lng('DirectLink')?>" href="<?php echo fm_enc(FM_ROOT_URL . (FM_PATH != '' ? '/' . FM_PATH : '') . '/' . $f . '/') ?>" target="_blank"><i class="fa fa-link" aria-hidden="true"></i></a> + </td> + </tr> + <?php + flush(); + $ii++; + } + $ik = 6070; + foreach ($files as $f) { + $is_link = is_link($path . '/' . $f); + $img = $is_link ? 'fa fa-file-text-o' : fm_get_file_icon_class($path . '/' . $f); + $modif_raw = filemtime($path . '/' . $f); + $modif = date(FM_DATETIME_FORMAT, $modif_raw); + $date_sorting = strtotime(date("F d Y H:i:s.", $modif_raw)); + $filesize_raw = fm_get_size($path . '/' . $f); + $filesize = fm_get_filesize($filesize_raw); + $filelink = '?p=' . urlencode(FM_PATH) . '&view=' . urlencode($f); + $all_files_size += $filesize_raw; + $perms = substr(decoct(fileperms($path . '/' . $f)), -4); + if (function_exists('posix_getpwuid') && function_exists('posix_getgrgid')) { + $owner = posix_getpwuid(fileowner($path . '/' . $f)); + $group = posix_getgrgid(filegroup($path . '/' . $f)); + } else { + $owner = array('name' => '?'); + $group = array('name' => '?'); + } + ?> + <tr> + <?php if (!FM_READONLY): ?> + <td class="custom-checkbox-td"> + <div class="custom-control custom-checkbox"> + <input type="checkbox" class="custom-control-input" id="<?php echo $ik ?>" name="file[]" value="<?php echo fm_enc($f) ?>"> + <label class="custom-control-label" for="<?php echo $ik ?>"></label> + </div> + </td><?php endif; ?> + <td data-sort=<?php echo fm_enc($f) ?>> + <div class="filename"> + <?php + if (in_array(strtolower(pathinfo($f, PATHINFO_EXTENSION)), array('gif', 'jpg', 'jpeg', 'png', 'bmp', 'ico', 'svg', 'webp', 'avif'))): ?> + <?php $imagePreview = fm_enc(FM_ROOT_URL . (FM_PATH != '' ? '/' . FM_PATH : '') . '/' . $f); ?> + <a href="<?php echo $filelink ?>" data-preview-image="<?php echo $imagePreview ?>" title="<?php echo fm_enc($f) ?>"> + <?php else: ?> + <a href="<?php echo $filelink ?>" title="<?php echo $f ?>"> + <?php endif; ?> + <i class="<?php echo $img ?>"></i> <?php echo fm_convert_win(fm_enc($f)) ?> + </a> + <?php echo($is_link ? ' → <i>' . readlink($path . '/' . $f) . '</i>' : '') ?> + </div> + </td> + <td data-order="b-<?php echo str_pad($filesize_raw, 18, "0", STR_PAD_LEFT); ?>"><span title="<?php printf('%s bytes', $filesize_raw) ?>"> + <?php echo $filesize; ?> + </span></td> + <td data-order="b-<?php echo $date_sorting;?>"><?php echo $modif ?></td> + <?php if (!FM_IS_WIN && !$hide_Cols): ?> + <td><?php if (!FM_READONLY): ?><a data-bs-toggle="tooltip" data-bs-title="<?php echo 'Change Permissions' ?>" href="?p=<?php echo urlencode(FM_PATH) ?>&chmod=<?php echo urlencode($f) ?>"><?php echo $perms ?></a><?php else: ?><?php echo $perms ?><?php endif; ?> + </td> + <td><?php echo fm_enc($owner['name'] . ':' . $group['name']) ?></td> + <?php endif; ?> + <td class="inline-actions"> + <?php if (!FM_READONLY): ?> + <a data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Delete') ?>" href="?p=<?php echo urlencode(FM_PATH) ?>&del=<?php echo urlencode($f) ?>" onclick="confirmDailog(event, 1209, '<?php echo lng('Delete').' '.lng('File'); ?>','<?php echo urlencode($f); ?>', this.href);"> <i class="fa fa-trash-o"></i></a> + <a data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Rename') ?>" href="#" onclick="rename('<?php echo fm_enc(addslashes(FM_PATH)) ?>', '<?php echo fm_enc(addslashes($f)) ?>');return false;"><i class="fa fa-pencil-square-o"></i></a> + <a data-bs-toggle="tooltip" data-bs-title="<?php echo lng('CopyTo') ?>..." + href="?p=<?php echo urlencode(FM_PATH) ?>&copy=<?php echo urlencode(trim(FM_PATH . '/' . $f, '/')) ?>"><i class="fa fa-files-o"></i></a> + <?php endif; ?> + <a data-bs-toggle="tooltip" data-bs-title="<?php echo lng('DirectLink') ?>" href="<?php echo fm_enc(FM_ROOT_URL . (FM_PATH != '' ? '/' . FM_PATH : '') . '/' . $f) ?>" target="_blank"><i class="fa fa-link"></i></a> + <a data-bs-toggle="tooltip" data-bs-title="<?php echo lng('Download') ?>" href="?p=<?php echo urlencode(FM_PATH) ?>&dl=<?php echo urlencode($f) ?>" onclick="confirmDailog(event, 1211, '<?php echo lng('Download'); ?>','<?php echo urlencode($f); ?>', this.href);"><i class="fa fa-download"></i></a> + </td> + </tr> + <?php + flush(); + $ik++; + } + + if (empty($folders) && empty($files)) { ?> + <tfoot> + <tr><?php if (!FM_READONLY): ?> + <td></td><?php endif; ?> + <td colspan="<?php echo (!FM_IS_WIN && !$hide_Cols) ? '6' : '4' ?>"><em><?php echo lng('Folder is empty') ?></em></td> + </tr> + </tfoot> + <?php + } else { ?> + <tfoot> + <tr> + <td class="gray" colspan="<?php echo (!FM_IS_WIN && !$hide_Cols) ? (FM_READONLY ? '6' :'7') : (FM_READONLY ? '4' : '5') ?>"> + <?php echo lng('FullSize').': <span class="badge text-bg-light border-radius-0">'.fm_get_filesize($all_files_size).'</span>' ?> + <?php echo lng('File').': <span class="badge text-bg-light border-radius-0">'.$num_files.'</span>' ?> + <?php echo lng('Folder').': <span class="badge text-bg-light border-radius-0">'.$num_folders.'</span>' ?> + </td> + </tr> + </tfoot> + <?php } ?> + </table> + </div> + + <div class="row"> + <?php if (!FM_READONLY): ?> + <div class="col-xs-12 col-sm-9"> + <ul class="list-inline footer-action"> + <li class="list-inline-item"> <a href="#/select-all" class="btn btn-small btn-outline-primary btn-2" onclick="select_all();return false;"><i class="fa fa-check-square"></i> <?php echo lng('SelectAll') ?> </a></li> + <li class="list-inline-item"><a href="#/unselect-all" class="btn btn-small btn-outline-primary btn-2" onclick="unselect_all();return false;"><i class="fa fa-window-close"></i> <?php echo lng('UnSelectAll') ?> </a></li> + <li class="list-inline-item"><a href="#/invert-all" class="btn btn-small btn-outline-primary btn-2" onclick="invert_all();return false;"><i class="fa fa-th-list"></i> <?php echo lng('InvertSelection') ?> </a></li> + <li class="list-inline-item"><input type="submit" class="hidden" name="delete" id="a-delete" value="Delete" onclick="return confirm('<?php echo lng('Delete selected files and folders?'); ?>')"> + <a href="javascript:document.getElementById('a-delete').click();" class="btn btn-small btn-outline-primary btn-2"><i class="fa fa-trash"></i> <?php echo lng('Delete') ?> </a></li> + <li class="list-inline-item"><input type="submit" class="hidden" name="zip" id="a-zip" value="zip" onclick="return confirm('<?php echo lng('Create archive?'); ?>')"> + <a href="javascript:document.getElementById('a-zip').click();" class="btn btn-small btn-outline-primary btn-2"><i class="fa fa-file-archive-o"></i> <?php echo lng('Zip') ?> </a></li> + <li class="list-inline-item"><input type="submit" class="hidden" name="tar" id="a-tar" value="tar" onclick="return confirm('<?php echo lng('Create archive?'); ?>')"> + <a href="javascript:document.getElementById('a-tar').click();" class="btn btn-small btn-outline-primary btn-2"><i class="fa fa-file-archive-o"></i> <?php echo lng('Tar') ?> </a></li> + <li class="list-inline-item"><input type="submit" class="hidden" name="copy" id="a-copy" value="Copy"> + <a href="javascript:document.getElementById('a-copy').click();" class="btn btn-small btn-outline-primary btn-2"><i class="fa fa-files-o"></i> <?php echo lng('Copy') ?> </a></li> + </ul> + </div> + <div class="col-3 d-none d-sm-block"><a href="https://tinyfilemanager.github.io" target="_blank" class="float-right text-muted">Tiny File Manager <?php echo VERSION; ?></a></div> + <?php else: ?> + <div class="col-12"><a href="https://tinyfilemanager.github.io" target="_blank" class="float-right text-muted">Tiny File Manager <?php echo VERSION; ?></a></div> + <?php endif; ?> + </div> +</form> + +<?php +fm_show_footer(); + +// --- END HTML --- + +// Functions + +/** + * Verify CSRF TOKEN and remove after cerify + * @param string $token + * @return bool + */ +function verifyToken($token) +{ + if (hash_equals($_SESSION['token'], $token)) { + return true; + } + return false; +} + +/** + * Delete file or folder (recursively) + * @param string $path + * @return bool + */ +function fm_rdelete($path) +{ + if (is_link($path)) { + return unlink($path); + } elseif (is_dir($path)) { + $objects = scandir($path); + $ok = true; + if (is_array($objects)) { + foreach ($objects as $file) { + if ($file != '.' && $file != '..') { + if (!fm_rdelete($path . '/' . $file)) { + $ok = false; + } + } + } + } + return ($ok) ? rmdir($path) : false; + } elseif (is_file($path)) { + return unlink($path); + } + return false; +} + +/** + * Recursive chmod + * @param string $path + * @param int $filemode + * @param int $dirmode + * @return bool + * @todo Will use in mass chmod + */ +function fm_rchmod($path, $filemode, $dirmode) +{ + if (is_dir($path)) { + if (!chmod($path, $dirmode)) { + return false; + } + $objects = scandir($path); + if (is_array($objects)) { + foreach ($objects as $file) { + if ($file != '.' && $file != '..') { + if (!fm_rchmod($path . '/' . $file, $filemode, $dirmode)) { + return false; + } + } + } + } + return true; + } elseif (is_link($path)) { + return true; + } elseif (is_file($path)) { + return chmod($path, $filemode); + } + return false; +} + +/** + * Check the file extension which is allowed or not + * @param string $filename + * @return bool + */ +function fm_is_valid_ext($filename) +{ + $allowed = (FM_FILE_EXTENSION) ? explode(',', FM_FILE_EXTENSION) : false; + + $ext = pathinfo($filename, PATHINFO_EXTENSION); + $isFileAllowed = ($allowed) ? in_array($ext, $allowed) : true; + + return ($isFileAllowed) ? true : false; +} + +/** + * Safely rename + * @param string $old + * @param string $new + * @return bool|null + */ +function fm_rename($old, $new) +{ + $isFileAllowed = fm_is_valid_ext($new); + + if(!is_dir($old)) { + if (!$isFileAllowed) return false; + } + + return (!file_exists($new) && file_exists($old)) ? rename($old, $new) : null; +} + +/** + * Copy file or folder (recursively). + * @param string $path + * @param string $dest + * @param bool $upd Update files + * @param bool $force Create folder with same names instead file + * @return bool + */ +function fm_rcopy($path, $dest, $upd = true, $force = true) +{ + if (is_dir($path)) { + if (!fm_mkdir($dest, $force)) { + return false; + } + $objects = scandir($path); + $ok = true; + if (is_array($objects)) { + foreach ($objects as $file) { + if ($file != '.' && $file != '..') { + if (!fm_rcopy($path . '/' . $file, $dest . '/' . $file)) { + $ok = false; + } + } + } + } + return $ok; + } elseif (is_file($path)) { + return fm_copy($path, $dest, $upd); + } + return false; +} + +/** + * Safely create folder + * @param string $dir + * @param bool $force + * @return bool + */ +function fm_mkdir($dir, $force) +{ + if (file_exists($dir)) { + if (is_dir($dir)) { + return $dir; + } elseif (!$force) { + return false; + } + unlink($dir); + } + return mkdir($dir, 0777, true); +} + +/** + * Safely copy file + * @param string $f1 + * @param string $f2 + * @param bool $upd Indicates if file should be updated with new content + * @return bool + */ +function fm_copy($f1, $f2, $upd) +{ + $time1 = filemtime($f1); + if (file_exists($f2)) { + $time2 = filemtime($f2); + if ($time2 >= $time1 && $upd) { + return false; + } + } + $ok = copy($f1, $f2); + if ($ok) { + touch($f2, $time1); + } + return $ok; +} + +/** + * Get mime type + * @param string $file_path + * @return mixed|string + */ +function fm_get_mime_type($file_path) +{ + if (function_exists('finfo_open')) { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $file_path); + finfo_close($finfo); + return $mime; + } elseif (function_exists('mime_content_type')) { + return mime_content_type($file_path); + } elseif (!stristr(ini_get('disable_functions'), 'shell_exec')) { + $file = escapeshellarg($file_path); + $mime = shell_exec('file -bi ' . $file); + return $mime; + } else { + return '--'; + } +} + +/** + * HTTP Redirect + * @param string $url + * @param int $code + */ +function fm_redirect($url, $code = 302) +{ + header('Location: ' . $url, true, $code); + exit; +} + +/** + * Path traversal prevention and clean the url + * It replaces (consecutive) occurrences of / and \\ with whatever is in DIRECTORY_SEPARATOR, and processes /. and /.. fine. + * @param $path + * @return string + */ +function get_absolute_path($path) { + $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path); + $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen'); + $absolutes = array(); + foreach ($parts as $part) { + if ('.' == $part) continue; + if ('..' == $part) { + array_pop($absolutes); + } else { + $absolutes[] = $part; + } + } + return implode(DIRECTORY_SEPARATOR, $absolutes); +} + +/** + * Clean path + * @param string $path + * @return string + */ +function fm_clean_path($path, $trim = true) +{ + $path = $trim ? trim($path) : $path; + $path = trim($path, '\\/'); + $path = str_replace(array('../', '..\\'), '', $path); + $path = get_absolute_path($path); + if ($path == '..') { + $path = ''; + } + return str_replace('\\', '/', $path); +} + +/** + * Get parent path + * @param string $path + * @return bool|string + */ +function fm_get_parent_path($path) +{ + $path = fm_clean_path($path); + if ($path != '') { + $array = explode('/', $path); + if (count($array) > 1) { + $array = array_slice($array, 0, -1); + return implode('/', $array); + } + return ''; + } + return false; +} + +/** + * Check file is in exclude list + * @param string $file + * @return bool + */ +function fm_is_exclude_items($file) { + $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + if (isset($exclude_items) and sizeof($exclude_items)) { + unset($exclude_items); + } + + $exclude_items = FM_EXCLUDE_ITEMS; + if (version_compare(PHP_VERSION, '7.0.0', '<')) { + $exclude_items = unserialize($exclude_items); + } + if (!in_array($file, $exclude_items) && !in_array("*.$ext", $exclude_items)) { + return true; + } + return false; +} + +/** + * get language translations from json file + * @param int $tr + * @return array + */ +function fm_get_translations($tr) { + try { + $content = @file_get_contents(__DIR__ . '/translation.json'); + if($content !== FALSE) { + $lng = json_decode($content, TRUE); + global $lang_list; + foreach ($lng["language"] as $key => $value) + { + $code = $value["code"]; + $lang_list[$code] = $value["name"]; + if ($tr) + $tr[$code] = $value["translation"]; + } + return $tr; + } + + } + catch (Exception $e) { + echo $e; + } +} + +/** + * @param string $file + * Recover all file sizes larger than > 2GB. + * Works on php 32bits and 64bits and supports linux + * @return int|string + */ +function fm_get_size($file) +{ + static $iswin; + static $isdarwin; + if (!isset($iswin)) { + $iswin = (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN'); + } + if (!isset($isdarwin)) { + $isdarwin = (strtoupper(substr(PHP_OS, 0)) == "DARWIN"); + } + + static $exec_works; + if (!isset($exec_works)) { + $exec_works = (function_exists('exec') && !ini_get('safe_mode') && @exec('echo EXEC') == 'EXEC'); + } + + // try a shell command + if ($exec_works) { + $arg = escapeshellarg($file); + $cmd = ($iswin) ? "for %F in (\"$file\") do @echo %~zF" : ($isdarwin ? "stat -f%z $arg" : "stat -c%s $arg"); + @exec($cmd, $output); + if (is_array($output) && ctype_digit($size = trim(implode("\n", $output)))) { + return $size; + } + } + + // try the Windows COM interface + if ($iswin && class_exists("COM")) { + try { + $fsobj = new COM('Scripting.FileSystemObject'); + $f = $fsobj->GetFile( realpath($file) ); + $size = $f->Size; + } catch (Exception $e) { + $size = null; + } + if (ctype_digit($size)) { + return $size; + } + } + + // if all else fails + return filesize($file); +} + +/** + * Get nice filesize + * @param int $size + * @return string + */ +function fm_get_filesize($size) +{ + $size = (float) $size; + $units = array('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'); + $power = ($size > 0) ? floor(log($size, 1024)) : 0; + $power = ($power > (count($units) - 1)) ? (count($units) - 1) : $power; + return sprintf('%s %s', round($size / pow(1024, $power), 2), $units[$power]); +} + +/** + * Get total size of directory tree. + * + * @param string $directory Relative or absolute directory name. + * @return int Total number of bytes. + */ +function fm_get_directorysize($directory) { + $bytes = 0; + $directory = realpath($directory); + if ($directory !== false && $directory != '' && file_exists($directory)){ + foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS)) as $file){ + $bytes += $file->getSize(); + } + } + return $bytes; +} + +/** + * Get info about zip archive + * @param string $path + * @return array|bool + */ +function fm_get_zif_info($path, $ext) { + if ($ext == 'zip' && function_exists('zip_open')) { + $arch = @zip_open($path); + if ($arch) { + $filenames = array(); + while ($zip_entry = @zip_read($arch)) { + $zip_name = @zip_entry_name($zip_entry); + $zip_folder = substr($zip_name, -1) == '/'; + $filenames[] = array( + 'name' => $zip_name, + 'filesize' => @zip_entry_filesize($zip_entry), + 'compressed_size' => @zip_entry_compressedsize($zip_entry), + 'folder' => $zip_folder + //'compression_method' => zip_entry_compressionmethod($zip_entry), + ); + } + @zip_close($arch); + return $filenames; + } + } elseif($ext == 'tar' && class_exists('PharData')) { + $archive = new PharData($path); + $filenames = array(); + foreach(new RecursiveIteratorIterator($archive) as $file) { + $parent_info = $file->getPathInfo(); + $zip_name = str_replace("phar://".$path, '', $file->getPathName()); + $zip_name = substr($zip_name, ($pos = strpos($zip_name, '/')) !== false ? $pos + 1 : 0); + $zip_folder = $parent_info->getFileName(); + $zip_info = new SplFileInfo($file); + $filenames[] = array( + 'name' => $zip_name, + 'filesize' => $zip_info->getSize(), + 'compressed_size' => $file->getCompressedSize(), + 'folder' => $zip_folder + ); + } + return $filenames; + } + return false; +} + +/** + * Encode html entities + * @param string $text + * @return string + */ +function fm_enc($text) +{ + return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); +} + +/** + * Prevent XSS attacks + * @param string $text + * @return string + */ +function fm_isvalid_filename($text) { + return (strpbrk($text, '/?%*:|"<>') === FALSE) ? true : false; +} + +/** + * Save message in session + * @param string $msg + * @param string $status + */ +function fm_set_msg($msg, $status = 'ok') +{ + $_SESSION[FM_SESSION_ID]['message'] = $msg; + $_SESSION[FM_SESSION_ID]['status'] = $status; +} + +/** + * Check if string is in UTF-8 + * @param string $string + * @return int + */ +function fm_is_utf8($string) +{ + return preg_match('//u', $string); +} + +/** + * Convert file name to UTF-8 in Windows + * @param string $filename + * @return string + */ +function fm_convert_win($filename) +{ + if (FM_IS_WIN && function_exists('iconv')) { + $filename = iconv(FM_ICONV_INPUT_ENC, 'UTF-8//IGNORE', $filename); + } + return $filename; +} + +/** + * @param $obj + * @return array + */ +function fm_object_to_array($obj) +{ + if (!is_object($obj) && !is_array($obj)) { + return $obj; + } + if (is_object($obj)) { + $obj = get_object_vars($obj); + } + return array_map('fm_object_to_array', $obj); +} + +/** + * Get CSS classname for file + * @param string $path + * @return string + */ +function fm_get_file_icon_class($path) +{ + // get extension + $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + + switch ($ext) { + case 'ico': + case 'gif': + case 'jpg': + case 'jpeg': + case 'jpc': + case 'jp2': + case 'jpx': + case 'xbm': + case 'wbmp': + case 'png': + case 'bmp': + case 'tif': + case 'tiff': + case 'webp': + case 'avif': + case 'svg': + $img = 'fa fa-picture-o'; + break; + case 'passwd': + case 'ftpquota': + case 'sql': + case 'js': + case 'ts': + case 'jsx': + case 'tsx': + case 'hbs': + case 'json': + case 'sh': + case 'config': + case 'twig': + case 'tpl': + case 'md': + case 'gitignore': + case 'c': + case 'cpp': + case 'cs': + case 'py': + case 'rs': + case 'map': + case 'lock': + case 'dtd': + $img = 'fa fa-file-code-o'; + break; + case 'txt': + case 'ini': + case 'conf': + case 'log': + case 'htaccess': + case 'yaml': + case 'yml': + case 'toml': + $img = 'fa fa-file-text-o'; + break; + case 'css': + case 'less': + case 'sass': + case 'scss': + $img = 'fa fa-css3'; + break; + case 'bz2': + case 'zip': + case 'rar': + case 'gz': + case 'tar': + case '7z': + case 'xz': + $img = 'fa fa-file-archive-o'; + break; + case 'php': + case 'php4': + case 'php5': + case 'phps': + case 'phtml': + $img = 'fa fa-code'; + break; + case 'htm': + case 'html': + case 'shtml': + case 'xhtml': + $img = 'fa fa-html5'; + break; + case 'xml': + case 'xsl': + $img = 'fa fa-file-excel-o'; + break; + case 'wav': + case 'mp3': + case 'mp2': + case 'm4a': + case 'aac': + case 'ogg': + case 'oga': + case 'wma': + case 'mka': + case 'flac': + case 'ac3': + case 'tds': + $img = 'fa fa-music'; + break; + case 'm3u': + case 'm3u8': + case 'pls': + case 'cue': + case 'xspf': + $img = 'fa fa-headphones'; + break; + case 'avi': + case 'mpg': + case 'mpeg': + case 'mp4': + case 'm4v': + case 'flv': + case 'f4v': + case 'ogm': + case 'ogv': + case 'mov': + case 'mkv': + case '3gp': + case 'asf': + case 'wmv': + case 'webm': + $img = 'fa fa-file-video-o'; + break; + case 'eml': + case 'msg': + $img = 'fa fa-envelope-o'; + break; + case 'xls': + case 'xlsx': + case 'ods': + $img = 'fa fa-file-excel-o'; + break; + case 'csv': + $img = 'fa fa-file-text-o'; + break; + case 'bak': + case 'swp': + $img = 'fa fa-clipboard'; + break; + case 'doc': + case 'docx': + case 'odt': + $img = 'fa fa-file-word-o'; + break; + case 'ppt': + case 'pptx': + $img = 'fa fa-file-powerpoint-o'; + break; + case 'ttf': + case 'ttc': + case 'otf': + case 'woff': + case 'woff2': + case 'eot': + case 'fon': + $img = 'fa fa-font'; + break; + case 'pdf': + $img = 'fa fa-file-pdf-o'; + break; + case 'psd': + case 'ai': + case 'eps': + case 'fla': + case 'swf': + $img = 'fa fa-file-image-o'; + break; + case 'exe': + case 'msi': + $img = 'fa fa-file-o'; + break; + case 'bat': + $img = 'fa fa-terminal'; + break; + default: + $img = 'fa fa-info-circle'; + } + + return $img; +} + +/** + * Get image files extensions + * @return array + */ +function fm_get_image_exts() +{ + return array('ico', 'gif', 'jpg', 'jpeg', 'jpc', 'jp2', 'jpx', 'xbm', 'wbmp', 'png', 'bmp', 'tif', 'tiff', 'psd', 'svg', 'webp', 'avif'); +} + +/** + * Get video files extensions + * @return array + */ +function fm_get_video_exts() +{ + return array('avi', 'webm', 'wmv', 'mp4', 'm4v', 'ogm', 'ogv', 'mov', 'mkv'); +} + +/** + * Get audio files extensions + * @return array + */ +function fm_get_audio_exts() +{ + return array('wav', 'mp3', 'ogg', 'm4a'); +} + +/** + * Get text file extensions + * @return array + */ +function fm_get_text_exts() +{ + return array( + 'txt', 'css', 'ini', 'conf', 'log', 'htaccess', 'passwd', 'ftpquota', 'sql', 'js', 'ts', 'jsx', 'tsx', 'mjs', 'json', 'sh', 'config', + 'php', 'php4', 'php5', 'phps', 'phtml', 'htm', 'html', 'shtml', 'xhtml', 'xml', 'xsl', 'm3u', 'm3u8', 'pls', 'cue', 'bash', 'tpl', 'vue', + 'eml', 'msg', 'csv', 'bat', 'twig', 'tpl', 'md', 'gitignore', 'less', 'sass', 'scss', 'c', 'cpp', 'cs', 'py', 'go', 'zsh', 'swift', 'yml', + 'map', 'lock', 'dtd', 'svg', 'scss', 'asp', 'aspx', 'asx', 'asmx', 'ashx', 'jsp', 'jspx', 'cfm', 'cgi', 'dockerfile', 'ruby', 'twig', + 'yml', 'yaml', 'toml', 'md', 'vhost', 'scpt', 'applescript', 'c', 'cs', 'csx', 'cshtml', 'cpp', 'c++', 'coffee', 'cfm', 'rb', + 'graphql', 'mustache', 'jinja', 'phtml', 'http', 'handlebars', 'lock', 'java', 'es', 'es6', 'markdown', 'wiki', 'vhost', 'sql', + ); +} + +/** + * Get mime types of text files + * @return array + */ +function fm_get_text_mimes() +{ + return array( + 'application/xml', + 'application/javascript', + 'application/x-javascript', + 'image/svg+xml', + 'message/rfc822', + 'application/json', + ); +} + +/** + * Get file names of text files w/o extensions + * @return array + */ +function fm_get_text_names() +{ + return array( + 'license', + 'readme', + 'authors', + 'contributors', + 'changelog', + ); +} + +/** + * Get online docs viewer supported files extensions + * @return array + */ +function fm_get_onlineViewer_exts() +{ + return array('doc', 'docx', 'xls', 'xlsx', 'pdf', 'ppt', 'pptx', 'ai', 'psd', 'dxf', 'xps', 'rar', 'odt', 'ods'); +} + +/** + * It returns the mime type of a file based on its extension. + * @param extension The file extension of the file you want to get the mime type for. + * @return string|string[] The mime type of the file. + */ +function fm_get_file_mimes($extension) +{ + $fileTypes['swf'] = 'application/x-shockwave-flash'; + $fileTypes['pdf'] = 'application/pdf'; + $fileTypes['exe'] = 'application/octet-stream'; + $fileTypes['zip'] = 'application/zip'; + $fileTypes['doc'] = 'application/msword'; + $fileTypes['xls'] = 'application/vnd.ms-excel'; + $fileTypes['ppt'] = 'application/vnd.ms-powerpoint'; + $fileTypes['gif'] = 'image/gif'; + $fileTypes['png'] = 'image/png'; + $fileTypes['jpeg'] = 'image/jpg'; + $fileTypes['jpg'] = 'image/jpg'; + $fileTypes['webp'] = 'image/webp'; + $fileTypes['avif'] = 'image/avif'; + $fileTypes['rar'] = 'application/rar'; + + $fileTypes['ra'] = 'audio/x-pn-realaudio'; + $fileTypes['ram'] = 'audio/x-pn-realaudio'; + $fileTypes['ogg'] = 'audio/x-pn-realaudio'; + + $fileTypes['wav'] = 'video/x-msvideo'; + $fileTypes['wmv'] = 'video/x-msvideo'; + $fileTypes['avi'] = 'video/x-msvideo'; + $fileTypes['asf'] = 'video/x-msvideo'; + $fileTypes['divx'] = 'video/x-msvideo'; + + $fileTypes['mp3'] = 'audio/mpeg'; + $fileTypes['mp4'] = 'audio/mpeg'; + $fileTypes['mpeg'] = 'video/mpeg'; + $fileTypes['mpg'] = 'video/mpeg'; + $fileTypes['mpe'] = 'video/mpeg'; + $fileTypes['mov'] = 'video/quicktime'; + $fileTypes['swf'] = 'video/quicktime'; + $fileTypes['3gp'] = 'video/quicktime'; + $fileTypes['m4a'] = 'video/quicktime'; + $fileTypes['aac'] = 'video/quicktime'; + $fileTypes['m3u'] = 'video/quicktime'; + + $fileTypes['php'] = ['application/x-php']; + $fileTypes['html'] = ['text/html']; + $fileTypes['txt'] = ['text/plain']; + //Unknown mime-types should be 'application/octet-stream' + if(empty($fileTypes[$extension])) { + $fileTypes[$extension] = ['application/octet-stream']; + } + return $fileTypes[$extension]; +} + +/** + * This function scans the files and folder recursively, and return matching files + * @param string $dir + * @param string $filter + * @return array|null + */ + function scan($dir = '', $filter = '') { + $path = FM_ROOT_PATH.'/'.$dir; + if($path) { + $ite = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)); + $rii = new RegexIterator($ite, "/(" . $filter . ")/i"); + + $files = array(); + foreach ($rii as $file) { + if (!$file->isDir()) { + $fileName = $file->getFilename(); + $location = str_replace(FM_ROOT_PATH, '', $file->getPath()); + $files[] = array( + "name" => $fileName, + "type" => "file", + "path" => $location, + ); + } + } + return $files; + } +} + +/** +* Parameters: downloadFile(File Location, File Name, +* max speed, is streaming +* If streaming - videos will show as videos, images as images +* instead of download prompt +* https://stackoverflow.com/a/13821992/1164642 +*/ +function fm_download_file($fileLocation, $fileName, $chunkSize = 1024) +{ + if (connection_status() != 0) + return (false); + $extension = pathinfo($fileName, PATHINFO_EXTENSION); + + $contentType = fm_get_file_mimes($extension); + + if(is_array($contentType)) { + $contentType = implode(' ', $contentType); + } + + $size = filesize($fileLocation); + + if ($size == 0) { + fm_set_msg(lng('Zero byte file! Aborting download'), 'error'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + + return (false); + } + + @ini_set('magic_quotes_runtime', 0); + $fp = fopen("$fileLocation", "rb"); + + if ($fp === false) { + fm_set_msg(lng('Cannot open file! Aborting download'), 'error'); + $FM_PATH=FM_PATH; fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH)); + return (false); + } + + // headers + header('Content-Description: File Transfer'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Pragma: public'); + header("Content-Transfer-Encoding: binary"); + header("Content-Type: $contentType"); + + $contentDisposition = 'attachment'; + + if (strstr($_SERVER['HTTP_USER_AGENT'], "MSIE")) { + $fileName = preg_replace('/\./', '%2e', $fileName, substr_count($fileName, '.') - 1); + header("Content-Disposition: $contentDisposition;filename=\"$fileName\""); + } else { + header("Content-Disposition: $contentDisposition;filename=\"$fileName\""); + } + + header("Accept-Ranges: bytes"); + $range = 0; + + if (isset($_SERVER['HTTP_RANGE'])) { + list($a, $range) = explode("=", $_SERVER['HTTP_RANGE']); + str_replace($range, "-", $range); + $size2 = $size - 1; + $new_length = $size - $range; + header("HTTP/1.1 206 Partial Content"); + header("Content-Length: $new_length"); + header("Content-Range: bytes $range$size2/$size"); + } else { + $size2 = $size - 1; + header("Content-Range: bytes 0-$size2/$size"); + header("Content-Length: " . $size); + } + $fileLocation = realpath($fileLocation); + while (ob_get_level()) ob_end_clean(); + readfile($fileLocation); + + fclose($fp); + + return ((connection_status() == 0) and !connection_aborted()); +} + +/** + * If the theme is dark, return the text-white and bg-dark classes. + * @return string the value of the variable. + */ +function fm_get_theme() { + $result = ''; + if(FM_THEME == "dark") { + $result = "text-white bg-dark"; + } + return $result; +} + +/** + * Class to work with zip files (using ZipArchive) + */ +class FM_Zipper +{ + private $zip; + + public function __construct() + { + $this->zip = new ZipArchive(); + } + + /** + * Create archive with name $filename and files $files (RELATIVE PATHS!) + * @param string $filename + * @param array|string $files + * @return bool + */ + public function create($filename, $files) + { + $res = $this->zip->open($filename, ZipArchive::CREATE); + if ($res !== true) { + return false; + } + if (is_array($files)) { + foreach ($files as $f) { + $f = fm_clean_path($f); + if (!$this->addFileOrDir($f)) { + $this->zip->close(); + return false; + } + } + $this->zip->close(); + return true; + } else { + if ($this->addFileOrDir($files)) { + $this->zip->close(); + return true; + } + return false; + } + } + + /** + * Extract archive $filename to folder $path (RELATIVE OR ABSOLUTE PATHS) + * @param string $filename + * @param string $path + * @return bool + */ + public function unzip($filename, $path) + { + $res = $this->zip->open($filename); + if ($res !== true) { + return false; + } + if ($this->zip->extractTo($path)) { + $this->zip->close(); + return true; + } + return false; + } + + /** + * Add file/folder to archive + * @param string $filename + * @return bool + */ + private function addFileOrDir($filename) + { + if (is_file($filename)) { + return $this->zip->addFile($filename); + } elseif (is_dir($filename)) { + return $this->addDir($filename); + } + return false; + } + + /** + * Add folder recursively + * @param string $path + * @return bool + */ + private function addDir($path) + { + if (!$this->zip->addEmptyDir($path)) { + return false; + } + $objects = scandir($path); + if (is_array($objects)) { + foreach ($objects as $file) { + if ($file != '.' && $file != '..') { + if (is_dir($path . '/' . $file)) { + if (!$this->addDir($path . '/' . $file)) { + return false; + } + } elseif (is_file($path . '/' . $file)) { + if (!$this->zip->addFile($path . '/' . $file)) { + return false; + } + } + } + } + return true; + } + return false; + } +} + +/** + * Class to work with Tar files (using PharData) + */ +class FM_Zipper_Tar +{ + private $tar; + + public function __construct() + { + $this->tar = null; + } + + /** + * Create archive with name $filename and files $files (RELATIVE PATHS!) + * @param string $filename + * @param array|string $files + * @return bool + */ + public function create($filename, $files) + { + $this->tar = new PharData($filename); + if (is_array($files)) { + foreach ($files as $f) { + $f = fm_clean_path($f); + if (!$this->addFileOrDir($f)) { + return false; + } + } + return true; + } else { + if ($this->addFileOrDir($files)) { + return true; + } + return false; + } + } + + /** + * Extract archive $filename to folder $path (RELATIVE OR ABSOLUTE PATHS) + * @param string $filename + * @param string $path + * @return bool + */ + public function unzip($filename, $path) + { + $res = $this->tar->open($filename); + if ($res !== true) { + return false; + } + if ($this->tar->extractTo($path)) { + return true; + } + return false; + } + + /** + * Add file/folder to archive + * @param string $filename + * @return bool + */ + private function addFileOrDir($filename) + { + if (is_file($filename)) { + try { + $this->tar->addFile($filename); + return true; + } catch (Exception $e) { + return false; + } + } elseif (is_dir($filename)) { + return $this->addDir($filename); + } + return false; + } + + /** + * Add folder recursively + * @param string $path + * @return bool + */ + private function addDir($path) + { + $objects = scandir($path); + if (is_array($objects)) { + foreach ($objects as $file) { + if ($file != '.' && $file != '..') { + if (is_dir($path . '/' . $file)) { + if (!$this->addDir($path . '/' . $file)) { + return false; + } + } elseif (is_file($path . '/' . $file)) { + try { + $this->tar->addFile($path . '/' . $file); + } catch (Exception $e) { + return false; + } + } + } + } + return true; + } + return false; + } +} + +/** + * Save Configuration + */ + class FM_Config +{ + var $data; + + function __construct() + { + global $root_path, $root_url, $CONFIG; + $fm_url = $root_url.$_SERVER["PHP_SELF"]; + $this->data = array( + 'lang' => 'en', + 'error_reporting' => true, + 'show_hidden' => true + ); + $data = false; + if (strlen($CONFIG)) { + $data = fm_object_to_array(json_decode($CONFIG)); + } else { + $msg = 'Tiny File Manager<br>Error: Cannot load configuration'; + if (substr($fm_url, -1) == '/') { + $fm_url = rtrim($fm_url, '/'); + $msg .= '<br>'; + $msg .= '<br>Seems like you have a trailing slash on the URL.'; + $msg .= '<br>Try this link: <a href="' . $fm_url . '">' . $fm_url . '</a>'; + } + die($msg); + } + if (is_array($data) && count($data)) $this->data = $data; + else $this->save(); + } + + function save() + { + $fm_file = __FILE__; + $var_name = '$CONFIG'; + $var_value = var_export(json_encode($this->data), true); + $config_string = "<?php" . chr(13) . chr(10) . "//Default Configuration".chr(13) . chr(10)."$var_name = $var_value;" . chr(13) . chr(10); + if (is_writable($fm_file)) { + $lines = file($fm_file); + if ($fh = @fopen($fm_file, "w")) { + @fputs($fh, $config_string, strlen($config_string)); + for ($x = 3; $x < count($lines); $x++) { + @fputs($fh, $lines[$x], strlen($lines[$x])); + } + @fclose($fh); + } + } + } +} + +//--- Templates Functions --- + +/** + * Show nav block + * @param string $path + */ +function fm_show_nav_path($path) +{ + global $lang, $sticky_navbar, $editFile; + $isStickyNavBar = $sticky_navbar ? 'fixed-top' : ''; + $getTheme = fm_get_theme(); + $getTheme .= " navbar-light"; + if(FM_THEME == "dark") { + $getTheme .= " navbar-dark"; + } else { + $getTheme .= " bg-white"; + } + ?> + <nav class="navbar navbar-expand-lg <?php echo $getTheme; ?> mb-4 main-nav <?php echo $isStickyNavBar ?>"> + <a class="navbar-brand"> <?php echo lng('AppTitle') ?> </a> + <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse" id="navbarSupportedContent"> + + <?php + $path = fm_clean_path($path); + $root_url = "<a href='?p='><i class='fa fa-home' aria-hidden='true' title='" . FM_ROOT_PATH . "'></i></a>"; + $sep = '<i class="bread-crumb"> / </i>'; + if ($path != '') { + $exploded = explode('/', $path); + $count = count($exploded); + $array = array(); + $parent = ''; + for ($i = 0; $i < $count; $i++) { + $parent = trim($parent . '/' . $exploded[$i], '/'); + $parent_enc = urlencode($parent); + $array[] = "<a href='?p={$parent_enc}'>" . fm_enc(fm_convert_win($exploded[$i])) . "</a>"; + } + $root_url .= $sep . implode($sep, $array); + } + echo '<div class="col-xs-6 col-sm-5">' . $root_url . $editFile . '</div>'; + ?> + + <div class="col-xs-6 col-sm-7"> + <ul class="navbar-nav justify-content-end <?php echo fm_get_theme(); ?>"> + <li class="nav-item mr-2"> + <div class="input-group input-group-sm mr-1" style="margin-top:4px;"> + <input type="text" class="form-control" placeholder="<?php echo lng('Filter') ?>" aria-label="<?php echo lng('Search') ?>" aria-describedby="search-addon2" id="search-addon"> + <div class="input-group-append"> + <span class="input-group-text brl-0 brr-0" id="search-addon2"><i class="fa fa-search"></i></span> + </div> + <div class="input-group-append btn-group"> + <span class="input-group-text dropdown-toggle brl-0" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span> + <div class="dropdown-menu dropdown-menu-right"> + <a class="dropdown-item" href="<?php echo $path2 = $path ? $path : '.'; ?>" id="js-search-modal" data-bs-toggle="modal" data-bs-target="#searchModal"><?php echo lng('Advanced Search') ?></a> + </div> + </div> + </div> + </li> + <?php if (!FM_READONLY): ?> + <li class="nav-item"> + <a title="<?php echo lng('Upload') ?>" class="nav-link" href="?p=<?php echo urlencode(FM_PATH) ?>&upload"><i class="fa fa-cloud-upload" aria-hidden="true"></i> <?php echo lng('Upload') ?></a> + </li> + <li class="nav-item"> + <a title="<?php echo lng('NewItem') ?>" class="nav-link" href="#createNewItem" data-bs-toggle="modal" data-bs-target="#createNewItem"><i class="fa fa-plus-square"></i> <?php echo lng('NewItem') ?></a> + </li> + <?php endif; ?> + <?php if (FM_USE_AUTH): ?> + <li class="nav-item avatar dropdown"> + <a class="nav-link dropdown-toggle" id="navbarDropdownMenuLink-5" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <i class="fa fa-user-circle"></i> <?php if(isset($_SESSION[FM_SESSION_ID]['logged'])) { echo $_SESSION[FM_SESSION_ID]['logged']; } ?></a> + <div class="dropdown-menu text-small shadow <?php echo fm_get_theme(); ?>" aria-labelledby="navbarDropdownMenuLink-5"> + <?php if (!FM_READONLY): ?> + <a title="<?php echo lng('Settings') ?>" class="dropdown-item nav-link" href="?p=<?php echo urlencode(FM_PATH) ?>&settings=1"><i class="fa fa-cog" aria-hidden="true"></i> <?php echo lng('Settings') ?></a> + <?php endif ?> + <a title="<?php echo lng('Help') ?>" class="dropdown-item nav-link" href="?p=<?php echo urlencode(FM_PATH) ?>&help=2"><i class="fa fa-exclamation-circle" aria-hidden="true"></i> <?php echo lng('Help') ?></a> + <a title="<?php echo lng('Logout') ?>" class="dropdown-item nav-link" href="?logout=1"><i class="fa fa-sign-out" aria-hidden="true"></i> <?php echo lng('Logout') ?></a> + </div> + </li> + <?php else: ?> + <?php if (!FM_READONLY): ?> + <li class="nav-item"> + <a title="<?php echo lng('Settings') ?>" class="dropdown-item nav-link" href="?p=<?php echo urlencode(FM_PATH) ?>&settings=1"><i class="fa fa-cog" aria-hidden="true"></i> <?php echo lng('Settings') ?></a> + </li> + <?php endif; ?> + <?php endif; ?> + </ul> + </div> + </div> + </nav> + <?php +} + +/** + * Show alert message from session + */ +function fm_show_message() +{ + if (isset($_SESSION[FM_SESSION_ID]['message'])) { + $class = isset($_SESSION[FM_SESSION_ID]['status']) ? $_SESSION[FM_SESSION_ID]['status'] : 'ok'; + echo '<p class="message ' . $class . '">' . $_SESSION[FM_SESSION_ID]['message'] . '</p>'; + unset($_SESSION[FM_SESSION_ID]['message']); + unset($_SESSION[FM_SESSION_ID]['status']); + } +} + +/** + * Show page header in Login Form + */ +function fm_show_header_login() +{ +$sprites_ver = '20160315'; +header("Content-Type: text/html; charset=utf-8"); +header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); +header("Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0"); +header("Pragma: no-cache"); + +global $lang, $root_url, $favicon_path; +?> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="description" content="Web based File Manager in PHP, Manage your files efficiently and easily with Tiny File Manager"> + <meta name="author" content="CCP Programmers"> + <meta name="robots" content="noindex, nofollow"> + <meta name="googlebot" content="noindex"> + <?php if($favicon_path) { echo '<link rel="icon" href="'.fm_enc($favicon_path).'" type="image/png">'; } ?> + <title><?php echo fm_enc(APP_TITLE) ?> + + + + + +"> +
+ + +
+ + + + + + + + + + + + + '; } ?> + <?php echo fm_enc(APP_TITLE) ?> + + + + + + + + + + + + + + + + + "> +
+ + + + + + + + + + + + + +
+ + + + + + + + + + +
+ + + diff --git a/web/app/controllers/tinyfilemanager/translation.json b/web/app/controllers/tinyfilemanager/translation.json new file mode 100644 index 0000000..5b1ab75 --- /dev/null +++ b/web/app/controllers/tinyfilemanager/translation.json @@ -0,0 +1,168 @@ +{ + "appName": "Tiny File Manager", + "version": "2.5.1", + "language": [ + { + "name": "简体中文", + "code": "zh-CN", + "translation": { + "AppName": "文件管理器", + "AppTitle": "文件管理器", + "Login": "登录", + "Username": "账号", + "Password": "密码", + "Logout": "退出", + "Move": "移动", + "Copy": "复制", + "Save": "保存", + "SelectAll": "全选", + "UnSelectAll": "取消全选", + "File": "文件", + "Back": "取消", + "Size": "大小", + "Perms": "权限", + "Modified": "修改时间", + "Owner": "拥有者", + "Search": "查找", + "NewItem": "创建新文件/文件夹", + "Folder": "文件夹", + "Delete": "删除", + "CopyTo": "复制到", + "DirectLink": "直链", + "UploadingFiles": "上传", + "ChangePermissions": "修改权限", + "Copying": "复制中", + "CreateNewItem": "创建新文件", + "Name": "文件名", + "AdvancedEditor": "高级编辑器", + "RememberMe": "记住登录信息", + "Actions": "执行操作", + "Upload": "上传", + "Cancel": "取消", + "InvertSelection": "反向选择", + "DestinationFolder": "目标文件夹", + "ItemType": "文件类型", + "ItemName": "创建名称", + "CreateNow": "创建", + "Download": "下载", + "UnZip": "解压缩", + "UnZipToFolder": "解压至目标文件夹", + "Edit": "编辑", + "NormalEditor": "编辑器", + "BackUp": "备份", + "SourceFolder": "源文件夹", + "Files": "文件", + "Change": "修改", + "Settings": "设置", + "Language": "语言", + "Open": "打开", + "Group": "用户组", + "Other": "其它用户", + "Read": "读取权限", + "Write": "写入权限", + "Execute": "执行权限", + "Rename": "重命名", + "enable": "启用", + "disable": "禁用", + "ErrorReporting": "上传错误报告", + "ShowHiddenFiles": "显示隐藏文件", + "Help": "帮助", + "HideColumns": "隐藏权限&拥有者", + "CalculateFolderSize": "显示文件夹大小", + "FullSize": "所有文件大小", + "MemoryUsed": "使用内存", + "PartitionSize": "可用空间", + "FreeOf": "磁盘大小", + "Check Latest Version": "检查更新", + "Generate new password hash": "生成新的hash密码", + "Report Issue": "报告问题", + "Help Documents": "帮助文档", + "Generate": "生成", + "Renamed from": "生成", + "Preview": "预览", + "Access denied. IP restriction applicable": "访问被拒绝。适用的IP限制", + "You are logged in": "您已登录", + "Login failed. Invalid username or password": "登录失败。用户名或密码无效", + "password_hash not supported, Upgrade PHP version": "不支持password_hash,请升级PHP版本", + "Root path": "根路径", + "not found!": "没有找到!", + "File not found": "找不到文件", + "Deleted": "删除", + "not deleted": "未删除", + "Invalid file or folder name": "无效的文件或文件夹名", + "Created": "已创建", + "File extension is not allowed": "不允许文件扩展名", + "already exists": "已经存在", + "not created": "未创建", + "Invalid characters in file or folder name": "文件或文件夹名称中的无效字符", + "Source path not defined": "未定义源路径", + "Moved from": "移动自", + "to": "至", + "File or folder with this path already exists": "具有此路径的文件或文件夹已存在", + "Error while moving from": "移动时出错", + "Copied from": "复制自", + "Error while copying from": "复制时出错", + "Paths must be not equal": "路径必须不相等", + "Nothing selected": "未选择任何内容", + "Error while renaming from": "重命名时出错", + "Invalid characters in file name": "文件名中的无效字符", + "Invalid Token.": "无效令牌。", + "Selected files and folder deleted": "已删除选定的文件和文件夹", + "Error while deleting items": "删除项目时出错", + "Operations with archives are not available": "存档操作不可用", + "Archive": "存档", + "Archive not created": "未创建存档", + "Archive unpacked": "存档未打包", + "Archive not unpacked": "存档未打开", + "Permissions changed": "权限已更改", + "Permissions not changed": "权限未更改", + "Select folder": "选择文件夹", + "Theme": "主题", + "light": "浅色", + "dark": "深色", + "Error while fetching archive info": "获取存档信息时出错", + "File Saved Successfully": "文件保存成功", + "FILE EXTENSION HAS NOT SUPPORTED": "文件扩展名不受支持", + "Folder is empty": "文件夹为空", + "Delete selected files and folders?": "是否删除选定的文件和文件夹?", + "Create archive?": "创建存档?", + "Zip": "Zip", + "Tar": "Tar", + "Zero byte file! Aborting download": "零字节文件!正在中止下载", + "Cannot open file! Aborting download": "无法打开文件!正在中止下载", + "Filter": "过滤器", + "Advanced Search": "高级搜索", + "Search file in folder and subfolders...": "在文件夹和子文件夹中搜索文件…", + "Are you sure want to": "你确定要", + "Okay": "确定", + "a files": "一个文件", + "Enter here...": "在此处输入...", + "Enter new file name": "输入新文件名", + "Full path": "完整路径", + "File size": "文件大小", + "MIME-type": "MIME类型", + "Image sizes": "图像大小", + "Charset": "编码格式", + "Image": "图片", + "Audio": "音频", + "Video": "视频", + "Upload from URL": "从URL上传", + "Files in archive": "档案文件", + "Total size": "总大小", + "Compression": "压缩", + "Size in archive": "存档中的大小", + "Invalid Token.": "无效令牌", + "Fullscreen": "全屏", + "Search": "搜索", + "Word Wrap": "自动换行", + "Undo": "撤消", + "Redo": "恢复", + "Select Document Type": "选择文档类型", + "Select Mode": "选择模式", + "Select Theme": "选择主题", + "Select Font Size": "选择字体大小", + "Are you sure want to rename?": "是否确实要重命名?" + } + } + ] +} diff --git a/web/app/controllers/user_msg.php b/web/app/controllers/user_msg.php index 6441997..98f8771 100644 --- a/web/app/controllers/user_msg.php +++ b/web/app/controllers/user_msg.php @@ -1,100 +1,130 @@ strlen($_POST['message']) || strlen($_POST['message']) > 65535) { return 'fail'; } - $receiver = $_POST['receiver']; - $esc_message = DB::escape($_POST['message']); - $sender = Auth::id(); + $receiver = UOJRequest::user(UOJRequest::POST, 'receiver'); + if (!$receiver) { + return 'fail'; + } + $message = $_POST['message']; - if (!validateUsername($receiver) || !UOJUser::query($receiver)) { + if ($receiver['username'] === Auth::id()) { return 'fail'; } - DB::query("insert into user_msg (sender, receiver, message, send_time) values ('$sender', '$receiver', '$esc_message', now())"); + DB::insert([ + "insert into user_msg", + "(sender, receiver, message, send_time)", + "values", DB::tuple([Auth::id(), $receiver['username'], $message, DB::now()]) + ]); return "ok"; } function getConversations() { $username = Auth::id(); - $result = DB::query("select * from user_msg where sender = '$username' or receiver = '$username' order by send_time DESC"); - $ret = array(); - while ($msg = DB::fetch($result)) { + + $res = DB::selectAll([ + "select * from user_msg", + "where", DB::lor([ + "sender" => $username, + "receiver" => $username, + ]), + "order by send_time DESC" + ]); + $ret = []; + foreach ($res as $msg) { if ($msg['sender'] !== $username) { if (isset($ret[$msg['sender']])) { $ret[$msg['sender']][1] |= ($msg['read_time'] == null); continue; } - $ret[$msg['sender']] = array($msg['send_time'], ($msg['read_time'] == null)); + + $ret[$msg['sender']] = [$msg['send_time'], ($msg['read_time'] == null), $msg['message']]; } else { - if (isset($ret[$msg['receiver']])) { - continue; - } - $ret[$msg['receiver']] = array($msg['send_time'], 0); + if (isset($ret[$msg['receiver']])) continue; + + $ret[$msg['receiver']] = [$msg['send_time'], 0, $msg['message']]; } } $res = []; foreach ($ret as $name => $con) { - $res[] = [$con[0], $con[1], $name]; + $user = UOJUser::query($name); + $res[] = [ + $con[0], + $con[1], + $name, + HTML::avatar_addr($user, 128), + UOJUser::getRealname($user), + UOJUser::getUserColor($user), + $con[2], + ]; } + usort($res, function ($a, $b) { return -strcmp($a[0], $b[0]); }); + return json_encode($res); } function getHistory() { $username = Auth::id(); - if (!isset($_GET['conversationName']) || !validateUsername($_GET['conversationName'])) { + $receiver = UOJRequest::user(UOJRequest::GET, 'conversationName'); + $page_num = UOJRequest::uint(UOJRequest::GET, 'pageNumber'); + if (!$receiver || $receiver['username'] === $username) { return '[]'; } - if (!isset($_GET['pageNumber']) || !validateUInt($_GET['pageNumber'])) { + if (!$page_num) { // false, null, or zero return '[]'; } - $conversationName = $_GET['conversationName']; - $pageNumber = ($_GET['pageNumber'] - 1) * 10; - DB::query("update user_msg set read_time = now() where sender = '$conversationName' and receiver = '$username' and read_time is null"); + DB::update([ + "update user_msg", + "set", ["read_time" => DB::now()], + "where", [ + "sender" => $receiver['username'], + "receiver" => $username, + "read_time" => null, + ] + ]); - $result = DB::query("select * from user_msg where (sender = '$username' and receiver = '$conversationName') or (sender = '$conversationName' and receiver = '$username') order by send_time DESC limit $pageNumber, 11"); - $ret = array(); - while ($msg = DB::fetch($result)) { - $ret[] = array($msg['message'], $msg['send_time'], $msg['read_time'], $msg['id'], ($msg['sender'] == $username)); + $result = DB::selectAll([ + "select * from user_msg", + "where", DB::lor([ + DB::land([ + "sender" => $username, + "receiver" => $receiver['username'] + ]), + DB::land([ + "sender" => $receiver['username'], + "receiver" => $username + ]) + ]), + "order by send_time DESC", DB::limit(($page_num - 1) * 10, 11) + ]); + $ret = []; + foreach ($result as $msg) { + $ret[] = [ + $msg['message'], + $msg['send_time'], + $msg['read_time'], + $msg['id'], + ($msg['sender'] === $username), + ]; } return json_encode($ret); } -/* - function deleteMsg($msgId) { - return 1; - $str = << -

私信

+

私信

-
+ -