mirror of
https://github.com/renbaoshuo/S2OJ.git
synced 2024-11-25 21:58:41 +00:00
Merge branch 'master' into uoj_form_v2
This commit is contained in:
commit
54a243b517
30
.drone.yml
30
.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
|
||||
|
@ -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
|
||||
|
112
.github/workflows/build.yml
vendored
112
.github/workflows/build.yml
vendored
@ -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 }}
|
||||
|
||||
- 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:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- image_name: db
|
||||
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:
|
||||
dockerfile: db/Dockerfile
|
||||
- image_name: judger
|
||||
context: judger
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
dockerfile: judger/Dockerfile
|
||||
- image_name: remote-judger
|
||||
context: remote_judger
|
||||
dockerfile: remote_judger/Dockerfile
|
||||
- image_name: web
|
||||
context: .
|
||||
dockerfile: web/Dockerfile
|
||||
fail-fast: false
|
||||
|
||||
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 }}
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@ docker-compose.local.yml
|
||||
.config.php
|
||||
.config.development.php
|
||||
.config.local.php
|
||||
*.development.env
|
||||
*.local.env
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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: ./
|
||||
|
@ -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
|
||||
|
@ -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', '内置评测机', '用于评测本地题目的评测机。');
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
3
remote_judger/.gitignore
vendored
Normal file
3
remote_judger/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
*-error.log
|
14
remote_judger/.prettierrc
Normal file
14
remote_judger/.prettierrc
Normal file
@ -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
|
||||
}
|
12
remote_judger/Dockerfile
Normal file
12
remote_judger/Dockerfile
Normal file
@ -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" ]
|
5
remote_judger/README
Normal file
5
remote_judger/README
Normal file
@ -0,0 +1,5 @@
|
||||
本模块借鉴了以下项目的源码:
|
||||
|
||||
- https://github.com/hydro-dev/Hydro/blob/feb51804766e35dbd13f7cb74fda95c0b783c49d/packages/vjudge/
|
||||
|
||||
在此表示感谢。
|
2
remote_judger/add_judger.sql
Normal file
2
remote_judger/add_judger.sql
Normal file
@ -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 评测机的虚拟评测机。');
|
2660
remote_judger/package-lock.json
generated
Normal file
2660
remote_judger/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
remote_judger/package.json
Normal file
33
remote_judger/package.json
Normal file
@ -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 <i@baoshuo.ren>",
|
||||
"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"
|
||||
}
|
||||
}
|
194
remote_judger/src/daemon.ts
Normal file
194
remote_judger/src/daemon.ts
Normal file
@ -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: '<info-block>Sample test is not available.</info-block>',
|
||||
}),
|
||||
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: `<error>${htmlspecialchars(details)}</error>`,
|
||||
}),
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
15
remote_judger/src/entrypoint.ts
Normal file
15
remote_judger/src/entrypoint.ts
Normal file
@ -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,
|
||||
});
|
35
remote_judger/src/interface.ts
Normal file
35
remote_judger/src/interface.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export interface RemoteAccount {
|
||||
type: string;
|
||||
cookie?: string[];
|
||||
handle: string;
|
||||
password: string;
|
||||
endpoint?: string;
|
||||
proxy?: string;
|
||||
}
|
||||
|
||||
export type NextFunction = (body: Partial<any>) => void;
|
||||
|
||||
export interface IBasicProvider {
|
||||
ensureLogin(): Promise<boolean | string>;
|
||||
submitProblem(
|
||||
id: string,
|
||||
lang: string,
|
||||
code: string,
|
||||
submissionId: number,
|
||||
next: NextFunction,
|
||||
end: NextFunction
|
||||
): Promise<string | void>;
|
||||
waitForSubmission(
|
||||
problem_id: string,
|
||||
id: string,
|
||||
next: NextFunction,
|
||||
end: NextFunction
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
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';
|
326
remote_judger/src/providers/atcoder.ts
Normal file
326
remote_judger/src/providers/atcoder.ts
Normal file
@ -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('<a href="/login">Sign In</a>')) 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(`<table>${result.Html}</table>`);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
384
remote_judger/src/providers/codeforces.ts
Normal file
384
remote_judger/src/providers/codeforces.ts
Normal file
@ -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 += `<test num="${i}" info="${info_text}" time="${
|
||||
body[`timeConsumed#${i}`]
|
||||
}" memory="${+body[`memoryConsumed#${i}`] / 1024}">`;
|
||||
|
||||
const parse = (id: string) => crlf(body[id], LF);
|
||||
|
||||
test_info += `<in>${parse(`input#${i}`)}</in>\n`;
|
||||
test_info += `<out>${parse(`output#${i}`)}</out>\n`;
|
||||
test_info += `<ans>${parse(`answer#${i}`)}</ans>\n`;
|
||||
test_info += `<res>${parse(`checkerStdoutAndStderr#${i}`)}</res>\n`;
|
||||
|
||||
test_info += '</test>';
|
||||
|
||||
tests.push(test_info);
|
||||
}
|
||||
|
||||
const details =
|
||||
'<div>' +
|
||||
`<info-block>REMOTE_SUBMISSION_ID = ${id}\nVERDICT = ${status}</info-block>` +
|
||||
`<tests>${tests.join('\n')}</tests>` +
|
||||
'</div>';
|
||||
|
||||
return await end({
|
||||
id,
|
||||
status,
|
||||
score: status === 'Accepted' ? 100 : 0,
|
||||
time,
|
||||
memory,
|
||||
details,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
459
remote_judger/src/providers/loj.ts
Normal file
459
remote_judger/src/providers/loj.ts
Normal file
@ -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 += `<test num="${
|
||||
index + 1
|
||||
}" info="${status}" time="${Math.round(testcase.time || 0)}" memory="${
|
||||
testcase.memory
|
||||
}">`;
|
||||
|
||||
if (testcase.input) {
|
||||
if (typeof testcase.input === 'string') {
|
||||
test_info += `<in>${parse(testcase.input)}</in>\n`;
|
||||
} else {
|
||||
test_info += `<in>${parse(testcase.input.data)}\n\n(${
|
||||
testcase.input.omittedLength
|
||||
} bytes omitted)</in>\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (testcase.userOutput) {
|
||||
if (typeof testcase.userOutput === 'string') {
|
||||
test_info += `<out>${parse(testcase.userOutput)}</out>\n`;
|
||||
} else {
|
||||
test_info += `<out>${parse(testcase.userOutput.data)}\n\n(${
|
||||
testcase.userOutput.omittedLength
|
||||
} bytes omitted)</out>\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (testcase.output) {
|
||||
if (typeof testcase.output === 'string') {
|
||||
test_info += `<ans>${parse(testcase.output)}</ans>\n`;
|
||||
} else {
|
||||
test_info += `<ans>${parse(testcase.output.data)}\n\n(${
|
||||
testcase.output.omittedLength
|
||||
} bytes omitted)</ans>\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (testcase.checkerMessage) {
|
||||
if (typeof testcase.checkerMessage === 'string') {
|
||||
test_info += `<res>${parse(testcase.checkerMessage)}</res>\n`;
|
||||
} else {
|
||||
test_info += `<res>${parse(testcase.checkerMessage.data)}\n\n(${
|
||||
testcase.checkerMessage.omittedLength
|
||||
} bytes omitted)</res>\n`;
|
||||
}
|
||||
}
|
||||
|
||||
test_info += '</test>';
|
||||
|
||||
return test_info;
|
||||
};
|
||||
|
||||
let details = '';
|
||||
|
||||
details += `<info-block>REMOTE_SUBMISSION_ID = ${id}\nVERDICT = ${status}</info-block>`;
|
||||
|
||||
// Samples
|
||||
if (body.progress.samples) {
|
||||
details += `<subtask title="Samples" info="${getSubtaskStatusDisplayText(
|
||||
body.progress.samples
|
||||
)}" num="0">${body.progress.samples
|
||||
.map((item, index) =>
|
||||
item.testcaseHash
|
||||
? getTestcaseBlock(item.testcaseHash, index)
|
||||
: `<test num="${index + 1}" info="Skipped"></test>`
|
||||
)
|
||||
.join('\n')}</subtask>`;
|
||||
}
|
||||
|
||||
// Tests
|
||||
if (body.progress.subtasks.length === 1) {
|
||||
details += `<tests>${body.progress.subtasks[0].testcases
|
||||
.map((item, index) =>
|
||||
item.testcaseHash
|
||||
? getTestcaseBlock(item.testcaseHash, index)
|
||||
: `<test num="${index + 1}" info="Skipped"></test>`
|
||||
)
|
||||
.join('\n')}</tests>`;
|
||||
} else {
|
||||
details += `<tests>${body.progress.subtasks
|
||||
.map(
|
||||
(subtask, index) =>
|
||||
`<subtask num="${index + 1}" info="${getSubtaskStatusDisplayText(
|
||||
subtask.testcases
|
||||
)}">${subtask.testcases
|
||||
.map((item, index) =>
|
||||
item.testcaseHash
|
||||
? getTestcaseBlock(item.testcaseHash, index)
|
||||
: `<test num="${index + 1}" info="Skipped"></test>`
|
||||
)
|
||||
.join('\n')}</subtask>`
|
||||
)
|
||||
.join('\n')}</tests>`;
|
||||
}
|
||||
|
||||
return await end({
|
||||
id,
|
||||
status: body.meta.status,
|
||||
score: body.meta.score,
|
||||
time: body.meta.timeUsed,
|
||||
memory: body.meta.memoryUsed,
|
||||
details: `<div>${details}</div>`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
266
remote_judger/src/providers/uoj.ts
Normal file
266
remote_judger/src/providers/uoj.ts
Normal file
@ -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('<title>登录')
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
7
remote_judger/src/proxy.ts
Normal file
7
remote_judger/src/proxy.ts
Normal file
@ -0,0 +1,7 @@
|
||||
declare module 'superagent' {
|
||||
interface Request {
|
||||
proxy(url: string): this;
|
||||
}
|
||||
}
|
||||
|
||||
export default {};
|
11
remote_judger/src/utils/htmlspecialchars.ts
Normal file
11
remote_judger/src/utils/htmlspecialchars.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export default function htmlspecialchars(text: string) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
};
|
||||
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
11
remote_judger/src/utils/logger.ts
Normal file
11
remote_judger/src/utils/logger.ts
Normal file
@ -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;
|
20
remote_judger/src/utils/parse.ts
Normal file
20
remote_judger/src/utils/parse.ts
Normal file
@ -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()]);
|
||||
}
|
5
remote_judger/src/utils/sleep.ts
Normal file
5
remote_judger/src/utils/sleep.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default function sleep(timeout: number) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(true), timeout);
|
||||
});
|
||||
}
|
5
remote_judger/src/utils/time.ts
Normal file
5
remote_judger/src/utils/time.ts
Normal file
@ -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;
|
46
remote_judger/src/verdict.ts
Normal file
46
remote_judger/src/verdict.ts
Normal file
@ -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;
|
||||
},
|
||||
}
|
||||
);
|
161
remote_judger/src/vjudge.ts
Normal file
161
remote_judger/src/vjudge.ts
Normal file
@ -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;
|
||||
}
|
20
remote_judger/tsconfig.json
Normal file
20
remote_judger/tsconfig.json
Normal file
@ -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"]
|
||||
}
|
13
s2oj-backup.sh
Executable file
13
s2oj-backup.sh
Executable file
@ -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
|
@ -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} ^$
|
||||
|
@ -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 ;\
|
||||
|
@ -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": [
|
||||
|
@ -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);
|
@ -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>
|
||||
|
@ -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(), '">';
|
||||
|
@ -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('找回密码') ?>
|
||||
|
@ -134,17 +134,16 @@ 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>
|
||||
<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>
|
||||
</div>
|
||||
<!-- end left col -->
|
||||
</div>
|
||||
|
||||
|
@ -60,17 +60,17 @@ $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">
|
||||
<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>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="mt-4 card card-default">
|
||||
<div class="card-body text-center">
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
167
web/app/controllers/new_remote_problem.php
Normal file
167
web/app/controllers/new_remote_problem.php
Normal file
@ -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() ?>
|
@ -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
|
||||
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") ?>">
|
||||
<a class="list-group-item list-group-item-action" href="<?= HTML::url(UOJProblem::cur()->getMainDataUri()) ?>">
|
||||
<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") ?>">
|
||||
<a class="list-group-item list-group-item-action" href="<?= HTML::url(UOJProblem::cur()->getAttachmentUri()) ?>">
|
||||
<i class="bi bi-download"></i>
|
||||
附件下载
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<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
|
||||
|
@ -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>
|
||||
|
@ -91,11 +91,13 @@ if (isSuperUser(Auth::user())) {
|
||||
管理者
|
||||
</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">
|
||||
|
53
web/app/controllers/problem_resources.php
Normal file
53
web/app/controllers/problem_resources.php
Normal file
@ -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');
|
@ -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,25 +262,20 @@ $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>
|
||||
<?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills') ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
</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">
|
||||
<?= $pag->pagination() ?>
|
||||
|
||||
<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">
|
||||
@ -286,9 +290,6 @@ $pag = new Paginator([
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $pag->pagination() ?>
|
||||
|
||||
<script type="text/javascript">
|
||||
$('#input-show_tags_mode').click(function() {
|
||||
@ -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>
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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>
|
||||
<?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>
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
requireLib('bootstrap5');
|
||||
requireLib('mathjax');
|
||||
requireLib('hljs');
|
||||
requireLib('pdf.js');
|
||||
requirePHPLib('form');
|
||||
|
||||
Auth::check() || redirectToLogin();
|
||||
|
@ -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>。
|
||||
|
123
web/app/controllers/tinyfilemanager/config.php
Normal file
123
web/app/controllers/tinyfilemanager/config.php
Normal file
@ -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
|
||||
);
|
||||
|
||||
?>
|
4078
web/app/controllers/tinyfilemanager/tinyfilemanager.php
Normal file
4078
web/app/controllers/tinyfilemanager/tinyfilemanager.php
Normal file
File diff suppressed because one or more lines are too long
168
web/app/controllers/tinyfilemanager/translation.json
Normal file
168
web/app/controllers/tinyfilemanager/translation.json
Normal file
@ -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?": "是否确实要重命名?"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,100 +1,130 @@
|
||||
<?php
|
||||
requireLib('bootstrap5');
|
||||
|
||||
if (!Auth::check()) {
|
||||
redirectToLogin();
|
||||
}
|
||||
|
||||
function handleMsgPost() {
|
||||
if (!isset($_POST['receiver'])) {
|
||||
return 'fail';
|
||||
}
|
||||
if (!isset($_POST['message'])) {
|
||||
return 'fail';
|
||||
}
|
||||
if (0 > 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 = <<<EOD
|
||||
select * from user_msg
|
||||
where id = $msgId
|
||||
and read_time is null
|
||||
EOD;
|
||||
$result = DB::query($str);
|
||||
if (DB::fetch($result)) {
|
||||
$str = <<<EOD
|
||||
delete from user_msg
|
||||
where id = $msgId
|
||||
EOD;
|
||||
DB::query($str);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
*/
|
||||
|
||||
if (isset($_POST['user_msg'])) {
|
||||
die(handleMsgPost());
|
||||
} elseif (isset($_GET['getConversations'])) {
|
||||
@ -106,48 +136,76 @@ if (isset($_POST['user_msg'])) {
|
||||
|
||||
<?php echoUOJPageHeader('私信') ?>
|
||||
|
||||
<h1 class="page-header">私信</h1>
|
||||
<h1>私信</h1>
|
||||
|
||||
<div id="conversations"></div>
|
||||
<style>
|
||||
@media (min-width: 768px) {
|
||||
.chat-container {
|
||||
height: calc(100ch - 10rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="history" style="display:none">
|
||||
<div class="card border-primary">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<button type="button" id="goBack" class="btn btn-info btn-sm" style="position:absolute">返回</button>
|
||||
<div class="card overflow-hidden-md chat-container">
|
||||
<div class="row gx-0 flex-grow-1 h-100">
|
||||
<div class="col-md-3 border-end h-100">
|
||||
<div class="list-group list-group-flush h-100 overflow-auto" id="conversations"></div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-9 h-100" id="history" style="display: none">
|
||||
<div class="card h-100 border-0 rounded-0">
|
||||
<div class="card-header">
|
||||
<button id="goBack" class="btn-close position-absolute" aria-label="关闭对话"></button>
|
||||
<div id="conversation-name" class="text-center"></div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="pagination top-buffer-no justify-content-between">
|
||||
<li class="previous"><a class="btn btn-outline-secondary text-primary" href="#" id="pageLeft">← 更早的消息</a></li>
|
||||
<li class="text-center" id="pageShow" style="line-height:32px"></li>
|
||||
<li class="next"><a class="btn btn-outline-secondary text-primary" href="#" id="pageRight">更新的消息 →</a></li>
|
||||
</ul>
|
||||
<div id="history-list" style="min-height: 200px;">
|
||||
<div class="card-body overflow-auto" id="history-list-container">
|
||||
<div id="history-list" style="min-height: 200px;"></div>
|
||||
</div>
|
||||
<ul class="pagination top-buffer-no justify-content-between">
|
||||
<li class="previous"><a class="btn btn-outline-secondary text-primary" href="#history" id="pageLeft2">← 更早的消息</a></li>
|
||||
<li class="next"><a class="btn btn-outline-secondary text-primary" href="#history" id="pageRight2">更新的消息 →</a></li>
|
||||
<div class="card-footer bg-transparent">
|
||||
<ul class="pagination pagination-sm justify-content-between mt-1">
|
||||
<li class="page-item">
|
||||
<button class="page-link rounded" id="pageLeft">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
更早的消息
|
||||
</button>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<button class="page-link rounded" id="pageRight">
|
||||
更新的消息
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<form id="form-message">
|
||||
<div class="form-group" id="form-group-message">
|
||||
<textarea id="input-message" class="form-control"></textarea>
|
||||
<form id="form-message" class="">
|
||||
<div id="form-group-message" class="flex-grow-1">
|
||||
<textarea id="input-message" class="form-control" style="resize: none;" data-no-autosize></textarea>
|
||||
<span id="help-message" class="help-block"></span>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button type="submit" id="message-submit" class="btn btn-info btn-md">发送</button>
|
||||
<div class="text-end mt-2">
|
||||
<span class="text-muted small">按 Ctrl+Enter 键发送</span>
|
||||
<button type="submit" id="message-submit" class="btn btn-primary flex-shrink-0 ms-3">
|
||||
发送
|
||||
<i class="bi bi-send"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var REFRESH_INTERVAL = 30 * 1000;
|
||||
|
||||
$(document).ready(function() {
|
||||
$.ajaxSetup({
|
||||
async: false
|
||||
});
|
||||
|
||||
refreshConversations();
|
||||
setInterval(refreshConversations, REFRESH_INTERVAL);
|
||||
|
||||
<?php if (isset($_GET['enter'])) : ?>
|
||||
enterConversation(<?= json_encode($_GET['enter']) ?>);
|
||||
<?php endif ?>
|
||||
@ -155,17 +213,64 @@ if (isset($_POST['user_msg'])) {
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
function addButton(conversationName, send_time, type) {
|
||||
<?php $enter_user = UOJRequest::user(UOJRequest::GET, 'enter'); ?>
|
||||
var conversations = {};
|
||||
var intervalId = 0;
|
||||
var user_avatar = '<?= HTML::avatar_addr(Auth::user(), 80) ?>';
|
||||
var enter_user = ['<?= $enter_user['username'] ?>', '<?= UOJUser::getRealname($enter_user) ?>', '<?= UOJUser::getUserColor($enter_user) ?>'];
|
||||
|
||||
function formatDate(date) {
|
||||
var d = new Date(date),
|
||||
month = '' + (d.getMonth() + 1),
|
||||
day = '' + d.getDate(),
|
||||
year = d.getFullYear();
|
||||
|
||||
if (month.length < 2)
|
||||
month = '0' + month;
|
||||
if (day.length < 2)
|
||||
day = '0' + day;
|
||||
|
||||
return [year, month, day].join('-');
|
||||
}
|
||||
|
||||
function formatTime(date) {
|
||||
var d = new Date(date),
|
||||
hour = '' + d.getHours(),
|
||||
minute = '' + d.getMinutes();
|
||||
|
||||
if (hour.length < 2)
|
||||
hour = '0' + hour;
|
||||
if (minute.length < 2)
|
||||
minute = '0' + minute;
|
||||
|
||||
return [hour, minute].join(':');
|
||||
}
|
||||
|
||||
function addButton(conversationName, send_time, type, avatar_addr, realname, color, last_message) {
|
||||
var now = new Date();
|
||||
var time = new Date(send_time);
|
||||
var timeStr = formatDate(send_time);
|
||||
|
||||
if (formatDate(now) === timeStr) {
|
||||
timeStr = formatTime(send_time);
|
||||
}
|
||||
|
||||
$("#conversations").append(
|
||||
'<div class="row top-buffer-sm">' +
|
||||
'<div class="col-sm-3">' +
|
||||
'<button type="button" class="btn btn-' + (type ? 'warning' : 'primary') + ' btn-block" ' +
|
||||
'<div class="list-group-item list-group-item-action p-2 d-flex ' + (type ? 'list-group-item-warning' : '') + '" style="cursor: pointer; user-select: none;" ' +
|
||||
'onclick="enterConversation(\'' + conversationName + '\')">' +
|
||||
conversationName +
|
||||
'</button>' +
|
||||
'<div class="flex-shrink-0 me-3">' +
|
||||
'<img class="rounded" width="56" height="56" src="' + avatar_addr + '" />' +
|
||||
'</div>' +
|
||||
'<div class="flex-grow-1 overflow-hidden">' +
|
||||
'<div class="d-flex justify-content-between">' +
|
||||
getUserSpan(conversationName, '', color) +
|
||||
'<span class="float-end text-muted small flex-shrink-0 lh-lg">' +
|
||||
timeStr +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'<div class="text-muted text-nowrap text-truncate">' +
|
||||
htmlspecialchars(last_message) +
|
||||
'</div>' +
|
||||
'<div class="col-sm-9" style="line-height:34px">' +
|
||||
'最后发送时间:' + send_time +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
@ -173,24 +278,22 @@ if (isset($_POST['user_msg'])) {
|
||||
|
||||
function addBubble(content, send_time, read_time, msgId, conversation, page, type) {
|
||||
$("#history-list").append(
|
||||
'<div style=' + (!type ? "margin-left:0%;margin-right:20%;" : "margin-left:20%;margin-right:0%;") + '>' +
|
||||
'<div class="card border-info mb-4">' +
|
||||
'<div class="card-body" style="background:#17a2b8; word-break: break-all">' +
|
||||
'<div style="white-space:pre-wrap">' +
|
||||
'<div class="d-flex align-items-end mt-3" style="' + (type ? 'margin-left:20%;' : 'margin-right:20%;') + '">' +
|
||||
(type ? '' : '<img class="flex-shrink-0 me-2 rounded" width="32" height="32" src="' + conversations[conversation][1] + '" style="user-select: none;" />') +
|
||||
'<div class="card flex-grow-1">' +
|
||||
'<div class="card-body px-3 py-2" style="white-space:pre-wrap">' +
|
||||
htmlspecialchars(content) +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<div class="row">' +
|
||||
'<div class="col-sm-6">' +
|
||||
'发送时间:' + send_time +
|
||||
'</div>' +
|
||||
'<div class="col-sm-6 text-right">' +
|
||||
'查看时间:' + (read_time == null ? '<strong>未查看</strong>' : read_time) +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="card-footer text-muted px-3 py-1">' +
|
||||
'<span class="small">' +
|
||||
'<i class="bi bi-clock"></i> ' + send_time +
|
||||
'</span>' +
|
||||
(read_time == null ?
|
||||
'<span class="float-end" data-bs-toggle="tooltip" data-bs-title="未读"><i class="bi bi-check2"></i></span>' :
|
||||
'<span class="float-end" data-bs-toggle="tooltip" data-bs-title="' + read_time + '"><i class="bi bi-check2-all"></i></span>') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
(type ? '<img class="flex-shrink-0 ms-2 rounded" width="32" height="32" src="' + user_avatar + '" style="user-select: none;" />' : '') +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
@ -204,7 +307,7 @@ if (isset($_POST['user_msg'])) {
|
||||
$('#help-message').text('');
|
||||
$('#form-group-message').removeClass('has-error');
|
||||
|
||||
$.post('', {
|
||||
$.post('/user_msg', {
|
||||
user_msg: 1,
|
||||
receiver: conversationName,
|
||||
message: $('#input-message').val()
|
||||
@ -216,9 +319,9 @@ if (isset($_POST['user_msg'])) {
|
||||
function refreshHistory(conversation, page) {
|
||||
$("#history-list").empty();
|
||||
var ret = false;
|
||||
$('#conversation-name').text(conversation);
|
||||
$('#conversation-name').html(getUserLink(conversation, conversation == enter_user[0] ? enter_user[1] : conversations[conversation][2], conversation == enter_user[0] ? enter_user[2] : conversations[conversation][3]));
|
||||
$('#pageShow').text("第" + page.toString() + "页");
|
||||
$.get('', {
|
||||
$.get('/user_msg', {
|
||||
getHistory: '',
|
||||
conversationName: conversation,
|
||||
pageNumber: page
|
||||
@ -236,29 +339,39 @@ if (isset($_POST['user_msg'])) {
|
||||
}
|
||||
var message = result[msg];
|
||||
addBubble(message[0], message[1], message[2], message[3], conversation, page, message[4]);
|
||||
if ((++cnt) + 1 == result.length && F) break;
|
||||
if ((++cnt) + 1 == result.length && F) {
|
||||
break;
|
||||
}
|
||||
if (result.length == 11) ret = true;
|
||||
}
|
||||
|
||||
if (result.length == 11) {
|
||||
ret = true;
|
||||
}
|
||||
|
||||
bootstrap.Tooltip.jQueryInterface.call($('#history-list [data-bs-toggle="tooltip"]'), {
|
||||
container: $('#history-list'),
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
function refreshConversations() {
|
||||
$("#conversations").empty();
|
||||
$.get('', {
|
||||
getConversations: ""
|
||||
$.get('/user_msg', {
|
||||
getConversations: 1
|
||||
}, function(msg) {
|
||||
var result = JSON.parse(msg);
|
||||
for (i in result) {
|
||||
var conversation = result[i];
|
||||
if (conversation[1] == 1) {
|
||||
addButton(conversation[2], conversation[0], conversation[1]);
|
||||
addButton(conversation[2], conversation[0], conversation[1], conversation[3], conversation[4], conversation[5], conversation[6]);
|
||||
}
|
||||
conversations[conversation[2]] = [conversation[0], conversation[3], conversation[4], conversation[5]];
|
||||
}
|
||||
for (i in result) {
|
||||
var conversation = result[i];
|
||||
if (conversation[1] == 0) {
|
||||
addButton(conversation[2], conversation[0], conversation[1]);
|
||||
addButton(conversation[2], conversation[0], conversation[1], conversation[3], conversation[4], conversation[5], conversation[6]);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -267,40 +380,50 @@ if (isset($_POST['user_msg'])) {
|
||||
function enterConversation(conversationName) {
|
||||
var slideTime = 300;
|
||||
var page = 1;
|
||||
$("#conversations").hide(slideTime);
|
||||
var changeAble = refreshHistory(conversationName, page);
|
||||
$("#history").slideDown(slideTime);
|
||||
clearInterval(intervalId);
|
||||
intervalId = setInterval(function() {
|
||||
changeAble = refreshHistory(conversationName, page);
|
||||
}, REFRESH_INTERVAL);
|
||||
$('#history').show();
|
||||
$('#conversations').addClass('d-none d-md-block')
|
||||
$("#history-list-container").scrollTop($("#history-list").height());
|
||||
$('#input-message').unbind('keydown').keydown(function(e) {
|
||||
if (e.keyCode == 13 && e.ctrlKey) {
|
||||
$('#message-submit').click();
|
||||
}
|
||||
});
|
||||
$('#form-message').unbind("submit").submit(function() {
|
||||
submitMessagePost(conversationName);
|
||||
page = 1;
|
||||
changeAble = refreshHistory(conversationName, page);
|
||||
refreshConversations();
|
||||
$("#history-list-container").scrollTop($("#history-list").height());
|
||||
return false;
|
||||
});
|
||||
$('#goBack').unbind("click").click(function() {
|
||||
clearInterval(intervalId);
|
||||
refreshConversations();
|
||||
$("#history").slideUp(slideTime);
|
||||
$("#conversations").show(slideTime);
|
||||
$("#history").hide();
|
||||
$("#conversations").removeClass('d-none d-md-block');
|
||||
return;
|
||||
});
|
||||
$('#pageLeft').unbind("click").click(function() {
|
||||
if (changeAble) page++;
|
||||
if (changeAble) {
|
||||
page++;
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
changeAble = refreshHistory(conversationName, page);
|
||||
return false;
|
||||
});
|
||||
$('#pageLeft2').unbind("click").click(function() {
|
||||
if (changeAble) page++;
|
||||
changeAble = refreshHistory(conversationName, page);
|
||||
});
|
||||
$('#pageRight').unbind("click").click(function() {
|
||||
if (page > 1) page--;
|
||||
if (page > 1) {
|
||||
page--;
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
changeAble = refreshHistory(conversationName, page);
|
||||
return false;
|
||||
});
|
||||
$('#pageRight2').unbind("click").click(function() {
|
||||
if (page > 1) page--;
|
||||
changeAble = refreshHistory(conversationName, page);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
function dataNewProblem($id) {
|
||||
mkdir("/var/uoj_data/upload/$id");
|
||||
mkdir("/var/uoj_data/$id");
|
||||
mkdir(UOJContext::storagePath() . "/problem_resources/$id");
|
||||
|
||||
UOJLocalRun::execAnd([
|
||||
['cd', '/var/uoj_data'],
|
||||
|
@ -465,6 +465,7 @@ class JudgmentDetailsPrinter {
|
||||
echo '</div>';
|
||||
} elseif ($node->nodeName == 'subtask') {
|
||||
$subtask_info = $node->getAttribute('info');
|
||||
$subtask_title = $node->getAttribute('title');
|
||||
$subtask_num = $node->getAttribute('num');
|
||||
$subtask_score = $node->getAttribute('score');
|
||||
$subtask_time = $this->_get_attr($node, 'time', -1);
|
||||
@ -482,10 +483,14 @@ class JudgmentDetailsPrinter {
|
||||
|
||||
echo '<div class="row">';
|
||||
echo '<div class="col-sm-4">';
|
||||
if ($subtask_title !== '') {
|
||||
echo '<h3 class="fs-5">', $subtask_title, ': ', '</h3>';
|
||||
} else {
|
||||
echo '<h3 class="fs-5">', 'Subtask #', $subtask_num, ': ', '</h3>';
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
if ($this->styler->show_score) {
|
||||
if ($this->styler->show_score && $subtask_score !== '') {
|
||||
echo '<div class="col-sm-2">';
|
||||
echo '<i class="bi bi-clipboard-check"></i> ', $subtask_score, ' pts';
|
||||
echo '</div>';
|
||||
@ -494,7 +499,7 @@ class JudgmentDetailsPrinter {
|
||||
echo htmlspecialchars($subtask_info);
|
||||
echo '</div>';
|
||||
} else {
|
||||
echo '<div class="col-sm-4">';
|
||||
echo '<div class="col-sm-4 uoj-status-text">';
|
||||
echo $this->styler->getTestInfoIcon($subtask_info);
|
||||
echo htmlspecialchars($subtask_info);
|
||||
echo '</div>';
|
||||
@ -541,6 +546,9 @@ class JudgmentDetailsPrinter {
|
||||
$accordion_parent .= "_collapse_subtask_{$this->subtask_num}_accordion";
|
||||
}
|
||||
$accordion_collapse = "{$accordion_parent}_collapse_test_{$test_num}";
|
||||
if ($this->subtask_num != null) {
|
||||
$accordion_collapse .= "_in_subtask_{$this->subtask_num}";
|
||||
}
|
||||
if (!$this->styler->shouldFadeDetails($test_info)) {
|
||||
echo '<div class="card-header uoj-submission-result-item bg-transparent rounded-0 border-0" data-bs-toggle="collapse" data-bs-parent="#', $accordion_parent, '" data-bs-target="#', $accordion_collapse, '">';
|
||||
} else {
|
||||
@ -555,7 +563,7 @@ class JudgmentDetailsPrinter {
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
if ($this->styler->show_score) {
|
||||
if ($this->styler->show_score && $test_score !== '') {
|
||||
echo '<div class="col-sm-2">';
|
||||
echo '<i class="bi bi-clipboard-check"></i> ', $test_score, ' pts';
|
||||
echo '</div>';
|
||||
@ -664,6 +672,11 @@ class JudgmentDetailsPrinter {
|
||||
echo '<pre class="bg-light p-3 rounded">', "\n";
|
||||
$this->_print_c($node);
|
||||
echo "\n</pre>";
|
||||
} elseif ($node->nodeName == 'ans') {
|
||||
echo '<h4 class="fs-6"><span>answer: </span></h4>';
|
||||
echo '<pre class="bg-light p-3 rounded">', "\n";
|
||||
$this->_print_c($node);
|
||||
echo "\n</pre>";
|
||||
} elseif ($node->nodeName == 'res') {
|
||||
echo '<h4 class="fs-6"><span>result: </span></h4>';
|
||||
echo '<pre class="bg-light p-3 rounded">', "\n";
|
||||
@ -687,7 +700,7 @@ class JudgmentDetailsPrinter {
|
||||
}
|
||||
echo '<h4 class="mb-2">', $node->getAttribute("title"), ":</h4>";
|
||||
}
|
||||
echo '<pre>', "\n";
|
||||
echo '<pre class="bg-light p-3 rounded">', "\n";
|
||||
$this->_print_c($node);
|
||||
echo "\n</pre>";
|
||||
echo '</div>';
|
||||
@ -757,7 +770,7 @@ class SubmissionDetailsStyler {
|
||||
}
|
||||
}
|
||||
public function shouldFadeDetails($info) {
|
||||
return $this->fade_all_details || $info == 'Extra Test Passed';
|
||||
return $this->fade_all_details || $info == 'Extra Test Passed' || $info == 'Skipped';
|
||||
}
|
||||
}
|
||||
class CustomTestSubmissionDetailsStyler {
|
||||
|
@ -218,3 +218,66 @@ function retry_loop(callable $f, $retry = 5, $ms = 10) {
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
function getAbsoluteUrl($relativeUrl, $baseUrl) {
|
||||
// if already absolute URL
|
||||
if (parse_url($relativeUrl, PHP_URL_SCHEME) !== null) {
|
||||
return $relativeUrl;
|
||||
}
|
||||
|
||||
// queries and anchors
|
||||
if ($relativeUrl[0] === '#' || $relativeUrl[0] === '?') {
|
||||
return $baseUrl . $relativeUrl;
|
||||
}
|
||||
|
||||
// parse base URL and convert to: $scheme, $host, $path, $query, $port, $user, $pass
|
||||
extract(parse_url($baseUrl));
|
||||
|
||||
// if base URL contains a path remove non-directory elements from $path
|
||||
if (isset($path) === true) {
|
||||
$path = preg_replace('#/[^/]*$#', '', $path);
|
||||
} else {
|
||||
$path = '';
|
||||
}
|
||||
|
||||
// if realtive URL starts with //
|
||||
if (substr($relativeUrl, 0, 2) === '//') {
|
||||
return $scheme . ':' . $relativeUrl;
|
||||
}
|
||||
|
||||
// if realtive URL starts with /
|
||||
if ($relativeUrl[0] === '/') {
|
||||
$path = null;
|
||||
}
|
||||
|
||||
$abs = null;
|
||||
|
||||
// if realtive URL contains a user
|
||||
if (isset($user) === true) {
|
||||
$abs .= $user;
|
||||
|
||||
// if realtive URL contains a password
|
||||
if (isset($pass) === true) {
|
||||
$abs .= ':' . $pass;
|
||||
}
|
||||
|
||||
$abs .= '@';
|
||||
}
|
||||
|
||||
$abs .= $host;
|
||||
|
||||
// if realtive URL contains a port
|
||||
if (isset($port) === true) {
|
||||
$abs .= ':' . $port;
|
||||
}
|
||||
|
||||
$abs .= $path . '/' . $relativeUrl . (isset($query) === true ? '?' . $query : null);
|
||||
|
||||
// replace // or /./ or /foo/../ with /
|
||||
$re = ['#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#'];
|
||||
for ($n = 1; $n > 0; $abs = preg_replace($re, '/', $abs, -1, $n)) {
|
||||
}
|
||||
|
||||
// return absolute URL
|
||||
return $scheme . '://' . $abs;
|
||||
}
|
||||
|
@ -75,3 +75,7 @@ function validateUserAndStoreByUsername($username, &$vdata) {
|
||||
function is_short_string($str) {
|
||||
return is_string($str) && strlen($str) <= 256;
|
||||
}
|
||||
|
||||
function validateCodeforcesProblemId($str) {
|
||||
return preg_match('/(|GYM)[1-9][0-9]{0,5}[A-Z][1-9]?/', $str) !== true;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ return [
|
||||
'problem list' => 'Problem List',
|
||||
'all problems' => 'All Problems',
|
||||
'template problems' => 'Template Problems',
|
||||
'remote problems' => 'Remote Problems',
|
||||
'add new' => 'Add new problem',
|
||||
'add new list' => 'Add new problem list',
|
||||
'my problem' => 'My problem',
|
||||
|
@ -4,6 +4,7 @@ return [
|
||||
'problem list' => '题单',
|
||||
'all problems' => '总题库',
|
||||
'template problems' => '模板题库',
|
||||
'remote problems' => '远程题库',
|
||||
'add new' => '添加新题',
|
||||
'add new list' => '添加新题单',
|
||||
'my problem' => '我的题目',
|
||||
|
@ -33,6 +33,7 @@ class Auth {
|
||||
"select remember_token from user_info",
|
||||
"where", ["username" => $username]
|
||||
]);
|
||||
|
||||
if ($remember_token == '') {
|
||||
$remember_token = uojRandString(60);
|
||||
DB::update([
|
||||
@ -54,10 +55,10 @@ class Auth {
|
||||
"where", ["username" => $username]
|
||||
]);
|
||||
}
|
||||
|
||||
public static function logout() {
|
||||
unset($_SESSION['username']);
|
||||
unset($_SESSION['last_login']);
|
||||
unset($_SESSION['last_visited']);
|
||||
session_unset();
|
||||
Cookie::safeUnset(session_name(), '/');
|
||||
Cookie::safeUnset('uoj_username', '/');
|
||||
Cookie::safeUnset('uoj_remember_token', '/');
|
||||
DB::update([
|
||||
|
@ -423,7 +423,7 @@ class HTML {
|
||||
return implode("&", $r);
|
||||
}
|
||||
|
||||
public static function purifier() {
|
||||
public static function purifier($extra_allowed_html = []) {
|
||||
$config = HTMLPurifier_Config::createDefault();
|
||||
$config->set('Output.Newline', true);
|
||||
$def = $config->getHTMLDefinition(true);
|
||||
@ -435,14 +435,18 @@ class HTML {
|
||||
$def->addElement('header', 'Block', 'Flow', 'Common');
|
||||
$def->addElement('footer', 'Block', 'Flow', 'Common');
|
||||
|
||||
$extra_allowed_html = [
|
||||
mergeConfig($extra_allowed_html, [
|
||||
'div' => [
|
||||
'data-pdf' => 'Text',
|
||||
'data-src' => 'URI',
|
||||
],
|
||||
'span' => [
|
||||
'class' => 'Enum#uoj-username',
|
||||
'data-realname' => 'Text',
|
||||
'data-color' => 'Color',
|
||||
],
|
||||
'img' => ['width' => 'Text'],
|
||||
];
|
||||
]);
|
||||
|
||||
foreach ($extra_allowed_html as $element => $attributes) {
|
||||
foreach ($attributes as $attribute => $type) {
|
||||
|
@ -129,8 +129,10 @@ class UOJBlogEditor {
|
||||
$elements = $dom->getElementsByTagName('li');
|
||||
|
||||
foreach ($elements as $element) {
|
||||
$element->setAttribute('class',
|
||||
$element->getAttribute('class') . ' fragment');
|
||||
$element->setAttribute(
|
||||
'class',
|
||||
$element->getAttribute('class') . ' fragment'
|
||||
);
|
||||
}
|
||||
|
||||
return $purifier->purify($dom->saveHTML());
|
||||
@ -181,7 +183,10 @@ class UOJBlogEditor {
|
||||
if (isset($_POST['need_preview'])) {
|
||||
ob_start();
|
||||
if ($this->type == 'blog') {
|
||||
$req_lib = array('mathjax' => '');
|
||||
$req_lib = [
|
||||
'mathjax' => '',
|
||||
'pdf.js' => '',
|
||||
];
|
||||
|
||||
if (isset($REQUIRE_LIB['bootstrap5'])) {
|
||||
$req_lib['bootstrap5'] = '';
|
||||
|
@ -64,21 +64,21 @@ class UOJLang {
|
||||
return [];
|
||||
}
|
||||
$is_avail = [];
|
||||
$dep_list = [
|
||||
['C++', 'C++11', 'C++14', 'C++17', 'C++20'],
|
||||
['Java8', 'Java11', 'Java17']
|
||||
];
|
||||
// $dep_list = [
|
||||
// ['C++98', 'C++03', 'C++11', 'C++', 'C++17', 'C++20'],
|
||||
// ['Java8', 'Java11', 'Java17']
|
||||
// ];
|
||||
foreach ($list as $lang) {
|
||||
$lang = static::getUpgradedLangCode($lang);
|
||||
foreach ($dep_list as $dep) {
|
||||
$ok = false;
|
||||
foreach ($dep as $d) {
|
||||
if ($ok || $d == $lang) {
|
||||
$is_avail[$d] = true;
|
||||
$ok = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// foreach ($dep_list as $dep) {
|
||||
// $ok = false;
|
||||
// foreach ($dep as $d) {
|
||||
// if ($ok || $d == $lang) {
|
||||
// $is_avail[$d] = true;
|
||||
// $ok = true;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
$is_avail[$lang] = true;
|
||||
}
|
||||
|
||||
|
@ -7,9 +7,12 @@ class UOJMarkdown extends ParsedownMath {
|
||||
|
||||
$this->options['username_with_color'] = $options['username_with_color'] ?: false;
|
||||
|
||||
// Special Block
|
||||
$this->inlineMarkerList .= '@';
|
||||
$this->InlineTypes['@'][] = 'SpecialBlock';
|
||||
|
||||
// https://gist.github.com/ShNURoK42/b5ce8baa570975db487c
|
||||
$this->InlineTypes['@'][] = 'UserMention';
|
||||
$this->inlineMarkerList .= '@';
|
||||
}
|
||||
|
||||
// https://github.com/taufik-nurrohman/parsedown-extra-plugin/blob/1653418c5a9cf5277cd28b0b23ba2d95d18e9bc4/ParsedownExtraPlugin.php#L340-L345
|
||||
@ -85,4 +88,35 @@ class UOJMarkdown extends ParsedownMath {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
protected function inlineSpecialBlock($Excerpt) {
|
||||
if (!isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') {
|
||||
return;
|
||||
}
|
||||
|
||||
$Excerpt['text'] = substr($Excerpt['text'], 1);
|
||||
|
||||
$Link = $this->inlineLink($Excerpt);
|
||||
|
||||
if ($Link === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$Inline = [
|
||||
'extent' => $Link['extent'] + 1,
|
||||
'element' => [
|
||||
'name' => 'div',
|
||||
'attributes' => [
|
||||
'data-src' => $Link['element']['attributes']['href'],
|
||||
"data-{$Link['element']['text']}" => $Link['element']['text'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$Inline['element']['attributes'] += $Link['element']['attributes'];
|
||||
|
||||
unset($Inline['element']['attributes']['href']);
|
||||
|
||||
return $Inline;
|
||||
}
|
||||
}
|
||||
|
@ -382,6 +382,26 @@ class UOJProblem {
|
||||
return UOJUser::getLink($this->info['uploader'] ?: "root");
|
||||
}
|
||||
|
||||
public function getProviderLink() {
|
||||
if ($this->info['type'] == 'local') {
|
||||
return HTML::tag('a', ['href' => HTML::url('/')], UOJConfig::$data['profile']['oj-name-short']);
|
||||
}
|
||||
|
||||
$remote_oj = $this->getExtraConfig('remote_online_judge');
|
||||
$remote_id = $this->getExtraConfig('remote_problem_id');
|
||||
|
||||
if (!$remote_oj || !array_key_exists($remote_oj, UOJRemoteProblem::$providers)) {
|
||||
return 'Error';
|
||||
}
|
||||
|
||||
$provider = UOJRemoteProblem::$providers[$remote_oj];
|
||||
|
||||
return HTML::tag('a', [
|
||||
'href' => UOJRemoteProblem::getProblemRemoteUrl($remote_oj, $remote_id),
|
||||
'target' => '_blank'
|
||||
], $provider['name']);
|
||||
}
|
||||
|
||||
public function getDifficultyHTML() {
|
||||
$difficulty = (int)$this->info['difficulty'];
|
||||
$difficulty_text = in_array($difficulty, static::$difficulty) ? $difficulty : '?';
|
||||
@ -440,10 +460,16 @@ class UOJProblem {
|
||||
return $key === null ? $extra_config : $extra_config[$key];
|
||||
}
|
||||
public function getCustomTestRequirement() {
|
||||
if ($this->info['type'] == 'remote') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$extra_config = json_decode($this->info['extra_config'], true);
|
||||
|
||||
if (isset($extra_config['custom_test_requirement'])) {
|
||||
return $extra_config['custom_test_requirement'];
|
||||
} else {
|
||||
}
|
||||
|
||||
$answer = [
|
||||
'name' => 'answer',
|
||||
'type' => 'source code',
|
||||
@ -462,7 +488,6 @@ class UOJProblem {
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function userCanView(array $user = null, array $cfg = []) {
|
||||
$cfg += ['ensure' => false];
|
||||
@ -646,6 +671,22 @@ class UOJProblem {
|
||||
return "{$this->getDataFolderPath()}/$name";
|
||||
}
|
||||
|
||||
public function getResourcesFolderPath() {
|
||||
return UOJContext::storagePath() . "/problem_resources/" . $this->info['id'];
|
||||
}
|
||||
|
||||
public function getResourcesPath($name = '') {
|
||||
return "{$this->getResourcesFolderPath()}/$name";
|
||||
}
|
||||
|
||||
public function getResourcesBaseUri() {
|
||||
return "/problem/{$this->info['id']}/resources";
|
||||
}
|
||||
|
||||
public function getResourcesUri($name = '') {
|
||||
return "{$this->getResourcesBaseUri()}/{$name}";
|
||||
}
|
||||
|
||||
public function getProblemConfArray(string $where = 'data') {
|
||||
if ($where === 'data') {
|
||||
return getUOJConf($this->getDataFilePath('problem.conf'));
|
||||
|
@ -5,10 +5,13 @@ class UOJRanklist {
|
||||
$cfg += [
|
||||
'top10' => false,
|
||||
'card' => false,
|
||||
'flush' => false,
|
||||
'group_id' => null,
|
||||
'page_len' => 50,
|
||||
];
|
||||
|
||||
$cfg['flush'] |= $cfg['card'];
|
||||
|
||||
$conds = [];
|
||||
|
||||
if ($cfg['group_id']) {
|
||||
@ -86,6 +89,9 @@ class UOJRanklist {
|
||||
|
||||
if ($cfg['card']) {
|
||||
echo '<div class="card my-3">';
|
||||
}
|
||||
|
||||
if ($cfg['flush']) {
|
||||
echo '<div class="list-group list-group-flush">';
|
||||
} else {
|
||||
echo '<div class="list-group">';
|
||||
@ -104,7 +110,18 @@ class UOJRanklist {
|
||||
if ($cfg['card']) {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
if ($pag->n_pages > 1) {
|
||||
if ($cfg['flush']) {
|
||||
echo '<div class="list-group-item">';
|
||||
}
|
||||
|
||||
echo $pag->pagination();
|
||||
|
||||
if ($cfg['flush']) {
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
459
web/app/models/UOJRemoteProblem.php
Normal file
459
web/app/models/UOJRemoteProblem.php
Normal file
@ -0,0 +1,459 @@
|
||||
<?php
|
||||
|
||||
class UOJRemoteProblem {
|
||||
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';
|
||||
|
||||
static $providers = [
|
||||
'codeforces' => [
|
||||
'name' => 'Codeforces',
|
||||
'short_name' => 'CF',
|
||||
'url' => 'https://codeforces.com',
|
||||
'not_exists_texts' => [
|
||||
'<th>Actions</th>',
|
||||
'Statement is not available on English language',
|
||||
'ограничение по времени на тест',
|
||||
],
|
||||
'languages' => ['C', 'C++', 'C++17', 'C++20', 'Java17', 'Pascal', 'Python2', 'Python3'],
|
||||
],
|
||||
'atcoder' => [
|
||||
'name' => 'AtCoder',
|
||||
'short_name' => 'AT',
|
||||
'url' => 'https://atcoder.jp',
|
||||
'not_exists_texts' => [
|
||||
'Task not found',
|
||||
'指定されたタスクが見つかりません',
|
||||
],
|
||||
'languages' => ['C', 'C++', 'Java11', 'Python3', 'Pascal'],
|
||||
],
|
||||
'uoj' => [
|
||||
'name' => 'UniversalOJ',
|
||||
'short_name' => 'UOJ',
|
||||
'url' => 'https://uoj.ac',
|
||||
'not_exist_texts' => [
|
||||
'未找到该页面',
|
||||
],
|
||||
'languages' => ['C', 'C++03', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java8', 'Java11', 'Java17', 'Pascal'],
|
||||
],
|
||||
'loj' => [
|
||||
'name' => 'LibreOJ',
|
||||
'short_name' => 'LOJ',
|
||||
'url' => 'https://loj.ac',
|
||||
'languages' => ['C', 'C++03', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java17', 'Pascal'],
|
||||
],
|
||||
];
|
||||
|
||||
static function curl_get($url) {
|
||||
$curl = new Curl\Curl();
|
||||
$curl->setUserAgent(static::USER_AGENT);
|
||||
|
||||
$res = retry_loop(function () use (&$curl, $url) {
|
||||
$curl->get($url);
|
||||
|
||||
if ($curl->error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'content-type' => $curl->responseHeaders['Content-Type'],
|
||||
'response' => $curl->response,
|
||||
];
|
||||
});
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
static function getCodeforcesProblemUrl($id) {
|
||||
if (str_starts_with($id, 'GYM')) {
|
||||
return static::$providers['codeforces']['url'] . '/gym/' . preg_replace_callback('/GYM([1-9][0-9]{0,5})([A-Z][1-9]?)/', fn ($matches) => $matches[1] . '/problem/' . $matches[2], $id);
|
||||
}
|
||||
|
||||
return static::$providers['codeforces']['url'] . '/problemset/problem/' . preg_replace_callback('/([1-9][0-9]{0,5})([A-Z][1-9]?)/', fn ($matches) => $matches[1] . '/' . $matches[2], $id);
|
||||
}
|
||||
|
||||
static function getAtcoderProblemUrl($id) {
|
||||
return static::$providers['atcoder']['url'] . '/contests/' . preg_replace_callback('/(\w+)([a-z][1-9]?)/', function ($matches) {
|
||||
$contest = str_replace('_', '', $matches[1]);
|
||||
|
||||
if (str_ends_with($matches[1], '_')) {
|
||||
return "{$contest}/tasks/{$matches[1]}{$matches[2]}";
|
||||
}
|
||||
|
||||
return "{$contest}/tasks/{$matches[1]}_{$matches[2]}";
|
||||
}, $id);
|
||||
}
|
||||
|
||||
static function getUojProblemUrl($id) {
|
||||
return static::$providers['uoj']['url'] . '/problem/' . $id;
|
||||
}
|
||||
|
||||
static function getLojProblemUrl($id) {
|
||||
return static::$providers['loj']['url'] . '/p/' . $id;
|
||||
}
|
||||
|
||||
static function getCodeforcesProblemBasicInfoFromHtml($id, $html) {
|
||||
$remote_provider = static::$providers['codeforces'];
|
||||
|
||||
$html = preg_replace('/\$\$\$/', '$', $html);
|
||||
$dom = new \IvoPetkov\HTML5DOMDocument();
|
||||
$dom->loadHTML($html);
|
||||
|
||||
$judgestatement = $dom->querySelector('html')->innerHTML;
|
||||
|
||||
foreach ($remote_provider['not_exists_texts'] as $text) {
|
||||
if (str_contains($judgestatement, $text)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$statement_dom = $dom->querySelector('.problem-statement');
|
||||
$title_prefix = str_starts_with($id, 'GYM') ? 'Gym' : 'CF';
|
||||
$title = explode('. ', trim($statement_dom->querySelector('.title')->innerHTML))[1];
|
||||
$title_id = str_starts_with($id, 'GYM') ? substr($id, 3) : $id;
|
||||
$title = "【{$title_prefix}{$title_id}】{$title}";
|
||||
$time_limit = intval(substr($statement_dom->querySelector('.time-limit')->innerHTML, 53));
|
||||
$memory_limit = intval(substr($statement_dom->querySelector('.memory-limit')->innerHTML, 55));
|
||||
$difficulty = -1;
|
||||
|
||||
foreach ($dom->querySelectorAll('.tag-box') as &$elem) {
|
||||
$matches = [];
|
||||
|
||||
if (preg_match('/\*([0-9]{3,4})/', trim($elem->innerHTML), $matches)) {
|
||||
$difficulty = intval($matches[1]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($difficulty != -1) {
|
||||
$closest = null;
|
||||
|
||||
foreach (UOJProblem::$difficulty as $val) {
|
||||
if ($closest === null || abs($val - $difficulty) < abs($closest - $difficulty)) {
|
||||
$closest = $val;
|
||||
}
|
||||
}
|
||||
|
||||
$difficulty = $closest;
|
||||
}
|
||||
|
||||
$statement_dom->removeChild($statement_dom->querySelector('.header'));
|
||||
$statement_dom->childNodes->item(0)->insertBefore($dom->createElement('h3', 'Description'), $statement_dom->childNodes->item(0)->childNodes->item(0));
|
||||
|
||||
foreach ($statement_dom->querySelectorAll('.section-title') as &$elem) {
|
||||
$elem->outerHTML = '<h3>' . $elem->innerHTML . '</h3>';
|
||||
}
|
||||
|
||||
$sample_input_cnt = 0;
|
||||
$sample_output_cnt = 0;
|
||||
|
||||
foreach ($statement_dom->querySelectorAll('.input') as &$input_dom) {
|
||||
$sample_input_cnt++;
|
||||
$input_text = '';
|
||||
|
||||
if ($input_dom->querySelector('.test-example-line')) {
|
||||
foreach ($input_dom->querySelectorAll('.test-example-line') as &$line) {
|
||||
$input_text .= HTML::stripTags($line->innerHTML) . "\n";
|
||||
}
|
||||
} else {
|
||||
$input_text = HTML::stripTags($input_dom->querySelector('pre')->innerHTML);
|
||||
}
|
||||
|
||||
$input_dom->outerHTML = HTML::tag('h4', [], "Input #{$sample_input_cnt}") . HTML::tag('pre', [], HTML::tag('code', [], $input_text));
|
||||
}
|
||||
|
||||
foreach ($statement_dom->querySelectorAll('.output') as &$output_dom) {
|
||||
$sample_output_cnt++;
|
||||
$output_text = '';
|
||||
|
||||
if ($output_dom->querySelector('.test-example-line')) {
|
||||
foreach ($output_dom->querySelectorAll('.test-example-line') as &$line) {
|
||||
$output_text .= HTML::stripTags($line->innerHTML) . "\n";
|
||||
}
|
||||
} else {
|
||||
$output_text = HTML::stripTags($output_dom->querySelector('pre')->innerHTML);
|
||||
}
|
||||
|
||||
$output_dom->outerHTML = HTML::tag('h4', [], "Output #{$sample_output_cnt}") . HTML::tag('pre', [], HTML::tag('code', [], $output_text));
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'html',
|
||||
'title' => $title,
|
||||
'time_limit' => $time_limit,
|
||||
'memory_limit' => $memory_limit,
|
||||
'difficulty' => $difficulty,
|
||||
'statement' => $statement_dom->innerHTML,
|
||||
];
|
||||
}
|
||||
|
||||
static function getCodeforcesProblemBasicInfo($id) {
|
||||
$res = static::curl_get(static::getCodeforcesProblemUrl($id));
|
||||
|
||||
if (!$res) return null;
|
||||
|
||||
if (str_starts_with($res['content-type'], 'text/html')) {
|
||||
return static::getCodeforcesProblemBasicInfoFromHtml($id, $res['response']);
|
||||
} else if (str_starts_with($res['content-type'], 'application/pdf')) {
|
||||
$title_prefix = str_starts_with($id, 'GYM') ? 'Gym' : 'CF';
|
||||
$title_id = str_starts_with($id, 'GYM') ? substr($id, 3) : $id;
|
||||
$title = "【{$title_prefix}{$title_id}】{$title_prefix}{$title_id}";
|
||||
|
||||
return [
|
||||
'type' => 'pdf',
|
||||
'title' => $title,
|
||||
'time_limit' => null,
|
||||
'memory_limit' => null,
|
||||
'difficulty' => -1,
|
||||
'pdf_data' => $res['response'],
|
||||
'statement' => HTML::tag('h3', [], '提示') .
|
||||
HTML::tag(
|
||||
'p',
|
||||
[],
|
||||
'若无法正常加载 PDF,请' .
|
||||
HTML::tag('a', ['href' => static::getCodeforcesProblemUrl($id), 'target' => '_blank'], '点此') .
|
||||
'查看原题面。'
|
||||
),
|
||||
];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function getAtcoderProblemBasicInfo($id) {
|
||||
$res = static::curl_get(static::getAtcoderProblemUrl($id));
|
||||
|
||||
if (!$res) return null;
|
||||
|
||||
$dom = new \IvoPetkov\HTML5DOMDocument();
|
||||
$dom->loadHTML($res);
|
||||
$container_dom = $dom->querySelectorAll('#main-container > div.row > div.col-sm-12')->item(1);
|
||||
|
||||
if (!$container_dom) return null;
|
||||
|
||||
$title_dom = $container_dom->querySelector('span.h2');
|
||||
$title = '【' . strtoupper($id) . '】' . preg_replace('/([A-Z][1-9]?) - (.*)/', '$2', explode("\n", trim($title_dom->textContent))[0]);
|
||||
|
||||
$limit_dom = $container_dom->querySelector('p');
|
||||
|
||||
$time_limit_matches = [];
|
||||
preg_match('/Time Limit: (\d+)/', $limit_dom->textContent, $time_limit_matches);
|
||||
$time_limit = intval($time_limit_matches[1]);
|
||||
|
||||
$memory_limit_matches = [];
|
||||
preg_match('/Memory Limit: (\d+)/', $limit_dom->textContent, $memory_limit_matches);
|
||||
$memory_limit = intval($memory_limit_matches[1]);
|
||||
|
||||
$statement_container_dom = $container_dom->querySelector('#task-statement');
|
||||
$statement_dom = $statement_container_dom->querySelector('.lang-en');
|
||||
|
||||
if (!$statement_dom) {
|
||||
$statement_dom = $statement_container_dom->querySelector('.lang-ja');
|
||||
}
|
||||
|
||||
$statement_first_child = $statement_dom->querySelector('p');
|
||||
$first_child_content = trim($statement_first_child->textContent);
|
||||
|
||||
if (str_starts_with($first_child_content, 'Score :') || str_starts_with($first_child_content, '配点 :')) {
|
||||
$statement_dom->removeChild($statement_first_child);
|
||||
}
|
||||
|
||||
foreach ($statement_dom->querySelectorAll('var') as &$elem) {
|
||||
$html = $elem->innerHTML;
|
||||
|
||||
// <sub> => _{
|
||||
$html = str_replace('<sub>', '_{', $html);
|
||||
|
||||
// </sub> => }
|
||||
$html = str_replace('</sub>', '}', $html);
|
||||
|
||||
// <sup> => ^{
|
||||
$html = str_replace('<sup>', '^{', $html);
|
||||
|
||||
// </sup> => }
|
||||
$html = str_replace('</sup>', '}', $html);
|
||||
|
||||
$elem->innerHTML = $html;
|
||||
}
|
||||
|
||||
$statement = $statement_dom->innerHTML;
|
||||
|
||||
// <var> => $
|
||||
$statement = str_replace('<var>', '\\(', $statement);
|
||||
|
||||
// </var> => $
|
||||
$statement = str_replace('</var>', '\\)', $statement);
|
||||
|
||||
return [
|
||||
'type' => 'html',
|
||||
'title' => $title,
|
||||
'time_limit' => $time_limit,
|
||||
'memory_limit' => $memory_limit,
|
||||
'difficulty' => -1,
|
||||
'statement' => $statement,
|
||||
];
|
||||
}
|
||||
|
||||
static function getUojProblemBasicInfo($id) {
|
||||
$remote_provider = static::$providers['uoj'];
|
||||
$res = static::curl_get(static::getUojProblemUrl($id));
|
||||
|
||||
if (!$res) return null;
|
||||
|
||||
$dom = new \IvoPetkov\HTML5DOMDocument();
|
||||
$dom->loadHTML($res);
|
||||
|
||||
$title_dom = $dom->querySelector('.page-header');
|
||||
$title_matches = [];
|
||||
preg_match('/^#[1-9][0-9]*\. (.*)$/', trim($title_dom->textContent), $title_matches);
|
||||
$title = "【{$remote_provider['short_name']}{$id}】{$title_matches[1]}";
|
||||
|
||||
$statement_dom = $dom->querySelector('.uoj-article');
|
||||
$statement = HTML::tag('h3', [], '题目描述');
|
||||
|
||||
foreach ($statement_dom->querySelectorAll('a') as &$elem) {
|
||||
$href = $elem->getAttribute('href');
|
||||
$href = getAbsoluteUrl($href, $remote_provider['url']);
|
||||
$elem->setAttribute('href', $href);
|
||||
}
|
||||
|
||||
$statement .= $statement_dom->innerHTML;
|
||||
|
||||
return [
|
||||
'type' => 'html',
|
||||
'title' => $title,
|
||||
'time_limit' => null,
|
||||
'memory_limit' => null,
|
||||
'difficulty' => -1,
|
||||
'statement' => $statement,
|
||||
];
|
||||
}
|
||||
|
||||
static function getLojProblemBasicInfo($id) {
|
||||
$remote_provider = static::$providers['loj'];
|
||||
$curl = new Curl\Curl();
|
||||
$curl->setUserAgent(static::USER_AGENT);
|
||||
$curl->setHeader('Content-Type', 'application/json');
|
||||
|
||||
$res = retry_loop(function () use (&$curl, $id) {
|
||||
$curl->post('https://api.loj.ac.cn/api/problem/getProblem', json_encode([
|
||||
'displayId' => (int)$id,
|
||||
'localizedContentsOfLocale' => 'zh_CN',
|
||||
'samples' => true,
|
||||
'judgeInfo' => true,
|
||||
]));
|
||||
|
||||
if ($curl->error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $curl->response;
|
||||
});
|
||||
|
||||
if (!$res) return null;
|
||||
|
||||
// Convert stdClass to array
|
||||
$res = json_decode(json_encode($res), true);
|
||||
|
||||
if (isset($res['error'])) return null;
|
||||
|
||||
$localized_contents = $res['localizedContentsOfLocale'];
|
||||
$statement = '';
|
||||
|
||||
foreach ($localized_contents['contentSections'] as $section) {
|
||||
$statement .= "\n###" . $section['sectionTitle'] . "\n\n";
|
||||
|
||||
if ($section['type'] === 'Text') {
|
||||
$statement .= $section['text'] . "\n";
|
||||
} else if ($section['type'] === 'Sample') {
|
||||
// assert($res['samples'][$section['sampleId']]);
|
||||
$display_sample_id = $section['sampleId'] + 1;
|
||||
$sample = $res['samples'][$section['sampleId']];
|
||||
|
||||
$statement .= "\n#### 样例输入 #{$display_sample_id}\n\n";
|
||||
$statement .= "\n```text\n{$sample['inputData']}\n```\n\n";
|
||||
|
||||
$statement .= "\n#### 样例输出 #{$display_sample_id}\n\n";
|
||||
$statement .= "\n```text\n{$sample['outputData']}\n```\n\n";
|
||||
|
||||
if (trim($section['text'])) {
|
||||
$statement .= "\n#### 样例解释 #{$display_sample_id}\n\n";
|
||||
$statement .= $section['text'] . "\n";
|
||||
}
|
||||
} else {
|
||||
// do nothing...
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'html',
|
||||
'title' => "【{$remote_provider['short_name']}{$id}】{$localized_contents['title']}",
|
||||
'time_limit' => (float)$res['judgeInfo']['timeLimit'] / 1000.0,
|
||||
'memory_limit' => $res['judgeInfo']['memoryLimit'],
|
||||
'difficulty' => -1,
|
||||
'statement' => HTML::parsedown()->text($statement),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getProblemRemoteUrl($oj, $id) {
|
||||
if ($oj === 'codeforces') {
|
||||
return static::getCodeforcesProblemUrl($id);
|
||||
} else if ($oj === 'atcoder') {
|
||||
return static::getAtcoderProblemUrl($id);
|
||||
} else if ($oj === 'uoj') {
|
||||
return static::getUojProblemUrl($id);
|
||||
} else if ($oj === 'loj') {
|
||||
return static::getLojProblemUrl($id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 传入 ID 需确保有效
|
||||
public static function getProblemBasicInfo($oj, $id) {
|
||||
if ($oj === 'codeforces') {
|
||||
return static::getCodeforcesProblemBasicInfo($id);
|
||||
} else if ($oj === 'atcoder') {
|
||||
return static::getAtcoderProblemBasicInfo($id);
|
||||
} else if ($oj === 'uoj') {
|
||||
return static::getUojProblemBasicInfo($id);
|
||||
} else if ($oj === 'loj') {
|
||||
return static::getLojProblemBasicInfo($id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function downloadImagesInRemoteContent($problem_id) {
|
||||
$curl = new Curl\Curl();
|
||||
$curl->setUserAgent(static::USER_AGENT);
|
||||
$curl->setRetry(5);
|
||||
|
||||
$problem = UOJProblem::query($problem_id);
|
||||
|
||||
if ($problem->info['type'] != 'remote') return;
|
||||
|
||||
$remote_provider = static::$providers[$problem->getExtraConfig('remote_online_judge')];
|
||||
$remote_content = $problem->queryContent()['remote_content'];
|
||||
|
||||
$dom = new IvoPetkov\HTML5DOMDocument();
|
||||
$dom->loadHTML($remote_content);
|
||||
|
||||
foreach ($dom->querySelectorAll('img') as &$elem) {
|
||||
$src = $elem->getAttribute('src');
|
||||
$url = getAbsoluteUrl($src, $remote_provider['url']);
|
||||
$filename = 'remote_image_' . hash('md5', $url);
|
||||
$curl->download($url, $problem->getResourcesPath($filename));
|
||||
$elem->setAttribute('src', $problem->getResourcesUri($filename));
|
||||
}
|
||||
|
||||
DB::update([
|
||||
"update problems_contents",
|
||||
"set", [
|
||||
"remote_content" => HTML::purifier(['a' => ['target' => 'Enum#_blank']])->purify($dom->saveHTML()),
|
||||
],
|
||||
"where", [
|
||||
"id" => $problem->info['id'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ class UOJResponse {
|
||||
if (UOJContext::isAjax()) {
|
||||
die($msg);
|
||||
} else {
|
||||
requireLib('bootstrap5');
|
||||
echoUOJPageHeader($title);
|
||||
echo $msg;
|
||||
echoUOJPageFooter();
|
||||
|
@ -87,11 +87,18 @@ class UOJSubmission {
|
||||
$judge_reason = '';
|
||||
|
||||
$content['config'][] = ['problem_id', UOJProblem::info('id')];
|
||||
|
||||
if (UOJProblem::info('type') == 'remote') {
|
||||
$content['config'][] = ['remote_online_judge', UOJProblem::cur()->getExtraConfig('remote_online_judge')];
|
||||
$content['config'][] = ['remote_problem_id', UOJProblem::cur()->getExtraConfig('remote_problem_id')];
|
||||
}
|
||||
|
||||
if ($is_contest_submission && UOJContestProblem::cur()->getJudgeTypeInContest() == 'sample') {
|
||||
$content['final_test_config'] = $content['config'];
|
||||
$content['config'][] = ['test_sample_only', 'on'];
|
||||
$judge_reason = json_encode(['text' => '样例测评']);
|
||||
}
|
||||
|
||||
$content_json = json_encode($content);
|
||||
|
||||
$language = static::getAndRememberSubmissionLanguage($content);
|
||||
@ -400,6 +407,10 @@ class UOJSubmission {
|
||||
}
|
||||
|
||||
public function userCanSeeMinorVersions(array $user = null) {
|
||||
if ($this->problem->info['type'] == 'remote') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSuperUser($user)) {
|
||||
return true;
|
||||
}
|
||||
@ -410,6 +421,7 @@ class UOJSubmission {
|
||||
if (isSuperUser($user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->userCanManageProblemOrContest($user) && $this->hasFullyJudged();
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ Route::pattern('tab', '\S{1,20}');
|
||||
Route::pattern('rand_str_id', '[0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]{20}');
|
||||
Route::pattern('image_name', '[0-9a-z]{1,20}');
|
||||
Route::pattern('upgrade_name', '[a-zA-Z0-9_]{1,50}');
|
||||
Route::pattern('sub_path', '.*');
|
||||
|
||||
Route::group(
|
||||
[
|
||||
@ -17,9 +18,12 @@ Route::group(
|
||||
Route::any('/', '/index.php');
|
||||
Route::any('/problems', '/problem_set.php');
|
||||
Route::any('/problems/template', '/problem_set.php?tab=template');
|
||||
Route::any('/problems/remote', '/problem_set.php?tab=remote');
|
||||
Route::any('/problems/remote/new', '/new_remote_problem.php');
|
||||
Route::any('/problem/{id}', '/problem.php');
|
||||
Route::any('/problem/{id}/solutions', '/problem_solutions.php');
|
||||
Route::any('/problem/{id}/statistics', '/problem_statistics.php');
|
||||
Route::any('/problem/{id}/resources(?:/{sub_path})?', '/problem_resources.php');
|
||||
Route::any('/problem/{id}/manage/statement', '/problem_statement_manage.php');
|
||||
Route::any('/problem/{id}/manage/managers', '/problem_managers_manage.php');
|
||||
Route::any('/problem/{id}/manage/data', '/problem_data_manage.php');
|
||||
@ -85,16 +89,12 @@ Route::group(
|
||||
|
||||
Route::any('/super_manage(?:/{tab})?', '/super_manage.php');
|
||||
|
||||
Route::any('/download/problem/{id}/data.zip', '/download.php?type=problem');
|
||||
Route::any('/download/problem/{id}/attachment.zip', '/download.php?type=attachment');
|
||||
|
||||
Route::any('/check-notice', '/check_notice.php');
|
||||
Route::any('/click-zan', '/click_zan.php');
|
||||
|
||||
// Apps
|
||||
Route::any('/image_hosting', '/app/image_hosting/index.php');
|
||||
Route::get('/image_hosting/{image_name}.png', '/app/image_hosting/get_image.php');
|
||||
Route::any('/html2markdown', '/app/html2markdown.php');
|
||||
Route::any('/apps/image_hosting', '/apps/image_hosting.php');
|
||||
Route::any('/apps/html2markdown', '/apps/html2markdown.php');
|
||||
}
|
||||
);
|
||||
|
||||
|
0
web/app/storage/problem_resources/.gitkeep
Normal file
0
web/app/storage/problem_resources/.gitkeep
Normal file
1
web/app/upgrade/28_remote_judge/README.md
Normal file
1
web/app/upgrade/28_remote_judge/README.md
Normal file
@ -0,0 +1 @@
|
||||
https://github.com/renbaoshuo/S2OJ/pull/28
|
6
web/app/upgrade/28_remote_judge/up.sql
Normal file
6
web/app/upgrade/28_remote_judge/up.sql
Normal file
@ -0,0 +1,6 @@
|
||||
ALTER TABLE `problems` ADD `type` varchar(20) NOT NULL DEFAULT 'local' AFTER `difficulty`;
|
||||
ALTER TABLE `problems` ADD KEY `type` (`type`);
|
||||
|
||||
ALTER TABLE `problems_contents` ADD `remote_content` longtext COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' AFTER `id`;
|
||||
|
||||
insert into judger_info (judger_name, password, ip, display_name, description) values ('remote_judger', '_judger_password_', 'uoj-remote-judger', '远端评测机', '用于桥接远端 OJ 评测机的虚拟评测机。');
|
13
web/app/upgrade/31_problem_resources/upgrade.php
Normal file
13
web/app/upgrade/31_problem_resources/upgrade.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
return function ($type) {
|
||||
if ($type == 'up') {
|
||||
DB::init();
|
||||
|
||||
$problems = DB::selectAll("select id from problems");
|
||||
|
||||
foreach ($problems as $row) {
|
||||
mkdir(UOJContext::storagePath() . "/problem_resources/{$row['id']}");
|
||||
}
|
||||
}
|
||||
};
|
145
web/app/vendor/composer/ClassLoader.php
vendored
145
web/app/vendor/composer/ClassLoader.php
vendored
@ -37,57 +37,130 @@ namespace Composer\Autoload;
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @see http://www.php-fig.org/psr/psr-0/
|
||||
* @see http://www.php-fig.org/psr/psr-4/
|
||||
* @see https://www.php-fig.org/psr/psr-0/
|
||||
* @see https://www.php-fig.org/psr/psr-4/
|
||||
*/
|
||||
class ClassLoader
|
||||
{
|
||||
/** @var ?string */
|
||||
private $vendorDir;
|
||||
|
||||
// PSR-4
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array<string, int>>
|
||||
*/
|
||||
private $prefixLengthsPsr4 = array();
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array<int, string>>
|
||||
*/
|
||||
private $prefixDirsPsr4 = array();
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, string>
|
||||
*/
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array<string, string[]>>
|
||||
*/
|
||||
private $prefixesPsr0 = array();
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, string>
|
||||
*/
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
/** @var bool */
|
||||
private $useIncludePath = false;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
* @psalm-var array<string, string>
|
||||
*/
|
||||
private $classMap = array();
|
||||
|
||||
/** @var bool */
|
||||
private $classMapAuthoritative = false;
|
||||
|
||||
/**
|
||||
* @var bool[]
|
||||
* @psalm-var array<string, bool>
|
||||
*/
|
||||
private $missingClasses = array();
|
||||
|
||||
/** @var ?string */
|
||||
private $apcuPrefix;
|
||||
|
||||
/**
|
||||
* @var self[]
|
||||
*/
|
||||
private static $registeredLoaders = array();
|
||||
|
||||
/**
|
||||
* @param ?string $vendorDir
|
||||
*/
|
||||
public function __construct($vendorDir = null)
|
||||
{
|
||||
$this->vendorDir = $vendorDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', $this->prefixesPsr0);
|
||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return array<string, array<int, string>>
|
||||
*/
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return array<string, string>
|
||||
*/
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return array<string, string>
|
||||
*/
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[] Array of classname => path
|
||||
* @psalm-return array<string, string>
|
||||
*/
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $classMap Class to filename map
|
||||
* @param string[] $classMap Class to filename map
|
||||
* @psalm-param array<string, string> $classMap
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addClassMap(array $classMap)
|
||||
{
|
||||
@ -103,8 +176,10 @@ class ClassLoader
|
||||
* appending or prepending to the ones previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param array|string $paths The PSR-0 root directories
|
||||
* @param string[]|string $paths The PSR-0 root directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add($prefix, $paths, $prepend = false)
|
||||
{
|
||||
@ -148,10 +223,12 @@ class ClassLoader
|
||||
* appending or prepending to the ones previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param array|string $paths The PSR-4 base directories
|
||||
* @param string[]|string $paths The PSR-4 base directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addPsr4($prefix, $paths, $prepend = false)
|
||||
{
|
||||
@ -196,7 +273,9 @@ class ClassLoader
|
||||
* replacing any others previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param array|string $paths The PSR-0 base directories
|
||||
* @param string[]|string $paths The PSR-0 base directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($prefix, $paths)
|
||||
{
|
||||
@ -212,9 +291,11 @@ class ClassLoader
|
||||
* replacing any others previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param array|string $paths The PSR-4 base directories
|
||||
* @param string[]|string $paths The PSR-4 base directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPsr4($prefix, $paths)
|
||||
{
|
||||
@ -234,6 +315,8 @@ class ClassLoader
|
||||
* Turns on searching the include path for class files.
|
||||
*
|
||||
* @param bool $useIncludePath
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUseIncludePath($useIncludePath)
|
||||
{
|
||||
@ -256,6 +339,8 @@ class ClassLoader
|
||||
* that have not been registered with the class map.
|
||||
*
|
||||
* @param bool $classMapAuthoritative
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||
{
|
||||
@ -276,6 +361,8 @@ class ClassLoader
|
||||
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||
*
|
||||
* @param string|null $apcuPrefix
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setApcuPrefix($apcuPrefix)
|
||||
{
|
||||
@ -296,25 +383,44 @@ class ClassLoader
|
||||
* Registers this instance as an autoloader.
|
||||
*
|
||||
* @param bool $prepend Whether to prepend the autoloader or not
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register($prepend = false)
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||
|
||||
if (null === $this->vendorDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($prepend) {
|
||||
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||
} else {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this instance as an autoloader.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
spl_autoload_unregister(array($this, 'loadClass'));
|
||||
|
||||
if (null !== $this->vendorDir) {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given class or interface.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
* @return bool|null True if loaded, null otherwise
|
||||
* @return true|null True if loaded, null otherwise
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
@ -323,6 +429,8 @@ class ClassLoader
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -367,6 +475,21 @@ class ClassLoader
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently registered loaders indexed by their corresponding vendor directories.
|
||||
*
|
||||
* @return self[]
|
||||
*/
|
||||
public static function getRegisteredLoaders()
|
||||
{
|
||||
return self::$registeredLoaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $ext
|
||||
* @return string|false
|
||||
*/
|
||||
private function findFileWithExtension($class, $ext)
|
||||
{
|
||||
// PSR-4 lookup
|
||||
@ -438,6 +561,10 @@ class ClassLoader
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
* @private
|
||||
*/
|
||||
function includeFile($file)
|
||||
{
|
||||
|
350
web/app/vendor/composer/InstalledVersions.php
vendored
Normal file
350
web/app/vendor/composer/InstalledVersions.php
vendored
Normal file
@ -0,0 +1,350 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* This class is copied in every Composer installed project and available to all
|
||||
*
|
||||
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||
*
|
||||
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private static $canGetVendors;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
|
||||
*/
|
||||
private static $installedByVendor = array();
|
||||
|
||||
/**
|
||||
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackages()
|
||||
{
|
||||
$packages = array();
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
$packages[] = array_keys($installed['versions']);
|
||||
}
|
||||
|
||||
if (1 === \count($packages)) {
|
||||
return $packages[0];
|
||||
}
|
||||
|
||||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all package names with a specific type e.g. 'library'
|
||||
*
|
||||
* @param string $type
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackagesByType($type)
|
||||
{
|
||||
$packagesByType = array();
|
||||
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
foreach ($installed['versions'] as $name => $package) {
|
||||
if (isset($package['type']) && $package['type'] === $type) {
|
||||
$packagesByType[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $packagesByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package is installed
|
||||
*
|
||||
* This also returns true if the package name is provided or replaced by another package
|
||||
*
|
||||
* @param string $packageName
|
||||
* @param bool $includeDevRequirements
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (isset($installed['versions'][$packageName])) {
|
||||
return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package satisfies a version constraint
|
||||
*
|
||||
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||
*
|
||||
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||
*
|
||||
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||
* @param string $packageName
|
||||
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||
* @return bool
|
||||
*/
|
||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||
{
|
||||
$constraint = $parser->parseConstraints($constraint);
|
||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||
|
||||
return $provided->matches($constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||
*
|
||||
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||
* whether a given version of a package is installed, and not just whether it exists
|
||||
*
|
||||
* @param string $packageName
|
||||
* @return string Version constraint usable with composer/semver
|
||||
*/
|
||||
public static function getVersionRanges($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ranges = array();
|
||||
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||
}
|
||||
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||
}
|
||||
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||
}
|
||||
|
||||
return implode(' || ', $ranges);
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getPrettyVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||
*/
|
||||
public static function getReference($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['reference'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||
*/
|
||||
public static function getInstallPath($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
|
||||
*/
|
||||
public static function getRootPackage()
|
||||
{
|
||||
$installed = self::getInstalled();
|
||||
|
||||
return $installed[0]['root'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw installed.php data for custom implementations
|
||||
*
|
||||
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||
* @return array[]
|
||||
* @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
|
||||
*/
|
||||
public static function getRawData()
|
||||
{
|
||||
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = include __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||
*
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
|
||||
*/
|
||||
public static function getAllRawData()
|
||||
{
|
||||
return self::getInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets you reload the static array from another file
|
||||
*
|
||||
* This is only useful for complex integrations in which a project needs to use
|
||||
* this class but then also needs to execute another project's autoloader in process,
|
||||
* and wants to ensure both projects have access to their version of installed.php.
|
||||
*
|
||||
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||
* the data it needs from this class, then call reload() with
|
||||
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||
* the project in which it runs can then also use this class safely, without
|
||||
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||
*
|
||||
* @param array[] $data A vendor/composer/installed.php data set
|
||||
* @return void
|
||||
*
|
||||
* @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data
|
||||
*/
|
||||
public static function reload($data)
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
|
||||
*/
|
||||
private static function getInstalled()
|
||||
{
|
||||
if (null === self::$canGetVendors) {
|
||||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||
}
|
||||
|
||||
$installed = array();
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
|
||||
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
||||
self::$installed = $installed[count($installed) - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = require __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
$installed[] = self::$installed;
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
@ -6,5 +6,11 @@ $vendorDir = dirname(dirname(__FILE__));
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
'ParsedownMath' => $vendorDir . '/parsedown-math/ParsedownMath.php',
|
||||
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
|
||||
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
|
||||
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
|
||||
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
|
||||
);
|
||||
|
3
web/app/vendor/composer/autoload_files.php
vendored
3
web/app/vendor/composer/autoload_files.php
vendored
@ -6,5 +6,8 @@ $vendorDir = dirname(dirname(__FILE__));
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
|
||||
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
|
||||
'2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
|
||||
'16eed290c5592c18dc3f16802ad3d0e4' => $vendorDir . '/ivopetkov/html5-dom-document-php/autoload.php',
|
||||
);
|
||||
|
2
web/app/vendor/composer/autoload_psr4.php
vendored
2
web/app/vendor/composer/autoload_psr4.php
vendored
@ -6,7 +6,9 @@ $vendorDir = dirname(dirname(__FILE__));
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
|
||||
'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
|
||||
'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
|
||||
'Gregwar\\' => array($vendorDir . '/gregwar/captcha/src/Gregwar'),
|
||||
'Curl\\' => array($vendorDir . '/php-curl-class/php-curl-class/src/Curl'),
|
||||
);
|
||||
|
15
web/app/vendor/composer/autoload_real.php
vendored
15
web/app/vendor/composer/autoload_real.php
vendored
@ -22,13 +22,15 @@ class ComposerAutoloaderInit0d7c2cd5c2dbf2120e4372996869e900
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
require __DIR__ . '/platform_check.php';
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInit0d7c2cd5c2dbf2120e4372996869e900', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInit0d7c2cd5c2dbf2120e4372996869e900', 'loadClassLoader'));
|
||||
|
||||
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
|
||||
if ($useStaticLoader) {
|
||||
require_once __DIR__ . '/autoload_static.php';
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900::getInitializer($loader));
|
||||
} else {
|
||||
@ -63,11 +65,16 @@ class ComposerAutoloaderInit0d7c2cd5c2dbf2120e4372996869e900
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileIdentifier
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
function composerRequire0d7c2cd5c2dbf2120e4372996869e900($fileIdentifier, $file)
|
||||
{
|
||||
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
|
||||
require $file;
|
||||
|
||||
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
|
||||
|
||||
require $file;
|
||||
}
|
||||
}
|
||||
|
22
web/app/vendor/composer/autoload_static.php
vendored
22
web/app/vendor/composer/autoload_static.php
vendored
@ -7,12 +7,16 @@ namespace Composer\Autoload;
|
||||
class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
|
||||
{
|
||||
public static $files = array (
|
||||
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
|
||||
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
|
||||
'2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
|
||||
'16eed290c5592c18dc3f16802ad3d0e4' => __DIR__ . '/..' . '/ivopetkov/html5-dom-document-php/autoload.php',
|
||||
);
|
||||
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'S' =>
|
||||
array (
|
||||
'Symfony\\Polyfill\\Php80\\' => 23,
|
||||
'Symfony\\Component\\Finder\\' => 25,
|
||||
),
|
||||
'P' =>
|
||||
@ -23,9 +27,17 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
|
||||
array (
|
||||
'Gregwar\\' => 8,
|
||||
),
|
||||
'C' =>
|
||||
array (
|
||||
'Curl\\' => 5,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'Symfony\\Polyfill\\Php80\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
|
||||
),
|
||||
'Symfony\\Component\\Finder\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/finder',
|
||||
@ -38,6 +50,10 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/gregwar/captcha/src/Gregwar',
|
||||
),
|
||||
'Curl\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/php-curl-class/php-curl-class/src/Curl',
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixesPsr0 = array (
|
||||
@ -58,7 +74,13 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
'ParsedownMath' => __DIR__ . '/..' . '/parsedown-math/ParsedownMath.php',
|
||||
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
|
||||
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
|
||||
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
|
||||
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
|
316
web/app/vendor/composer/installed.json
vendored
316
web/app/vendor/composer/installed.json
vendored
@ -1,4 +1,5 @@
|
||||
[
|
||||
{
|
||||
"packages": [
|
||||
{
|
||||
"name": "erusev/parsedown",
|
||||
"version": "1.7.4",
|
||||
@ -45,7 +46,8 @@
|
||||
"keywords": [
|
||||
"markdown",
|
||||
"parser"
|
||||
]
|
||||
],
|
||||
"install-path": "../erusev/parsedown"
|
||||
},
|
||||
{
|
||||
"name": "ezyang/htmlpurifier",
|
||||
@ -104,7 +106,8 @@
|
||||
"homepage": "http://htmlpurifier.org/",
|
||||
"keywords": [
|
||||
"html"
|
||||
]
|
||||
],
|
||||
"install-path": "../ezyang/htmlpurifier"
|
||||
},
|
||||
{
|
||||
"name": "gregwar/captcha",
|
||||
@ -159,8 +162,135 @@
|
||||
"bot",
|
||||
"captcha",
|
||||
"spam"
|
||||
],
|
||||
"install-path": "../gregwar/captcha"
|
||||
},
|
||||
{
|
||||
"name": "ivopetkov/html5-dom-document-php",
|
||||
"version": "v2.4.0",
|
||||
"version_normalized": "2.4.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ivopetkov/html5-dom-document-php.git",
|
||||
"reference": "32c5ba748d661a9654c190bf70ce2854eaf5ad22"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ivopetkov/html5-dom-document-php/zipball/32c5ba748d661a9654c190bf70ce2854eaf5ad22",
|
||||
"reference": "32c5ba748d661a9654c190bf70ce2854eaf5ad22",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"php": "7.0.*|7.1.*|7.2.*|7.3.*|7.4.*|8.0.*|8.1.*|8.2.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"ivopetkov/docs-generator": "1.*"
|
||||
},
|
||||
"time": "2022-12-17T00:20:55+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"autoload.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ivo Petkov",
|
||||
"email": "ivo@ivopetkov.com",
|
||||
"homepage": "http://ivopetkov.com"
|
||||
}
|
||||
],
|
||||
"description": "HTML5 DOMDocument PHP library (extends DOMDocument)",
|
||||
"support": {
|
||||
"issues": "https://github.com/ivopetkov/html5-dom-document-php/issues",
|
||||
"source": "https://github.com/ivopetkov/html5-dom-document-php/tree/v2.4.0"
|
||||
},
|
||||
"install-path": "../ivopetkov/html5-dom-document-php"
|
||||
},
|
||||
{
|
||||
"name": "php-curl-class/php-curl-class",
|
||||
"version": "9.13.1",
|
||||
"version_normalized": "9.13.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-curl-class/php-curl-class.git",
|
||||
"reference": "1fa71f639275987092c60ac117ff5c8a3771898d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-curl-class/php-curl-class/zipball/1fa71f639275987092c60ac117ff5c8a3771898d",
|
||||
"reference": "1fa71f639275987092c60ac117ff5c8a3771898d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"php": ">=7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "*",
|
||||
"ext-gd": "*",
|
||||
"phpcompatibility/php-compatibility": "dev-develop",
|
||||
"phpcsstandards/phpcsutils": "@alpha",
|
||||
"phpunit/phpunit": "*",
|
||||
"squizlabs/php_codesniffer": "*",
|
||||
"vimeo/psalm": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"time": "2023-01-16T01:29:25+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Curl\\": "src/Curl/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Unlicense"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Zach Borboa"
|
||||
}
|
||||
],
|
||||
"description": "PHP Curl Class makes it easy to send HTTP requests and integrate with web APIs.",
|
||||
"homepage": "https://github.com/php-curl-class/php-curl-class",
|
||||
"keywords": [
|
||||
"API-Client",
|
||||
"api",
|
||||
"class",
|
||||
"client",
|
||||
"curl",
|
||||
"framework",
|
||||
"http",
|
||||
"http-client",
|
||||
"http-proxy",
|
||||
"json",
|
||||
"php",
|
||||
"php-curl",
|
||||
"php-curl-library",
|
||||
"proxy",
|
||||
"requests",
|
||||
"restful",
|
||||
"web-scraper",
|
||||
"web-scraping ",
|
||||
"web-service",
|
||||
"xml"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-curl-class/php-curl-class/issues",
|
||||
"source": "https://github.com/php-curl-class/php-curl-class/tree/9.13.1"
|
||||
},
|
||||
"install-path": "../php-curl-class/php-curl-class"
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v6.6.5",
|
||||
@ -235,30 +365,97 @@
|
||||
"url": "https://github.com/Synchro",
|
||||
"type": "github"
|
||||
}
|
||||
]
|
||||
],
|
||||
"install-path": "../phpmailer/phpmailer"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v6.1.3",
|
||||
"version_normalized": "6.1.3.0",
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v2.5.2",
|
||||
"version_normalized": "2.5.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709"
|
||||
"url": "https://github.com/symfony/deprecation-contracts.git",
|
||||
"reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/39696bff2c2970b3779a5cac7bf9f0b88fc2b709",
|
||||
"reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
|
||||
"reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/filesystem": "^6.0"
|
||||
"time": "2022-01-02T09:53:40+00:00",
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.5-dev"
|
||||
},
|
||||
"time": "2022-07-29T07:42:06+00:00",
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"function.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"install-path": "../symfony/deprecation-contracts"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v5.4.11",
|
||||
"version_normalized": "5.4.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/7872a66f57caffa2916a584db1aa7f12adc76f8c",
|
||||
"reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/deprecation-contracts": "^2.1|^3",
|
||||
"symfony/polyfill-php80": "^1.16"
|
||||
},
|
||||
"time": "2022-07-29T07:37:50+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
@ -298,6 +495,93 @@
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
]
|
||||
],
|
||||
"install-path": "../symfony/finder"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "v1.26.0",
|
||||
"version_normalized": "1.26.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
|
||||
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"time": "2022-05-10T07:21:04+00:00",
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.26-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Php80\\": ""
|
||||
},
|
||||
"classmap": [
|
||||
"Resources/stubs"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ion Bazan",
|
||||
"email": "ion.bazan@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"install-path": "../symfony/polyfill-php80"
|
||||
}
|
||||
],
|
||||
"dev": true,
|
||||
"dev-package-names": []
|
||||
}
|
||||
|
104
web/app/vendor/composer/installed.php
vendored
Normal file
104
web/app/vendor/composer/installed.php
vendored
Normal file
@ -0,0 +1,104 @@
|
||||
<?php return array(
|
||||
'root' => array(
|
||||
'pretty_version' => 'dev-master',
|
||||
'version' => 'dev-master',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'reference' => 'aa6bf6a363501f9836d2bb26a513c2ac3985de83',
|
||||
'name' => '__root__',
|
||||
'dev' => true,
|
||||
),
|
||||
'versions' => array(
|
||||
'__root__' => array(
|
||||
'pretty_version' => 'dev-master',
|
||||
'version' => 'dev-master',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'reference' => 'aa6bf6a363501f9836d2bb26a513c2ac3985de83',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'erusev/parsedown' => array(
|
||||
'pretty_version' => '1.7.4',
|
||||
'version' => '1.7.4.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../erusev/parsedown',
|
||||
'aliases' => array(),
|
||||
'reference' => 'cb17b6477dfff935958ba01325f2e8a2bfa6dab3',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'ezyang/htmlpurifier' => array(
|
||||
'pretty_version' => 'v4.16.0',
|
||||
'version' => '4.16.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../ezyang/htmlpurifier',
|
||||
'aliases' => array(),
|
||||
'reference' => '523407fb06eb9e5f3d59889b3978d5bfe94299c8',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'gregwar/captcha' => array(
|
||||
'pretty_version' => 'v1.1.9',
|
||||
'version' => '1.1.9.0',
|
||||
'type' => 'captcha',
|
||||
'install_path' => __DIR__ . '/../gregwar/captcha',
|
||||
'aliases' => array(),
|
||||
'reference' => '4bb668e6b40e3205a020ca5ee4ca8cff8b8780c5',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'ivopetkov/html5-dom-document-php' => array(
|
||||
'pretty_version' => 'v2.4.0',
|
||||
'version' => '2.4.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../ivopetkov/html5-dom-document-php',
|
||||
'aliases' => array(),
|
||||
'reference' => '32c5ba748d661a9654c190bf70ce2854eaf5ad22',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'php-curl-class/php-curl-class' => array(
|
||||
'pretty_version' => '9.13.1',
|
||||
'version' => '9.13.1.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../php-curl-class/php-curl-class',
|
||||
'aliases' => array(),
|
||||
'reference' => '1fa71f639275987092c60ac117ff5c8a3771898d',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'phpmailer/phpmailer' => array(
|
||||
'pretty_version' => 'v6.6.5',
|
||||
'version' => '6.6.5.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phpmailer/phpmailer',
|
||||
'aliases' => array(),
|
||||
'reference' => '8b6386d7417526d1ea4da9edb70b8352f7543627',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/deprecation-contracts' => array(
|
||||
'pretty_version' => 'v2.5.2',
|
||||
'version' => '2.5.2.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
|
||||
'aliases' => array(),
|
||||
'reference' => 'e8b495ea28c1d97b5e0c121748d6f9b53d075c66',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/finder' => array(
|
||||
'pretty_version' => 'v5.4.11',
|
||||
'version' => '5.4.11.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/finder',
|
||||
'aliases' => array(),
|
||||
'reference' => '7872a66f57caffa2916a584db1aa7f12adc76f8c',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-php80' => array(
|
||||
'pretty_version' => 'v1.26.0',
|
||||
'version' => '1.26.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
|
||||
'aliases' => array(),
|
||||
'reference' => 'cfa0ae98841b9e461207c13ab093d76b0fa7bace',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
26
web/app/vendor/composer/platform_check.php
vendored
Normal file
26
web/app/vendor/composer/platform_check.php
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
// platform_check.php @generated by Composer
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 70205)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.5". You are running ' . PHP_VERSION . '.';
|
||||
}
|
||||
|
||||
if ($issues) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||
} elseif (!headers_sent()) {
|
||||
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues),
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
21
web/app/vendor/ivopetkov/html5-dom-document-php/LICENSE
vendored
Normal file
21
web/app/vendor/ivopetkov/html5-dom-document-php/LICENSE
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) Ivo Petkov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
22
web/app/vendor/ivopetkov/html5-dom-document-php/autoload.php
vendored
Normal file
22
web/app/vendor/ivopetkov/html5-dom-document-php/autoload.php
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* HTML5 DOMDocument PHP library (extends DOMDocument)
|
||||
* https://github.com/ivopetkov/html5-dom-document-php
|
||||
* Copyright (c) Ivo Petkov
|
||||
* Free to use under the MIT license.
|
||||
*/
|
||||
|
||||
$classes = array(
|
||||
'IvoPetkov\HTML5DOMDocument' => __DIR__ . '/src/HTML5DOMDocument.php',
|
||||
'IvoPetkov\HTML5DOMDocument\Internal\QuerySelectors' => __DIR__ . '/src/HTML5DOMDocument/Internal/QuerySelectors.php',
|
||||
'IvoPetkov\HTML5DOMElement' => __DIR__ . '/src/HTML5DOMElement.php',
|
||||
'IvoPetkov\HTML5DOMNodeList' => __DIR__ . '/src/HTML5DOMNodeList.php',
|
||||
'IvoPetkov\HTML5DOMTokenList' => __DIR__ . '/src/HTML5DOMTokenList.php'
|
||||
);
|
||||
|
||||
spl_autoload_register(function ($class) use ($classes) {
|
||||
if (isset($classes[$class])) {
|
||||
require $classes[$class];
|
||||
}
|
||||
});
|
24
web/app/vendor/ivopetkov/html5-dom-document-php/composer.json
vendored
Normal file
24
web/app/vendor/ivopetkov/html5-dom-document-php/composer.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "ivopetkov/html5-dom-document-php",
|
||||
"description": "HTML5 DOMDocument PHP library (extends DOMDocument)",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ivo Petkov",
|
||||
"email": "ivo@ivopetkov.com",
|
||||
"homepage": "http://ivopetkov.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "7.0.*|7.1.*|7.2.*|7.3.*|7.4.*|8.0.*|8.1.*|8.2.*",
|
||||
"ext-dom": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"ivopetkov/docs-generator": "1.*"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"autoload.php"
|
||||
]
|
||||
}
|
||||
}
|
747
web/app/vendor/ivopetkov/html5-dom-document-php/src/HTML5DOMDocument.php
vendored
Normal file
747
web/app/vendor/ivopetkov/html5-dom-document-php/src/HTML5DOMDocument.php
vendored
Normal file
@ -0,0 +1,747 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* HTML5 DOMDocument PHP library (extends DOMDocument)
|
||||
* https://github.com/ivopetkov/html5-dom-document-php
|
||||
* Copyright (c) Ivo Petkov
|
||||
* Free to use under the MIT license.
|
||||
*/
|
||||
|
||||
namespace IvoPetkov;
|
||||
|
||||
use IvoPetkov\HTML5DOMDocument\Internal\QuerySelectors;
|
||||
|
||||
/**
|
||||
* Represents a live (can be manipulated) representation of a HTML5 document.
|
||||
*
|
||||
* @method \IvoPetkov\HTML5DOMElement|false createElement(string $localName, string $value = '') Create new element node.
|
||||
* @method \IvoPetkov\HTML5DOMElement|false createElementNS(?string $namespace, string $qualifiedName, string $value = '') Create new element node with an associated namespace.
|
||||
* @method ?\IvoPetkov\HTML5DOMElement getElementById(string $elementId) Searches for an element with a certain id.
|
||||
*/
|
||||
class HTML5DOMDocument extends \DOMDocument
|
||||
{
|
||||
|
||||
use QuerySelectors;
|
||||
|
||||
/**
|
||||
* An option passed to loadHTML() and loadHTMLFile() to disable duplicate element IDs exception.
|
||||
*/
|
||||
const ALLOW_DUPLICATE_IDS = 67108864;
|
||||
|
||||
/**
|
||||
* A modification (passed to modify()) that removes all but the last title elements.
|
||||
*/
|
||||
const FIX_MULTIPLE_TITLES = 2;
|
||||
|
||||
/**
|
||||
* A modification (passed to modify()) that removes all but the last metatags with matching name or property attributes.
|
||||
*/
|
||||
const FIX_DUPLICATE_METATAGS = 4;
|
||||
|
||||
/**
|
||||
* A modification (passed to modify()) that merges multiple head elements.
|
||||
*/
|
||||
const FIX_MULTIPLE_HEADS = 8;
|
||||
|
||||
/**
|
||||
* A modification (passed to modify()) that merges multiple body elements.
|
||||
*/
|
||||
const FIX_MULTIPLE_BODIES = 16;
|
||||
|
||||
/**
|
||||
* A modification (passed to modify()) that moves charset metatag and title elements first.
|
||||
*/
|
||||
const OPTIMIZE_HEAD = 32;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
static private $newObjectsCache = [];
|
||||
|
||||
/**
|
||||
* Indicates whether an HTML code is loaded.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private $loaded = false;
|
||||
|
||||
/**
|
||||
* Creates a new HTML5DOMDocument object.
|
||||
*
|
||||
* @param string $version The version number of the document as part of the XML declaration.
|
||||
* @param string $encoding The encoding of the document as part of the XML declaration.
|
||||
*/
|
||||
public function __construct(string $version = '1.0', string $encoding = '')
|
||||
{
|
||||
parent::__construct($version, $encoding);
|
||||
$this->registerNodeClass('DOMElement', '\IvoPetkov\HTML5DOMElement');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load HTML from a string.
|
||||
*
|
||||
* @param string $source The HTML code.
|
||||
* @param int $options Additional Libxml parameters.
|
||||
* @return boolean TRUE on success or FALSE on failure.
|
||||
*/
|
||||
public function loadHTML($source, $options = 0)
|
||||
{
|
||||
// Enables libxml errors handling
|
||||
$internalErrorsOptionValue = libxml_use_internal_errors();
|
||||
if ($internalErrorsOptionValue === false) {
|
||||
libxml_use_internal_errors(true);
|
||||
}
|
||||
|
||||
$source = trim($source);
|
||||
|
||||
// Add CDATA around script tags content
|
||||
$matches = null;
|
||||
preg_match_all('/<script(.*?)>/', $source, $matches);
|
||||
if (isset($matches[0])) {
|
||||
$matches[0] = array_unique($matches[0]);
|
||||
foreach ($matches[0] as $match) {
|
||||
if (substr($match, -2, 1) !== '/') { // check if ends with />
|
||||
$source = str_replace($match, $match . '<![CDATA[-html5-dom-document-internal-cdata', $source); // Add CDATA after the open tag
|
||||
}
|
||||
}
|
||||
}
|
||||
$source = str_replace('</script>', '-html5-dom-document-internal-cdata]]></script>', $source); // Add CDATA before the end tag
|
||||
$source = str_replace('<![CDATA[-html5-dom-document-internal-cdata-html5-dom-document-internal-cdata]]>', '', $source); // Clean empty script tags
|
||||
$matches = null;
|
||||
preg_match_all('/\<!\[CDATA\[-html5-dom-document-internal-cdata.*?-html5-dom-document-internal-cdata\]\]>/s', $source, $matches);
|
||||
if (isset($matches[0])) {
|
||||
$matches[0] = array_unique($matches[0]);
|
||||
foreach ($matches[0] as $match) {
|
||||
if (strpos($match, '</') !== false) { // check if contains </
|
||||
$source = str_replace($match, str_replace('</', '<-html5-dom-document-internal-cdata-endtagfix/', $match), $source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$autoAddHtmlAndBodyTags = !defined('LIBXML_HTML_NOIMPLIED') || ($options & LIBXML_HTML_NOIMPLIED) === 0;
|
||||
$autoAddDoctype = !defined('LIBXML_HTML_NODEFDTD') || ($options & LIBXML_HTML_NODEFDTD) === 0;
|
||||
|
||||
$allowDuplicateIDs = ($options & self::ALLOW_DUPLICATE_IDS) !== 0;
|
||||
|
||||
// Add body tag if missing
|
||||
if ($autoAddHtmlAndBodyTags && $source !== '' && preg_match('/\<!DOCTYPE.*?\>/', $source) === 0 && preg_match('/\<html.*?\>/', $source) === 0 && preg_match('/\<body.*?\>/', $source) === 0 && preg_match('/\<head.*?\>/', $source) === 0) {
|
||||
$source = '<body>' . $source . '</body>';
|
||||
}
|
||||
|
||||
// Add DOCTYPE if missing
|
||||
if ($autoAddDoctype && strtoupper(substr($source, 0, 9)) !== '<!DOCTYPE') {
|
||||
$source = "<!DOCTYPE html>\n" . $source;
|
||||
}
|
||||
|
||||
// Adds temporary head tag
|
||||
$charsetTag = '<meta data-html5-dom-document-internal-attribute="charset-meta" http-equiv="content-type" content="text/html; charset=utf-8" />';
|
||||
$matches = [];
|
||||
preg_match('/\<head.*?\>/', $source, $matches);
|
||||
$removeHeadTag = false;
|
||||
$removeHtmlTag = false;
|
||||
if (isset($matches[0])) { // has head tag
|
||||
$insertPosition = strpos($source, $matches[0]) + strlen($matches[0]);
|
||||
$source = substr($source, 0, $insertPosition) . $charsetTag . substr($source, $insertPosition);
|
||||
} else {
|
||||
$matches = [];
|
||||
preg_match('/\<html.*?\>/', $source, $matches);
|
||||
if (isset($matches[0])) { // has html tag
|
||||
$source = str_replace($matches[0], $matches[0] . '<head>' . $charsetTag . '</head>', $source);
|
||||
} else {
|
||||
$source = '<head>' . $charsetTag . '</head>' . $source;
|
||||
$removeHtmlTag = true;
|
||||
}
|
||||
$removeHeadTag = true;
|
||||
}
|
||||
|
||||
// Preserve html entities
|
||||
$source = preg_replace('/&([a-zA-Z]*);/', 'html5-dom-document-internal-entity1-$1-end', $source);
|
||||
$source = preg_replace('/&#([0-9]*);/', 'html5-dom-document-internal-entity2-$1-end', $source);
|
||||
|
||||
$result = parent::loadHTML('<?xml encoding="utf-8" ?>' . $source, $options);
|
||||
if ($internalErrorsOptionValue === false) {
|
||||
libxml_use_internal_errors(false);
|
||||
}
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
$this->encoding = 'utf-8';
|
||||
foreach ($this->childNodes as $item) {
|
||||
if ($item->nodeType === XML_PI_NODE) {
|
||||
$this->removeChild($item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/** @var HTML5DOMElement|null */
|
||||
$metaTagElement = $this->getElementsByTagName('meta')->item(0);
|
||||
if ($metaTagElement !== null) {
|
||||
if ($metaTagElement->getAttribute('data-html5-dom-document-internal-attribute') === 'charset-meta') {
|
||||
$headElement = $metaTagElement->parentNode;
|
||||
$htmlElement = $headElement->parentNode;
|
||||
$metaTagElement->parentNode->removeChild($metaTagElement);
|
||||
if ($removeHeadTag && $headElement !== null && $headElement->parentNode !== null && ($headElement->firstChild === null || ($headElement->childNodes->length === 1 && $headElement->firstChild instanceof \DOMText))) {
|
||||
$headElement->parentNode->removeChild($headElement);
|
||||
}
|
||||
if ($removeHtmlTag && $htmlElement !== null && $htmlElement->parentNode !== null && $htmlElement->firstChild === null) {
|
||||
$htmlElement->parentNode->removeChild($htmlElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$allowDuplicateIDs) {
|
||||
$matches = [];
|
||||
preg_match_all('/\sid[\s]*=[\s]*(["\'])(.*?)\1/', $source, $matches);
|
||||
if (!empty($matches[2]) && max(array_count_values($matches[2])) > 1) {
|
||||
$elementIDs = [];
|
||||
$walkChildren = function ($element) use (&$walkChildren, &$elementIDs) {
|
||||
foreach ($element->childNodes as $child) {
|
||||
if ($child instanceof \DOMElement) {
|
||||
if ($child->attributes->length > 0) { // Performance optimization
|
||||
$id = $child->getAttribute('id');
|
||||
if ($id !== '') {
|
||||
if (isset($elementIDs[$id])) {
|
||||
throw new \Exception('A DOM node with an ID value "' . $id . '" already exists! Pass the HTML5DOMDocument::ALLOW_DUPLICATE_IDS option to disable this check.');
|
||||
} else {
|
||||
$elementIDs[$id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$walkChildren($child);
|
||||
}
|
||||
}
|
||||
};
|
||||
$walkChildren($this);
|
||||
}
|
||||
}
|
||||
|
||||
$this->loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load HTML from a file.
|
||||
*
|
||||
* @param string $filename The path to the HTML file.
|
||||
* @param int $options Additional Libxml parameters.
|
||||
*/
|
||||
public function loadHTMLFile($filename, $options = 0)
|
||||
{
|
||||
return $this->loadHTML(file_get_contents($filename), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the HTML tag to the document if missing.
|
||||
*
|
||||
* @return boolean TRUE on success, FALSE otherwise.
|
||||
*/
|
||||
private function addHtmlElementIfMissing(): bool
|
||||
{
|
||||
if ($this->getElementsByTagName('html')->length === 0) {
|
||||
if (!isset(self::$newObjectsCache['htmlelement'])) {
|
||||
self::$newObjectsCache['htmlelement'] = new \DOMElement('html');
|
||||
}
|
||||
$this->appendChild(clone (self::$newObjectsCache['htmlelement']));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the HEAD tag to the document if missing.
|
||||
*
|
||||
* @return boolean TRUE on success, FALSE otherwise.
|
||||
*/
|
||||
private function addHeadElementIfMissing(): bool
|
||||
{
|
||||
if ($this->getElementsByTagName('head')->length === 0) {
|
||||
$htmlElement = $this->getElementsByTagName('html')->item(0);
|
||||
if (!isset(self::$newObjectsCache['headelement'])) {
|
||||
self::$newObjectsCache['headelement'] = new \DOMElement('head');
|
||||
}
|
||||
$headElement = clone (self::$newObjectsCache['headelement']);
|
||||
if ($htmlElement->firstChild === null) {
|
||||
$htmlElement->appendChild($headElement);
|
||||
} else {
|
||||
$htmlElement->insertBefore($headElement, $htmlElement->firstChild);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the BODY tag to the document if missing.
|
||||
*
|
||||
* @return boolean TRUE on success, FALSE otherwise.
|
||||
*/
|
||||
private function addBodyElementIfMissing(): bool
|
||||
{
|
||||
if ($this->getElementsByTagName('body')->length === 0) {
|
||||
if (!isset(self::$newObjectsCache['bodyelement'])) {
|
||||
self::$newObjectsCache['bodyelement'] = new \DOMElement('body');
|
||||
}
|
||||
$this->getElementsByTagName('html')->item(0)->appendChild(clone (self::$newObjectsCache['bodyelement']));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps the internal document into a string using HTML formatting.
|
||||
*
|
||||
* @param \DOMNode $node Optional parameter to output a subset of the document.
|
||||
* @return string The document (or node) HTML code as string.
|
||||
*/
|
||||
public function saveHTML(\DOMNode $node = null): string
|
||||
{
|
||||
$nodeMode = $node !== null;
|
||||
if ($nodeMode && $node instanceof \DOMDocument) {
|
||||
$nodeMode = false;
|
||||
}
|
||||
|
||||
if ($nodeMode) {
|
||||
if (!isset(self::$newObjectsCache['html5domdocument'])) {
|
||||
self::$newObjectsCache['html5domdocument'] = new HTML5DOMDocument();
|
||||
}
|
||||
$tempDomDocument = clone (self::$newObjectsCache['html5domdocument']);
|
||||
if ($node->nodeName === 'html') {
|
||||
$tempDomDocument->loadHTML('<!DOCTYPE html>');
|
||||
$tempDomDocument->appendChild($tempDomDocument->importNode(clone ($node), true));
|
||||
$html = $tempDomDocument->saveHTML();
|
||||
$html = substr($html, 16); // remove the DOCTYPE + the new line after
|
||||
} elseif ($node->nodeName === 'head' || $node->nodeName === 'body') {
|
||||
$tempDomDocument->loadHTML("<!DOCTYPE html>\n<html></html>");
|
||||
$tempDomDocument->childNodes[1]->appendChild($tempDomDocument->importNode(clone ($node), true));
|
||||
$html = $tempDomDocument->saveHTML();
|
||||
$html = substr($html, 22, -7); // remove the DOCTYPE + the new line after + html tag
|
||||
} else {
|
||||
$isInHead = false;
|
||||
$parentNode = $node;
|
||||
for ($i = 0; $i < 1000; $i++) {
|
||||
$parentNode = $parentNode->parentNode;
|
||||
if ($parentNode === null) {
|
||||
break;
|
||||
}
|
||||
if ($parentNode->nodeName === 'body') {
|
||||
break;
|
||||
} elseif ($parentNode->nodeName === 'head') {
|
||||
$isInHead = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$tempDomDocument->loadHTML("<!DOCTYPE html>\n<html>" . ($isInHead ? '<head></head>' : '<body></body>') . '</html>');
|
||||
$tempDomDocument->childNodes[1]->childNodes[0]->appendChild($tempDomDocument->importNode(clone ($node), true));
|
||||
$html = $tempDomDocument->saveHTML();
|
||||
$html = substr($html, 28, -14); // remove the DOCTYPE + the new line + html + body or head tags
|
||||
}
|
||||
$html = trim($html);
|
||||
} else {
|
||||
$removeHtmlElement = false;
|
||||
$removeHeadElement = false;
|
||||
$headElement = $this->getElementsByTagName('head')->item(0);
|
||||
if ($headElement === null) {
|
||||
if ($this->addHtmlElementIfMissing()) {
|
||||
$removeHtmlElement = true;
|
||||
}
|
||||
if ($this->addHeadElementIfMissing()) {
|
||||
$removeHeadElement = true;
|
||||
}
|
||||
$headElement = $this->getElementsByTagName('head')->item(0);
|
||||
}
|
||||
$meta = $this->createElement('meta');
|
||||
$meta->setAttribute('data-html5-dom-document-internal-attribute', 'charset-meta');
|
||||
$meta->setAttribute('http-equiv', 'content-type');
|
||||
$meta->setAttribute('content', 'text/html; charset=utf-8');
|
||||
if ($headElement->firstChild !== null) {
|
||||
$headElement->insertBefore($meta, $headElement->firstChild);
|
||||
} else {
|
||||
$headElement->appendChild($meta);
|
||||
}
|
||||
$html = parent::saveHTML();
|
||||
$html = rtrim($html, "\n");
|
||||
|
||||
if ($removeHeadElement) {
|
||||
$headElement->parentNode->removeChild($headElement);
|
||||
} else {
|
||||
$meta->parentNode->removeChild($meta);
|
||||
}
|
||||
|
||||
if (strpos($html, 'html5-dom-document-internal-entity') !== false) {
|
||||
$html = preg_replace('/html5-dom-document-internal-entity1-(.*?)-end/', '&$1;', $html);
|
||||
$html = preg_replace('/html5-dom-document-internal-entity2-(.*?)-end/', '&#$1;', $html);
|
||||
}
|
||||
|
||||
$codeToRemove = [
|
||||
'html5-dom-document-internal-content',
|
||||
'<meta data-html5-dom-document-internal-attribute="charset-meta" http-equiv="content-type" content="text/html; charset=utf-8">',
|
||||
'</area>', '</base>', '</br>', '</col>', '</command>', '</embed>', '</hr>', '</img>', '</input>', '</keygen>', '</link>', '</meta>', '</param>', '</source>', '</track>', '</wbr>',
|
||||
'<![CDATA[-html5-dom-document-internal-cdata', '-html5-dom-document-internal-cdata]]>', '-html5-dom-document-internal-cdata-endtagfix'
|
||||
];
|
||||
if ($removeHeadElement) {
|
||||
$codeToRemove[] = '<head></head>';
|
||||
}
|
||||
if ($removeHtmlElement) {
|
||||
$codeToRemove[] = '<html></html>';
|
||||
}
|
||||
|
||||
$html = str_replace($codeToRemove, '', $html);
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps the internal document into a file using HTML formatting.
|
||||
*
|
||||
* @param string $filename The path to the saved HTML document.
|
||||
* @return int|false the number of bytes written or FALSE if an error occurred.
|
||||
*/
|
||||
#[\ReturnTypeWillChange] // Return type "int|false" is invalid in older supported versions.
|
||||
public function saveHTMLFile($filename)
|
||||
{
|
||||
if (!is_writable($filename)) {
|
||||
return false;
|
||||
}
|
||||
$result = $this->saveHTML();
|
||||
file_put_contents($filename, $result);
|
||||
$bytesWritten = filesize($filename);
|
||||
if ($bytesWritten === strlen($result)) {
|
||||
return $bytesWritten;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first document element matching the selector.
|
||||
*
|
||||
* @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname.classname.classname2, .classname.classname2, tagname[attribute-selector], [attribute-selector], "div, p", div p, div > p, div + p and p ~ ul.
|
||||
* @return HTML5DOMElement|null The result DOMElement or null if not found.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function querySelector(string $selector)
|
||||
{
|
||||
return $this->internalQuerySelector($selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of document elements matching the selector.
|
||||
*
|
||||
* @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname.classname.classname2, .classname.classname2, tagname[attribute-selector], [attribute-selector], "div, p", div p, div > p, div + p and p ~ ul.
|
||||
* @return HTML5DOMNodeList Returns a list of DOMElements matching the criteria.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function querySelectorAll(string $selector)
|
||||
{
|
||||
return $this->internalQuerySelectorAll($selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an element that will be replaced by the new body in insertHTML.
|
||||
*
|
||||
* @param string $name The name of the insert target.
|
||||
* @return HTML5DOMElement A new DOMElement that must be set in the place where the new body will be inserted.
|
||||
*/
|
||||
public function createInsertTarget(string $name)
|
||||
{
|
||||
if (!$this->loaded) {
|
||||
$this->loadHTML('');
|
||||
}
|
||||
$element = $this->createElement('html5-dom-document-insert-target');
|
||||
$element->setAttribute('name', $name);
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a HTML document into the current document. The elements from the head and the body will be moved to their proper locations.
|
||||
*
|
||||
* @param string $source The HTML code to be inserted.
|
||||
* @param string $target Body target position. Available values: afterBodyBegin, beforeBodyEnd or insertTarget name.
|
||||
*/
|
||||
public function insertHTML(string $source, string $target = 'beforeBodyEnd')
|
||||
{
|
||||
$this->insertHTMLMulti([['source' => $source, 'target' => $target]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts multiple HTML documents into the current document. The elements from the head and the body will be moved to their proper locations.
|
||||
*
|
||||
* @param array $sources An array containing the source of the document to be inserted in the following format: [ ['source'=>'', 'target'=>''], ['source'=>'', 'target'=>''], ... ]
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function insertHTMLMulti(array $sources)
|
||||
{
|
||||
if (!$this->loaded) {
|
||||
$this->loadHTML('');
|
||||
}
|
||||
|
||||
if (!isset(self::$newObjectsCache['html5domdocument'])) {
|
||||
self::$newObjectsCache['html5domdocument'] = new HTML5DOMDocument();
|
||||
}
|
||||
|
||||
$currentDomDocument = &$this;
|
||||
|
||||
$copyAttributes = function ($sourceNode, $targetNode) {
|
||||
foreach ($sourceNode->attributes as $attributeName => $attribute) {
|
||||
$targetNode->setAttribute($attributeName, $attribute->value);
|
||||
}
|
||||
};
|
||||
|
||||
$currentDomHTMLElement = null;
|
||||
$currentDomHeadElement = null;
|
||||
$currentDomBodyElement = null;
|
||||
|
||||
$insertTargetsList = null;
|
||||
$prepareInsertTargetsList = function () use (&$insertTargetsList) {
|
||||
if ($insertTargetsList === null) {
|
||||
$insertTargetsList = [];
|
||||
$targetElements = $this->getElementsByTagName('html5-dom-document-insert-target');
|
||||
foreach ($targetElements as $targetElement) {
|
||||
$insertTargetsList[$targetElement->getAttribute('name')] = $targetElement;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
foreach ($sources as $sourceData) {
|
||||
if (!isset($sourceData['source'])) {
|
||||
throw new \Exception('Missing source key');
|
||||
}
|
||||
$source = $sourceData['source'];
|
||||
$target = isset($sourceData['target']) ? $sourceData['target'] : 'beforeBodyEnd';
|
||||
|
||||
$domDocument = clone (self::$newObjectsCache['html5domdocument']);
|
||||
$domDocument->loadHTML($source, self::ALLOW_DUPLICATE_IDS);
|
||||
|
||||
$htmlElement = $domDocument->getElementsByTagName('html')->item(0);
|
||||
if ($htmlElement !== null) {
|
||||
if ($htmlElement->attributes->length > 0) {
|
||||
if ($currentDomHTMLElement === null) {
|
||||
$currentDomHTMLElement = $this->getElementsByTagName('html')->item(0);
|
||||
if ($currentDomHTMLElement === null) {
|
||||
$this->addHtmlElementIfMissing();
|
||||
$currentDomHTMLElement = $this->getElementsByTagName('html')->item(0);
|
||||
}
|
||||
}
|
||||
$copyAttributes($htmlElement, $currentDomHTMLElement);
|
||||
}
|
||||
}
|
||||
|
||||
$headElement = $domDocument->getElementsByTagName('head')->item(0);
|
||||
if ($headElement !== null) {
|
||||
if ($currentDomHeadElement === null) {
|
||||
$currentDomHeadElement = $this->getElementsByTagName('head')->item(0);
|
||||
if ($currentDomHeadElement === null) {
|
||||
$this->addHtmlElementIfMissing();
|
||||
$this->addHeadElementIfMissing();
|
||||
$currentDomHeadElement = $this->getElementsByTagName('head')->item(0);
|
||||
}
|
||||
}
|
||||
foreach ($headElement->childNodes as $headElementChild) {
|
||||
$newNode = $currentDomDocument->importNode($headElementChild, true);
|
||||
if ($newNode !== null) {
|
||||
$currentDomHeadElement->appendChild($newNode);
|
||||
}
|
||||
}
|
||||
if ($headElement->attributes->length > 0) {
|
||||
$copyAttributes($headElement, $currentDomHeadElement);
|
||||
}
|
||||
}
|
||||
|
||||
$bodyElement = $domDocument->getElementsByTagName('body')->item(0);
|
||||
if ($bodyElement !== null) {
|
||||
if ($currentDomBodyElement === null) {
|
||||
$currentDomBodyElement = $this->getElementsByTagName('body')->item(0);
|
||||
if ($currentDomBodyElement === null) {
|
||||
$this->addHtmlElementIfMissing();
|
||||
$this->addBodyElementIfMissing();
|
||||
$currentDomBodyElement = $this->getElementsByTagName('body')->item(0);
|
||||
}
|
||||
}
|
||||
$bodyElementChildren = $bodyElement->childNodes;
|
||||
if ($target === 'afterBodyBegin') {
|
||||
$bodyElementChildrenCount = $bodyElementChildren->length;
|
||||
for ($i = $bodyElementChildrenCount - 1; $i >= 0; $i--) {
|
||||
$newNode = $currentDomDocument->importNode($bodyElementChildren->item($i), true);
|
||||
if ($newNode !== null) {
|
||||
if ($currentDomBodyElement->firstChild === null) {
|
||||
$currentDomBodyElement->appendChild($newNode);
|
||||
} else {
|
||||
$currentDomBodyElement->insertBefore($newNode, $currentDomBodyElement->firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($target === 'beforeBodyEnd') {
|
||||
foreach ($bodyElementChildren as $bodyElementChild) {
|
||||
$newNode = $currentDomDocument->importNode($bodyElementChild, true);
|
||||
if ($newNode !== null) {
|
||||
$currentDomBodyElement->appendChild($newNode);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$prepareInsertTargetsList();
|
||||
if (isset($insertTargetsList[$target])) {
|
||||
$targetElement = $insertTargetsList[$target];
|
||||
$targetElementParent = $targetElement->parentNode;
|
||||
foreach ($bodyElementChildren as $bodyElementChild) {
|
||||
$newNode = $currentDomDocument->importNode($bodyElementChild, true);
|
||||
if ($newNode !== null) {
|
||||
$targetElementParent->insertBefore($newNode, $targetElement);
|
||||
}
|
||||
}
|
||||
$targetElementParent->removeChild($targetElement);
|
||||
}
|
||||
}
|
||||
if ($bodyElement->attributes->length > 0) {
|
||||
$copyAttributes($bodyElement, $currentDomBodyElement);
|
||||
}
|
||||
} else { // clear the insert target when there is no body element
|
||||
$prepareInsertTargetsList();
|
||||
if (isset($insertTargetsList[$target])) {
|
||||
$targetElement = $insertTargetsList[$target];
|
||||
$targetElement->parentNode->removeChild($targetElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the modifications specified to the DOM document.
|
||||
*
|
||||
* @param int $modifications The modifications to apply. Available values:
|
||||
* - HTML5DOMDocument::FIX_MULTIPLE_TITLES - removes all but the last title elements.
|
||||
* - HTML5DOMDocument::FIX_DUPLICATE_METATAGS - removes all but the last metatags with matching name or property attributes.
|
||||
* - HTML5DOMDocument::FIX_MULTIPLE_HEADS - merges multiple head elements.
|
||||
* - HTML5DOMDocument::FIX_MULTIPLE_BODIES - merges multiple body elements.
|
||||
* - HTML5DOMDocument::OPTIMIZE_HEAD - moves charset metatag and title elements first.
|
||||
*/
|
||||
public function modify($modifications = 0)
|
||||
{
|
||||
|
||||
$fixMultipleTitles = ($modifications & self::FIX_MULTIPLE_TITLES) !== 0;
|
||||
$fixDuplicateMetatags = ($modifications & self::FIX_DUPLICATE_METATAGS) !== 0;
|
||||
$fixMultipleHeads = ($modifications & self::FIX_MULTIPLE_HEADS) !== 0;
|
||||
$fixMultipleBodies = ($modifications & self::FIX_MULTIPLE_BODIES) !== 0;
|
||||
$optimizeHead = ($modifications & self::OPTIMIZE_HEAD) !== 0;
|
||||
|
||||
/** @var \DOMNodeList<HTML5DOMElement> */
|
||||
$headElements = $this->getElementsByTagName('head');
|
||||
|
||||
if ($fixMultipleHeads) { // Merges multiple head elements.
|
||||
if ($headElements->length > 1) {
|
||||
$firstHeadElement = $headElements->item(0);
|
||||
while ($headElements->length > 1) {
|
||||
$nextHeadElement = $headElements->item(1);
|
||||
$nextHeadElementChildren = $nextHeadElement->childNodes;
|
||||
$nextHeadElementChildrenCount = $nextHeadElementChildren->length;
|
||||
for ($i = 0; $i < $nextHeadElementChildrenCount; $i++) {
|
||||
$firstHeadElement->appendChild($nextHeadElementChildren->item(0));
|
||||
}
|
||||
$nextHeadElement->parentNode->removeChild($nextHeadElement);
|
||||
}
|
||||
$headElements = [$firstHeadElement];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($headElements as $headElement) {
|
||||
|
||||
if ($fixMultipleTitles) { // Remove all title elements except the last one.
|
||||
$titleTags = $headElement->getElementsByTagName('title');
|
||||
$titleTagsCount = $titleTags->length;
|
||||
for ($i = 0; $i < $titleTagsCount - 1; $i++) {
|
||||
$node = $titleTags->item($i);
|
||||
$node->parentNode->removeChild($node);
|
||||
}
|
||||
}
|
||||
|
||||
if ($fixDuplicateMetatags) { // Remove all meta tags that has matching name or property attributes.
|
||||
$metaTags = $headElement->getElementsByTagName('meta');
|
||||
if ($metaTags->length > 0) {
|
||||
$list = [];
|
||||
$idsList = [];
|
||||
foreach ($metaTags as $metaTag) {
|
||||
$id = $metaTag->getAttribute('name');
|
||||
if ($id !== '') {
|
||||
$id = 'name:' . $id;
|
||||
} else {
|
||||
$id = $metaTag->getAttribute('property');
|
||||
if ($id !== '') {
|
||||
$id = 'property:' . $id;
|
||||
} else {
|
||||
$id = $metaTag->getAttribute('charset');
|
||||
if ($id !== '') {
|
||||
$id = 'charset';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isset($idsList[$id])) {
|
||||
$idsList[$id] = 0;
|
||||
}
|
||||
$idsList[$id]++;
|
||||
$list[] = [$metaTag, $id];
|
||||
}
|
||||
foreach ($idsList as $id => $count) {
|
||||
if ($count > 1 && $id !== '') {
|
||||
foreach ($list as $i => $item) {
|
||||
if ($item[1] === $id) {
|
||||
$node = $item[0];
|
||||
$node->parentNode->removeChild($node);
|
||||
unset($list[$i]);
|
||||
$count--;
|
||||
}
|
||||
if ($count === 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($optimizeHead) { // Moves charset metatag and title elements first.
|
||||
$titleElement = $headElement->getElementsByTagName('title')->item(0);
|
||||
$hasTitleElement = false;
|
||||
if ($titleElement !== null && $titleElement->previousSibling !== null) {
|
||||
$headElement->insertBefore($titleElement, $headElement->firstChild);
|
||||
$hasTitleElement = true;
|
||||
}
|
||||
$metaTags = $headElement->getElementsByTagName('meta');
|
||||
$metaTagsLength = $metaTags->length;
|
||||
if ($metaTagsLength > 0) {
|
||||
$charsetMetaTag = null;
|
||||
$nodesToMove = [];
|
||||
for ($i = $metaTagsLength - 1; $i >= 0; $i--) {
|
||||
$nodesToMove[$i] = $metaTags->item($i);
|
||||
}
|
||||
for ($i = $metaTagsLength - 1; $i >= 0; $i--) {
|
||||
$nodeToMove = $nodesToMove[$i];
|
||||
if ($charsetMetaTag === null && $nodeToMove->getAttribute('charset') !== '') {
|
||||
$charsetMetaTag = $nodeToMove;
|
||||
}
|
||||
$referenceNode = $headElement->childNodes->item($hasTitleElement ? 1 : 0);
|
||||
if ($nodeToMove !== $referenceNode) {
|
||||
$headElement->insertBefore($nodeToMove, $referenceNode);
|
||||
}
|
||||
}
|
||||
if ($charsetMetaTag !== null && $charsetMetaTag->previousSibling !== null) {
|
||||
$headElement->insertBefore($charsetMetaTag, $headElement->firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($fixMultipleBodies) { // Merges multiple body elements.
|
||||
$bodyElements = $this->getElementsByTagName('body');
|
||||
if ($bodyElements->length > 1) {
|
||||
$firstBodyElement = $bodyElements->item(0);
|
||||
while ($bodyElements->length > 1) {
|
||||
$nextBodyElement = $bodyElements->item(1);
|
||||
$nextBodyElementChildren = $nextBodyElement->childNodes;
|
||||
$nextBodyElementChildrenCount = $nextBodyElementChildren->length;
|
||||
for ($i = 0; $i < $nextBodyElementChildrenCount; $i++) {
|
||||
$firstBodyElement->appendChild($nextBodyElementChildren->item(0));
|
||||
}
|
||||
$nextBodyElement->parentNode->removeChild($nextBodyElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
514
web/app/vendor/ivopetkov/html5-dom-document-php/src/HTML5DOMDocument/Internal/QuerySelectors.php
vendored
Normal file
514
web/app/vendor/ivopetkov/html5-dom-document-php/src/HTML5DOMDocument/Internal/QuerySelectors.php
vendored
Normal file
@ -0,0 +1,514 @@
|
||||
<?php
|
||||
|
||||
namespace IvoPetkov\HTML5DOMDocument\Internal;
|
||||
|
||||
use IvoPetkov\HTML5DOMElement;
|
||||
|
||||
trait QuerySelectors
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns the first element matching the selector.
|
||||
*
|
||||
* @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname[attribute-selector] and [attribute-selector].
|
||||
* @return HTML5DOMElement|null The result DOMElement or null if not found
|
||||
*/
|
||||
private function internalQuerySelector(string $selector)
|
||||
{
|
||||
$result = $this->internalQuerySelectorAll($selector, 1);
|
||||
return $result->item(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of document elements matching the selector.
|
||||
*
|
||||
* @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname[attribute-selector] and [attribute-selector].
|
||||
* @param int|null $preferredLimit Preferred maximum number of elements to return.
|
||||
* @return DOMNodeList Returns a list of DOMElements matching the criteria.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
private function internalQuerySelectorAll(string $selector, $preferredLimit = null)
|
||||
{
|
||||
$selector = trim($selector);
|
||||
|
||||
$cache = [];
|
||||
$walkChildren = function (\DOMNode $context, $tagNames, callable $callback) use (&$cache) {
|
||||
if (!empty($tagNames)) {
|
||||
$children = [];
|
||||
foreach ($tagNames as $tagName) {
|
||||
$elements = $context->getElementsByTagName($tagName);
|
||||
foreach ($elements as $element) {
|
||||
$children[] = $element;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$getChildren = function () use ($context) {
|
||||
$result = [];
|
||||
$process = function (\DOMNode $node) use (&$process, &$result) {
|
||||
foreach ($node->childNodes as $child) {
|
||||
if ($child instanceof \DOMElement) {
|
||||
$result[] = $child;
|
||||
$process($child);
|
||||
}
|
||||
}
|
||||
};
|
||||
$process($context);
|
||||
return $result;
|
||||
};
|
||||
if ($this === $context) {
|
||||
$cacheKey = 'walk_children';
|
||||
if (!isset($cache[$cacheKey])) {
|
||||
$cache[$cacheKey] = $getChildren();
|
||||
}
|
||||
$children = $cache[$cacheKey];
|
||||
} else {
|
||||
$children = $getChildren();
|
||||
}
|
||||
}
|
||||
foreach ($children as $child) {
|
||||
if ($callback($child) === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$getElementById = function (\DOMNode $context, $id, $tagName) use (&$walkChildren) {
|
||||
if ($context instanceof \DOMDocument) {
|
||||
$element = $context->getElementById($id);
|
||||
if ($element && ($tagName === null || $element->tagName === $tagName)) {
|
||||
return $element;
|
||||
}
|
||||
} else {
|
||||
$foundElement = null;
|
||||
$walkChildren($context, $tagName !== null ? [$tagName] : null, function ($element) use ($id, &$foundElement) {
|
||||
if ($element->attributes->length > 0 && $element->getAttribute('id') === $id) {
|
||||
$foundElement = $element;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return $foundElement;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
$simpleSelectors = [];
|
||||
|
||||
// all
|
||||
$simpleSelectors['\*'] = function (string $mode, array $matches, \DOMNode $context, callable $add = null) use ($walkChildren) {
|
||||
if ($mode === 'validate') {
|
||||
return true;
|
||||
} else {
|
||||
$walkChildren($context, [], function ($element) use ($add) {
|
||||
if ($add($element)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// tagname
|
||||
$simpleSelectors['[a-zA-Z0-9\-]+'] = function (string $mode, array $matches, \DOMNode $context, callable $add = null) use ($walkChildren) {
|
||||
$tagNames = [];
|
||||
foreach ($matches as $match) {
|
||||
$tagNames[] = strtolower($match[0]);
|
||||
}
|
||||
if ($mode === 'validate') {
|
||||
return array_search($context->tagName, $tagNames) !== false;
|
||||
}
|
||||
$walkChildren($context, $tagNames, function ($element) use ($add) {
|
||||
if ($add($element)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// tagname[target] or [target] // Available values for targets: attr, attr="value", attr~="value", attr|="value", attr^="value", attr$="value", attr*="value"
|
||||
$simpleSelectors['(?:[a-zA-Z0-9\-]*)(?:\[.+?\])'] = function (string $mode, array $matches, \DOMNode $context, callable $add = null) use ($walkChildren) {
|
||||
$run = function ($match) use ($mode, $context, $add, $walkChildren) {
|
||||
$attributeSelectors = explode('][', substr($match[2], 1, -1));
|
||||
foreach ($attributeSelectors as $i => $attributeSelector) {
|
||||
$attributeSelectorMatches = null;
|
||||
if (preg_match('/^(.+?)(=|~=|\|=|\^=|\$=|\*=)\"(.+?)\"$/', $attributeSelector, $attributeSelectorMatches) === 1) {
|
||||
$attributeSelectors[$i] = [
|
||||
'name' => strtolower($attributeSelectorMatches[1]),
|
||||
'value' => $attributeSelectorMatches[3],
|
||||
'operator' => $attributeSelectorMatches[2]
|
||||
];
|
||||
} else {
|
||||
$attributeSelectors[$i] = [
|
||||
'name' => $attributeSelector
|
||||
];
|
||||
}
|
||||
}
|
||||
$tagName = strlen($match[1]) > 0 ? strtolower($match[1]) : null;
|
||||
$check = function ($element) use ($attributeSelectors) {
|
||||
if ($element->attributes->length > 0) {
|
||||
foreach ($attributeSelectors as $attributeSelector) {
|
||||
$isMatch = false;
|
||||
$attributeValue = $element->getAttribute($attributeSelector['name']);
|
||||
if (isset($attributeSelector['value'])) {
|
||||
$valueToMatch = $attributeSelector['value'];
|
||||
switch ($attributeSelector['operator']) {
|
||||
case '=':
|
||||
if ($attributeValue === $valueToMatch) {
|
||||
$isMatch = true;
|
||||
}
|
||||
break;
|
||||
case '~=':
|
||||
$words = preg_split("/[\s]+/", $attributeValue);
|
||||
if (array_search($valueToMatch, $words) !== false) {
|
||||
$isMatch = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case '|=':
|
||||
if ($attributeValue === $valueToMatch || strpos($attributeValue, $valueToMatch . '-') === 0) {
|
||||
$isMatch = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case '^=':
|
||||
if (strpos($attributeValue, $valueToMatch) === 0) {
|
||||
$isMatch = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case '$=':
|
||||
if (substr($attributeValue, -strlen($valueToMatch)) === $valueToMatch) {
|
||||
$isMatch = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case '*=':
|
||||
if (strpos($attributeValue, $valueToMatch) !== false) {
|
||||
$isMatch = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if ($attributeValue !== '') {
|
||||
$isMatch = true;
|
||||
}
|
||||
}
|
||||
if (!$isMatch) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if ($mode === 'validate') {
|
||||
return ($tagName === null ? true : $context->tagName === $tagName) && $check($context);
|
||||
} else {
|
||||
$walkChildren($context, $tagName !== null ? [$tagName] : null, function ($element) use ($check, $add) {
|
||||
if ($check($element)) {
|
||||
if ($add($element)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
// todo optimize
|
||||
foreach ($matches as $match) {
|
||||
if ($mode === 'validate') {
|
||||
if ($run($match)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
$run($match);
|
||||
}
|
||||
}
|
||||
if ($mode === 'validate') {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// tagname#id or #id
|
||||
$simpleSelectors['(?:[a-zA-Z0-9\-]*)#(?:[a-zA-Z0-9\-\_]+?)'] = function (string $mode, array $matches, \DOMNode $context, callable $add = null) use ($getElementById) {
|
||||
$run = function ($match) use ($mode, $context, $add, $getElementById) {
|
||||
$tagName = strlen($match[1]) > 0 ? strtolower($match[1]) : null;
|
||||
$id = $match[2];
|
||||
if ($mode === 'validate') {
|
||||
return ($tagName === null ? true : $context->tagName === $tagName) && $context->getAttribute('id') === $id;
|
||||
} else {
|
||||
$element = $getElementById($context, $id, $tagName);
|
||||
if ($element) {
|
||||
$add($element);
|
||||
}
|
||||
}
|
||||
};
|
||||
// todo optimize
|
||||
foreach ($matches as $match) {
|
||||
if ($mode === 'validate') {
|
||||
if ($run($match)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
$run($match);
|
||||
}
|
||||
}
|
||||
if ($mode === 'validate') {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// tagname.classname, .classname, tagname.classname.classname2, .classname.classname2
|
||||
$simpleSelectors['(?:[a-zA-Z0-9\-]*)\.(?:[a-zA-Z0-9\-\_\.]+?)'] = function (string $mode, array $matches, \DOMNode $context, callable $add = null) use ($walkChildren) {
|
||||
$rawData = []; // Array containing [tag, classnames]
|
||||
$tagNames = [];
|
||||
foreach ($matches as $match) {
|
||||
$tagName = strlen($match[1]) > 0 ? $match[1] : null;
|
||||
$classes = explode('.', $match[2]);
|
||||
if (empty($classes)) {
|
||||
continue;
|
||||
}
|
||||
$rawData[] = [$tagName, $classes];
|
||||
if ($tagName !== null) {
|
||||
$tagNames[] = $tagName;
|
||||
}
|
||||
}
|
||||
$check = function ($element) use ($rawData) {
|
||||
if ($element->attributes->length > 0) {
|
||||
$classAttribute = ' ' . $element->getAttribute('class') . ' ';
|
||||
$tagName = $element->tagName;
|
||||
foreach ($rawData as $rawMatch) {
|
||||
if ($rawMatch[0] !== null && $tagName !== $rawMatch[0]) {
|
||||
continue;
|
||||
}
|
||||
$allClassesFound = true;
|
||||
foreach ($rawMatch[1] as $class) {
|
||||
if (strpos($classAttribute, ' ' . $class . ' ') === false) {
|
||||
$allClassesFound = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($allClassesFound) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if ($mode === 'validate') {
|
||||
return $check($context);
|
||||
}
|
||||
$walkChildren($context, $tagNames, function ($element) use ($check, $add) {
|
||||
if ($check($element)) {
|
||||
if ($add($element)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$isMatchingElement = function (\DOMNode $context, string $selector) use ($simpleSelectors) {
|
||||
foreach ($simpleSelectors as $simpleSelector => $callback) {
|
||||
$match = null;
|
||||
if (preg_match('/^' . (str_replace('?:', '', $simpleSelector)) . '$/', $selector, $match) === 1) {
|
||||
return call_user_func($callback, 'validate', [$match], $context);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$complexSelectors = [];
|
||||
|
||||
$getMatchingElements = function (\DOMNode $context, string $selector, $preferredLimit = null) use (&$simpleSelectors, &$complexSelectors) {
|
||||
|
||||
$processSelector = function (string $mode, string $selector, $operator = null) use (&$processSelector, $simpleSelectors, $complexSelectors, $context, $preferredLimit) {
|
||||
$supportedSimpleSelectors = array_keys($simpleSelectors);
|
||||
$supportedSimpleSelectorsExpression = '(?:(?:' . implode(')|(?:', $supportedSimpleSelectors) . '))';
|
||||
$supportedSelectors = $supportedSimpleSelectors;
|
||||
$supportedComplexOperators = array_keys($complexSelectors);
|
||||
if ($operator === null) {
|
||||
$operator = ',';
|
||||
foreach ($supportedComplexOperators as $complexOperator) {
|
||||
array_unshift($supportedSelectors, '(?:(?:(?:' . $supportedSimpleSelectorsExpression . '\s*\\' . $complexOperator . '\s*))+' . $supportedSimpleSelectorsExpression . ')');
|
||||
}
|
||||
}
|
||||
$supportedSelectorsExpression = '(?:(?:' . implode(')|(?:', $supportedSelectors) . '))';
|
||||
|
||||
$vallidationExpression = '/^(?:(?:' . $supportedSelectorsExpression . '\s*\\' . $operator . '\s*))*' . $supportedSelectorsExpression . '$/';
|
||||
if (preg_match($vallidationExpression, $selector) !== 1) {
|
||||
return false;
|
||||
}
|
||||
$selector .= $operator; // append the seprator at the back for easier matching below
|
||||
|
||||
$result = [];
|
||||
if ($mode === 'execute') {
|
||||
$add = function ($element) use ($preferredLimit, &$result) {
|
||||
$found = false;
|
||||
foreach ($result as $addedElement) {
|
||||
if ($addedElement === $element) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
$result[] = $element;
|
||||
if ($preferredLimit !== null && sizeof($result) >= $preferredLimit) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
$selectorsToCall = [];
|
||||
$addSelectorToCall = function ($type, $selector, $argument) use (&$selectorsToCall) {
|
||||
$previousIndex = sizeof($selectorsToCall) - 1;
|
||||
// todo optimize complex too
|
||||
if ($type === 1 && isset($selectorsToCall[$previousIndex]) && $selectorsToCall[$previousIndex][0] === $type && $selectorsToCall[$previousIndex][1] === $selector) {
|
||||
$selectorsToCall[$previousIndex][2][] = $argument;
|
||||
} else {
|
||||
$selectorsToCall[] = [$type, $selector, [$argument]];
|
||||
}
|
||||
};
|
||||
for ($i = 0; $i < 100000; $i++) {
|
||||
$matches = null;
|
||||
preg_match('/^(?<subselector>' . $supportedSelectorsExpression . ')\s*\\' . $operator . '\s*/', $selector, $matches); // getting the next subselector
|
||||
if (isset($matches['subselector'])) {
|
||||
$subSelector = $matches['subselector'];
|
||||
$selectorFound = false;
|
||||
foreach ($simpleSelectors as $simpleSelector => $callback) {
|
||||
$match = null;
|
||||
if (preg_match('/^' . (str_replace('?:', '', $simpleSelector)) . '$/', $subSelector, $match) === 1) { // if simple selector
|
||||
if ($mode === 'parse') {
|
||||
$result[] = $match[0];
|
||||
} else {
|
||||
$addSelectorToCall(1, $simpleSelector, $match);
|
||||
//call_user_func($callback, 'execute', $match, $context, $add);
|
||||
}
|
||||
$selectorFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$selectorFound) {
|
||||
foreach ($complexSelectors as $complexOperator => $callback) {
|
||||
$subSelectorParts = $processSelector('parse', $subSelector, $complexOperator);
|
||||
if ($subSelectorParts !== false) {
|
||||
$addSelectorToCall(2, $complexOperator, $subSelectorParts);
|
||||
//call_user_func($callback, $subSelectorParts, $context, $add);
|
||||
$selectorFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$selectorFound) {
|
||||
throw new \Exception('Internal error for selector "' . $selector . '"!');
|
||||
}
|
||||
$selector = substr($selector, strlen($matches[0])); // remove the matched subselector and continue parsing
|
||||
if (strlen($selector) === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($selectorsToCall as $selectorToCall) {
|
||||
if ($selectorToCall[0] === 1) { // is simple selector
|
||||
call_user_func($simpleSelectors[$selectorToCall[1]], 'execute', $selectorToCall[2], $context, $add);
|
||||
} else { // is complex selector
|
||||
call_user_func($complexSelectors[$selectorToCall[1]], $selectorToCall[2][0], $context, $add); // todo optimize and send all arguments
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
};
|
||||
|
||||
return $processSelector('execute', $selector);
|
||||
};
|
||||
|
||||
// div p (space between) - all <p> elements inside <div> elements
|
||||
$complexSelectors[' '] = function (array $parts, \DOMNode $context, callable $add = null) use (&$getMatchingElements) {
|
||||
$elements = null;
|
||||
foreach ($parts as $part) {
|
||||
if ($elements === null) {
|
||||
$elements = $getMatchingElements($context, $part);
|
||||
} else {
|
||||
$temp = [];
|
||||
foreach ($elements as $element) {
|
||||
$temp = array_merge($temp, $getMatchingElements($element, $part));
|
||||
}
|
||||
$elements = $temp;
|
||||
}
|
||||
}
|
||||
foreach ($elements as $element) {
|
||||
$add($element);
|
||||
}
|
||||
};
|
||||
|
||||
// div > p - all <p> elements where the parent is a <div> element
|
||||
$complexSelectors['>'] = function (array $parts, \DOMNode $context, callable $add = null) use (&$getMatchingElements, &$isMatchingElement) {
|
||||
$elements = null;
|
||||
foreach ($parts as $part) {
|
||||
if ($elements === null) {
|
||||
$elements = $getMatchingElements($context, $part);
|
||||
} else {
|
||||
$temp = [];
|
||||
foreach ($elements as $element) {
|
||||
foreach ($element->childNodes as $child) {
|
||||
if ($child instanceof \DOMElement && $isMatchingElement($child, $part)) {
|
||||
$temp[] = $child;
|
||||
}
|
||||
}
|
||||
}
|
||||
$elements = $temp;
|
||||
}
|
||||
}
|
||||
foreach ($elements as $element) {
|
||||
$add($element);
|
||||
}
|
||||
};
|
||||
|
||||
// div + p - all <p> elements that are placed immediately after <div> elements
|
||||
$complexSelectors['+'] = function (array $parts, \DOMNode $context, callable $add = null) use (&$getMatchingElements, &$isMatchingElement) {
|
||||
$elements = null;
|
||||
foreach ($parts as $part) {
|
||||
if ($elements === null) {
|
||||
$elements = $getMatchingElements($context, $part);
|
||||
} else {
|
||||
$temp = [];
|
||||
foreach ($elements as $element) {
|
||||
if ($element->nextSibling !== null && $isMatchingElement($element->nextSibling, $part)) {
|
||||
$temp[] = $element->nextSibling;
|
||||
}
|
||||
}
|
||||
$elements = $temp;
|
||||
}
|
||||
}
|
||||
foreach ($elements as $element) {
|
||||
$add($element);
|
||||
}
|
||||
};
|
||||
|
||||
// p ~ ul - all <ul> elements that are preceded by a <p> element
|
||||
$complexSelectors['~'] = function (array $parts, \DOMNode $context, callable $add = null) use (&$getMatchingElements, &$isMatchingElement) {
|
||||
$elements = null;
|
||||
foreach ($parts as $part) {
|
||||
if ($elements === null) {
|
||||
$elements = $getMatchingElements($context, $part);
|
||||
} else {
|
||||
$temp = [];
|
||||
foreach ($elements as $element) {
|
||||
$nextSibling = $element->nextSibling;
|
||||
while ($nextSibling !== null) {
|
||||
if ($isMatchingElement($nextSibling, $part)) {
|
||||
$temp[] = $nextSibling;
|
||||
}
|
||||
$nextSibling = $nextSibling->nextSibling;
|
||||
}
|
||||
}
|
||||
$elements = $temp;
|
||||
}
|
||||
}
|
||||
foreach ($elements as $element) {
|
||||
$add($element);
|
||||
}
|
||||
};
|
||||
|
||||
$result = $getMatchingElements($this, $selector, $preferredLimit);
|
||||
if ($result === false) {
|
||||
throw new \InvalidArgumentException('Unsupported selector (' . $selector . ')');
|
||||
}
|
||||
return new \IvoPetkov\HTML5DOMNodeList($result);
|
||||
}
|
||||
}
|
240
web/app/vendor/ivopetkov/html5-dom-document-php/src/HTML5DOMElement.php
vendored
Normal file
240
web/app/vendor/ivopetkov/html5-dom-document-php/src/HTML5DOMElement.php
vendored
Normal file
@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* HTML5 DOMDocument PHP library (extends DOMDocument)
|
||||
* https://github.com/ivopetkov/html5-dom-document-php
|
||||
* Copyright (c) Ivo Petkov
|
||||
* Free to use under the MIT license.
|
||||
*/
|
||||
|
||||
namespace IvoPetkov;
|
||||
|
||||
use IvoPetkov\HTML5DOMDocument\Internal\QuerySelectors;
|
||||
|
||||
/**
|
||||
* Represents a live (can be manipulated) representation of an element in a HTML5 document.
|
||||
*
|
||||
* @property string $innerHTML The HTML code inside the element.
|
||||
* @property string $outerHTML The HTML code for the element including the code inside.
|
||||
* @property \IvoPetkov\HTML5DOMTokenList $classList A collection of the class attributes of the element.
|
||||
*/
|
||||
class HTML5DOMElement extends \DOMElement
|
||||
{
|
||||
|
||||
use QuerySelectors;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
static private $foundEntitiesCache = [[], []];
|
||||
|
||||
/**
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
static private $newObjectsCache = [];
|
||||
|
||||
/*
|
||||
*
|
||||
* @var HTML5DOMTokenList
|
||||
*/
|
||||
private $classList = null;
|
||||
|
||||
/**
|
||||
* Returns the value for the property specified.
|
||||
*
|
||||
* @param string $name
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __get(string $name)
|
||||
{
|
||||
if ($name === 'innerHTML') {
|
||||
if ($this->firstChild === null) {
|
||||
return '';
|
||||
}
|
||||
$html = $this->ownerDocument->saveHTML($this);
|
||||
$nodeName = $this->nodeName;
|
||||
return preg_replace('@^<' . $nodeName . '[^>]*>|</' . $nodeName . '>$@', '', $html);
|
||||
} elseif ($name === 'outerHTML') {
|
||||
if ($this->firstChild === null) {
|
||||
$nodeName = $this->nodeName;
|
||||
$attributes = $this->getAttributes();
|
||||
$result = '<' . $nodeName . '';
|
||||
foreach ($attributes as $name => $value) {
|
||||
$result .= ' ' . $name . '="' . htmlentities($value) . '"';
|
||||
}
|
||||
if (array_search($nodeName, ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']) === false) {
|
||||
$result .= '></' . $nodeName . '>';
|
||||
} else {
|
||||
$result .= '/>';
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
return $this->ownerDocument->saveHTML($this);
|
||||
} elseif ($name === 'classList') {
|
||||
if ($this->classList === null) {
|
||||
$this->classList = new HTML5DOMTokenList($this, 'class');
|
||||
}
|
||||
return $this->classList;
|
||||
}
|
||||
throw new \Exception('Undefined property: HTML5DOMElement::$' . $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value for the property specified.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __set(string $name, $value)
|
||||
{
|
||||
if ($name === 'innerHTML') {
|
||||
while ($this->hasChildNodes()) {
|
||||
$this->removeChild($this->firstChild);
|
||||
}
|
||||
if (!isset(self::$newObjectsCache['html5domdocument'])) {
|
||||
self::$newObjectsCache['html5domdocument'] = new \IvoPetkov\HTML5DOMDocument();
|
||||
}
|
||||
$tmpDoc = clone (self::$newObjectsCache['html5domdocument']);
|
||||
$tmpDoc->loadHTML('<body>' . $value . '</body>', HTML5DOMDocument::ALLOW_DUPLICATE_IDS);
|
||||
foreach ($tmpDoc->getElementsByTagName('body')->item(0)->childNodes as $node) {
|
||||
$node = $this->ownerDocument->importNode($node, true);
|
||||
$this->appendChild($node);
|
||||
}
|
||||
return;
|
||||
} elseif ($name === 'outerHTML') {
|
||||
if (!isset(self::$newObjectsCache['html5domdocument'])) {
|
||||
self::$newObjectsCache['html5domdocument'] = new \IvoPetkov\HTML5DOMDocument();
|
||||
}
|
||||
$tmpDoc = clone (self::$newObjectsCache['html5domdocument']);
|
||||
$tmpDoc->loadHTML('<body>' . $value . '</body>', HTML5DOMDocument::ALLOW_DUPLICATE_IDS);
|
||||
foreach ($tmpDoc->getElementsByTagName('body')->item(0)->childNodes as $node) {
|
||||
$node = $this->ownerDocument->importNode($node, true);
|
||||
$this->parentNode->insertBefore($node, $this);
|
||||
}
|
||||
$this->parentNode->removeChild($this);
|
||||
return;
|
||||
} elseif ($name === 'classList') {
|
||||
$this->setAttribute('class', $value);
|
||||
return;
|
||||
}
|
||||
throw new \Exception('Undefined property: HTML5DOMElement::$' . $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the result value before returning it.
|
||||
*
|
||||
* @param string $value
|
||||
* @return string The updated value
|
||||
*/
|
||||
private function updateResult(string $value): string
|
||||
{
|
||||
$value = str_replace(self::$foundEntitiesCache[0], self::$foundEntitiesCache[1], $value);
|
||||
if (strstr($value, 'html5-dom-document-internal-entity') !== false) {
|
||||
$search = [];
|
||||
$replace = [];
|
||||
$matches = [];
|
||||
preg_match_all('/html5-dom-document-internal-entity([12])-(.*?)-end/', $value, $matches);
|
||||
$matches[0] = array_unique($matches[0]);
|
||||
foreach ($matches[0] as $i => $match) {
|
||||
$search[] = $match;
|
||||
$replace[] = html_entity_decode(($matches[1][$i] === '1' ? '&' : '&#') . $matches[2][$i] . ';');
|
||||
}
|
||||
$value = str_replace($search, $replace, $value);
|
||||
self::$foundEntitiesCache[0] = array_merge(self::$foundEntitiesCache[0], $search);
|
||||
self::$foundEntitiesCache[1] = array_merge(self::$foundEntitiesCache[1], $replace);
|
||||
unset($search);
|
||||
unset($replace);
|
||||
unset($matches);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the updated nodeValue Property
|
||||
*
|
||||
* @return string The updated $nodeValue
|
||||
*/
|
||||
public function getNodeValue(): string
|
||||
{
|
||||
return $this->updateResult($this->nodeValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the updated $textContent Property
|
||||
*
|
||||
* @return string The updated $textContent
|
||||
*/
|
||||
public function getTextContent(): string
|
||||
{
|
||||
return $this->updateResult($this->textContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the attribute name specified.
|
||||
*
|
||||
* @param string $name The attribute name.
|
||||
* @return string The attribute value.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getAttribute($name): string
|
||||
{
|
||||
if ($this->attributes->length === 0) { // Performance optimization
|
||||
return '';
|
||||
}
|
||||
$value = parent::getAttribute($name);
|
||||
return $value !== '' ? (strstr($value, 'html5-dom-document-internal-entity') !== false ? $this->updateResult($value) : $value) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing all attributes.
|
||||
*
|
||||
* @return array An associative array containing all attributes.
|
||||
*/
|
||||
public function getAttributes(): array
|
||||
{
|
||||
$attributes = [];
|
||||
foreach ($this->attributes as $attributeName => $attribute) {
|
||||
$value = $attribute->value;
|
||||
$attributes[$attributeName] = $value !== '' ? (strstr($value, 'html5-dom-document-internal-entity') !== false ? $this->updateResult($value) : $value) : '';
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element outerHTML.
|
||||
*
|
||||
* @return string The element outerHTML.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->outerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first child element matching the selector.
|
||||
*
|
||||
* @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname.classname.classname2, .classname.classname2, tagname[attribute-selector], [attribute-selector], "div, p", div p, div > p, div + p and p ~ ul.
|
||||
* @return HTML5DOMElement|null The result DOMElement or null if not found.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function querySelector(string $selector)
|
||||
{
|
||||
return $this->internalQuerySelector($selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of children elements matching the selector.
|
||||
*
|
||||
* @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname.classname.classname2, .classname.classname2, tagname[attribute-selector], [attribute-selector], "div, p", div p, div > p, div + p and p ~ ul.
|
||||
* @return HTML5DOMNodeList Returns a list of DOMElements matching the criteria.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function querySelectorAll(string $selector)
|
||||
{
|
||||
return $this->internalQuerySelectorAll($selector);
|
||||
}
|
||||
}
|
45
web/app/vendor/ivopetkov/html5-dom-document-php/src/HTML5DOMNodeList.php
vendored
Normal file
45
web/app/vendor/ivopetkov/html5-dom-document-php/src/HTML5DOMNodeList.php
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* HTML5 DOMDocument PHP library (extends DOMDocument)
|
||||
* https://github.com/ivopetkov/html5-dom-document-php
|
||||
* Copyright (c) Ivo Petkov
|
||||
* Free to use under the MIT license.
|
||||
*/
|
||||
|
||||
namespace IvoPetkov;
|
||||
|
||||
/**
|
||||
* Represents a list of DOM nodes.
|
||||
*
|
||||
* @property-read int $length The list items count
|
||||
*/
|
||||
class HTML5DOMNodeList extends \ArrayObject
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns the item at the specified index.
|
||||
*
|
||||
* @param int $index The item index.
|
||||
* @return \IvoPetkov\HTML5DOMElement|null The item at the specified index or null if not existent.
|
||||
*/
|
||||
public function item(int $index)
|
||||
{
|
||||
return $this->offsetExists($index) ? $this->offsetGet($index) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the property specified.
|
||||
*
|
||||
* @param string $name The name of the property.
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __get(string $name)
|
||||
{
|
||||
if ($name === 'length') {
|
||||
return sizeof($this);
|
||||
}
|
||||
throw new \Exception('Undefined property: \IvoPetkov\HTML5DOMNodeList::$' . $name);
|
||||
}
|
||||
}
|
266
web/app/vendor/ivopetkov/html5-dom-document-php/src/HTML5DOMTokenList.php
vendored
Normal file
266
web/app/vendor/ivopetkov/html5-dom-document-php/src/HTML5DOMTokenList.php
vendored
Normal file
@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* HTML5 DOMDocument PHP library (extends DOMDocument)
|
||||
* https://github.com/ivopetkov/html5-dom-document-php
|
||||
* Copyright (c) Ivo Petkov
|
||||
* Free to use under the MIT license.
|
||||
*/
|
||||
|
||||
namespace IvoPetkov;
|
||||
|
||||
use ArrayIterator;
|
||||
use DOMElement;
|
||||
|
||||
/**
|
||||
* Represents a set of space-separated tokens of an element attribute.
|
||||
*
|
||||
* @property-read int $length The number of tokens.
|
||||
* @property-read string $value A space-separated list of the tokens.
|
||||
*/
|
||||
class HTML5DOMTokenList
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $attributeName;
|
||||
|
||||
/**
|
||||
* @var DOMElement
|
||||
*/
|
||||
private $element;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $tokens;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $previousValue;
|
||||
|
||||
/**
|
||||
* Creates a list of space-separated tokens based on the attribute value of an element.
|
||||
*
|
||||
* @param DOMElement $element The DOM element.
|
||||
* @param string $attributeName The name of the attribute.
|
||||
*/
|
||||
public function __construct(DOMElement $element, string $attributeName)
|
||||
{
|
||||
$this->element = $element;
|
||||
$this->attributeName = $attributeName;
|
||||
$this->previousValue = null;
|
||||
$this->tokenize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given tokens to the list.
|
||||
*
|
||||
* @param string[] $tokens The tokens you want to add to the list.
|
||||
* @return void
|
||||
*/
|
||||
public function add(string ...$tokens)
|
||||
{
|
||||
if (count($tokens) === 0) {
|
||||
return;
|
||||
}
|
||||
foreach ($tokens as $t) {
|
||||
if (in_array($t, $this->tokens)) {
|
||||
continue;
|
||||
}
|
||||
$this->tokens[] = $t;
|
||||
}
|
||||
$this->setAttributeValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified tokens from the list. If the string does not exist in the list, no error is thrown.
|
||||
*
|
||||
* @param string[] $tokens The token you want to remove from the list.
|
||||
* @return void
|
||||
*/
|
||||
public function remove(string ...$tokens)
|
||||
{
|
||||
if (count($tokens) === 0) {
|
||||
return;
|
||||
}
|
||||
if (count($this->tokens) === 0) {
|
||||
return;
|
||||
}
|
||||
foreach ($tokens as $t) {
|
||||
$i = array_search($t, $this->tokens);
|
||||
if ($i === false) {
|
||||
continue;
|
||||
}
|
||||
array_splice($this->tokens, $i, 1);
|
||||
}
|
||||
$this->setAttributeValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an item in the list by its index (returns null if the number is greater than or equal to the length of the list).
|
||||
*
|
||||
* @param int $index The zero-based index of the item you want to return.
|
||||
* @return null|string
|
||||
*/
|
||||
public function item(int $index)
|
||||
{
|
||||
$this->tokenize();
|
||||
if ($index >= count($this->tokens)) {
|
||||
return null;
|
||||
}
|
||||
return $this->tokens[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a given token from the list and returns false. If token doesn't exist it's added and the function returns true.
|
||||
*
|
||||
* @param string $token The token you want to toggle.
|
||||
* @param bool $force A Boolean that, if included, turns the toggle into a one way-only operation. If set to false, the token will only be removed but not added again. If set to true, the token will only be added but not removed again.
|
||||
* @return bool false if the token is not in the list after the call, or true if the token is in the list after the call.
|
||||
*/
|
||||
public function toggle(string $token, bool $force = null): bool
|
||||
{
|
||||
$this->tokenize();
|
||||
$isThereAfter = false;
|
||||
$i = array_search($token, $this->tokens);
|
||||
if (is_null($force)) {
|
||||
if ($i === false) {
|
||||
$this->tokens[] = $token;
|
||||
$isThereAfter = true;
|
||||
} else {
|
||||
array_splice($this->tokens, $i, 1);
|
||||
}
|
||||
} else {
|
||||
if ($force) {
|
||||
if ($i === false) {
|
||||
$this->tokens[] = $token;
|
||||
}
|
||||
$isThereAfter = true;
|
||||
} else {
|
||||
if ($i !== false) {
|
||||
array_splice($this->tokens, $i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->setAttributeValue();
|
||||
return $isThereAfter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the list contains the given token, otherwise false.
|
||||
*
|
||||
* @param string $token The token you want to check for the existence of in the list.
|
||||
* @return bool true if the list contains the given token, otherwise false.
|
||||
*/
|
||||
public function contains(string $token): bool
|
||||
{
|
||||
$this->tokenize();
|
||||
return in_array($token, $this->tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an existing token with a new token.
|
||||
*
|
||||
* @param string $old The token you want to replace.
|
||||
* @param string $new The token you want to replace $old with.
|
||||
* @return void
|
||||
*/
|
||||
public function replace(string $old, string $new)
|
||||
{
|
||||
if ($old === $new) {
|
||||
return;
|
||||
}
|
||||
$this->tokenize();
|
||||
$i = array_search($old, $this->tokens);
|
||||
if ($i !== false) {
|
||||
$j = array_search($new, $this->tokens);
|
||||
if ($j === false) {
|
||||
$this->tokens[$i] = $new;
|
||||
} else {
|
||||
array_splice($this->tokens, $i, 1);
|
||||
}
|
||||
$this->setAttributeValue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$this->tokenize();
|
||||
return implode(' ', $this->tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator allowing you to go through all tokens contained in the list.
|
||||
*
|
||||
* @return ArrayIterator
|
||||
*/
|
||||
public function entries(): ArrayIterator
|
||||
{
|
||||
$this->tokenize();
|
||||
return new ArrayIterator($this->tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the property specified
|
||||
*
|
||||
* @param string $name The name of the property
|
||||
* @return string The value of the property specified
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __get(string $name)
|
||||
{
|
||||
if ($name === 'length') {
|
||||
$this->tokenize();
|
||||
return count($this->tokens);
|
||||
} elseif ($name === 'value') {
|
||||
return $this->__toString();
|
||||
}
|
||||
throw new \Exception('Undefined property: HTML5DOMTokenList::$' . $name);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function tokenize()
|
||||
{
|
||||
$current = $this->element->getAttribute($this->attributeName);
|
||||
if ($this->previousValue === $current) {
|
||||
return;
|
||||
}
|
||||
$this->previousValue = $current;
|
||||
$tokens = explode(' ', $current);
|
||||
$finals = [];
|
||||
foreach ($tokens as $token) {
|
||||
if ($token === '') {
|
||||
continue;
|
||||
}
|
||||
if (in_array($token, $finals)) {
|
||||
continue;
|
||||
}
|
||||
$finals[] = $token;
|
||||
}
|
||||
$this->tokens = $finals;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function setAttributeValue()
|
||||
{
|
||||
$value = implode(' ', $this->tokens);
|
||||
if ($this->previousValue === $value) {
|
||||
return;
|
||||
}
|
||||
$this->previousValue = $value;
|
||||
$this->element->setAttribute($this->attributeName, $value);
|
||||
}
|
||||
}
|
214
web/app/vendor/php-curl-class/php-curl-class/CHANGELOG.md
vendored
Normal file
214
web/app/vendor/php-curl-class/php-curl-class/CHANGELOG.md
vendored
Normal file
@ -0,0 +1,214 @@
|
||||
# Change Log
|
||||
|
||||
PHP Curl Class uses semantic versioning with version numbers written as `MAJOR.MINOR.PATCH`. You may safely update
|
||||
`MINOR` and `PATCH` version changes. It is recommended to review `MAJOR` changes prior to upgrade as there may be
|
||||
backwards-incompatible changes that will affect existing usage.
|
||||
|
||||
<!-- CHANGELOG_PLACEHOLDER -->
|
||||
|
||||
## 9.13.1 - 2023-01-16
|
||||
|
||||
- Allow uploads with CURLStringFile type ([#762](https://github.com/php-curl-class/php-curl-class/pull/762))
|
||||
|
||||
## 9.13.0 - 2023-01-13
|
||||
|
||||
- Implement abstract class BaseCurl for Curl and MultiCurl ([#759](https://github.com/php-curl-class/php-curl-class/pull/759))
|
||||
- Display error messages found in Curl::diagnose() ([#758](https://github.com/php-curl-class/php-curl-class/pull/758))
|
||||
- Fix Curl::diagnose() request type output for POST requests ([#757](https://github.com/php-curl-class/php-curl-class/pull/757))
|
||||
|
||||
## 9.12.6 - 2023-01-11
|
||||
|
||||
- Replace use of #[\AllowDynamicProperties] ([#756](https://github.com/php-curl-class/php-curl-class/pull/756))
|
||||
- silence PHP 8.2 deprecation notices ([#754](https://github.com/php-curl-class/php-curl-class/pull/754))
|
||||
|
||||
## 9.12.5 - 2022-12-20
|
||||
|
||||
- Fix static analysis error ([#752](https://github.com/php-curl-class/php-curl-class/pull/752))
|
||||
|
||||
## 9.12.4 - 2022-12-17
|
||||
|
||||
- Exclude additional files from git archive ([#751](https://github.com/php-curl-class/php-curl-class/pull/751))
|
||||
|
||||
## 9.12.3 - 2022-12-13
|
||||
|
||||
- Ensure string response before gzip decode ([#749](https://github.com/php-curl-class/php-curl-class/pull/749))
|
||||
|
||||
## 9.12.2 - 2022-12-11
|
||||
|
||||
- Disable warning when gzip-decoding response errors ([#748](https://github.com/php-curl-class/php-curl-class/pull/748))
|
||||
|
||||
## 9.12.1 - 2022-12-08
|
||||
|
||||
- Include option constant that uses the CURLINFO_ prefix ([#745](https://github.com/php-curl-class/php-curl-class/pull/745))
|
||||
|
||||
## 9.12.0 - 2022-12-07
|
||||
|
||||
- Add automatic gzip decoding of response ([#744](https://github.com/php-curl-class/php-curl-class/pull/744))
|
||||
|
||||
## 9.11.1 - 2022-12-06
|
||||
|
||||
- change: remove unused namespace import ([#743](https://github.com/php-curl-class/php-curl-class/pull/743))
|
||||
|
||||
## 9.11.0 - 2022-12-05
|
||||
|
||||
- Add Curl::diagnose() HTTP method check matches methods allowed ([#741](https://github.com/php-curl-class/php-curl-class/pull/741))
|
||||
- Add temporary fix missing template params ([#742](https://github.com/php-curl-class/php-curl-class/pull/742))
|
||||
|
||||
## 9.10.0 - 2022-11-07
|
||||
|
||||
- Display request options in Curl::diagnose() output ([#739](https://github.com/php-curl-class/php-curl-class/pull/739))
|
||||
|
||||
## 9.9.0 - 2022-11-06
|
||||
|
||||
- Fix MultiCurl::setCookieString() ([#738](https://github.com/php-curl-class/php-curl-class/pull/738))
|
||||
- Pass MultiCurl options to new Curl instances earlier ([#737](https://github.com/php-curl-class/php-curl-class/pull/737))
|
||||
- Add deferred constant curlErrorCodeConstants ([#736](https://github.com/php-curl-class/php-curl-class/pull/736))
|
||||
|
||||
## 9.8.0 - 2022-10-01
|
||||
|
||||
- Include curl error code constant in curl error message ([#733](https://github.com/php-curl-class/php-curl-class/pull/733))
|
||||
|
||||
## 9.7.0 - 2022-09-29
|
||||
|
||||
- Implement ArrayUtil::arrayRandomIndex() ([#732](https://github.com/php-curl-class/php-curl-class/pull/732))
|
||||
|
||||
## 9.6.3 - 2022-09-24
|
||||
|
||||
- Remove filter flag constants deprecated as of PHP 7.3 ([#730](https://github.com/php-curl-class/php-curl-class/pull/730))
|
||||
|
||||
## 9.6.2 - 2022-09-24
|
||||
|
||||
- Call MultiCurl::beforeSend() before each request is made ([#723](https://github.com/php-curl-class/php-curl-class/pull/723))
|
||||
- Encode keys for post data with numeric keys ([#726](https://github.com/php-curl-class/php-curl-class/pull/726))
|
||||
- Fix building post data with object ([#728](https://github.com/php-curl-class/php-curl-class/pull/728))
|
||||
|
||||
## 9.6.1 - 2022-06-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- Attempt to stop active requests when `MultiCurl::stop()` is called
|
||||
[#714](https://github.com/php-curl-class/php-curl-class/issues/714)
|
||||
[#718](https://github.com/php-curl-class/php-curl-class/issues/718)
|
||||
- Retain keys for arrays with null values when building post data
|
||||
[#712](https://github.com/php-curl-class/php-curl-class/issues/712)
|
||||
|
||||
## 9.6.0 - 2022-03-17
|
||||
|
||||
### Added
|
||||
|
||||
- Method `MultiCurl::stop()` for stopping subsequent requests
|
||||
[#708](https://github.com/php-curl-class/php-curl-class/issues/708)
|
||||
|
||||
## 9.5.1 - 2021-12-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Silence PHP 8.1 deprecations [#691](https://github.com/php-curl-class/php-curl-class/issues/691)
|
||||
- Remove data parameter from additional request types
|
||||
[#689](https://github.com/php-curl-class/php-curl-class/issues/689)
|
||||
|
||||
## 9.5.0 - 2021-11-21
|
||||
|
||||
### Added
|
||||
|
||||
- Method `Curl::setStop()` for stopping requests early without downloading the full response body
|
||||
[#681](https://github.com/php-curl-class/php-curl-class/issues/681)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed constructing request url when using `MultiCurl::addPost()`
|
||||
[#686](https://github.com/php-curl-class/php-curl-class/issues/686)
|
||||
|
||||
## 9.4.0 - 2021-09-04
|
||||
|
||||
### Changed
|
||||
|
||||
- Method `Url::parseUrl()` is now public
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix parsing schemeless urls [#679](https://github.com/php-curl-class/php-curl-class/issues/679)
|
||||
|
||||
## 9.3.1 - 2021-08-05
|
||||
|
||||
### Changed
|
||||
|
||||
- Enabled strict types (`declare(strict_types=1);`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `Curl::downloadFileName` not being set correctly
|
||||
|
||||
## 9.3.0 - 2021-07-23
|
||||
|
||||
### Added
|
||||
|
||||
- Method `Curl::diagnose()` for troubleshooting requests
|
||||
|
||||
## 9.2.0 - 2021-06-23
|
||||
|
||||
### Added
|
||||
|
||||
- Additional Curl::set\* and MultiCurl::set\* helper methods
|
||||
|
||||
```
|
||||
Curl::setAutoReferer()
|
||||
Curl::setAutoReferrer()
|
||||
Curl::setFollowLocation()
|
||||
Curl::setForbidReuse()
|
||||
Curl::setMaximumRedirects()
|
||||
MultiCurl::setAutoReferer()
|
||||
MultiCurl::setAutoReferrer()
|
||||
MultiCurl::setFollowLocation()
|
||||
MultiCurl::setForbidReuse()
|
||||
MultiCurl::setMaximumRedirects()
|
||||
```
|
||||
|
||||
### Fixed
|
||||
|
||||
- Closing curl handles [#670](https://github.com/php-curl-class/php-curl-class/issues/670)
|
||||
- Use of "$this" in non-object context [#671](https://github.com/php-curl-class/php-curl-class/pull/671)
|
||||
|
||||
## 9.1.0 - 2021-03-24
|
||||
|
||||
### Added
|
||||
|
||||
- Support for using relative urls with MultiCurl::add\*() methods [#628](https://github.com/php-curl-class/php-curl-class/issues/628)
|
||||
|
||||
## 9.0.0 - 2021-03-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Use short array syntax
|
||||
|
||||
### Removed
|
||||
|
||||
- Support for PHP 5.3, 5.4, 5.5, and 5.6 [#380](https://github.com/php-curl-class/php-curl-class/issues/380)
|
||||
|
||||
## Manual Review
|
||||
|
||||
A manual review of changes is possible using the
|
||||
[comparison page](https://github.com/php-curl-class/php-curl-class/compare/). For example, visit
|
||||
[7.4.0...8.0.0](https://github.com/php-curl-class/php-curl-class/compare/7.4.0...8.0.0) to compare the changes for
|
||||
the `MAJOR` upgrade from 7.4.0 to 8.0.0. Comparing against `HEAD` is also possible using the `tag...HEAD` syntax
|
||||
([8.3.0...HEAD](https://github.com/php-curl-class/php-curl-class/compare/8.3.0...HEAD)).
|
||||
|
||||
View the log between releases:
|
||||
|
||||
$ git fetch --tags
|
||||
$ git log 7.4.0...8.0.0
|
||||
|
||||
View the code changes between releases:
|
||||
|
||||
$ git fetch --tags
|
||||
$ git diff 7.4.0...8.0.0
|
||||
|
||||
View only the source log and code changes between releases:
|
||||
|
||||
$ git log 7.4.0...8.0.0 "src/"
|
||||
$ git diff 7.4.0...8.0.0 "src/"
|
||||
|
||||
View only the source log and code changes between a release and the current checked-out commit:
|
||||
|
||||
$ git log 8.0.0...head "src/"
|
||||
$ git diff 8.0.0...head "src/"
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user