feat: Remote Judge (#28)
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Baoshuo Ren 2023-01-24 17:49:37 +08:00 committed by GitHub
commit 259bf77d58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
131 changed files with 11130 additions and 687 deletions

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -6,3 +6,5 @@ docker-compose.local.yml
.config.php
.config.development.php
.config.local.php
*.development.env
*.local.env

View File

@ -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;

View File

@ -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: ./

View File

@ -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

View File

@ -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
View File

@ -0,0 +1,3 @@
dist/
node_modules/
*-error.log

14
remote_judger/.prettierrc Normal file
View 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
View 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
View File

@ -0,0 +1,5 @@
本模块借鉴了以下项目的源码:
- https://github.com/hydro-dev/Hydro/blob/feb51804766e35dbd13f7cb74fda95c0b783c49d/packages/vjudge/
在此表示感谢。

View 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

File diff suppressed because it is too large Load Diff

View 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
View 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);
}
}
}

View 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,
});

View 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';

View 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,
});
}
}
}

View 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(/&nbsp;/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,
});
}
}
}

View 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,
});
}
}
}

View 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,
});
}
}
}

View File

@ -0,0 +1,7 @@
declare module 'superagent' {
interface Request {
proxy(url: string): this;
}
}
export default {};

View File

@ -0,0 +1,11 @@
export default function htmlspecialchars(text: string) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;',
};
return text.replace(/[&<>"']/g, m => map[m]);
}

View 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;

View 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()]);
}

View File

@ -0,0 +1,5 @@
export default function sleep(timeout: number) {
return new Promise(resolve => {
setTimeout(() => resolve(true), timeout);
});
}

View 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;

View 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
View 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;
}

View 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"]
}

View File

@ -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 ;\

View File

@ -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": [

View File

@ -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;

View File

@ -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> ';
}

View 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() ?>

View File

@ -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">

View File

@ -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();

View File

@ -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">

View File

@ -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>

View File

@ -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">
提交记录可视权限

View File

@ -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>';

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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) {

View File

@ -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,

View File

@ -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;
}

View File

@ -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];

View 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;
}
}

View File

@ -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();
}

View File

@ -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');

View File

@ -0,0 +1 @@
https://github.com/renbaoshuo/S2OJ/pull/28

View 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 评测机的虚拟评测机。');

View File

@ -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)
{

View 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;
}
}

View File

@ -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',
);

View File

@ -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',
);

View File

@ -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'),

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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
View 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,
),
),
);

View 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
);
}

View 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.

View 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];
}
});

View 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"
]
}
}

View 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);
}
}
}
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@ -0,0 +1,2 @@
vendor/
composer.lock

View 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

View 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/>

View 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',
));
```

View 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/"]
}
}

View 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";

View 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";

View 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 . '" />';
}

View 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);

View 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";

View 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);
}

View 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');
}
}

View 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;
}

View File

@ -0,0 +1 @@
server.php

View File

@ -0,0 +1,7 @@
Log {
Level = Verbose
}
Server {
DefaultDocument = index.php
}

View 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;

View File

@ -0,0 +1,8 @@
<phpunit>
<testsuite name="PHPCurlClass">
<directory>.</directory>
</testsuite>
<logging>
<log type="coverage-text" target="php://stdout" showUncoveredFiles="false" />
</logging>
</phpunit>

View File

@ -0,0 +1,4 @@
php -S 127.0.0.1:8000 -t PHPCurlClass/ &
pid=$!
phpunit --configuration phpunit.xml
kill $pid

View File

@ -0,0 +1 @@
phpcs --standard=PSR2 ../src/Curl.class.php

View File

@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View 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

View 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.

View 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.

View 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"
}
}
}

View 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);
}
}

View File

@ -1,11 +1,6 @@
CHANGELOG
=========
6.0
---
* Remove `Comparator::setTarget()` and `Comparator::setOperator()`
5.4.0
-----

View File

@ -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;
}
}

View File

@ -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]));
}

View File

@ -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