mirror of
https://github.com/renbaoshuo/S2OJ.git
synced 2024-11-26 02:28:41 +00:00
feat: Remote Judge (#28)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
commit
259bf77d58
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,8 @@ 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());
|
||||
/*!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', '内置评测机', '用于评测本地题目的评测机。');
|
||||
|
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 评测机的虚拟评测机。');
|
2600
remote_judger/package-lock.json
generated
Normal file
2600
remote_judger/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
remote_judger/package.json
Normal file
32
remote_judger/package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"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": {
|
||||
"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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
347
remote_judger/src/providers/codeforces.ts
Normal file
347
remote_judger/src/providers/codeforces.ts
Normal file
@ -0,0 +1,347 @@
|
||||
import { JSDOM } from 'jsdom';
|
||||
import superagent from 'superagent';
|
||||
import proxy from 'superagent-proxy';
|
||||
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: 1,
|
||||
status: 'Compile Error',
|
||||
message: body['checkerStdoutAndStderr#1'],
|
||||
});
|
||||
}
|
||||
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 status =
|
||||
VERDICT[
|
||||
Object.keys(VERDICT).find(k => normalize(body.verdict).includes(k))
|
||||
];
|
||||
return await end({
|
||||
id,
|
||||
status,
|
||||
score: status === 'Accepted' ? 100 : 0,
|
||||
time,
|
||||
memory,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
308
remote_judger/src/providers/loj.ts
Normal file
308
remote_judger/src/providers/loj.ts
Normal file
@ -0,0 +1,308 @@
|
||||
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/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(2000);
|
||||
const { body, error } = await this.post('/submission/getSubmissionDetail')
|
||||
.send({ submissionId: String(id), locale: 'zh_CN' })
|
||||
.retry(3);
|
||||
|
||||
if (error) continue;
|
||||
|
||||
if (body.progress.progressType !== 'Finished') {
|
||||
await next({
|
||||
status: `${body.progress.progressType}: ${body.progress.status}`,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (body.meta.status === 'CompilationError') {
|
||||
await end({
|
||||
error: true,
|
||||
id,
|
||||
status: 'Compile Error',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
['SystemError', 'JudgementFailed', 'ConfigurationError'].includes(
|
||||
body.meta.status
|
||||
)
|
||||
) {
|
||||
await end({
|
||||
error: true,
|
||||
id,
|
||||
status: 'Judgment Failed',
|
||||
});
|
||||
}
|
||||
|
||||
return await end({
|
||||
id,
|
||||
status: body.meta.status,
|
||||
score: body.meta.score,
|
||||
time: body.meta.timeUsed,
|
||||
memory: body.meta.memoryUsed,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
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;
|
32
remote_judger/src/verdict.ts
Normal file
32
remote_judger/src/verdict.ts
Normal file
@ -0,0 +1,32 @@
|
||||
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',
|
||||
},
|
||||
{
|
||||
get(self, key) {
|
||||
if (typeof key === 'symbol') return null;
|
||||
key = normalize(key);
|
||||
if (self[key]) return self[key];
|
||||
return null;
|
||||
},
|
||||
}
|
||||
);
|
160
remote_judger/src/vjudge.ts
Normal file
160
remote_judger/src/vjudge.ts
Normal file
@ -0,0 +1,160 @@
|
||||
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:
|
||||
'<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"]
|
||||
}
|
@ -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": "^2.0",
|
||||
"ext-dom": "20031129",
|
||||
"ivopetkov/html5-dom-document-php": "2.*"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
|
@ -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 .= ' ' . HTML::tag('span', ['class' => 'badge text-bg-success'], '远端评测题');
|
||||
}
|
||||
if ($info['is_hidden']) {
|
||||
$html .= ' <span class="badge text-bg-danger"><i class="bi bi-eye-slash-fill"></i> ' . UOJLocale::get('hidden') . '</span> ';
|
||||
}
|
||||
|
153
web/app/controllers/new_remote_problem.php
Normal file
153
web/app/controllers/new_remote_problem.php
Normal file
@ -0,0 +1,153 @@
|
||||
<?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();
|
||||
|
||||
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']), '', ''])
|
||||
]);
|
||||
dataNewProblem($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() ?>
|
@ -223,7 +223,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 +232,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 +241,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 +262,14 @@ if (UOJContest::cur()) {
|
||||
<article class="mt-3 markdown-body">
|
||||
<?= $problem_content['statement'] ?>
|
||||
</article>
|
||||
|
||||
<hr>
|
||||
|
||||
<?php if (UOJProblem::info('type') == 'remote') : ?>
|
||||
<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 +403,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">
|
||||
|
@ -10,6 +10,7 @@ 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();
|
||||
|
||||
|
@ -82,11 +82,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">
|
||||
|
@ -55,7 +55,7 @@ if (UOJProblem::userCanCreateProblem(Auth::user())) {
|
||||
如有,在此处填写其他于题意或数据相关的说明。
|
||||
|
||||
EOD;
|
||||
$new_problem_form = new UOJBs4Form('new_problem');
|
||||
$new_problem_form = new UOJForm('new_problem');
|
||||
$new_problem_form->handle = function () use ($default_statement) {
|
||||
DB::insert([
|
||||
"insert into problems",
|
||||
@ -77,11 +77,10 @@ EOD;
|
||||
redirectTo("/problem/{$id}/manage/statement");
|
||||
die();
|
||||
};
|
||||
$new_problem_form->submit_button_config['align'] = 'right';
|
||||
$new_problem_form->submit_button_config['class_str'] = 'btn btn-primary';
|
||||
$new_problem_form->submit_button_config['text'] = UOJLocale::get('problems::add new');
|
||||
$new_problem_form->submit_button_config['smart_confirm'] = '';
|
||||
|
||||
$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();
|
||||
}
|
||||
|
||||
@ -95,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('span', ['class' => 'badge text-bg-success'], '远端评测题');
|
||||
}
|
||||
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> ';
|
||||
}
|
||||
@ -254,25 +256,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">
|
||||
@ -287,9 +284,6 @@ $pag = new Paginator([
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $pag->pagination() ?>
|
||||
|
||||
<script type="text/javascript">
|
||||
$('#input-show_tags_mode').click(function() {
|
||||
@ -390,6 +384,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/new/remote">
|
||||
<i class="bi bi-cloud-plus"></i>
|
||||
新建远端评测题目
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- sidebar -->
|
||||
<?php uojIncludeView('sidebar') ?>
|
||||
</aside>
|
||||
|
@ -84,6 +84,81 @@ $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'),
|
||||
],
|
||||
]);
|
||||
|
||||
DB::update([
|
||||
"update problems_contents",
|
||||
"set", [
|
||||
"remote_content" => HTML::purifier()->purify($data['statement']),
|
||||
],
|
||||
"where", [
|
||||
"id" => UOJProblem::info('id'),
|
||||
],
|
||||
]);
|
||||
|
||||
redirectTo(UOJProblem::cur()->getUri());
|
||||
};
|
||||
$re_crawl_form->runAtServer();
|
||||
}
|
||||
|
||||
$view_type_form = new UOJForm('view_type');
|
||||
$view_type_form->addSelect('view_content_type', [
|
||||
'div_class' => 'row align-items-center g-0',
|
||||
@ -198,11 +273,13 @@ $solution_view_type_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">
|
||||
@ -358,6 +435,17 @@ $solution_view_type_form->runAtServer();
|
||||
</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>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header fw-bold">
|
||||
提交记录可视权限
|
||||
|
@ -687,7 +687,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>';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,14 @@ class HTML {
|
||||
$def->addElement('header', 'Block', 'Flow', 'Common');
|
||||
$def->addElement('footer', 'Block', 'Flow', 'Common');
|
||||
|
||||
$extra_allowed_html = [
|
||||
mergeConfig($extra_allowed_html, [
|
||||
'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) {
|
||||
|
@ -120,6 +120,53 @@ class UOJForm {
|
||||
$this->add($name, $html, $validator_php, $validator_js);
|
||||
}
|
||||
|
||||
public function addInput($name, $config) {
|
||||
$config += [
|
||||
'type' => 'text',
|
||||
'div_class' => '',
|
||||
'input_class' => 'form-control',
|
||||
'default_value' => '',
|
||||
'label' => '',
|
||||
'label_class' => 'form-label',
|
||||
'placeholder' => '',
|
||||
'help' => '',
|
||||
'help_class' => 'form-text',
|
||||
'validator_php' => function ($x) {
|
||||
return '';
|
||||
},
|
||||
'validator_js' => null,
|
||||
];
|
||||
|
||||
$html = '';
|
||||
$html .= HTML::tag_begin('div', ['class' => $config['div_class'], 'id' => "div-$name"]);
|
||||
|
||||
if ($config['label']) {
|
||||
$html .= HTML::tag('label', [
|
||||
'class' => $config['label_class'],
|
||||
'for' => "input-$name",
|
||||
'id' => "label-$name"
|
||||
], $config['label']);
|
||||
}
|
||||
|
||||
$html .= HTML::empty_tag('input', [
|
||||
'class' => $config['input_class'],
|
||||
'type' => $config['type'],
|
||||
'name' => $name,
|
||||
'id' => "input-$name",
|
||||
'value' => $config['default_value'],
|
||||
'placeholder' => $config['placeholder'],
|
||||
]);
|
||||
$html .= HTML::tag('div', ['class' => 'invalid-feedback', 'id' => "help-$name"], '');
|
||||
|
||||
if ($config['help']) {
|
||||
$html .= HTML::tag('div', ['class' => $config['help_class']], $config['help']);
|
||||
}
|
||||
|
||||
$html .= HTML::tag_end('div');
|
||||
|
||||
$this->add($name, $html, $config['validator_php'], $config['validator_js']);
|
||||
}
|
||||
|
||||
public function addCheckbox($name, $config) {
|
||||
$config += [
|
||||
'checked' => false,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
|
439
web/app/models/UOJRemoteProblem.php
Normal file
439
web/app/models/UOJRemoteProblem.php
Normal file
@ -0,0 +1,439 @@
|
||||
<?php
|
||||
|
||||
class UOJRemoteProblem {
|
||||
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 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) {
|
||||
$curl = new Curl();
|
||||
$curl->setUserAgent('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');
|
||||
|
||||
$res = retry_loop(function () use (&$curl, $id) {
|
||||
$curl->get(static::getCodeforcesProblemUrl($id));
|
||||
|
||||
if ($curl->error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'content-type' => $curl->response_headers['Content-Type'],
|
||||
'response' => $curl->response,
|
||||
];
|
||||
});
|
||||
|
||||
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,
|
||||
'statement' => HTML::tag('h3', [], '提示') .
|
||||
HTML::tag(
|
||||
'p',
|
||||
[],
|
||||
'本题题面为 PDF 题面,请' .
|
||||
HTML::tag('a', ['href' => static::getCodeforcesProblemUrl($id), 'target' => '_blank'], '点此') .
|
||||
'以查看题面。'
|
||||
),
|
||||
];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function getAtcoderProblemBasicInfo($id) {
|
||||
$curl = new Curl();
|
||||
$curl->setUserAgent('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');
|
||||
$curl->setCookie('language', 'en');
|
||||
|
||||
$res = retry_loop(function () use (&$curl, $id) {
|
||||
$curl->get(static::getAtcoderProblemUrl($id));
|
||||
|
||||
if ($curl->error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $curl->response;
|
||||
});
|
||||
|
||||
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'];
|
||||
$curl = new Curl();
|
||||
$curl->setUserAgent('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');
|
||||
|
||||
$res = retry_loop(function () use (&$curl, $id) {
|
||||
$curl->get(static::getUojProblemUrl($id));
|
||||
|
||||
if ($curl->error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $curl->response;
|
||||
});
|
||||
|
||||
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->setUserAgent('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');
|
||||
$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::purifier()->purify(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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ Route::group(
|
||||
Route::any('/', '/index.php');
|
||||
Route::any('/problems', '/problem_set.php');
|
||||
Route::any('/problems/template', '/problem_set.php?tab=template');
|
||||
Route::any('/problems/new/remote', '/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');
|
||||
@ -85,9 +86,6 @@ 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');
|
||||
|
||||
|
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 评测机的虚拟评测机。');
|
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,13 @@ $vendorDir = dirname(dirname(__FILE__));
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
||||
'CaseInsensitiveArray' => $vendorDir . '/php-curl-class/php-curl-class/src/Curl.class.php',
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
'Curl' => $vendorDir . '/php-curl-class/php-curl-class/src/Curl.class.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',
|
||||
);
|
||||
|
1
web/app/vendor/composer/autoload_psr4.php
vendored
1
web/app/vendor/composer/autoload_psr4.php
vendored
@ -6,6 +6,7 @@ $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'),
|
||||
|
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;
|
||||
}
|
||||
}
|
||||
|
16
web/app/vendor/composer/autoload_static.php
vendored
16
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' =>
|
||||
@ -26,6 +30,10 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'Symfony\\Polyfill\\Php80\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
|
||||
),
|
||||
'Symfony\\Component\\Finder\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/finder',
|
||||
@ -58,7 +66,15 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
||||
'CaseInsensitiveArray' => __DIR__ . '/..' . '/php-curl-class/php-curl-class/src/Curl.class.php',
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
'Curl' => __DIR__ . '/..' . '/php-curl-class/php-curl-class/src/Curl.class.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)
|
||||
|
271
web/app/vendor/composer/installed.json
vendored
271
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,88 @@
|
||||
"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": "2.0.0",
|
||||
"version_normalized": "2.0.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-curl-class/php-curl-class.git",
|
||||
"reference": "24a93bdc51058ad50d219842b63f7f2e0cb350ac"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-curl-class/php-curl-class/zipball/24a93bdc51058ad50d219842b63f7f2e0cb350ac",
|
||||
"reference": "24a93bdc51058ad50d219842b63f7f2e0cb350ac",
|
||||
"shasum": ""
|
||||
},
|
||||
"time": "2014-04-12T09:46:33+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"description": "PHP Curl Class is an object-oriented wrapper of the PHP cURL extension.",
|
||||
"support": {
|
||||
"issues": "https://github.com/php-curl-class/php-curl-class/issues",
|
||||
"source": "https://github.com/php-curl-class/php-curl-class/tree/2.0.0"
|
||||
},
|
||||
"install-path": "../php-curl-class/php-curl-class"
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v6.6.5",
|
||||
@ -235,30 +318,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 +448,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' => 'd6997b84758fbe52d9d90a2d5fe2f2e06806b176',
|
||||
'name' => '__root__',
|
||||
'dev' => true,
|
||||
),
|
||||
'versions' => array(
|
||||
'__root__' => array(
|
||||
'pretty_version' => 'dev-master',
|
||||
'version' => 'dev-master',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'reference' => 'd6997b84758fbe52d9d90a2d5fe2f2e06806b176',
|
||||
'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' => '2.0.0',
|
||||
'version' => '2.0.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../php-curl-class/php-curl-class',
|
||||
'aliases' => array(),
|
||||
'reference' => '24a93bdc51058ad50d219842b63f7f2e0cb350ac',
|
||||
'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);
|
||||
}
|
||||
}
|
2
web/app/vendor/php-curl-class/php-curl-class/.gitignore
vendored
Normal file
2
web/app/vendor/php-curl-class/php-curl-class/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
vendor/
|
||||
composer.lock
|
19
web/app/vendor/php-curl-class/php-curl-class/.travis.yml
vendored
Normal file
19
web/app/vendor/php-curl-class/php-curl-class/.travis.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
language: php
|
||||
php:
|
||||
- 5.3
|
||||
- 5.4
|
||||
- 5.5
|
||||
- 5.6
|
||||
- hhvm
|
||||
matrix:
|
||||
allow_failures:
|
||||
- php: 5.6
|
||||
- php: hhvm
|
||||
before_script:
|
||||
- if [[ "$TRAVIS_PHP_VERSION" == "5.4" ]]; then sh -c "php -S 127.0.0.1:8000 -t tests/PHPCurlClass/ &"; fi
|
||||
- if [[ "$TRAVIS_PHP_VERSION" == "5.5" ]]; then sh -c "php -S 127.0.0.1:8000 -t tests/PHPCurlClass/ &"; fi
|
||||
- if [[ "$TRAVIS_PHP_VERSION" == "5.6" ]]; then sh -c "php -S 127.0.0.1:8000 -t tests/PHPCurlClass/ &"; fi
|
||||
- if [[ "$TRAVIS_PHP_VERSION" == "hhvm" ]]; then sh -c "cd tests && hhvm --mode server --port 8000 --config PHPCurlClass/server.hdf &"; fi
|
||||
script:
|
||||
- php -l src/*
|
||||
- if [[ "$TRAVIS_PHP_VERSION" != "5.3" ]]; then sh -c "cd tests && phpunit --configuration phpunit.xml"; fi
|
24
web/app/vendor/php-curl-class/php-curl-class/LICENSE
vendored
Normal file
24
web/app/vendor/php-curl-class/php-curl-class/LICENSE
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
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 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.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
136
web/app/vendor/php-curl-class/php-curl-class/README.md
vendored
Normal file
136
web/app/vendor/php-curl-class/php-curl-class/README.md
vendored
Normal file
@ -0,0 +1,136 @@
|
||||
# php-curl-class
|
||||
|
||||
[![Build Status](https://travis-ci.org/php-curl-class/php-curl-class.png?branch=master)](https://travis-ci.org/php-curl-class/php-curl-class)
|
||||
|
||||
PHP Curl Class is an object-oriented wrapper of the PHP cURL extension.
|
||||
|
||||
### Composer
|
||||
|
||||
$ composer require php-curl-class/php-curl-class
|
||||
|
||||
### Quick Start and Examples
|
||||
|
||||
```php
|
||||
require 'Curl.class.php';
|
||||
|
||||
$curl = new Curl();
|
||||
$curl->get('http://www.example.com/');
|
||||
```
|
||||
|
||||
```php
|
||||
$curl = new Curl();
|
||||
$curl->get('http://www.example.com/search', array(
|
||||
'q' => 'keyword',
|
||||
));
|
||||
```
|
||||
|
||||
```php
|
||||
$curl = new Curl();
|
||||
$curl->post('http://www.example.com/login/', array(
|
||||
'username' => 'myusername',
|
||||
'password' => 'mypassword',
|
||||
));
|
||||
```
|
||||
|
||||
```php
|
||||
$curl = new Curl();
|
||||
$curl->setBasicAuthentication('username', 'password');
|
||||
$curl->setUserAgent('');
|
||||
$curl->setReferrer('');
|
||||
$curl->setHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
$curl->setCookie('key', 'value');
|
||||
$curl->get('http://www.example.com/');
|
||||
|
||||
if ($curl->error) {
|
||||
echo $curl->error_code;
|
||||
}
|
||||
else {
|
||||
echo $curl->response;
|
||||
}
|
||||
|
||||
var_dump($curl->request_headers);
|
||||
var_dump($curl->response_headers);
|
||||
```
|
||||
|
||||
```php
|
||||
$curl = new Curl();
|
||||
$curl->setOpt(CURLOPT_SSL_VERIFYPEER, false);
|
||||
$curl->get('https://encrypted.example.com/');
|
||||
```
|
||||
|
||||
```php
|
||||
$curl = new Curl();
|
||||
$curl->put('http://api.example.com/user/', array(
|
||||
'first_name' => 'Zach',
|
||||
'last_name' => 'Borboa',
|
||||
));
|
||||
```
|
||||
|
||||
```php
|
||||
$curl = new Curl();
|
||||
$curl->patch('http://api.example.com/profile/', array(
|
||||
'image' => '@path/to/file.jpg',
|
||||
));
|
||||
```
|
||||
|
||||
```php
|
||||
$curl = new Curl();
|
||||
$curl->delete('http://api.example.com/user/', array(
|
||||
'id' => '1234',
|
||||
));
|
||||
```
|
||||
|
||||
```php
|
||||
// Enable gzip compression.
|
||||
$curl = new Curl();
|
||||
$curl->setOpt(CURLOPT_ENCODING , 'gzip');
|
||||
$curl->get('https://www.example.com/image.png');
|
||||
```
|
||||
|
||||
```php
|
||||
// Case-insensitive access to headers.
|
||||
$curl = new Curl();
|
||||
$curl->get('https://www.example.com/image.png');
|
||||
echo $curl->response_headers['Content-Type'] . "\n"; // image/png
|
||||
echo $curl->response_headers['CoNTeNT-TyPE'] . "\n"; // image/png
|
||||
```
|
||||
|
||||
```php
|
||||
$curl->close();
|
||||
```
|
||||
|
||||
```php
|
||||
// Example access to curl object.
|
||||
curl_set_opt($curl->curl, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1');
|
||||
curl_close($curl->curl);
|
||||
```
|
||||
|
||||
```php
|
||||
// Requests in parallel with callback functions.
|
||||
$curl = new Curl();
|
||||
$curl->setOpt(CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1');
|
||||
|
||||
$curl->success(function($instance) {
|
||||
echo 'call was successful. response was' . "\n";
|
||||
echo $instance->response . "\n";
|
||||
});
|
||||
$curl->error(function($instance) {
|
||||
echo 'call was unsuccessful.' . "\n";
|
||||
echo 'error code:' . $instance->error_code . "\n";
|
||||
echo 'error message:' . $instance->error_message . "\n";
|
||||
});
|
||||
$curl->complete(function($instance) {
|
||||
echo 'call completed' . "\n";
|
||||
});
|
||||
|
||||
$curl->get(array(
|
||||
'https://duckduckgo.com/',
|
||||
'https://search.yahoo.com/search',
|
||||
'https://www.bing.com/search',
|
||||
'http://www.dogpile.com/search/web',
|
||||
'https://www.google.com/search',
|
||||
'https://www.wolframalpha.com/input/',
|
||||
), array(
|
||||
'q' => 'hello world',
|
||||
));
|
||||
```
|
7
web/app/vendor/php-curl-class/php-curl-class/composer.json
vendored
Normal file
7
web/app/vendor/php-curl-class/php-curl-class/composer.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "php-curl-class/php-curl-class",
|
||||
"description": "PHP Curl Class is an object-oriented wrapper of the PHP cURL extension.",
|
||||
"autoload": {
|
||||
"classmap": ["src/"]
|
||||
}
|
||||
}
|
22
web/app/vendor/php-curl-class/php-curl-class/examples/coinbase_account_balance.php
vendored
Normal file
22
web/app/vendor/php-curl-class/php-curl-class/examples/coinbase_account_balance.php
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
require '../src/Curl.class.php';
|
||||
|
||||
|
||||
define('API_KEY', '');
|
||||
define('API_SECRET', '');
|
||||
|
||||
$url = 'https://coinbase.com/api/v1/account/balance';
|
||||
|
||||
$nonce = (int)(microtime(true) * 1e6);
|
||||
$message = $nonce . $url;
|
||||
$signature = hash_hmac('sha256', $message, API_SECRET);
|
||||
|
||||
$curl = new Curl();
|
||||
$curl->setHeader('ACCESS_KEY', API_KEY);
|
||||
$curl->setHeader('ACCESS_SIGNATURE', $signature);
|
||||
$curl->setHeader('ACCESS_NONCE', $nonce);
|
||||
$curl->get($url);
|
||||
|
||||
echo
|
||||
'My current account balance at Coinbase is ' .
|
||||
$curl->response->amount . ' ' . $curl->response->currency . '.' . "\n";
|
10
web/app/vendor/php-curl-class/php-curl-class/examples/coinbase_spot_rate.php
vendored
Normal file
10
web/app/vendor/php-curl-class/php-curl-class/examples/coinbase_spot_rate.php
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
require '../src/Curl.class.php';
|
||||
|
||||
|
||||
$curl = new Curl();
|
||||
$curl->get('https://coinbase.com/api/v1/prices/spot_rate');
|
||||
|
||||
echo
|
||||
'The current price of bitcoin at Coinbase is ' .
|
||||
'$' . $curl->response->amount . ' ' . $curl->response->currency . '.' . "\n";
|
17
web/app/vendor/php-curl-class/php-curl-class/examples/instagram_search_photos.php
vendored
Normal file
17
web/app/vendor/php-curl-class/php-curl-class/examples/instagram_search_photos.php
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
require '../src/Curl.class.php';
|
||||
|
||||
|
||||
define('INSTAGRAM_CLIENT_ID', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
|
||||
|
||||
$curl = new Curl();
|
||||
$curl->get('https://api.instagram.com/v1/media/search', array(
|
||||
'client_id' => INSTAGRAM_CLIENT_ID,
|
||||
'lat' => '37.8296',
|
||||
'lng' => '-122.4832',
|
||||
));
|
||||
|
||||
foreach ($curl->response->data as $media) {
|
||||
$image = $media->images->low_resolution;
|
||||
echo '<img alt="" src="' . $image->url . '" width="' . $image->width . '" height="' . $image->height . '" />';
|
||||
}
|
14
web/app/vendor/php-curl-class/php-curl-class/examples/put.php
vendored
Normal file
14
web/app/vendor/php-curl-class/php-curl-class/examples/put.php
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
require '../src/Curl.class.php';
|
||||
|
||||
|
||||
// curl -X PUT -d "id=1&first_name=Zach&last_name=Borboa" "http://httpbin.org/put"
|
||||
$curl = new Curl();
|
||||
$curl->put('http://httpbin.org/put', array(
|
||||
'id' => 1,
|
||||
'first_name' => 'Zach',
|
||||
'last_name' => 'Borboa',
|
||||
));
|
||||
|
||||
echo 'Data server received via PUT:' . "\n";
|
||||
var_dump($curl->response->form);
|
35
web/app/vendor/php-curl-class/php-curl-class/examples/twitter_post_tweet.php
vendored
Normal file
35
web/app/vendor/php-curl-class/php-curl-class/examples/twitter_post_tweet.php
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
require '../src/Curl.class.php';
|
||||
|
||||
|
||||
define('API_KEY', 'XXXXXXXXXXXXXXXXXXXXXXXXX');
|
||||
define('API_SECRET', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
|
||||
define('OAUTH_ACCESS_TOKEN', 'XXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
|
||||
define('OAUTH_TOKEN_SECRET', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
|
||||
|
||||
$status = 'I love php curl class. https://github.com/php-curl-class/php-curl-class';
|
||||
|
||||
$oauth_data = array(
|
||||
'oauth_consumer_key' => API_KEY,
|
||||
'oauth_nonce' => md5(microtime() . mt_rand()),
|
||||
'oauth_signature_method' => 'HMAC-SHA1',
|
||||
'oauth_timestamp' => time(),
|
||||
'oauth_token' => OAUTH_ACCESS_TOKEN,
|
||||
'oauth_version' => '1.0',
|
||||
'status' => $status,
|
||||
);
|
||||
|
||||
$url = 'https://api.twitter.com/1.1/statuses/update.json';
|
||||
$request = implode('&', array(
|
||||
'POST',
|
||||
rawurlencode($url),
|
||||
rawurlencode(http_build_query($oauth_data, '', '&', PHP_QUERY_RFC3986)),
|
||||
));
|
||||
$key = implode('&', array(API_SECRET, OAUTH_TOKEN_SECRET));
|
||||
$oauth_data['oauth_signature'] = base64_encode(hash_hmac('sha1', $request, $key, true));
|
||||
$data = http_build_query($oauth_data, '', '&');
|
||||
|
||||
$curl = new Curl();
|
||||
$curl->post($url, $data);
|
||||
|
||||
echo 'Posted "' . $curl->response->text . '" at ' . $curl->response->created_at . '.' . "\n";
|
496
web/app/vendor/php-curl-class/php-curl-class/src/Curl.class.php
vendored
Normal file
496
web/app/vendor/php-curl-class/php-curl-class/src/Curl.class.php
vendored
Normal file
@ -0,0 +1,496 @@
|
||||
<?php
|
||||
|
||||
class Curl
|
||||
{
|
||||
const USER_AGENT = 'PHP-Curl-Class/2.0 (+https://github.com/php-curl-class/php-curl-class)';
|
||||
|
||||
private $cookies = array();
|
||||
private $headers = array();
|
||||
private $options = array();
|
||||
|
||||
private $multi_parent = false;
|
||||
private $multi_child = false;
|
||||
private $before_send_function = null;
|
||||
private $success_function = null;
|
||||
private $error_function = null;
|
||||
private $complete_function = null;
|
||||
|
||||
public $curl;
|
||||
public $curls;
|
||||
|
||||
public $error = false;
|
||||
public $error_code = 0;
|
||||
public $error_message = null;
|
||||
|
||||
public $curl_error = false;
|
||||
public $curl_error_code = 0;
|
||||
public $curl_error_message = null;
|
||||
|
||||
public $http_error = false;
|
||||
public $http_status_code = 0;
|
||||
public $http_error_message = null;
|
||||
|
||||
public $request_headers = null;
|
||||
public $response_headers = null;
|
||||
public $response = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (!extension_loaded('curl')) {
|
||||
throw new \ErrorException('cURL library is not loaded');
|
||||
}
|
||||
|
||||
$this->curl = curl_init();
|
||||
$this->setUserAgent(self::USER_AGENT);
|
||||
$this->setOpt(CURLINFO_HEADER_OUT, true);
|
||||
$this->setOpt(CURLOPT_HEADER, true);
|
||||
$this->setOpt(CURLOPT_RETURNTRANSFER, true);
|
||||
}
|
||||
|
||||
public function get($url_mixed, $data = array())
|
||||
{
|
||||
if (is_array($url_mixed)) {
|
||||
$curl_multi = curl_multi_init();
|
||||
$this->multi_parent = true;
|
||||
|
||||
$this->curls = array();
|
||||
|
||||
foreach ($url_mixed as $url) {
|
||||
$curl = new Curl();
|
||||
$curl->multi_child = true;
|
||||
$curl->setOpt(CURLOPT_URL, $this->buildURL($url, $data), $curl->curl);
|
||||
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'GET');
|
||||
$curl->setOpt(CURLOPT_HTTPGET, true);
|
||||
$this->call($this->before_send_function, $curl);
|
||||
$this->curls[] = $curl;
|
||||
|
||||
$curlm_error_code = curl_multi_add_handle($curl_multi, $curl->curl);
|
||||
if (!($curlm_error_code === CURLM_OK)) {
|
||||
throw new \ErrorException('cURL multi add handle error: ' . curl_multi_strerror($curlm_error_code));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->curls as $ch) {
|
||||
foreach ($this->options as $key => $value) {
|
||||
$ch->setOpt($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
$status = curl_multi_exec($curl_multi, $active);
|
||||
} while ($status === CURLM_CALL_MULTI_PERFORM || $active);
|
||||
|
||||
foreach ($this->curls as $ch) {
|
||||
$this->exec($ch);
|
||||
}
|
||||
} else {
|
||||
$this->setopt(CURLOPT_URL, $this->buildURL($url_mixed, $data));
|
||||
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'GET');
|
||||
$this->setopt(CURLOPT_HTTPGET, true);
|
||||
return $this->exec();
|
||||
}
|
||||
}
|
||||
|
||||
public function post($url, $data = array())
|
||||
{
|
||||
if (is_array($data) && empty($data)) {
|
||||
$this->setHeader('Content-Length');
|
||||
}
|
||||
|
||||
$this->setOpt(CURLOPT_URL, $this->buildURL($url));
|
||||
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
|
||||
$this->setOpt(CURLOPT_POST, true);
|
||||
$this->setOpt(CURLOPT_POSTFIELDS, $this->postfields($data));
|
||||
return $this->exec();
|
||||
}
|
||||
|
||||
public function put($url, $data = array())
|
||||
{
|
||||
$this->setOpt(CURLOPT_URL, $url);
|
||||
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'PUT');
|
||||
$put_data = http_build_query($data);
|
||||
if (empty($this->options[CURLOPT_INFILE]) && empty($this->options[CURLOPT_INFILESIZE])) {
|
||||
$this->setHeader('Content-Length', strlen($put_data));
|
||||
}
|
||||
$this->setOpt(CURLOPT_POSTFIELDS, $put_data);
|
||||
return $this->exec();
|
||||
}
|
||||
|
||||
public function patch($url, $data = array())
|
||||
{
|
||||
$this->setHeader('Content-Length');
|
||||
$this->setOpt(CURLOPT_URL, $this->buildURL($url));
|
||||
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'PATCH');
|
||||
$this->setOpt(CURLOPT_POSTFIELDS, $data);
|
||||
return $this->exec();
|
||||
}
|
||||
|
||||
public function delete($url, $data = array())
|
||||
{
|
||||
$this->setHeader('Content-Length');
|
||||
$this->setOpt(CURLOPT_URL, $this->buildURL($url, $data));
|
||||
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'DELETE');
|
||||
return $this->exec();
|
||||
}
|
||||
|
||||
public function head($url, $data = array())
|
||||
{
|
||||
$this->setOpt(CURLOPT_URL, $this->buildURL($url, $data));
|
||||
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'HEAD');
|
||||
$this->setOpt(CURLOPT_NOBODY, true);
|
||||
return $this->exec();
|
||||
}
|
||||
|
||||
public function options($url, $data = array())
|
||||
{
|
||||
$this->setHeader('Content-Length');
|
||||
$this->setOpt(CURLOPT_URL, $this->buildURL($url, $data));
|
||||
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'OPTIONS');
|
||||
return $this->exec();
|
||||
}
|
||||
|
||||
public function setBasicAuthentication($username, $password)
|
||||
{
|
||||
$this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
||||
$this->setOpt(CURLOPT_USERPWD, $username . ':' . $password);
|
||||
}
|
||||
|
||||
public function setHeader($key, $value = '')
|
||||
{
|
||||
$this->headers[$key] = $key . ': ' . $value;
|
||||
$this->setOpt(CURLOPT_HTTPHEADER, array_values($this->headers));
|
||||
}
|
||||
|
||||
public function setUserAgent($user_agent)
|
||||
{
|
||||
$this->setOpt(CURLOPT_USERAGENT, $user_agent);
|
||||
}
|
||||
|
||||
public function setReferrer($referrer)
|
||||
{
|
||||
$this->setOpt(CURLOPT_REFERER, $referrer);
|
||||
}
|
||||
|
||||
public function setCookie($key, $value)
|
||||
{
|
||||
$this->cookies[$key] = $value;
|
||||
$this->setOpt(CURLOPT_COOKIE, http_build_query($this->cookies, '', '; '));
|
||||
}
|
||||
|
||||
public function setCookieFile($cookie_file)
|
||||
{
|
||||
$this->setOpt(CURLOPT_COOKIEFILE, $cookie_file);
|
||||
}
|
||||
|
||||
public function setCookieJar($cookie_jar)
|
||||
{
|
||||
$this->setOpt(CURLOPT_COOKIEJAR, $cookie_jar);
|
||||
}
|
||||
|
||||
public function setOpt($option, $value, $_ch = null)
|
||||
{
|
||||
$ch = is_null($_ch) ? $this->curl : $_ch;
|
||||
|
||||
$required_options = array(
|
||||
CURLINFO_HEADER_OUT => 'CURLINFO_HEADER_OUT',
|
||||
CURLOPT_HEADER => 'CURLOPT_HEADER',
|
||||
CURLOPT_RETURNTRANSFER => 'CURLOPT_RETURNTRANSFER',
|
||||
);
|
||||
|
||||
if (in_array($option, array_keys($required_options), true) && !($value === true)) {
|
||||
trigger_error($required_options[$option] . ' is a required option', E_USER_WARNING);
|
||||
}
|
||||
|
||||
$this->options[$option] = $value;
|
||||
return curl_setopt($ch, $option, $value);
|
||||
}
|
||||
|
||||
public function verbose($on = true)
|
||||
{
|
||||
$this->setOpt(CURLOPT_VERBOSE, $on);
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if ($this->multi_parent) {
|
||||
foreach ($this->curls as $curl) {
|
||||
$curl->close();
|
||||
}
|
||||
}
|
||||
|
||||
if (is_resource($this->curl)) {
|
||||
curl_close($this->curl);
|
||||
}
|
||||
}
|
||||
|
||||
public function beforeSend($function)
|
||||
{
|
||||
$this->before_send_function = $function;
|
||||
}
|
||||
|
||||
public function success($callback)
|
||||
{
|
||||
$this->success_function = $callback;
|
||||
}
|
||||
|
||||
public function error($callback)
|
||||
{
|
||||
$this->error_function = $callback;
|
||||
}
|
||||
|
||||
public function complete($callback)
|
||||
{
|
||||
$this->complete_function = $callback;
|
||||
}
|
||||
|
||||
private function buildURL($url, $data = array())
|
||||
{
|
||||
return $url . (empty($data) ? '' : '?' . http_build_query($data));
|
||||
}
|
||||
|
||||
private function parseHeaders($raw_headers)
|
||||
{
|
||||
$raw_headers = preg_split('/\r\n/', $raw_headers, null, PREG_SPLIT_NO_EMPTY);
|
||||
$http_headers = new CaseInsensitiveArray();
|
||||
|
||||
for ($i = 1; $i < count($raw_headers); $i++) {
|
||||
list($key, $value) = explode(':', $raw_headers[$i], 2);
|
||||
$key = trim($key);
|
||||
$value = trim($value);
|
||||
// Use isset() as array_key_exists() and ArrayAccess are not compatible.
|
||||
if (isset($http_headers[$key])) {
|
||||
$http_headers[$key] .= ',' . $value;
|
||||
} else {
|
||||
$http_headers[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return array(isset($raw_headers['0']) ? $raw_headers['0'] : '', $http_headers);
|
||||
}
|
||||
|
||||
private function parseRequestHeaders($raw_headers)
|
||||
{
|
||||
$request_headers = new CaseInsensitiveArray();
|
||||
list($first_line, $headers) = $this->parseHeaders($raw_headers);
|
||||
$request_headers['Request-Line'] = $first_line;
|
||||
foreach ($headers as $key => $value) {
|
||||
$request_headers[$key] = $value;
|
||||
}
|
||||
return $request_headers;
|
||||
}
|
||||
|
||||
private function parseResponseHeaders($raw_headers)
|
||||
{
|
||||
$response_headers = new CaseInsensitiveArray();
|
||||
list($first_line, $headers) = $this->parseHeaders($raw_headers);
|
||||
$response_headers['Status-Line'] = $first_line;
|
||||
foreach ($headers as $key => $value) {
|
||||
$response_headers[$key] = $value;
|
||||
}
|
||||
return $response_headers;
|
||||
}
|
||||
|
||||
private function postfields($data)
|
||||
{
|
||||
if (is_array($data)) {
|
||||
if (is_array_multidim($data)) {
|
||||
$data = http_build_multi_query($data);
|
||||
} else {
|
||||
foreach ($data as $key => $value) {
|
||||
// Fix "Notice: Array to string conversion" when $value in
|
||||
// curl_setopt($ch, CURLOPT_POSTFIELDS, $value) is an array
|
||||
// that contains an empty array.
|
||||
if (is_array($value) && empty($value)) {
|
||||
$data[$key] = '';
|
||||
// Fix "curl_setopt(): The usage of the @filename API for
|
||||
// file uploading is deprecated. Please use the CURLFile
|
||||
// class instead".
|
||||
} elseif (is_string($value) && strpos($value, '@') === 0) {
|
||||
if (class_exists('CURLFile')) {
|
||||
$data[$key] = new CURLFile(substr($value, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function exec($_ch = null)
|
||||
{
|
||||
$ch = is_null($_ch) ? $this : $_ch;
|
||||
|
||||
if ($ch->multi_child) {
|
||||
$ch->response = curl_multi_getcontent($ch->curl);
|
||||
} else {
|
||||
$ch->response = curl_exec($ch->curl);
|
||||
}
|
||||
|
||||
$ch->curl_error_code = curl_errno($ch->curl);
|
||||
$ch->curl_error_message = curl_error($ch->curl);
|
||||
$ch->curl_error = !($ch->curl_error_code === 0);
|
||||
$ch->http_status_code = curl_getinfo($ch->curl, CURLINFO_HTTP_CODE);
|
||||
$ch->http_error = in_array(floor($ch->http_status_code / 100), array(4, 5));
|
||||
$ch->error = $ch->curl_error || $ch->http_error;
|
||||
$ch->error_code = $ch->error ? ($ch->curl_error ? $ch->curl_error_code : $ch->http_status_code) : 0;
|
||||
|
||||
$ch->request_headers = $this->parseRequestHeaders(curl_getinfo($ch->curl, CURLINFO_HEADER_OUT));
|
||||
$ch->response_headers = '';
|
||||
if (!(strpos($ch->response, "\r\n\r\n") === false)) {
|
||||
list($response_header, $ch->response) = explode("\r\n\r\n", $ch->response, 2);
|
||||
if ($response_header === 'HTTP/1.1 100 Continue') {
|
||||
list($response_header, $ch->response) = explode("\r\n\r\n", $ch->response, 2);
|
||||
}
|
||||
$ch->response_headers = $this->parseResponseHeaders($response_header);
|
||||
|
||||
if (isset($ch->response_headers['Content-Type'])) {
|
||||
if (preg_match('/^application\/json/i', $ch->response_headers['Content-Type'])) {
|
||||
$json_obj = json_decode($ch->response, false);
|
||||
if (!is_null($json_obj)) {
|
||||
$ch->response = $json_obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$ch->http_error_message = '';
|
||||
if ($ch->error) {
|
||||
if (isset($ch->response_headers['Status-Line'])) {
|
||||
$ch->http_error_message = $ch->response_headers['Status-Line'];
|
||||
}
|
||||
}
|
||||
$ch->error_message = $ch->curl_error ? $ch->curl_error_message : $ch->http_error_message;
|
||||
|
||||
if (!$ch->error) {
|
||||
$ch->call($this->success_function, $ch);
|
||||
} else {
|
||||
$ch->call($this->error_function, $ch);
|
||||
}
|
||||
|
||||
$ch->call($this->complete_function, $ch);
|
||||
|
||||
return $ch->error_code;
|
||||
}
|
||||
|
||||
private function call($function)
|
||||
{
|
||||
if (is_callable($function)) {
|
||||
$args = func_get_args();
|
||||
array_shift($args);
|
||||
call_user_func_array($function, $args);
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
}
|
||||
|
||||
class CaseInsensitiveArray implements ArrayAccess, Countable, Iterator
|
||||
{
|
||||
private $container = array();
|
||||
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
if (is_null($offset)) {
|
||||
$this->container[] = $value;
|
||||
} else {
|
||||
$index = array_search(strtolower($offset), array_keys(array_change_key_case($this->container, CASE_LOWER)));
|
||||
if (!($index === false)) {
|
||||
$keys = array_keys($this->container);
|
||||
unset($this->container[$keys[$index]]);
|
||||
}
|
||||
$this->container[$offset] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
return array_key_exists(strtolower($offset), array_change_key_case($this->container, CASE_LOWER));
|
||||
}
|
||||
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
unset($this->container[$offset]);
|
||||
}
|
||||
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
$index = array_search(strtolower($offset), array_keys(array_change_key_case($this->container, CASE_LOWER)));
|
||||
if ($index === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$values = array_values($this->container);
|
||||
return $values[$index];
|
||||
}
|
||||
|
||||
public function count()
|
||||
{
|
||||
return count($this->container);
|
||||
}
|
||||
|
||||
public function current()
|
||||
{
|
||||
return current($this->container);
|
||||
}
|
||||
|
||||
public function next()
|
||||
{
|
||||
return next($this->container);
|
||||
}
|
||||
|
||||
public function key()
|
||||
{
|
||||
return key($this->container);
|
||||
}
|
||||
|
||||
public function valid()
|
||||
{
|
||||
return !($this->current() === false);
|
||||
}
|
||||
|
||||
public function rewind()
|
||||
{
|
||||
reset($this->container);
|
||||
}
|
||||
}
|
||||
|
||||
function is_array_assoc($array)
|
||||
{
|
||||
return (bool)count(array_filter(array_keys($array), 'is_string'));
|
||||
}
|
||||
|
||||
function is_array_multidim($array)
|
||||
{
|
||||
if (!is_array($array)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(count($array) === count($array, COUNT_RECURSIVE));
|
||||
}
|
||||
|
||||
function http_build_multi_query($data, $key = null)
|
||||
{
|
||||
$query = array();
|
||||
|
||||
if (empty($data)) {
|
||||
return $key . '=';
|
||||
}
|
||||
|
||||
$is_array_assoc = is_array_assoc($data);
|
||||
|
||||
foreach ($data as $k => $value) {
|
||||
if (is_string($value) || is_numeric($value)) {
|
||||
$brackets = $is_array_assoc ? '[' . $k . ']' : '[]';
|
||||
$query[] = urlencode(is_null($key) ? $k : $key . $brackets) . '=' . rawurlencode($value);
|
||||
} elseif (is_array($value)) {
|
||||
$nested = is_null($key) ? $k : $key . '[' . $k . ']';
|
||||
$query[] = http_build_multi_query($value, $nested);
|
||||
}
|
||||
}
|
||||
|
||||
return implode('&', $query);
|
||||
}
|
740
web/app/vendor/php-curl-class/php-curl-class/tests/PHPCurlClass/PHPCurlClassTest.php
vendored
Normal file
740
web/app/vendor/php-curl-class/php-curl-class/tests/PHPCurlClass/PHPCurlClassTest.php
vendored
Normal file
@ -0,0 +1,740 @@
|
||||
<?php
|
||||
// Usage: phpunit --verbose run.php
|
||||
|
||||
require '../src/Curl.class.php';
|
||||
require 'helper.inc.php';
|
||||
|
||||
|
||||
class CurlTest extends PHPUnit_Framework_TestCase {
|
||||
public function testExtensionLoaded() {
|
||||
$this->assertTrue(extension_loaded('curl'));
|
||||
}
|
||||
|
||||
public function testArrayAssociative() {
|
||||
$this->assertTrue(is_array_assoc(array(
|
||||
'foo' => 'wibble',
|
||||
'bar' => 'wubble',
|
||||
'baz' => 'wobble',
|
||||
)));
|
||||
}
|
||||
|
||||
public function testArrayIndexed() {
|
||||
$this->assertFalse(is_array_assoc(array(
|
||||
'wibble',
|
||||
'wubble',
|
||||
'wobble',
|
||||
)));
|
||||
}
|
||||
|
||||
public function testCaseInsensitiveArrayGet() {
|
||||
$array = new CaseInsensitiveArray();
|
||||
$this->assertTrue(is_object($array));
|
||||
$this->assertCount(0, $array);
|
||||
$this->assertNull($array[(string)rand()]);
|
||||
|
||||
$array['foo'] = 'bar';
|
||||
$this->assertNotEmpty($array);
|
||||
$this->assertCount(1, $array);
|
||||
}
|
||||
|
||||
public function testCaseInsensitiveArraySet() {
|
||||
function assertions($array, $count=1) {
|
||||
PHPUnit_Framework_Assert::assertCount($count, $array);
|
||||
PHPUnit_Framework_Assert::assertTrue($array['foo'] === 'bar');
|
||||
PHPUnit_Framework_Assert::assertTrue($array['Foo'] === 'bar');
|
||||
PHPUnit_Framework_Assert::assertTrue($array['FOo'] === 'bar');
|
||||
PHPUnit_Framework_Assert::assertTrue($array['FOO'] === 'bar');
|
||||
}
|
||||
|
||||
$array = new CaseInsensitiveArray();
|
||||
$array['foo'] = 'bar';
|
||||
assertions($array);
|
||||
|
||||
$array['Foo'] = 'bar';
|
||||
assertions($array);
|
||||
|
||||
$array['FOo'] = 'bar';
|
||||
assertions($array);
|
||||
|
||||
$array['FOO'] = 'bar';
|
||||
assertions($array);
|
||||
|
||||
$array['baz'] = 'qux';
|
||||
assertions($array, 2);
|
||||
}
|
||||
|
||||
public function testUserAgent() {
|
||||
$test = new Test();
|
||||
$test->curl->setUserAgent(Curl::USER_AGENT);
|
||||
$this->assertTrue($test->server('server', 'GET', array(
|
||||
'key' => 'HTTP_USER_AGENT',
|
||||
)) === Curl::USER_AGENT);
|
||||
}
|
||||
|
||||
public function testGet() {
|
||||
$test = new Test();
|
||||
$this->assertTrue($test->server('server', 'GET', array(
|
||||
'key' => 'REQUEST_METHOD',
|
||||
)) === 'GET');
|
||||
}
|
||||
|
||||
public function testPostRequestMethod() {
|
||||
$test = new Test();
|
||||
$this->assertTrue($test->server('server', 'POST', array(
|
||||
'key' => 'REQUEST_METHOD',
|
||||
)) === 'POST');
|
||||
}
|
||||
|
||||
public function testPostData() {
|
||||
$test = new Test();
|
||||
$this->assertTrue($test->server('post', 'POST', array(
|
||||
'key' => 'value',
|
||||
)) === 'key=value');
|
||||
}
|
||||
|
||||
public function testPostAssociativeArrayData() {
|
||||
$test = new Test();
|
||||
$this->assertTrue($test->server('post_multidimensional', 'POST', array(
|
||||
'username' => 'myusername',
|
||||
'password' => 'mypassword',
|
||||
'more_data' => array(
|
||||
'param1' => 'something',
|
||||
'param2' => 'other thing',
|
||||
'param3' => 123,
|
||||
'param4' => 3.14,
|
||||
),
|
||||
)) === 'username=myusername&password=mypassword&more_data%5Bparam1%5D=something&more_data%5Bparam2%5D=other%20thing&more_data%5Bparam3%5D=123&more_data%5Bparam4%5D=3.14');
|
||||
}
|
||||
|
||||
public function testPostMultidimensionalData() {
|
||||
$test = new Test();
|
||||
$this->assertTrue($test->server('post_multidimensional', 'POST', array(
|
||||
'key' => 'file',
|
||||
'file' => array(
|
||||
'wibble',
|
||||
'wubble',
|
||||
'wobble',
|
||||
),
|
||||
)) === 'key=file&file%5B%5D=wibble&file%5B%5D=wubble&file%5B%5D=wobble');
|
||||
}
|
||||
|
||||
public function testPostFilePathUpload() {
|
||||
$file_path = get_png();
|
||||
|
||||
$test = new Test();
|
||||
$this->assertTrue($test->server('post_file_path_upload', 'POST', array(
|
||||
'key' => 'image',
|
||||
'image' => '@' . $file_path,
|
||||
)) === 'image/png');
|
||||
|
||||
unlink($file_path);
|
||||
$this->assertFalse(file_exists($file_path));
|
||||
}
|
||||
|
||||
public function testPostCurlFileUpload() {
|
||||
if (class_exists('CURLFile')) {
|
||||
$file_path = get_png();
|
||||
|
||||
$test = new Test();
|
||||
$this->assertTrue($test->server('post_file_path_upload', 'POST', array(
|
||||
'key' => 'image',
|
||||
'image' => new CURLFile($file_path),
|
||||
)) === 'image/png');
|
||||
|
||||
unlink($file_path);
|
||||
$this->assertFalse(file_exists($file_path));
|
||||
}
|
||||
}
|
||||
|
||||
public function testPutRequestMethod() {
|
||||
$test = new Test();
|
||||
$this->assertTrue($test->server('request_method', 'PUT') === 'PUT');
|
||||
}
|
||||
|
||||
public function testPutData() {
|
||||
$test = new Test();
|
||||
$this->assertTrue($test->server('put', 'PUT', array(
|
||||
'key' => 'value',
|
||||
)) === 'key=value');
|
||||
}
|
||||
|
||||
public function testPutFileHandle() {
|
||||
$png = create_png();
|
||||
$tmp_file = create_tmp_file($png);
|
||||
|
||||
$test = new Test();
|
||||
$test->curl->setHeader('X-DEBUG-TEST', 'put_file_handle');
|
||||
$test->curl->setOpt(CURLOPT_PUT, true);
|
||||
$test->curl->setOpt(CURLOPT_INFILE, $tmp_file);
|
||||
$test->curl->setOpt(CURLOPT_INFILESIZE, strlen($png));
|
||||
$test->curl->put(Test::TEST_URL);
|
||||
|
||||
fclose($tmp_file);
|
||||
|
||||
$this->assertTrue($test->curl->response === 'image/png');
|
||||
}
|
||||
|
||||
public function testPatchRequestMethod() {
|
||||
$test = new Test();
|
||||
$this->assertTrue($test->server('request_method', 'PATCH') === 'PATCH');
|
||||
}
|
||||
|
||||
public function testDelete() {
|
||||
$test = new Test();
|
||||
$this->assertTrue($test->server('server', 'DELETE', array(
|
||||
'key' => 'REQUEST_METHOD',
|
||||
)) === 'DELETE');
|
||||
|
||||
$test = new Test();
|
||||
$this->assertTrue($test->server('delete', 'DELETE', array(
|
||||
'test' => 'delete',
|
||||
'key' => 'test',
|
||||
)) === 'delete');
|
||||
}
|
||||
|
||||
public function testHeadRequestMethod() {
|
||||
$test = new Test();
|
||||
$test->server('request_method', 'HEAD', array(
|
||||
'key' => 'REQUEST_METHOD',
|
||||
));
|
||||
$this->assertEquals($test->curl->response_headers['X-REQUEST-METHOD'], 'HEAD');
|
||||
$this->assertEmpty($test->curl->response);
|
||||
}
|
||||
|
||||
public function testOptionsRequestMethod() {
|
||||
$test = new Test();
|
||||
$test->server('request_method', 'OPTIONS', array(
|
||||
'key' => 'REQUEST_METHOD',
|
||||
));
|
||||
$this->assertEquals($test->curl->response_headers['X-REQUEST-METHOD'], 'OPTIONS');
|
||||
}
|
||||
|
||||
public function testBasicHttpAuth401Unauthorized() {
|
||||
$test = new Test();
|
||||
$this->assertTrue($test->server('http_basic_auth', 'GET') === 'canceled');
|
||||
}
|
||||
|
||||
public function testBasicHttpAuthSuccess() {
|
||||
$username = 'myusername';
|
||||
$password = 'mypassword';
|
||||
$test = new Test();
|
||||
$test->curl->setBasicAuthentication($username, $password);
|
||||
$test->server('http_basic_auth', 'GET');
|
||||
$json = $test->curl->response;
|
||||
$this->assertTrue($json->username === $username);
|
||||
$this->assertTrue($json->password === $password);
|
||||
}
|
||||
|
||||
public function testReferrer() {
|
||||
$test = new Test();
|
||||
$test->curl->setReferrer('myreferrer');
|
||||
$this->assertTrue($test->server('server', 'GET', array(
|
||||
'key' => 'HTTP_REFERER',
|
||||
)) === 'myreferrer');
|
||||
}
|
||||
|
||||
public function testCookies() {
|
||||
$test = new Test();
|
||||
$test->curl->setCookie('mycookie', 'yum');
|
||||
$this->assertTrue($test->server('cookie', 'GET', array(
|
||||
'key' => 'mycookie',
|
||||
)) === 'yum');
|
||||
}
|
||||
|
||||
public function testCookieFile() {
|
||||
$cookie_file = dirname(__FILE__) . '/cookies.txt';
|
||||
$cookie_data = implode("\t", array(
|
||||
'127.0.0.1', // domain
|
||||
'FALSE', // tailmatch
|
||||
'/', // path
|
||||
'FALSE', // secure
|
||||
'0', // expires
|
||||
'mycookie', // name
|
||||
'yum', // value
|
||||
));
|
||||
file_put_contents($cookie_file, $cookie_data);
|
||||
|
||||
$test = new Test();
|
||||
$test->curl->setCookieFile($cookie_file);
|
||||
$this->assertTrue($test->server('cookie', 'GET', array(
|
||||
'key' => 'mycookie',
|
||||
)) === 'yum');
|
||||
|
||||
unlink($cookie_file);
|
||||
$this->assertFalse(file_exists($cookie_file));
|
||||
}
|
||||
|
||||
public function testCookieJar() {
|
||||
$cookie_file = dirname(__FILE__) . '/cookies.txt';
|
||||
|
||||
$test = new Test();
|
||||
$test->curl->setCookieJar($cookie_file);
|
||||
$test->server('cookiejar', 'GET');
|
||||
$test->curl->close();
|
||||
|
||||
$this->assertTrue(!(strpos(file_get_contents($cookie_file), "\t" . 'mycookie' . "\t" . 'yum') === false));
|
||||
unlink($cookie_file);
|
||||
$this->assertFalse(file_exists($cookie_file));
|
||||
}
|
||||
|
||||
public function testMultipleCookieResponse() {
|
||||
$expected_response = 'cookie1=scrumptious,cookie2=mouthwatering';
|
||||
|
||||
// github.com/facebook/hhvm/issues/2345
|
||||
if (defined('HHVM_VERSION')) {
|
||||
$expected_response = 'cookie2=mouthwatering,cookie1=scrumptious';
|
||||
}
|
||||
|
||||
$test = new Test();
|
||||
$test->server('multiple_cookie', 'GET');
|
||||
$this->assertEquals($test->curl->response_headers['Set-Cookie'], $expected_response);
|
||||
}
|
||||
|
||||
public function testError() {
|
||||
$test = new Test();
|
||||
$test->curl->setOpt(CURLOPT_CONNECTTIMEOUT_MS, 4000);
|
||||
$test->curl->get(Test::ERROR_URL);
|
||||
$this->assertTrue($test->curl->error);
|
||||
$this->assertTrue($test->curl->curl_error);
|
||||
$this->assertTrue($test->curl->curl_error_code === CURLE_OPERATION_TIMEOUTED);
|
||||
}
|
||||
|
||||
public function testErrorMessage() {
|
||||
$test = new Test();
|
||||
$test->server('error_message', 'GET');
|
||||
|
||||
$expected_response = 'HTTP/1.1 401 Unauthorized';
|
||||
if (defined('HHVM_VERSION')) {
|
||||
$expected_response = 'HTTP/1.1 401';
|
||||
}
|
||||
|
||||
$this->assertEquals($test->curl->error_message, $expected_response);
|
||||
}
|
||||
|
||||
public function testHeaders() {
|
||||
$test = new Test();
|
||||
$test->curl->setHeader('Content-Type', 'application/json');
|
||||
$test->curl->setHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
$test->curl->setHeader('Accept', 'application/json');
|
||||
$this->assertTrue($test->server('server', 'GET', array(
|
||||
'key' => 'HTTP_CONTENT_TYPE', // OR "CONTENT_TYPE".
|
||||
)) === 'application/json');
|
||||
$this->assertTrue($test->server('server', 'GET', array(
|
||||
'key' => 'HTTP_X_REQUESTED_WITH',
|
||||
)) === 'XMLHttpRequest');
|
||||
$this->assertTrue($test->server('server', 'GET', array(
|
||||
'key' => 'HTTP_ACCEPT',
|
||||
)) === 'application/json');
|
||||
}
|
||||
|
||||
public function testHeaderCaseSensitivity() {
|
||||
$content_type = 'application/json';
|
||||
$test = new Test();
|
||||
$test->curl->setHeader('Content-Type', $content_type);
|
||||
$test->server('response_header', 'GET');
|
||||
|
||||
$request_headers = $test->curl->request_headers;
|
||||
$response_headers = $test->curl->response_headers;
|
||||
|
||||
$this->assertEquals($request_headers['Content-Type'], $content_type);
|
||||
$this->assertEquals($request_headers['content-type'], $content_type);
|
||||
$this->assertEquals($request_headers['CONTENT-TYPE'], $content_type);
|
||||
$this->assertEquals($request_headers['cOnTeNt-TyPe'], $content_type);
|
||||
|
||||
$etag = $response_headers['ETag'];
|
||||
$this->assertEquals($response_headers['ETAG'], $etag);
|
||||
$this->assertEquals($response_headers['etag'], $etag);
|
||||
$this->assertEquals($response_headers['eTAG'], $etag);
|
||||
$this->assertEquals($response_headers['eTaG'], $etag);
|
||||
}
|
||||
|
||||
public function testRequestURL() {
|
||||
$test = new Test();
|
||||
$this->assertFalse(substr($test->server('request_uri', 'GET'), -1) === '?');
|
||||
$test = new Test();
|
||||
$this->assertFalse(substr($test->server('request_uri', 'POST'), -1) === '?');
|
||||
$test = new Test();
|
||||
$this->assertFalse(substr($test->server('request_uri', 'PUT'), -1) === '?');
|
||||
$test = new Test();
|
||||
$this->assertFalse(substr($test->server('request_uri', 'PATCH'), -1) === '?');
|
||||
$test = new Test();
|
||||
$this->assertFalse(substr($test->server('request_uri', 'DELETE'), -1) === '?');
|
||||
}
|
||||
|
||||
public function testNestedData() {
|
||||
$test = new Test();
|
||||
$data = array(
|
||||
'username' => 'myusername',
|
||||
'password' => 'mypassword',
|
||||
'more_data' => array(
|
||||
'param1' => 'something',
|
||||
'param2' => 'other thing',
|
||||
'another' => array(
|
||||
'extra' => 'level',
|
||||
'because' => 'I need it',
|
||||
),
|
||||
),
|
||||
);
|
||||
$this->assertTrue(
|
||||
$test->server('post', 'POST', $data) === http_build_query($data)
|
||||
);
|
||||
}
|
||||
|
||||
public function testPostContentTypes() {
|
||||
$test = new Test();
|
||||
$test->server('server', 'POST', 'foo=bar');
|
||||
$this->assertEquals($test->curl->request_headers['Content-Type'], 'application/x-www-form-urlencoded');
|
||||
|
||||
$test = new Test();
|
||||
$test->server('server', 'POST', array(
|
||||
'foo' => 'bar',
|
||||
));
|
||||
$this->assertEquals($test->curl->request_headers['Expect'], '100-continue');
|
||||
preg_match('/^multipart\/form-data; boundary=/', $test->curl->request_headers['Content-Type'], $content_type);
|
||||
$this->assertTrue(!empty($content_type));
|
||||
}
|
||||
|
||||
public function testJSONResponse() {
|
||||
function assertion($key, $value) {
|
||||
$test = new Test();
|
||||
$test->server('json_response', 'POST', array(
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
));
|
||||
|
||||
$response = $test->curl->response;
|
||||
PHPUnit_Framework_Assert::assertNotNull($response);
|
||||
PHPUnit_Framework_Assert::assertNull($response->null);
|
||||
PHPUnit_Framework_Assert::assertTrue($response->true);
|
||||
PHPUnit_Framework_Assert::assertFalse($response->false);
|
||||
PHPUnit_Framework_Assert::assertTrue(is_int($response->integer));
|
||||
PHPUnit_Framework_Assert::assertTrue(is_float($response->float));
|
||||
PHPUnit_Framework_Assert::assertEmpty($response->empty);
|
||||
PHPUnit_Framework_Assert::assertTrue(is_string($response->string));
|
||||
}
|
||||
|
||||
assertion('Content-Type', 'application/json; charset=utf-8');
|
||||
assertion('content-type', 'application/json; charset=utf-8');
|
||||
assertion('Content-Type', 'application/json');
|
||||
assertion('content-type', 'application/json');
|
||||
assertion('CONTENT-TYPE', 'application/json');
|
||||
assertion('CONTENT-TYPE', 'APPLICATION/JSON');
|
||||
}
|
||||
|
||||
public function testArrayToStringConversion() {
|
||||
$test = new Test();
|
||||
$test->server('post', 'POST', array(
|
||||
'foo' => 'bar',
|
||||
'baz' => array(
|
||||
),
|
||||
));
|
||||
$this->assertTrue($test->curl->response === 'foo=bar&baz=');
|
||||
|
||||
$test = new Test();
|
||||
$test->server('post', 'POST', array(
|
||||
'foo' => 'bar',
|
||||
'baz' => array(
|
||||
'qux' => array(
|
||||
),
|
||||
),
|
||||
));
|
||||
$this->assertTrue(urldecode($test->curl->response) ===
|
||||
'foo=bar&baz[qux]='
|
||||
);
|
||||
|
||||
$test = new Test();
|
||||
$test->server('post', 'POST', array(
|
||||
'foo' => 'bar',
|
||||
'baz' => array(
|
||||
'qux' => array(
|
||||
),
|
||||
'wibble' => 'wobble',
|
||||
),
|
||||
));
|
||||
$this->assertTrue(urldecode($test->curl->response) ===
|
||||
'foo=bar&baz[qux]=&baz[wibble]=wobble'
|
||||
);
|
||||
}
|
||||
|
||||
public function testParallelRequests() {
|
||||
$test = new Test();
|
||||
$curl = $test->curl;
|
||||
$curl->beforeSend(function($instance) {
|
||||
$instance->setHeader('X-DEBUG-TEST', 'request_uri');
|
||||
});
|
||||
$curl->get(array(
|
||||
Test::TEST_URL . 'a/',
|
||||
Test::TEST_URL . 'b/',
|
||||
Test::TEST_URL . 'c/',
|
||||
), array(
|
||||
'foo' => 'bar',
|
||||
));
|
||||
|
||||
$len = strlen('/a/?foo=bar');
|
||||
$this->assertTrue(substr($curl->curls['0']->response, - $len) === '/a/?foo=bar');
|
||||
$this->assertTrue(substr($curl->curls['1']->response, - $len) === '/b/?foo=bar');
|
||||
$this->assertTrue(substr($curl->curls['2']->response, - $len) === '/c/?foo=bar');
|
||||
}
|
||||
|
||||
public function testParallelSetOptions() {
|
||||
$test = new Test();
|
||||
$curl = $test->curl;
|
||||
$curl->setHeader('X-DEBUG-TEST', 'server');
|
||||
$curl->setOpt(CURLOPT_USERAGENT, 'useragent');
|
||||
$curl->complete(function($instance) {
|
||||
PHPUnit_Framework_Assert::assertTrue($instance->response === 'useragent');
|
||||
});
|
||||
$curl->get(array(
|
||||
Test::TEST_URL,
|
||||
), array(
|
||||
'key' => 'HTTP_USER_AGENT',
|
||||
));
|
||||
}
|
||||
|
||||
public function testSuccessCallback() {
|
||||
$success_called = false;
|
||||
$error_called = false;
|
||||
$complete_called = false;
|
||||
|
||||
$test = new Test();
|
||||
$curl = $test->curl;
|
||||
$curl->setHeader('X-DEBUG-TEST', 'get');
|
||||
|
||||
$curl->success(function($instance) use (&$success_called, &$error_called, &$complete_called) {
|
||||
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
|
||||
PHPUnit_Framework_Assert::assertFalse($success_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($error_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($complete_called);
|
||||
$success_called = true;
|
||||
});
|
||||
$curl->error(function($instance) use (&$success_called, &$error_called, &$complete_called, &$curl) {
|
||||
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
|
||||
PHPUnit_Framework_Assert::assertFalse($success_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($error_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($complete_called);
|
||||
$error_called = true;
|
||||
});
|
||||
$curl->complete(function($instance) use (&$success_called, &$error_called, &$complete_called) {
|
||||
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
|
||||
PHPUnit_Framework_Assert::assertTrue($success_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($error_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($complete_called);
|
||||
$complete_called = true;
|
||||
});
|
||||
|
||||
$curl->get(Test::TEST_URL);
|
||||
|
||||
$this->assertTrue($success_called);
|
||||
$this->assertFalse($error_called);
|
||||
$this->assertTrue($complete_called);
|
||||
}
|
||||
|
||||
public function testParallelSuccessCallback() {
|
||||
$success_called = false;
|
||||
$error_called = false;
|
||||
$complete_called = false;
|
||||
|
||||
$success_called_once = false;
|
||||
$error_called_once = false;
|
||||
$complete_called_once = false;
|
||||
|
||||
$test = new Test();
|
||||
$curl = $test->curl;
|
||||
$curl->setHeader('X-DEBUG-TEST', 'get');
|
||||
|
||||
$curl->success(function($instance) use (&$success_called,
|
||||
&$error_called,
|
||||
&$complete_called,
|
||||
&$success_called_once) {
|
||||
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
|
||||
PHPUnit_Framework_Assert::assertFalse($success_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($error_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($complete_called);
|
||||
$success_called = true;
|
||||
$success_called_once = true;
|
||||
});
|
||||
$curl->error(function($instance) use (&$success_called,
|
||||
&$error_called,
|
||||
&$complete_called,
|
||||
&$curl,
|
||||
&$error_called_once) {
|
||||
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
|
||||
PHPUnit_Framework_Assert::assertFalse($success_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($error_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($complete_called);
|
||||
$error_called = true;
|
||||
$error_called_once = true;
|
||||
});
|
||||
$curl->complete(function($instance) use (&$success_called,
|
||||
&$error_called,
|
||||
&$complete_called,
|
||||
&$complete_called_once) {
|
||||
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
|
||||
PHPUnit_Framework_Assert::assertTrue($success_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($error_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($complete_called);
|
||||
$complete_called = true;
|
||||
$complete_called_once = true;
|
||||
|
||||
PHPUnit_Framework_Assert::assertTrue($success_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($error_called);
|
||||
PHPUnit_Framework_Assert::assertTrue($complete_called);
|
||||
|
||||
$success_called = false;
|
||||
$error_called = false;
|
||||
$complete_called = false;
|
||||
});
|
||||
|
||||
$curl->get(array(
|
||||
Test::TEST_URL . 'a/',
|
||||
Test::TEST_URL . 'b/',
|
||||
Test::TEST_URL . 'c/',
|
||||
));
|
||||
|
||||
PHPUnit_Framework_Assert::assertTrue($success_called_once || $error_called_once);
|
||||
PHPUnit_Framework_Assert::assertTrue($complete_called_once);
|
||||
}
|
||||
|
||||
public function testErrorCallback() {
|
||||
$success_called = false;
|
||||
$error_called = false;
|
||||
$complete_called = false;
|
||||
|
||||
$test = new Test();
|
||||
$curl = $test->curl;
|
||||
$curl->setHeader('X-DEBUG-TEST', 'get');
|
||||
$curl->setOpt(CURLOPT_CONNECTTIMEOUT_MS, 2000);
|
||||
|
||||
$curl->success(function($instance) use (&$success_called, &$error_called, &$complete_called) {
|
||||
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
|
||||
PHPUnit_Framework_Assert::assertFalse($success_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($error_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($complete_called);
|
||||
$success_called = true;
|
||||
});
|
||||
$curl->error(function($instance) use (&$success_called, &$error_called, &$complete_called, &$curl) {
|
||||
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
|
||||
PHPUnit_Framework_Assert::assertFalse($success_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($error_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($complete_called);
|
||||
$error_called = true;
|
||||
});
|
||||
$curl->complete(function($instance) use (&$success_called, &$error_called, &$complete_called) {
|
||||
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
|
||||
PHPUnit_Framework_Assert::assertFalse($success_called);
|
||||
PHPUnit_Framework_Assert::assertTrue($error_called);
|
||||
PHPUnit_Framework_Assert::assertFalse($complete_called);
|
||||
$complete_called = true;
|
||||
});
|
||||
|
||||
$curl->get(Test::ERROR_URL);
|
||||
|
||||
$this->assertFalse($success_called);
|
||||
$this->assertTrue($error_called);
|
||||
$this->assertTrue($complete_called);
|
||||
}
|
||||
|
||||
public function testClose() {
|
||||
$test = new Test();
|
||||
$curl = $test->curl;
|
||||
$curl->setHeader('X-DEBUG-TEST', 'post');
|
||||
$curl->post(Test::TEST_URL);
|
||||
$this->assertTrue(is_resource($curl->curl));
|
||||
$curl->close();
|
||||
$this->assertFalse(is_resource($curl->curl));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException PHPUnit_Framework_Error_Warning
|
||||
*/
|
||||
public function testRequiredOptionCurlInfoHeaderOutEmitsWarning() {
|
||||
$curl = new Curl();
|
||||
$curl->setOpt(CURLINFO_HEADER_OUT, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException PHPUnit_Framework_Error_Warning
|
||||
*/
|
||||
public function testRequiredOptionCurlOptHeaderEmitsWarning() {
|
||||
$curl = new Curl();
|
||||
$curl->setOpt(CURLOPT_HEADER, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException PHPUnit_Framework_Error_Warning
|
||||
*/
|
||||
public function testRequiredOptionCurlOptReturnTransferEmitsWarning() {
|
||||
$curl = new Curl();
|
||||
$curl->setOpt(CURLOPT_RETURNTRANSFER, false);
|
||||
}
|
||||
|
||||
public function testRequestMethodSuccessiveGetRequests() {
|
||||
$test = new Test();
|
||||
test($test, 'GET', 'POST');
|
||||
test($test, 'GET', 'PUT');
|
||||
test($test, 'GET', 'PATCH');
|
||||
test($test, 'GET', 'DELETE');
|
||||
test($test, 'GET', 'HEAD');
|
||||
test($test, 'GET', 'OPTIONS');
|
||||
}
|
||||
|
||||
public function testRequestMethodSuccessivePostRequests() {
|
||||
$test = new Test();
|
||||
test($test, 'POST', 'GET');
|
||||
test($test, 'POST', 'PUT');
|
||||
test($test, 'POST', 'PATCH');
|
||||
test($test, 'POST', 'DELETE');
|
||||
test($test, 'POST', 'HEAD');
|
||||
test($test, 'POST', 'OPTIONS');
|
||||
}
|
||||
|
||||
public function testRequestMethodSuccessivePutRequests() {
|
||||
$test = new Test();
|
||||
test($test, 'PUT', 'GET');
|
||||
test($test, 'PUT', 'POST');
|
||||
test($test, 'PUT', 'PATCH');
|
||||
test($test, 'PUT', 'DELETE');
|
||||
test($test, 'PUT', 'HEAD');
|
||||
test($test, 'PUT', 'OPTIONS');
|
||||
}
|
||||
|
||||
public function testRequestMethodSuccessivePatchRequests() {
|
||||
$test = new Test();
|
||||
test($test, 'PATCH', 'GET');
|
||||
test($test, 'PATCH', 'POST');
|
||||
test($test, 'PATCH', 'PUT');
|
||||
test($test, 'PATCH', 'DELETE');
|
||||
test($test, 'PATCH', 'HEAD');
|
||||
test($test, 'PATCH', 'OPTIONS');
|
||||
}
|
||||
|
||||
public function testRequestMethodSuccessiveDeleteRequests() {
|
||||
$test = new Test();
|
||||
test($test, 'DELETE', 'GET');
|
||||
test($test, 'DELETE', 'POST');
|
||||
test($test, 'DELETE', 'PUT');
|
||||
test($test, 'DELETE', 'PATCH');
|
||||
test($test, 'DELETE', 'HEAD');
|
||||
test($test, 'DELETE', 'OPTIONS');
|
||||
}
|
||||
|
||||
public function testRequestMethodSuccessiveHeadRequests() {
|
||||
$test = new Test();
|
||||
test($test, 'HEAD', 'GET');
|
||||
test($test, 'HEAD', 'POST');
|
||||
test($test, 'HEAD', 'PUT');
|
||||
test($test, 'HEAD', 'PATCH');
|
||||
test($test, 'HEAD', 'DELETE');
|
||||
test($test, 'HEAD', 'OPTIONS');
|
||||
}
|
||||
|
||||
public function testRequestMethodSuccessiveOptionsRequests() {
|
||||
$test = new Test();
|
||||
test($test, 'OPTIONS', 'GET');
|
||||
test($test, 'OPTIONS', 'POST');
|
||||
test($test, 'OPTIONS', 'PUT');
|
||||
test($test, 'OPTIONS', 'PATCH');
|
||||
test($test, 'OPTIONS', 'DELETE');
|
||||
test($test, 'OPTIONS', 'HEAD');
|
||||
}
|
||||
}
|
47
web/app/vendor/php-curl-class/php-curl-class/tests/PHPCurlClass/helper.inc.php
vendored
Normal file
47
web/app/vendor/php-curl-class/php-curl-class/tests/PHPCurlClass/helper.inc.php
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
class Test {
|
||||
const TEST_URL = 'http://127.0.0.1:8000/';
|
||||
const ERROR_URL = 'https://1.2.3.4/';
|
||||
|
||||
function __construct() {
|
||||
$this->curl = new Curl();
|
||||
$this->curl->setOpt(CURLOPT_SSL_VERIFYPEER, false);
|
||||
$this->curl->setOpt(CURLOPT_SSL_VERIFYHOST, false);
|
||||
}
|
||||
|
||||
function server($test, $request_method, $data=array()) {
|
||||
$this->curl->setHeader('X-DEBUG-TEST', $test);
|
||||
$request_method = strtolower($request_method);
|
||||
$this->curl->$request_method(self::TEST_URL, $data);
|
||||
return $this->curl->response;
|
||||
}
|
||||
}
|
||||
|
||||
function test($instance, $before, $after) {
|
||||
$instance->server('request_method', $before);
|
||||
PHPUnit_Framework_Assert::assertEquals($instance->curl->response_headers['X-REQUEST-METHOD'], $before);
|
||||
$instance->server('request_method', $after);
|
||||
PHPUnit_Framework_Assert::assertEquals($instance->curl->response_headers['X-REQUEST-METHOD'], $after);
|
||||
}
|
||||
|
||||
function create_png() {
|
||||
// PNG image data, 1 x 1, 1-bit colormap, non-interlaced
|
||||
ob_start();
|
||||
imagepng(imagecreatefromstring(base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7')));
|
||||
$raw_image = ob_get_contents();
|
||||
ob_end_clean();
|
||||
return $raw_image;
|
||||
}
|
||||
|
||||
function create_tmp_file($data) {
|
||||
$tmp_file = tmpfile();
|
||||
fwrite($tmp_file, $data);
|
||||
rewind($tmp_file);
|
||||
return $tmp_file;
|
||||
}
|
||||
|
||||
function get_png() {
|
||||
$tmp_filename = tempnam('/tmp', 'php-curl-class.');
|
||||
file_put_contents($tmp_filename, create_png());
|
||||
return $tmp_filename;
|
||||
}
|
1
web/app/vendor/php-curl-class/php-curl-class/tests/PHPCurlClass/index.php
vendored
Symbolic link
1
web/app/vendor/php-curl-class/php-curl-class/tests/PHPCurlClass/index.php
vendored
Symbolic link
@ -0,0 +1 @@
|
||||
server.php
|
7
web/app/vendor/php-curl-class/php-curl-class/tests/PHPCurlClass/server.hdf
vendored
Normal file
7
web/app/vendor/php-curl-class/php-curl-class/tests/PHPCurlClass/server.hdf
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
Log {
|
||||
Level = Verbose
|
||||
}
|
||||
|
||||
Server {
|
||||
DefaultDocument = index.php
|
||||
}
|
132
web/app/vendor/php-curl-class/php-curl-class/tests/PHPCurlClass/server.php
vendored
Normal file
132
web/app/vendor/php-curl-class/php-curl-class/tests/PHPCurlClass/server.php
vendored
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
$http_raw_post_data = file_get_contents('php://input');
|
||||
$_PUT = array();
|
||||
$_PATCH = array();
|
||||
|
||||
$request_method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '';
|
||||
$content_type = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '';
|
||||
$data_values = $_GET;
|
||||
if ($request_method === 'POST') {
|
||||
$data_values = $_POST;
|
||||
}
|
||||
else if ($request_method === 'PUT') {
|
||||
if (strpos($content_type, 'application/x-www-form-urlencoded') === 0) {
|
||||
parse_str($http_raw_post_data, $_PUT);
|
||||
$data_values = $_PUT;
|
||||
}
|
||||
}
|
||||
else if ($request_method === 'PATCH') {
|
||||
if (strpos($content_type, 'application/x-www-form-urlencoded') === 0) {
|
||||
parse_str($http_raw_post_data, $_PATCH);
|
||||
$data_values = $_PATCH;
|
||||
}
|
||||
}
|
||||
|
||||
$test = isset($_SERVER['HTTP_X_DEBUG_TEST']) ? $_SERVER['HTTP_X_DEBUG_TEST'] : '';
|
||||
$key = isset($data_values['key']) ? $data_values['key'] : '';
|
||||
|
||||
if ($test == 'http_basic_auth') {
|
||||
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
header('WWW-Authenticate: Basic realm="My Realm"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
echo 'canceled';
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(array(
|
||||
'username' => $_SERVER['PHP_AUTH_USER'],
|
||||
'password' => $_SERVER['PHP_AUTH_PW'],
|
||||
));
|
||||
exit;
|
||||
}
|
||||
else if ($test === 'get') {
|
||||
echo http_build_query($_GET);
|
||||
exit;
|
||||
}
|
||||
else if ($test === 'post') {
|
||||
echo http_build_query($_POST);
|
||||
exit;
|
||||
}
|
||||
else if ($test === 'put') {
|
||||
echo $http_raw_post_data;
|
||||
exit;
|
||||
}
|
||||
else if ($test === 'post_multidimensional') {
|
||||
echo $http_raw_post_data;
|
||||
exit;
|
||||
}
|
||||
else if ($test === 'post_file_path_upload') {
|
||||
echo mime_content_type($_FILES[$key]['tmp_name']);
|
||||
exit;
|
||||
}
|
||||
else if ($test === 'put_file_handle') {
|
||||
$tmp_filename = tempnam('/tmp', 'php-curl-class.');
|
||||
file_put_contents($tmp_filename, $http_raw_post_data);
|
||||
echo mime_content_type($tmp_filename);
|
||||
unlink($tmp_filename);
|
||||
exit;
|
||||
}
|
||||
else if ($test === 'request_method') {
|
||||
header('X-REQUEST-METHOD: ' . $request_method);
|
||||
echo $request_method;
|
||||
exit;
|
||||
}
|
||||
else if ($test === 'request_uri') {
|
||||
echo $_SERVER['REQUEST_URI'];
|
||||
exit;
|
||||
}
|
||||
else if ($test === 'cookiejar') {
|
||||
setcookie('mycookie', 'yum');
|
||||
exit;
|
||||
}
|
||||
else if ($test === 'multiple_cookie') {
|
||||
setcookie('cookie1', 'scrumptious');
|
||||
setcookie('cookie2', 'mouthwatering');
|
||||
exit;
|
||||
}
|
||||
else if ($test === 'response_header') {
|
||||
header('Content-Type: application/json');
|
||||
header('ETag: ' . md5('worldpeace'));
|
||||
exit;
|
||||
}
|
||||
else if ($test === 'json_response') {
|
||||
$key = $_POST['key'];
|
||||
$value = $_POST['value'];
|
||||
header($key . ': ' . $value);
|
||||
echo json_encode(array(
|
||||
'null' => null,
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'integer' => 1,
|
||||
'float' => 3.14,
|
||||
'empty' => '',
|
||||
'string' => 'string',
|
||||
));
|
||||
exit;
|
||||
}
|
||||
else if ($test === 'error_message') {
|
||||
if (function_exists('http_response_code')) {
|
||||
http_response_code(401);
|
||||
}
|
||||
else {
|
||||
header('HTTP/1.1 401 Unauthorized');
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$data_mapping = array(
|
||||
'cookie' => '_COOKIE',
|
||||
'delete' => '_GET',
|
||||
'get' => '_GET',
|
||||
'patch' => '_PATCH',
|
||||
'post' => '_POST',
|
||||
'put' => '_PUT',
|
||||
'server' => '_SERVER',
|
||||
);
|
||||
|
||||
$data = $$data_mapping[$test];
|
||||
$value = isset($data[$key]) ? $data[$key] : '';
|
||||
echo $value;
|
8
web/app/vendor/php-curl-class/php-curl-class/tests/phpunit.xml
vendored
Normal file
8
web/app/vendor/php-curl-class/php-curl-class/tests/phpunit.xml
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
<phpunit>
|
||||
<testsuite name="PHPCurlClass">
|
||||
<directory>.</directory>
|
||||
</testsuite>
|
||||
<logging>
|
||||
<log type="coverage-text" target="php://stdout" showUncoveredFiles="false" />
|
||||
</logging>
|
||||
</phpunit>
|
4
web/app/vendor/php-curl-class/php-curl-class/tests/run.sh
vendored
Normal file
4
web/app/vendor/php-curl-class/php-curl-class/tests/run.sh
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
php -S 127.0.0.1:8000 -t PHPCurlClass/ &
|
||||
pid=$!
|
||||
phpunit --configuration phpunit.xml
|
||||
kill $pid
|
1
web/app/vendor/php-curl-class/php-curl-class/tests/syntax.sh
vendored
Normal file
1
web/app/vendor/php-curl-class/php-curl-class/tests/syntax.sh
vendored
Normal file
@ -0,0 +1 @@
|
||||
phpcs --standard=PSR2 ../src/Curl.class.php
|
3
web/app/vendor/symfony/deprecation-contracts/.gitignore
vendored
Normal file
3
web/app/vendor/symfony/deprecation-contracts/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
vendor/
|
||||
composer.lock
|
||||
phpunit.xml
|
5
web/app/vendor/symfony/deprecation-contracts/CHANGELOG.md
vendored
Normal file
5
web/app/vendor/symfony/deprecation-contracts/CHANGELOG.md
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
The changelog is maintained for all Symfony contracts at the following URL:
|
||||
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
|
19
web/app/vendor/symfony/deprecation-contracts/LICENSE
vendored
Normal file
19
web/app/vendor/symfony/deprecation-contracts/LICENSE
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2020-2022 Fabien Potencier
|
||||
|
||||
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.
|
26
web/app/vendor/symfony/deprecation-contracts/README.md
vendored
Normal file
26
web/app/vendor/symfony/deprecation-contracts/README.md
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
Symfony Deprecation Contracts
|
||||
=============================
|
||||
|
||||
A generic function and convention to trigger deprecation notices.
|
||||
|
||||
This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
|
||||
|
||||
By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
|
||||
the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
|
||||
|
||||
The function requires at least 3 arguments:
|
||||
- the name of the Composer package that is triggering the deprecation
|
||||
- the version of the package that introduced the deprecation
|
||||
- the message of the deprecation
|
||||
- more arguments can be provided: they will be inserted in the message using `printf()` formatting
|
||||
|
||||
Example:
|
||||
```php
|
||||
trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
|
||||
```
|
||||
|
||||
This will generate the following message:
|
||||
`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
|
||||
|
||||
While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty
|
||||
`function trigger_deprecation() {}` in your application.
|
35
web/app/vendor/symfony/deprecation-contracts/composer.json
vendored
Normal file
35
web/app/vendor/symfony/deprecation-contracts/composer.json
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"type": "library",
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"function.php"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.5-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
}
|
||||
}
|
27
web/app/vendor/symfony/deprecation-contracts/function.php
vendored
Normal file
27
web/app/vendor/symfony/deprecation-contracts/function.php
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
if (!function_exists('trigger_deprecation')) {
|
||||
/**
|
||||
* Triggers a silenced deprecation notice.
|
||||
*
|
||||
* @param string $package The name of the Composer package that is triggering the deprecation
|
||||
* @param string $version The version of the package that introduced the deprecation
|
||||
* @param string $message The message of the deprecation
|
||||
* @param mixed ...$args Values to insert in the message using printf() formatting
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
function trigger_deprecation(string $package, string $version, string $message, ...$args): void
|
||||
{
|
||||
@trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
|
||||
}
|
||||
}
|
5
web/app/vendor/symfony/finder/CHANGELOG.md
vendored
5
web/app/vendor/symfony/finder/CHANGELOG.md
vendored
@ -1,11 +1,6 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
6.0
|
||||
---
|
||||
|
||||
* Remove `Comparator::setTarget()` and `Comparator::setOperator()`
|
||||
|
||||
5.4.0
|
||||
-----
|
||||
|
||||
|
@ -16,47 +16,102 @@ namespace Symfony\Component\Finder\Comparator;
|
||||
*/
|
||||
class Comparator
|
||||
{
|
||||
private string $target;
|
||||
private string $operator;
|
||||
private $target;
|
||||
private $operator = '==';
|
||||
|
||||
public function __construct(string $target, string $operator = '==')
|
||||
public function __construct(string $target = null, string $operator = '==')
|
||||
{
|
||||
if (!\in_array($operator, ['>', '<', '>=', '<=', '==', '!='])) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid operator "%s".', $operator));
|
||||
if (null === $target) {
|
||||
trigger_deprecation('symfony/finder', '5.4', 'Constructing a "%s" without setting "$target" is deprecated.', __CLASS__);
|
||||
}
|
||||
|
||||
$this->target = $target;
|
||||
$this->operator = $operator;
|
||||
$this->doSetOperator($operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the target value.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTarget(): string
|
||||
public function getTarget()
|
||||
{
|
||||
if (null === $this->target) {
|
||||
trigger_deprecation('symfony/finder', '5.4', 'Calling "%s" without initializing the target is deprecated.', __METHOD__);
|
||||
}
|
||||
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the comparison operator.
|
||||
* @deprecated set the target via the constructor instead
|
||||
*/
|
||||
public function getOperator(): string
|
||||
public function setTarget(string $target)
|
||||
{
|
||||
trigger_deprecation('symfony/finder', '5.4', '"%s" is deprecated. Set the target via the constructor instead.', __METHOD__);
|
||||
|
||||
$this->target = $target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the comparison operator.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOperator()
|
||||
{
|
||||
return $this->operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests against the target.
|
||||
* Sets the comparison operator.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @deprecated set the operator via the constructor instead
|
||||
*/
|
||||
public function test(mixed $test): bool
|
||||
public function setOperator(string $operator)
|
||||
{
|
||||
return match ($this->operator) {
|
||||
'>' => $test > $this->target,
|
||||
'>=' => $test >= $this->target,
|
||||
'<' => $test < $this->target,
|
||||
'<=' => $test <= $this->target,
|
||||
'!=' => $test != $this->target,
|
||||
default => $test == $this->target,
|
||||
};
|
||||
trigger_deprecation('symfony/finder', '5.4', '"%s" is deprecated. Set the operator via the constructor instead.', __METHOD__);
|
||||
|
||||
$this->doSetOperator('' === $operator ? '==' : $operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests against the target.
|
||||
*
|
||||
* @param mixed $test A test value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function test($test)
|
||||
{
|
||||
if (null === $this->target) {
|
||||
trigger_deprecation('symfony/finder', '5.4', 'Calling "%s" without initializing the target is deprecated.', __METHOD__);
|
||||
}
|
||||
|
||||
switch ($this->operator) {
|
||||
case '>':
|
||||
return $test > $this->target;
|
||||
case '>=':
|
||||
return $test >= $this->target;
|
||||
case '<':
|
||||
return $test < $this->target;
|
||||
case '<=':
|
||||
return $test <= $this->target;
|
||||
case '!=':
|
||||
return $test != $this->target;
|
||||
}
|
||||
|
||||
return $test == $this->target;
|
||||
}
|
||||
|
||||
private function doSetOperator(string $operator): void
|
||||
{
|
||||
if (!\in_array($operator, ['>', '<', '>=', '<=', '==', '!='])) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid operator "%s".', $operator));
|
||||
}
|
||||
|
||||
$this->operator = $operator;
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ class DateComparator extends Comparator
|
||||
try {
|
||||
$date = new \DateTime($matches[2]);
|
||||
$target = $date->format('U');
|
||||
} catch (\Exception) {
|
||||
} catch (\Exception $e) {
|
||||
throw new \InvalidArgumentException(sprintf('"%s" is not a valid date.', $matches[2]));
|
||||
}
|
||||
|
||||
|
114
web/app/vendor/symfony/finder/Finder.php
vendored
114
web/app/vendor/symfony/finder/Finder.php
vendored
@ -45,27 +45,27 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
public const IGNORE_DOT_FILES = 2;
|
||||
public const IGNORE_VCS_IGNORED_FILES = 4;
|
||||
|
||||
private int $mode = 0;
|
||||
private array $names = [];
|
||||
private array $notNames = [];
|
||||
private array $exclude = [];
|
||||
private array $filters = [];
|
||||
private array $depths = [];
|
||||
private array $sizes = [];
|
||||
private bool $followLinks = false;
|
||||
private bool $reverseSorting = false;
|
||||
private \Closure|int|false $sort = false;
|
||||
private int $ignore = 0;
|
||||
private array $dirs = [];
|
||||
private array $dates = [];
|
||||
private array $iterators = [];
|
||||
private array $contains = [];
|
||||
private array $notContains = [];
|
||||
private array $paths = [];
|
||||
private array $notPaths = [];
|
||||
private bool $ignoreUnreadableDirs = false;
|
||||
private $mode = 0;
|
||||
private $names = [];
|
||||
private $notNames = [];
|
||||
private $exclude = [];
|
||||
private $filters = [];
|
||||
private $depths = [];
|
||||
private $sizes = [];
|
||||
private $followLinks = false;
|
||||
private $reverseSorting = false;
|
||||
private $sort = false;
|
||||
private $ignore = 0;
|
||||
private $dirs = [];
|
||||
private $dates = [];
|
||||
private $iterators = [];
|
||||
private $contains = [];
|
||||
private $notContains = [];
|
||||
private $paths = [];
|
||||
private $notPaths = [];
|
||||
private $ignoreUnreadableDirs = false;
|
||||
|
||||
private static array $vcsPatterns = ['.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg'];
|
||||
private static $vcsPatterns = ['.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@ -74,8 +74,10 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
|
||||
/**
|
||||
* Creates a new Finder.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function create(): static
|
||||
public static function create()
|
||||
{
|
||||
return new static();
|
||||
}
|
||||
@ -85,7 +87,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function directories(): static
|
||||
public function directories()
|
||||
{
|
||||
$this->mode = Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES;
|
||||
|
||||
@ -97,7 +99,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function files(): static
|
||||
public function files()
|
||||
{
|
||||
$this->mode = Iterator\FileTypeFilterIterator::ONLY_FILES;
|
||||
|
||||
@ -120,7 +122,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
* @see DepthRangeFilterIterator
|
||||
* @see NumberComparator
|
||||
*/
|
||||
public function depth(string|int|array $levels): static
|
||||
public function depth($levels)
|
||||
{
|
||||
foreach ((array) $levels as $level) {
|
||||
$this->depths[] = new Comparator\NumberComparator($level);
|
||||
@ -148,7 +150,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
* @see DateRangeFilterIterator
|
||||
* @see DateComparator
|
||||
*/
|
||||
public function date(string|array $dates): static
|
||||
public function date($dates)
|
||||
{
|
||||
foreach ((array) $dates as $date) {
|
||||
$this->dates[] = new Comparator\DateComparator($date);
|
||||
@ -173,7 +175,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see FilenameFilterIterator
|
||||
*/
|
||||
public function name(string|array $patterns): static
|
||||
public function name($patterns)
|
||||
{
|
||||
$this->names = array_merge($this->names, (array) $patterns);
|
||||
|
||||
@ -189,7 +191,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see FilenameFilterIterator
|
||||
*/
|
||||
public function notName(string|array $patterns): static
|
||||
public function notName($patterns)
|
||||
{
|
||||
$this->notNames = array_merge($this->notNames, (array) $patterns);
|
||||
|
||||
@ -211,7 +213,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see FilecontentFilterIterator
|
||||
*/
|
||||
public function contains(string|array $patterns): static
|
||||
public function contains($patterns)
|
||||
{
|
||||
$this->contains = array_merge($this->contains, (array) $patterns);
|
||||
|
||||
@ -233,7 +235,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see FilecontentFilterIterator
|
||||
*/
|
||||
public function notContains(string|array $patterns): static
|
||||
public function notContains($patterns)
|
||||
{
|
||||
$this->notContains = array_merge($this->notContains, (array) $patterns);
|
||||
|
||||
@ -257,7 +259,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see FilenameFilterIterator
|
||||
*/
|
||||
public function path(string|array $patterns): static
|
||||
public function path($patterns)
|
||||
{
|
||||
$this->paths = array_merge($this->paths, (array) $patterns);
|
||||
|
||||
@ -281,7 +283,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see FilenameFilterIterator
|
||||
*/
|
||||
public function notPath(string|array $patterns): static
|
||||
public function notPath($patterns)
|
||||
{
|
||||
$this->notPaths = array_merge($this->notPaths, (array) $patterns);
|
||||
|
||||
@ -303,7 +305,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
* @see SizeRangeFilterIterator
|
||||
* @see NumberComparator
|
||||
*/
|
||||
public function size(string|int|array $sizes): static
|
||||
public function size($sizes)
|
||||
{
|
||||
foreach ((array) $sizes as $size) {
|
||||
$this->sizes[] = new Comparator\NumberComparator($size);
|
||||
@ -325,7 +327,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see ExcludeDirectoryFilterIterator
|
||||
*/
|
||||
public function exclude(string|array $dirs): static
|
||||
public function exclude($dirs)
|
||||
{
|
||||
$this->exclude = array_merge($this->exclude, (array) $dirs);
|
||||
|
||||
@ -341,7 +343,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see ExcludeDirectoryFilterIterator
|
||||
*/
|
||||
public function ignoreDotFiles(bool $ignoreDotFiles): static
|
||||
public function ignoreDotFiles(bool $ignoreDotFiles)
|
||||
{
|
||||
if ($ignoreDotFiles) {
|
||||
$this->ignore |= static::IGNORE_DOT_FILES;
|
||||
@ -361,7 +363,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see ExcludeDirectoryFilterIterator
|
||||
*/
|
||||
public function ignoreVCS(bool $ignoreVCS): static
|
||||
public function ignoreVCS(bool $ignoreVCS)
|
||||
{
|
||||
if ($ignoreVCS) {
|
||||
$this->ignore |= static::IGNORE_VCS_FILES;
|
||||
@ -379,7 +381,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function ignoreVCSIgnored(bool $ignoreVCSIgnored): static
|
||||
public function ignoreVCSIgnored(bool $ignoreVCSIgnored)
|
||||
{
|
||||
if ($ignoreVCSIgnored) {
|
||||
$this->ignore |= static::IGNORE_VCS_IGNORED_FILES;
|
||||
@ -397,7 +399,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @param string|string[] $pattern VCS patterns to ignore
|
||||
*/
|
||||
public static function addVCSPattern(string|array $pattern)
|
||||
public static function addVCSPattern($pattern)
|
||||
{
|
||||
foreach ((array) $pattern as $p) {
|
||||
self::$vcsPatterns[] = $p;
|
||||
@ -417,7 +419,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see SortableIterator
|
||||
*/
|
||||
public function sort(\Closure $closure): static
|
||||
public function sort(\Closure $closure)
|
||||
{
|
||||
$this->sort = $closure;
|
||||
|
||||
@ -433,7 +435,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see SortableIterator
|
||||
*/
|
||||
public function sortByName(bool $useNaturalSort = false): static
|
||||
public function sortByName(bool $useNaturalSort = false)
|
||||
{
|
||||
$this->sort = $useNaturalSort ? Iterator\SortableIterator::SORT_BY_NAME_NATURAL : Iterator\SortableIterator::SORT_BY_NAME;
|
||||
|
||||
@ -449,7 +451,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see SortableIterator
|
||||
*/
|
||||
public function sortByType(): static
|
||||
public function sortByType()
|
||||
{
|
||||
$this->sort = Iterator\SortableIterator::SORT_BY_TYPE;
|
||||
|
||||
@ -467,7 +469,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see SortableIterator
|
||||
*/
|
||||
public function sortByAccessedTime(): static
|
||||
public function sortByAccessedTime()
|
||||
{
|
||||
$this->sort = Iterator\SortableIterator::SORT_BY_ACCESSED_TIME;
|
||||
|
||||
@ -479,7 +481,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function reverseSorting(): static
|
||||
public function reverseSorting()
|
||||
{
|
||||
$this->reverseSorting = true;
|
||||
|
||||
@ -499,7 +501,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see SortableIterator
|
||||
*/
|
||||
public function sortByChangedTime(): static
|
||||
public function sortByChangedTime()
|
||||
{
|
||||
$this->sort = Iterator\SortableIterator::SORT_BY_CHANGED_TIME;
|
||||
|
||||
@ -517,7 +519,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see SortableIterator
|
||||
*/
|
||||
public function sortByModifiedTime(): static
|
||||
public function sortByModifiedTime()
|
||||
{
|
||||
$this->sort = Iterator\SortableIterator::SORT_BY_MODIFIED_TIME;
|
||||
|
||||
@ -534,7 +536,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @see CustomFilterIterator
|
||||
*/
|
||||
public function filter(\Closure $closure): static
|
||||
public function filter(\Closure $closure)
|
||||
{
|
||||
$this->filters[] = $closure;
|
||||
|
||||
@ -546,7 +548,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function followLinks(): static
|
||||
public function followLinks()
|
||||
{
|
||||
$this->followLinks = true;
|
||||
|
||||
@ -560,7 +562,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function ignoreUnreadableDirs(bool $ignore = true): static
|
||||
public function ignoreUnreadableDirs(bool $ignore = true)
|
||||
{
|
||||
$this->ignoreUnreadableDirs = $ignore;
|
||||
|
||||
@ -576,7 +578,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @throws DirectoryNotFoundException if one of the directories does not exist
|
||||
*/
|
||||
public function in(string|array $dirs): static
|
||||
public function in($dirs)
|
||||
{
|
||||
$resolvedDirs = [];
|
||||
|
||||
@ -585,7 +587,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
$resolvedDirs[] = [$this->normalizeDir($dir)];
|
||||
} elseif ($glob = glob($dir, (\defined('GLOB_BRACE') ? \GLOB_BRACE : 0) | \GLOB_ONLYDIR | \GLOB_NOSORT)) {
|
||||
sort($glob);
|
||||
$resolvedDirs[] = array_map($this->normalizeDir(...), $glob);
|
||||
$resolvedDirs[] = array_map([$this, 'normalizeDir'], $glob);
|
||||
} else {
|
||||
throw new DirectoryNotFoundException(sprintf('The "%s" directory does not exist.', $dir));
|
||||
}
|
||||
@ -605,7 +607,8 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @throws \LogicException if the in() method has not been called
|
||||
*/
|
||||
public function getIterator(): \Iterator
|
||||
#[\ReturnTypeWillChange]
|
||||
public function getIterator()
|
||||
{
|
||||
if (0 === \count($this->dirs) && 0 === \count($this->iterators)) {
|
||||
throw new \LogicException('You must call one of in() or append() methods before iterating over a Finder.');
|
||||
@ -648,7 +651,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
*
|
||||
* @throws \InvalidArgumentException when the given argument is not iterable
|
||||
*/
|
||||
public function append(iterable $iterator): static
|
||||
public function append(iterable $iterator)
|
||||
{
|
||||
if ($iterator instanceof \IteratorAggregate) {
|
||||
$this->iterators[] = $iterator->getIterator();
|
||||
@ -670,8 +673,10 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
|
||||
/**
|
||||
* Check if any results were found.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasResults(): bool
|
||||
public function hasResults()
|
||||
{
|
||||
foreach ($this->getIterator() as $_) {
|
||||
return true;
|
||||
@ -682,8 +687,11 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
|
||||
/**
|
||||
* Counts all the results collected by the iterators.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
#[\ReturnTypeWillChange]
|
||||
public function count()
|
||||
{
|
||||
return iterator_count($this->getIterator());
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user