Merge branch 'master' into uoj_form_v2

This commit is contained in:
Baoshuo Ren 2023-01-31 18:30:53 +08:00
commit 54a243b517
Signed by: baoshuo
GPG Key ID: 00CB9680AB29F51A
617 changed files with 26222 additions and 1121 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,9 @@ INSERT INTO `upgrades` (`name`, `status`, `updated_at`) VALUES
('16_list_v3', 'up', now()),
('18_user_permissions', 'up', now()),
('20_problem_difficulty', 'up', now()),
('21_problem_difficulty', 'up', now());
('21_problem_difficulty', 'up', now()),
('28_remote_judge', 'up', now()),
('31_problem_resources', 'up', now());
/*!40000 ALTER TABLE `upgrades` ENABLE KEYS */;
UNLOCK TABLES;

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', '内置评测机', '用于评测本地题目的评测机。');

View File

@ -133,6 +133,9 @@ string file_preview(const string &name, const int &len = 100) {
return "";
}
struct stat stat_buf;
stat(name.c_str(), &stat_buf);
string res = "";
if (len == -1) {
int c;
@ -145,8 +148,9 @@ string file_preview(const string &name, const int &len = 100) {
res += c;
}
if ((int)res.size() > len + 3) {
int omitted = (int)stat_buf.st_size - len;
res.resize(len);
res += "...";
res += "\n\n(" + to_string(omitted) + " bytes omitted)";
}
}
fclose(f);

View File

@ -130,6 +130,9 @@ string file_preview(const string &name, const int &len = 100) {
return "";
}
struct stat stat_buf;
stat(name.c_str(), &stat_buf);
string res = "";
if (len == -1) {
int c;
@ -142,8 +145,9 @@ string file_preview(const string &name, const int &len = 100) {
res += c;
}
if ((int)res.size() > len + 3) {
int omitted = (int)stat_buf.st_size - len;
res.resize(len);
res += "...";
res += "\n\n(" + to_string(omitted) + " bytes omitted)";
}
}
fclose(f);

3
remote_judger/.gitignore vendored Normal file
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 评测机的虚拟评测机。');

2660
remote_judger/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
{
"name": "s2oj-remote-judger",
"version": "0.0.0",
"description": "Remote judger of S2OJ.",
"scripts": {
"build": "tsc -p .",
"start": "node dist/entrypoint.js"
},
"type": "module",
"repository": "https://github.com/renbaoshuo/S2OJ",
"author": "Baoshuo <i@baoshuo.ren>",
"license": "AGPL-3.0",
"private": true,
"dependencies": {
"crlf-normalize": "^1.0.18",
"fs-extra": "^11.1.0",
"jsdom": "^21.0.0",
"math-sum": "^2.0.0",
"reggol": "^1.3.4",
"superagent": "^8.0.6",
"superagent-proxy": "^3.0.0"
},
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^20.0.1",
"@types/node": "^18.11.18",
"@types/superagent": "^4.1.16",
"@types/superagent-proxy": "^3.0.0",
"js-yaml": "^4.1.0",
"typescript": "^4.9.4"
}
}

194
remote_judger/src/daemon.ts Normal file
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,384 @@
import { JSDOM } from 'jsdom';
import superagent from 'superagent';
import proxy from 'superagent-proxy';
import { crlf, LF } from 'crlf-normalize';
import sleep from '../utils/sleep';
import mathSum from 'math-sum';
import { IBasicProvider, RemoteAccount, USER_AGENT } from '../interface';
import { normalize, VERDICT } from '../verdict';
import Logger from '../utils/logger';
proxy(superagent);
const logger = new Logger('remote/codeforces');
const langs_map = {
C: {
name: 'GNU GCC C11 5.1.0',
id: 43,
comment: '//',
},
'C++': {
name: 'GNU G++14 6.4.0',
id: 50,
comment: '//',
},
'C++17': {
name: 'GNU G++17 7.3.0',
id: 54,
comment: '//',
},
'C++20': {
name: 'GNU G++20 11.2.0 (64 bit, winlibs)',
id: 73,
comment: '//',
},
Pascal: {
name: 'Free Pascal 3.0.2',
id: 4,
comment: '//',
},
'Python2.7': {
name: 'Python 2.7.18',
id: 7,
comment: '#',
},
Python3: {
name: 'Python 3.9.1',
id: 31,
comment: '#',
},
};
export function getAccountInfoFromEnv(): RemoteAccount | null {
const {
CODEFORCES_HANDLE,
CODEFORCES_PASSWORD,
CODEFORCES_ENDPOINT = 'https://codeforces.com',
CODEFORCES_PROXY,
} = process.env;
if (!CODEFORCES_HANDLE || !CODEFORCES_PASSWORD) return null;
const account: RemoteAccount = {
type: 'codeforces',
handle: CODEFORCES_HANDLE,
password: CODEFORCES_PASSWORD,
endpoint: CODEFORCES_ENDPOINT,
};
if (CODEFORCES_PROXY) account.proxy = CODEFORCES_PROXY;
return account;
}
function parseProblemId(id: string) {
const [, type, contestId, problemId] = id.startsWith('921')
? ['', '921', '01']
: /^(|GYM)(\d+)([A-Z]+[0-9]*)$/.exec(id);
if (type === 'GYM' && +contestId < 100000) {
return [type, (+contestId + 100000).toString(), problemId];
}
return [type, contestId, problemId];
}
export default class CodeforcesProvider implements IBasicProvider {
constructor(public account: RemoteAccount) {
if (account.cookie) this.cookie = account.cookie;
this.account.endpoint ||= 'https://codeforces.com';
}
cookie: string[] = [];
csrf: string;
get(url: string) {
logger.debug('get', url);
if (!url.includes('//')) url = `${this.account.endpoint}${url}`;
const req = superagent
.get(url)
.set('Cookie', this.cookie)
.set('User-Agent', USER_AGENT);
if (this.account.proxy) return req.proxy(this.account.proxy);
return req;
}
post(url: string) {
logger.debug('post', url, this.cookie);
if (!url.includes('//')) url = `${this.account.endpoint}${url}`;
const req = superagent
.post(url)
.type('form')
.set('Cookie', this.cookie)
.set('User-Agent', USER_AGENT);
if (this.account.proxy) return req.proxy(this.account.proxy);
return req;
}
getCookie(target: string) {
return this.cookie
.find(i => i.startsWith(`${target}=`))
?.split('=')[1]
?.split(';')[0];
}
setCookie(target: string, value: string) {
this.cookie = this.cookie.filter(i => !i.startsWith(`${target}=`));
this.cookie.push(`${target}=${value}`);
}
tta(_39ce7: string) {
let _tta = 0;
for (let c = 0; c < _39ce7.length; c++) {
_tta = (_tta + (c + 1) * (c + 2) * _39ce7.charCodeAt(c)) % 1009;
if (c % 3 === 0) _tta++;
if (c % 2 === 0) _tta *= 2;
if (c > 0)
_tta -=
Math.floor(_39ce7.charCodeAt(Math.floor(c / 2)) / 2) * (_tta % 5);
_tta = ((_tta % 1009) + 1009) % 1009;
}
return _tta;
}
async getCsrfToken(url: string) {
const { text: html } = await this.get(url);
const {
window: { document },
} = new JSDOM(html);
if (document.body.children.length < 2 && html.length < 512) {
throw new Error(document.body.textContent!);
}
const ftaa = this.getCookie('70a7c28f3de') || 'n/a';
const bfaa = this.getCookie('raa') || this.getCookie('bfaa') || 'n/a';
return [
(
document.querySelector('meta[name="X-Csrf-Token"]') ||
document.querySelector('input[name="csrf_token"]')
)?.getAttribute('content'),
ftaa,
bfaa,
];
}
get loggedIn() {
return this.get('/enter').then(res => {
const html = res.text;
if (html.includes('Login into Codeforces')) return false;
if (html.length < 1000 && html.includes('Redirecting...')) {
logger.debug('Got a redirect', html);
return false;
}
return true;
});
}
async ensureLogin() {
if (await this.loggedIn) return true;
logger.info('retry normal login');
const [csrf, ftaa, bfaa] = await this.getCsrfToken('/enter');
const { header } = await this.get('/enter');
if (header['set-cookie']) {
this.cookie = header['set-cookie'];
}
const res = await this.post('/enter').send({
csrf_token: csrf,
action: 'enter',
ftaa,
bfaa,
handleOrEmail: this.account.handle,
password: this.account.password,
remember: 'on',
_tta: this.tta(this.getCookie('39ce7')),
});
const cookie = res.header['set-cookie'];
if (cookie) {
this.cookie = cookie;
}
if (await this.loggedIn) {
logger.success('Logged in');
return true;
}
return false;
}
async submitProblem(
id: string,
lang: string,
code: string,
submissionId: number,
next,
end
) {
const programType = langs_map[lang] || langs_map['C++'];
const comment = programType.comment;
if (comment) {
const msg = `S2OJ Submission #${submissionId} @ ${new Date().getTime()}`;
if (typeof comment === 'string') code = `${comment} ${msg}\n${code}`;
else if (comment instanceof Array)
code = `${comment[0]} ${msg} ${comment[1]}\n${code}`;
}
const [type, contestId, problemId] = parseProblemId(id);
const [csrf, ftaa, bfaa] = await this.getCsrfToken(
type !== 'GYM' ? '/problemset/submit' : `/gym/${contestId}/submit`
);
logger.debug(
'Submitting',
id,
programType,
lang,
`(S2OJ Submission #${submissionId})`
);
// TODO: check submit time to ensure submission
const { text: submit, error } = await this.post(
`/${
type !== 'GYM' ? 'problemset' : `gym/${contestId}`
}/submit?csrf_token=${csrf}`
).send({
csrf_token: csrf,
action: 'submitSolutionFormSubmitted',
programTypeId: programType.id,
source: code,
tabsize: 4,
sourceFile: '',
ftaa,
bfaa,
_tta: this.tta(this.getCookie('39ce7')),
...(type !== 'GYM'
? {
submittedProblemCode: contestId + problemId,
sourceCodeConfirmed: true,
}
: {
submittedProblemIndex: problemId,
}),
});
if (error) {
end({
error: true,
status: 'Judgment Failed',
message: 'Failed to submit code.',
});
return null;
}
const {
window: { document: statusDocument },
} = new JSDOM(submit);
const message = Array.from(statusDocument.querySelectorAll('.error'))
.map(i => i.textContent)
.join('')
.replace(/&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: true,
status: 'Compile Error',
message: crlf(body['checkerStdoutAndStderr#1'], LF),
});
}
const time = mathSum(
Object.keys(body)
.filter(k => k.startsWith('timeConsumed#'))
.map(k => +body[k])
);
const memory =
Math.max(
...Object.keys(body)
.filter(k => k.startsWith('memoryConsumed#'))
.map(k => +body[k])
) / 1024;
await next({ test_id: body.testCount });
if (body.waiting === 'true') continue;
const testCount = +body.testCount;
const status =
VERDICT[
Object.keys(VERDICT).find(k => normalize(body.verdict).includes(k))
];
let tests: string[] = [];
for (let i = 1; i <= testCount; i++) {
let test_info = '';
let info_text =
VERDICT[
Object.keys(VERDICT).find(k =>
normalize(body[`verdict#${i}`]).includes(k)
)
];
test_info += `<test num="${i}" info="${info_text}" time="${
body[`timeConsumed#${i}`]
}" memory="${+body[`memoryConsumed#${i}`] / 1024}">`;
const parse = (id: string) => crlf(body[id], LF);
test_info += `<in>${parse(`input#${i}`)}</in>\n`;
test_info += `<out>${parse(`output#${i}`)}</out>\n`;
test_info += `<ans>${parse(`answer#${i}`)}</ans>\n`;
test_info += `<res>${parse(`checkerStdoutAndStderr#${i}`)}</res>\n`;
test_info += '</test>';
tests.push(test_info);
}
const details =
'<div>' +
`<info-block>REMOTE_SUBMISSION_ID = ${id}\nVERDICT = ${status}</info-block>` +
`<tests>${tests.join('\n')}</tests>` +
'</div>';
return await end({
id,
status,
score: status === 'Accepted' ? 100 : 0,
time,
memory,
details,
});
}
}
}

View File

@ -0,0 +1,459 @@
import superagent from 'superagent';
import proxy from 'superagent-proxy';
import { stripVTControlCharacters } from 'util';
import sleep from '../utils/sleep';
import { IBasicProvider, RemoteAccount, USER_AGENT } from '../interface';
import Logger from '../utils/logger';
import { normalize, VERDICT } from '../verdict';
import { crlf, LF } from 'crlf-normalize';
proxy(superagent);
const logger = new Logger('remote/loj');
const langs_map = {
C: {
name: 'C (gcc, c11, O2, m64)',
info: {
language: 'c',
compileAndRunOptions: {
compiler: 'gcc',
O: '2',
m: '64',
std: 'c11',
},
},
comment: '//',
},
'C++03': {
name: 'C++ (g++, c++03, O2, m64)',
info: {
language: 'cpp',
compileAndRunOptions: {
compiler: 'g++',
std: 'c++03',
O: '2',
m: '64',
},
},
comment: '//',
},
'C++11': {
name: 'C++ (g++, c++11, O2, m64)',
info: {
language: 'cpp',
compileAndRunOptions: {
compiler: 'g++',
std: 'c++11',
O: '2',
m: '64',
},
},
comment: '//',
},
'C++': {
name: 'C++ (g++, c++14, O2, m64)',
info: {
language: 'cpp',
compileAndRunOptions: {
compiler: 'g++',
std: 'c++14',
O: '2',
m: '64',
},
},
comment: '//',
},
'C++17': {
name: 'C++ (g++, c++17, O2, m64)',
info: {
language: 'cpp',
compileAndRunOptions: {
compiler: 'g++',
std: 'c++17',
O: '2',
m: '64',
},
},
comment: '//',
},
'C++20': {
name: 'C++ (g++, c++20, O2, m64)',
info: {
language: 'cpp',
compileAndRunOptions: {
compiler: 'g++',
std: 'c++20',
O: '2',
m: '64',
},
},
comment: '//',
},
'Python2.7': {
name: 'Python (2.7)',
info: {
language: 'python',
compileAndRunOptions: {
version: '2.7',
},
},
comment: '#',
},
Python3: {
name: 'Python (3.10)',
info: {
language: 'python',
compileAndRunOptions: {
version: '3.10',
},
},
comment: '#',
},
Java17: {
name: 'Java',
info: {
language: 'java',
compileAndRunOptions: {},
},
comment: '//',
},
Pascal: {
name: 'Pascal',
info: {
language: 'pascal',
compileAndRunOptions: {
O: '2',
},
},
comment: '//',
},
};
export function getAccountInfoFromEnv(): RemoteAccount | null {
const {
LOJ_HANDLE,
LOJ_TOKEN,
LOJ_ENDPOINT = 'https://api.loj.ac.cn/api',
LOJ_PROXY,
} = process.env;
if (!LOJ_TOKEN) return null;
const account: RemoteAccount = {
type: 'loj',
handle: LOJ_HANDLE,
password: LOJ_TOKEN,
endpoint: LOJ_ENDPOINT,
};
if (LOJ_PROXY) account.proxy = LOJ_PROXY;
return account;
}
export default class LibreojProvider implements IBasicProvider {
constructor(public account: RemoteAccount) {
this.account.endpoint ||= 'https://api.loj.ac.cn/api';
}
get(url: string) {
logger.debug('get', url);
if (!url.includes('//')) url = `${this.account.endpoint}${url}`;
const req = superagent
.get(url)
.auth(this.account.password, { type: 'bearer' })
.set('User-Agent', USER_AGENT);
if (this.account.proxy) return req.proxy(this.account.proxy);
return req;
}
post(url: string) {
logger.debug('post', url);
if (!url.includes('//')) url = `${this.account.endpoint}${url}`;
const req = superagent
.post(url)
.type('json')
.auth(this.account.password, { type: 'bearer' })
.set('User-Agent', USER_AGENT)
.set('x-recaptcha-token', 'skip');
if (this.account.proxy) return req.proxy(this.account.proxy);
return req;
}
get loggedIn() {
return this.get('/auth/getSessionInfo?token=' + this.account.password).then(
res =>
res.body.userMeta && res.body.userMeta.username === this.account.handle
);
}
async ensureLogin() {
if (await this.loggedIn) return true;
logger.info('retry login');
// TODO: login
return false;
}
async getProblemId(displayId: number) {
const { body } = await this.post('/problem/getProblem').send({ displayId });
return body.meta.id;
}
async submitProblem(
displayId: string,
lang: string,
code: string,
submissionId: number,
next,
end
) {
const programType = langs_map[lang] || langs_map['C++'];
const comment = programType.comment;
if (comment) {
const msg = `S2OJ Submission #${submissionId} @ ${new Date().getTime()}`;
if (typeof comment === 'string') code = `${comment} ${msg}\n${code}`;
else if (comment instanceof Array)
code = `${comment[0]} ${msg} ${comment[1]}\n${code}`;
}
const id = await this.getProblemId(parseInt(displayId));
logger.debug(
'Submitting',
id,
`(displayId: ${displayId})`,
programType,
lang,
`(S2OJ Submission #${submissionId})`
);
const { body, error } = await this.post('/submission/submit').send({
problemId: id,
content: {
code,
...programType.info,
},
uploadInfo: null,
});
if (error) {
await end({
error: true,
status: 'Judgment Failed',
message: 'Failed to submit code.',
});
return null;
}
return body.submissionId;
}
async waitForSubmission(problem_id: string, id: string, next, end) {
let i = 0;
while (true) {
if (++i > 60) {
return await end({
id,
error: true,
status: 'Judgment Failed',
message: 'Failed to fetch submission details.',
});
}
await sleep(1000);
const { body, error } = await this.post('/submission/getSubmissionDetail')
.send({ submissionId: String(id), locale: 'zh_CN' })
.retry(3);
if (error) continue;
if (!body.progress) {
await next({ status: 'Waiting for Remote Judge' });
continue;
}
await next({
status: `${body.progress.progressType}`,
});
if (body.progress.progressType !== 'Finished') {
continue;
}
const status =
VERDICT[
Object.keys(VERDICT).find(k =>
normalize(body.meta.status).includes(k)
)
];
if (status === 'Compile Error') {
await end({
error: true,
id,
status: 'Compile Error',
message: stripVTControlCharacters(body.progress.compile.message),
});
}
if (status === 'Judgment Failed') {
await end({
error: true,
id,
status: 'Judgment Failed',
message: 'Error occurred on remote online judge.',
});
}
const parse = (str: string) => crlf(str, LF);
const getSubtaskStatusDisplayText = (testcases: any): string => {
let result: string = null;
for (const testcase of testcases) {
if (!testcase.testcaseHash) {
result = 'Skipped';
break;
} else if (
body.progress.testcaseResult[testcase.testcaseHash].status !==
'Accepted'
) {
result = body.progress.testcaseResult[testcase.testcaseHash].status;
break;
}
}
result ||= 'Accepted';
result =
VERDICT[
Object.keys(VERDICT).find(k => normalize(result).includes(k))
];
return result;
};
const getTestcaseBlock = (id: string, index: number): string => {
const testcase = body.progress.testcaseResult[id];
if (!testcase) return '';
const status =
VERDICT[
Object.keys(VERDICT).find(k =>
normalize(testcase.status).includes(k)
)
];
let test_info = '';
test_info += `<test num="${
index + 1
}" info="${status}" time="${Math.round(testcase.time || 0)}" memory="${
testcase.memory
}">`;
if (testcase.input) {
if (typeof testcase.input === 'string') {
test_info += `<in>${parse(testcase.input)}</in>\n`;
} else {
test_info += `<in>${parse(testcase.input.data)}\n\n(${
testcase.input.omittedLength
} bytes omitted)</in>\n`;
}
}
if (testcase.userOutput) {
if (typeof testcase.userOutput === 'string') {
test_info += `<out>${parse(testcase.userOutput)}</out>\n`;
} else {
test_info += `<out>${parse(testcase.userOutput.data)}\n\n(${
testcase.userOutput.omittedLength
} bytes omitted)</out>\n`;
}
}
if (testcase.output) {
if (typeof testcase.output === 'string') {
test_info += `<ans>${parse(testcase.output)}</ans>\n`;
} else {
test_info += `<ans>${parse(testcase.output.data)}\n\n(${
testcase.output.omittedLength
} bytes omitted)</ans>\n`;
}
}
if (testcase.checkerMessage) {
if (typeof testcase.checkerMessage === 'string') {
test_info += `<res>${parse(testcase.checkerMessage)}</res>\n`;
} else {
test_info += `<res>${parse(testcase.checkerMessage.data)}\n\n(${
testcase.checkerMessage.omittedLength
} bytes omitted)</res>\n`;
}
}
test_info += '</test>';
return test_info;
};
let details = '';
details += `<info-block>REMOTE_SUBMISSION_ID = ${id}\nVERDICT = ${status}</info-block>`;
// Samples
if (body.progress.samples) {
details += `<subtask title="Samples" info="${getSubtaskStatusDisplayText(
body.progress.samples
)}" num="0">${body.progress.samples
.map((item, index) =>
item.testcaseHash
? getTestcaseBlock(item.testcaseHash, index)
: `<test num="${index + 1}" info="Skipped"></test>`
)
.join('\n')}</subtask>`;
}
// Tests
if (body.progress.subtasks.length === 1) {
details += `<tests>${body.progress.subtasks[0].testcases
.map((item, index) =>
item.testcaseHash
? getTestcaseBlock(item.testcaseHash, index)
: `<test num="${index + 1}" info="Skipped"></test>`
)
.join('\n')}</tests>`;
} else {
details += `<tests>${body.progress.subtasks
.map(
(subtask, index) =>
`<subtask num="${index + 1}" info="${getSubtaskStatusDisplayText(
subtask.testcases
)}">${subtask.testcases
.map((item, index) =>
item.testcaseHash
? getTestcaseBlock(item.testcaseHash, index)
: `<test num="${index + 1}" info="Skipped"></test>`
)
.join('\n')}</subtask>`
)
.join('\n')}</tests>`;
}
return await end({
id,
status: body.meta.status,
score: body.meta.score,
time: body.meta.timeUsed,
memory: body.meta.memoryUsed,
details: `<div>${details}</div>`,
});
}
}
}

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,46 @@
export function normalize(key: string) {
return key.toUpperCase().replace(/ /g, '_');
}
export const VERDICT = new Proxy<Record<string, string>>(
{
RUNTIME_ERROR: 'Runtime Error',
WRONG_ANSWER: 'Wrong Answer',
OK: 'Accepted',
COMPILING: 'Compiling',
TIME_LIMIT_EXCEEDED: 'Time Limit Exceeded',
MEMORY_LIMIT_EXCEEDED: 'Memory Limit Exceeded',
IDLENESS_LIMIT_EXCEEDED: 'Idleness Limit Exceeded',
ACCEPTED: 'Accepted',
PRESENTATION_ERROR: 'Wrong Answer',
OUTPUT_LIMIT_EXCEEDED: 'Output Limit Exceeded',
EXTRA_TEST_PASSED: 'Accepted',
COMPILE_ERROR: 'Compile Error',
'RUNNING_&_JUDGING': 'Judging',
// Codeforces
'HAPPY_NEW_YEAR!': 'Accepted',
// LibreOJ
COMPILATIONERROR: 'Compile Error',
COMPILEERROR: 'Compile Error',
FILEERROR: 'File Error',
RUNTIMEERROR: 'Runtime Error',
TIMELIMITEXCEEDED: 'Time Limit Exceeded',
MEMORYLIMITEXCEEDED: 'Memory Limit Exceeded',
OUTPUTLIMITEXCEEDED: 'Output Limit Exceeded',
WRONGANSWER: 'Wrong Answer',
PARTIALLYCORRECT: 'Partially Correct',
JUDGEMENTFAILED: 'Judgment Failed',
SYSTEMERROR: 'Judgment Failed',
CONFIGURATIONERROR: 'Judgment Failed',
},
{
get(self, key) {
if (typeof key === 'symbol') return null;
key = normalize(key);
if (self[key]) return self[key];
return null;
},
}
);

161
remote_judger/src/vjudge.ts Normal file
View File

@ -0,0 +1,161 @@
import type { BasicProvider, IBasicProvider, RemoteAccount } from './interface';
import * as Time from './utils/time';
import Logger from './utils/logger';
import htmlspecialchars from './utils/htmlspecialchars';
const logger = new Logger('vjudge');
class AccountService {
api: IBasicProvider;
constructor(
public Provider: BasicProvider,
public account: RemoteAccount,
private request: any
) {
this.api = new Provider(account);
this.main().catch(e =>
logger.error(`Error occured in ${account.type}/${account.handle}`, e)
);
}
async judge(
id: number,
problem_id: string,
language: string,
code: string,
judge_time: string
) {
const next = async payload => {
return await this.request('/submit', {
'update-status': true,
fetch_new: false,
id,
status:
payload.status ||
(payload.test_id ? `Judging Test #${payload.test_id}` : 'Judging'),
});
};
const end = async payload => {
if (payload.error) {
return await this.request('/submit', {
submit: true,
fetch_new: false,
id,
result: JSON.stringify({
status: 'Judged',
score: 0,
error: payload.status,
details:
'<div>' +
`<info-block>ID = ${payload.id || 'None'}</info-block>` +
`<error>${htmlspecialchars(payload.message)}</error>` +
'</div>',
}),
judge_time,
});
}
return await this.request('/submit', {
submit: true,
fetch_new: false,
id,
result: JSON.stringify({
status: 'Judged',
score: payload.score,
time: payload.time,
memory: payload.memory,
details:
payload.details ||
'<div>' +
`<info-block>ID = ${payload.id || 'None'}</info-block>` +
`<info-block>VERDICT = ${payload.status}</info-block>` +
'</div>',
}),
judge_time,
});
};
try {
const rid = await this.api.submitProblem(
problem_id,
language,
code,
id,
next,
end
);
if (!rid) return;
await this.api.waitForSubmission(problem_id, rid, next, end);
} catch (e) {
logger.error(e);
await end({ error: true, message: e.message });
}
}
async login() {
const login = await this.api.ensureLogin();
if (login === true) {
logger.info(`${this.account.type}/${this.account.handle}: logged in`);
return true;
}
logger.warn(
`${this.account.type}/${this.account.handle}: login fail`,
login || ''
);
return false;
}
async main() {
const res = await this.login();
if (!res) return;
setInterval(() => this.login(), Time.hour);
}
}
class VJudge {
private providers: Record<string, AccountService> = {};
constructor(private request: any) {}
async addProvider(type: string) {
if (this.providers[type]) throw new Error(`duplicate provider ${type}`);
const provider = await import(`./providers/${type}`);
const account = provider.getAccountInfoFromEnv();
if (!account) throw new Error(`no account info for ${type}`);
this.providers[type] = new AccountService(
provider.default,
account,
this.request
);
}
async judge(
id: number,
type: string,
problem_id: string,
language: string,
code: string,
judge_time: string
) {
if (!this.providers[type]) throw new Error(`no provider ${type}`);
this.providers[type].judge(id, problem_id, language, code, judge_time);
}
}
export async function apply(request: any) {
const vjudge = new VJudge(request);
await vjudge.addProvider('codeforces');
await vjudge.addProvider('atcoder');
await vjudge.addProvider('uoj');
await vjudge.addProvider('loj');
return vjudge;
}

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

13
s2oj-backup.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
## 0 3 * * * /data/s2oj/s2oj-backup.sh >/dev/null 2>&1
if pidof -o %PPID -x "/data/s2oj/s2oj-backup.sh"; then
echo "Already running"
exit 1
fi
rclone sync /data/s2oj baoshuo-s3-de-fra2:s2oj --include "/docker-compose.local.yml" --include "/.config.local.php" --include "/uoj_data/web/data/*.zip" --include "/uoj_data/web/storage/**" --transfers=20 --buffer-size=500M --checkers=20
/usr/local/bin/docker-compose -f /data/s2oj/docker-compose.local.yml exec uoj-db mysqldump -uroot --password="${MYSQL_ROOT_PASSWORD:-root}" app_uoj233 > "/data/s2oj-db-backup/s2oj-database_$(date +'%F-%H%M%S').sql"
find /data/s2oj-db-backup/ -mtime +7 -name "*.sql" -exec rm -rf {} \;
rclone sync /data/s2oj-db-backup/ baoshuo-s3-de-fra2:s2oj-db-backup --buffer-size=500M

View File

@ -2,10 +2,10 @@ Options -Indexes
php_value session.save_path /var/lib/php/uoj_sessions
php_value session.gc_maxlifetime 172800
php_value session.cookie_lifetime 31536000
php_value session.cookie_lifetime 604800
php_value post_max_size 1000M
php_value upload_max_filesize 1000M
php_value post_max_size 1024M
php_value upload_max_filesize 1024M
php_value session.gc_probability 1
php_value session.gc_divisor 1000
@ -16,6 +16,26 @@ DirectorySlash Off
DirectoryIndex
<filesMatch "\.(ico|gif|jpg|png)$">
ExpiresActive On
ExpiresDefault "access plus 6 month"
Header append Cache-Control "public"
</filesMatch>
<filesMatch "\.(css|js)$">
ExpiresActive On
ExpiresDefault "access plus 1 week"
Header append Cache-Control "public"
</filesMatch>
<filesMatch "\.(pdf)$">
ExpiresActive On
ExpiresDefault "access plus 1 month"
Header append Cache-Control "public"
</filesMatch>
RequestHeader append X-Author "Baoshuo ( https://baoshuo.ren )"
RewriteEngine On
RewriteCond %{QUERY_STRING} ^$

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": "^9.13",
"ext-dom": "20031129",
"ivopetkov/html5-dom-document-php": "2.*"
},
"autoload": {
"classmap": [

View File

@ -1,23 +0,0 @@
<?php
if (!Auth::check()) {
become403Page(UOJLocale::get('need login'));
}
$name = $_GET['image_name'];
if (!validateString($name)) {
become404Page();
}
$file_name = UOJContext::storagePath() . "/image_hosting/$name.png";
$finfo = finfo_open(FILEINFO_MIME);
$mimetype = finfo_file($finfo, $file_name);
if ($mimetype === false) {
become404Page();
}
finfo_close($finfo);
header("X-Sendfile: $file_name");
header("Content-type: $mimetype");
header("Cache-Control: max-age=604800", true);

View File

@ -36,9 +36,13 @@ if ($_POST['image_upload_file_submit'] == 'submit') {
}
if (!isset($_SESSION['phrase']) || !PhraseBuilder::comparePhrases($_SESSION['phrase'], $_POST['captcha'])) {
unset($_SESSION['phrase']);
throwError("bad_captcha");
}
unset($_SESSION['phrase']);
if ($_FILES["image_upload_file"]["error"] > 0) {
throwError($_FILES["image_upload_file"]["error"]);
}
@ -408,7 +412,7 @@ $pag = new Paginator($pag_config);
<?php foreach ($pag->get() as $idx => $row) : ?>
<div class="col">
<div class="card">
<img src="<?= $row['path'] ?>" class="card-img-top" height="200" style="object-fit: contain">
<img src="<?= $row['path'] ?>" class="card-img-top" height="200" style="object-fit: contain" loading="lazy" decoding="async">
<div class="card-footer bg-transparent small px-2">
<div class="d-flex flex-wrap justify-content-between">
<time><?= $row['upload_time'] ?></time>
@ -454,36 +458,40 @@ $pag = new Paginator($pag_config);
<?= $pag->pagination() ?>
</div>
<div class="toast-container position-fixed bottom-0 start-0 ms-3 mb-4">
<div id="copy-url-toast" class="toast text-bg-success align-items-center border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
复制成功!
</div>
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
<script>
$(document).ready(function() {
[...document.querySelectorAll('[data-bs-toggle="tooltip"]')].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
});
var copy_url_toast = new bootstrap.Toast('#copy-url-toast', {
delay: 2000
});
$('.image-copy-url-button').click(function() {
var _this = this;
var url = new URL($(this).data('image-path'), location.origin);
navigator.clipboard.writeText(url);
copy_url_toast.show();
navigator.clipboard.writeText(url).then(function() {
$(_this).addClass('btn-success');
$(_this).removeClass('btn-outline-secondary');
$(_this).html('<i class="bi bi-check2"></i>');
setTimeout(function() {
$(_this).addClass('btn-outline-secondary');
$(_this).removeClass('btn-success');
$(_this).html('<i class="bi bi-clipboard"></i>');
}, 1000);
});
});
$('.image-copy-md-button').click(function() {
var _this = this;
var url = new URL($(this).data('image-path'), location.origin);
navigator.clipboard.writeText('![](' + url + ')');
copy_url_toast.show();
navigator.clipboard.writeText('![](' + url + ')').then(function() {
$(_this).addClass('btn-success');
$(_this).removeClass('btn-outline-secondary');
$(_this).html('<i class="bi bi-check2"></i>');
setTimeout(function() {
$(_this).addClass('btn-outline-secondary');
$(_this).removeClass('btn-success');
$(_this).html('<i class="bi bi-markdown"></i>');
}, 1000);
});
});
</script>

View File

@ -588,11 +588,11 @@ EOD);
</tr>
EOD,
function ($row) {
$problem = UOJProblem::query($row['problem_id']);
$problem = UOJContestProblem::query($row['problem_id'], UOJContest::cur());
echo '<tr>';
echo '<td>', $row['problem_id'], '</td>';
echo '<td>', $problem->getLink(['with' => 'none']), '</td>';
echo '<td>', isset($contest['extra_config']["problem_{$problem->info['id']}"]) ? $contest['extra_config']["problem_{$problem->info['id']}"] : 'default', '</td>';
echo '<td>', $problem->getJudgeTypeInContest(), '</td>';
echo '<td>';
echo '<form class="d-inline-block" method="POST" target="_self" onsubmit=\'return confirm("你确定要将题目 #', $problem->info['id'], ' 从比赛中移除吗?")\'>';
echo '<input type="hidden" name="_token" value="', crsf_token(), '">';

View File

@ -38,9 +38,13 @@ $forgot_form->handle = function (&$vdata) {
$password = $user["password"];
if (!isset($_SESSION['phrase']) || !PhraseBuilder::comparePhrases($_SESSION['phrase'], $_POST['captcha'])) {
unset($_SESSION['phrase']);
becomeMsgPage('验证码错误!');
}
unset($_SESSION['phrase']);
if (!$user['email']) {
becomeMsgPage('用户未填写邮件地址,请联系管理员重置!');
}
@ -99,7 +103,6 @@ EOD;
}
};
$forgot_form->submit_button_config['align'] = 'offset';
$forgot_form->runAtServer();
?>
<?php echoUOJPageHeader('找回密码') ?>

View File

@ -134,17 +134,16 @@ UOJGroup::cur()->userCanView(Auth::user(), ['ensure' => true]);
</div>
</div>
<div class="card card-default mb-3">
<div class="card-body">
<h2 class="card-title h3">
<?= UOJLocale::get('top solver') ?>
</h2>
<div class="card mb-3">
<div class="card-header bg-transparent">
<h2 class="h3 mb-0"><?= UOJLocale::get('top solver') ?></h2>
</div>
<?php UOJRanklist::printHTML([
'page_len' => 15,
'group_id' => UOJGroup::info('id'),
'flush' => true,
]) ?>
</div>
</div>
<!-- end left col -->
</div>

View File

@ -60,17 +60,17 @@ $friend_links = DB::selectAll([
</div>
</div>
<?php if (Auth::check()) : ?>
<div class="mt-4 card">
<div class="card-body">
<h4 class="card-title mb-2"><?= UOJLocale::get('top solver') ?></h4>
<?php UOJRanklist::printHTML(['top10' => true]) ?>
<div class="text-center mt-2">
<a href="/solverlist" class="text-decoration-none">
<div class="card mt-4">
<div class="card-header bg-transparent">
<h4 class="mb-0"><?= UOJLocale::get('top solver') ?></h4>
</div>
<?php UOJRanklist::printHTML(['top10' => true, 'flush' => true]) ?>
<div class="card-footer bg-transparent text-center">
<a href="/solverlist">
<?= UOJLocale::get('view all') ?>
</a>
</div>
</div>
</div>
<?php else : ?>
<div class="mt-4 card card-default">
<div class="card-body text-center">

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::tag('a', ['class' => 'badge text-bg-success', 'href' => '/problems/remote'], '远端评测题');
}
if ($info['is_hidden']) {
$html .= ' <span class="badge text-bg-danger"><i class="bi bi-eye-slash-fill"></i> ' . UOJLocale::get('hidden') . '</span> ';
}
@ -74,15 +77,7 @@ $pag_config = [
'page_len' => 20,
'col_names' => [
'best_ac_submissions.submission_id as submission_id',
'problems.id as id',
'problems.is_hidden as is_hidden',
'problems.title as title',
'problems.submit_num as submit_num',
'problems.ac_num as ac_num',
'problems.zan as zan',
'problems.difficulty as difficulty',
'problems.extra_config as extra_config',
'problems.uploader as uploader',
'problems.*',
],
'table_name' => [
"problems",

View File

@ -0,0 +1,167 @@
<?php
requireLib('bootstrap5');
requirePHPLib('form');
requirePHPLib('data');
Auth::check() || redirectToLogin();
UOJProblem::userCanCreateProblem(Auth::user()) || UOJResponse::page403();
$new_remote_problem_form = new UOJForm('new_remote_problem');
$new_remote_problem_form->addSelect('remote_online_judge', [
'label' => '远程 OJ',
'options' => array_map(fn ($provider) => $provider['name'], UOJRemoteProblem::$providers),
]);
$new_remote_problem_form->addInput('remote_problem_id', [
'div_class' => 'mt-3',
'label' => '远程 OJ 上的题目 ID',
'validator_php' => function ($id, &$vdata) {
$remote_oj = $_POST['remote_online_judge'];
if ($remote_oj === 'codeforces') {
$id = trim(strtoupper($id));
if (!validateCodeforcesProblemId($id)) {
return '不合法的题目 ID';
}
$vdata['remote_problem_id'] = $id;
return '';
} else if ($remote_oj === 'atcoder') {
$id = trim(strtolower($id));
if (!validateString($id)) {
return '不合法的题目 ID';
}
$vdata['remote_problem_id'] = $id;
return '';
} else if ($remote_oj === 'uoj') {
if (!validateUInt($id)) {
return '不合法的题目 ID';
}
$vdata['remote_problem_id'] = $id;
return '';
} else if ($remote_oj === 'loj') {
if (!validateUInt($id)) {
return '不合法的题目 ID';
}
$vdata['remote_problem_id'] = $id;
return '';
}
return '不合法的远程 OJ 类型';
},
]);
$new_remote_problem_form->handle = function (&$vdata) {
$remote_online_judge = $_POST['remote_online_judge'];
$remote_problem_id = $vdata['remote_problem_id'];
$remote_provider = UOJRemoteProblem::$providers[$remote_online_judge];
try {
$data = UOJRemoteProblem::getProblemBasicInfo($remote_online_judge, $remote_problem_id);
} catch (Exception $e) {
$data = null;
UOJLog::error($e->getMessage());
}
if ($data === null) {
UOJResponse::page500('题目抓取失败,可能是题目不存在或者没有题面!如果题目没有问题,请稍后再试。<a href="">返回</a>');
}
$submission_requirement = [
[
"name" => "answer",
"type" => "source code",
"file_name" => "answer.code",
"languages" => $remote_provider['languages'],
]
];
$enc_submission_requirement = json_encode($submission_requirement);
$extra_config = [
'remote_online_judge' => $remote_online_judge,
'remote_problem_id' => $remote_problem_id,
'time_limit' => $data['time_limit'],
'memory_limit' => $data['memory_limit'],
];
$enc_extra_config = json_encode($extra_config);
DB::insert([
"insert into problems",
"(title, uploader, is_hidden, submission_requirement, extra_config, difficulty, type)",
"values", DB::tuple([$data['title'], Auth::id(), 1, $enc_submission_requirement, $enc_extra_config, $data['difficulty'] ?: -1, "remote"])
]);
$id = DB::insert_id();
dataNewProblem($id);
if ($data['type'] == 'pdf') {
file_put_contents(UOJContext::storagePath(), "/problem_resources/$id/statement.pdf", $data['pdf_data']);
$data['statement'] = "<div data-pdf data-src=\"/problem/$id/resources/statement.pdf\"></div>\n" . $data['statement'];
}
DB::insert([
"insert into problems_contents",
"(id, remote_content, statement, statement_md)",
"values",
DB::tuple([$id, HTML::purifier(['a' => ['target' => 'Enum#_blank']])->purify($data['statement']), '', ''])
]);
DB::insert([
"insert into problems_tags",
"(problem_id, tag)",
"values",
DB::tuple([$id, $remote_provider['name']]),
]);
UOJRemoteProblem::downloadImagesInRemoteContent(strval($id));
redirectTo("/problem/{$id}");
die();
};
$new_remote_problem_form->runAtServer();
?>
<?php echoUOJPageHeader('导入远程题库') ?>
<h1>导入远程题库</h1>
<div class="row">
<div class="col-md-9">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<?php $new_remote_problem_form->printHTML() ?>
</div>
<div class="col-md-6 mt-3 mt-md-0">
<h4>使用帮助</h4>
<ul>
<li>
<p>目前支持导入以下题库的题目作为远端评测题:</p>
<ul class="mb-3">
<li><a href="https://codeforces.com/problemset">Codeforces</a></li>
<li><a href="https://codeforces.com/gyms">Codeforces::Gym</a>(题号前加 <code>GYM</code></li>
<li><a href="https://atcoder.jp/contests/archive">AtCoder</a></li>
<li><a href="https://uoj.ac/problems">UniversalOJ</a></li>
<li><a href="https://loj.ac/p">LibreOJ</a></li>
</ul>
</li>
<li>在导入题目前请先搜索题库中是否已经存在相应题目,避免重复添加。</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<?php uojIncludeView('sidebar') ?>
</div>
</div>
<?php echoUOJPageFooter() ?>

View File

@ -2,6 +2,7 @@
requireLib('bootstrap5');
requireLib('hljs');
requireLib('mathjax');
requireLib('pdf.js');
requirePHPLib('form');
requirePHPLib('judger');
@ -223,7 +224,6 @@ if (UOJContest::cur()) {
<div class="row">
<!-- Left col -->
<div class="col-lg-9">
<?php if (isset($tabs_info)) : ?>
<!-- 比赛导航 -->
<div class="mb-2">
@ -233,7 +233,6 @@ if (UOJContest::cur()) {
<div class="card card-default mb-2">
<div class="card-body">
<h1 class="card-title text-center">
<?php if (UOJContest::cur()) : ?>
<?= UOJProblem::cur()->getTitle(['with' => 'letter', 'simplify' => true]) ?>
@ -243,8 +242,13 @@ if (UOJContest::cur()) {
</h1>
<?php
if (UOJProblem::info('type') == 'local') {
$time_limit = $conf instanceof UOJProblemConf ? $conf->getVal('time_limit', 1) : null;
$memory_limit = $conf instanceof UOJProblemConf ? $conf->getVal('memory_limit', 256) : null;
} else if (UOJProblem::info('type') == 'remote') {
$time_limit = UOJProblem::cur()->getExtraConfig('time_limit');
$memory_limit = UOJProblem::cur()->getExtraConfig('memory_limit');
}
?>
<div class="text-center small">
时间限制: <?= $time_limit ? "$time_limit s" : "N/A" ?>
@ -259,6 +263,14 @@ if (UOJContest::cur()) {
<article class="mt-3 markdown-body">
<?= $problem_content['statement'] ?>
</article>
<?php if (UOJProblem::info('type') == 'remote') : ?>
<hr>
<article class="mt-3 markdown-body remote-content">
<?= UOJProblem::cur()->queryContent()['remote_content'] ?>
</article>
<?php endif ?>
</div>
<div class="tab-pane" id="submit">
<?php if ($pre_submit_check_ret !== true) : ?>
@ -392,6 +404,14 @@ if (UOJContest::cur()) {
<?= UOJProblem::cur()->getUploaderLink() ?>
</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="flex-shrink-0">
题目来源
</span>
<span>
<?= UOJProblem::cur()->getProviderLink() ?>
</span>
</li>
<?php if (!UOJContest::cur() || UOJContest::cur()->progress() >= CONTEST_FINISHED) : ?>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="flex-shrink-0">
@ -448,23 +468,24 @@ if (UOJContest::cur()) {
</div>
<!-- 附件 -->
<div class="card card-default mb-2">
<ul class="nav nav-fill flex-column">
<div class="card mb-2">
<div class="card-header fw-bold">附件</div>
<div class="list-group list-group-flush">
<?php if (UOJProblem::cur()->userCanDownloadTestData(Auth::user())) : ?>
<li class="nav-item text-start">
<a class="nav-link" href="<?= HTML::url("/download/problem/{$problem['id']}/data.zip") ?>">
<a class="list-group-item list-group-item-action" href="<?= HTML::url(UOJProblem::cur()->getMainDataUri()) ?>">
<i class="bi bi-hdd-stack"></i>
测试数据
</a>
</li>
<?php endif ?>
<li class="nav-item text-start">
<a class="nav-link" href="<?= HTML::url("/download/problem/{$problem['id']}/attachment.zip") ?>">
<a class="list-group-item list-group-item-action" href="<?= HTML::url(UOJProblem::cur()->getAttachmentUri()) ?>">
<i class="bi bi-download"></i>
附件下载
</a>
</li>
</ul>
<a class="list-group-item list-group-item-action" href="<?= HTML::url(UOJProblem::cur()->getResourcesBaseUri()) ?>">
<i class="bi bi-folder2-open"></i>
相关资源
</a>
</div>
</div>
<?php

View File

@ -10,10 +10,11 @@ requirePHPLib('data');
UOJProblem::init(UOJRequest::get('id')) || UOJResponse::page404();
UOJProblem::cur()->userCanManage(Auth::user()) || UOJResponse::page403();
UOJProblem::info('type') === 'local' || UOJResponse::page404();
$problem = UOJProblem::info();
$problem_extra_config = UOJProblem::cur()->getExtraConfig();
$data_dir = "/var/uoj_data/${problem['id']}";
$data_dir = "/var/uoj_data/{$problem['id']}";
function echoFileNotFound($file_name) {
echo '<h5>', htmlspecialchars($file_name), '</h5>';
@ -466,93 +467,6 @@ $rejudgege97_form->submit_button_config['class_str'] = 'btn btn-danger d-block w
$rejudgege97_form->submit_button_config['text'] = '重测 >=97 的程序';
$rejudgege97_form->submit_button_config['smart_confirm'] = '';
$view_type_form = new UOJBs4Form('view_type');
$view_type_form->addVSelect(
'view_content_type',
array(
'NONE' => '禁止',
'ALL_AFTER_AC' => 'AC后',
'ALL' => '所有人'
),
'查看提交文件:',
$problem_extra_config['view_content_type']
);
$view_type_form->addVSelect(
'view_all_details_type',
array(
'NONE' => '禁止',
'SELF' => '仅自己',
'ALL_AFTER_AC' => 'AC后',
'ALL' => '所有人'
),
'查看全部详细信息:',
$problem_extra_config['view_all_details_type']
);
$view_type_form->addVSelect(
'view_details_type',
array(
'NONE' => '禁止',
'SELF' => '仅自己',
'ALL_AFTER_AC' => 'AC后',
'ALL' => '所有人'
),
'查看测试点详细信息:',
$problem_extra_config['view_details_type']
);
$view_type_form->handle = function () {
global $problem, $problem_extra_config;
$config = $problem_extra_config;
$config['view_content_type'] = $_POST['view_content_type'];
$config['view_all_details_type'] = $_POST['view_all_details_type'];
$config['view_details_type'] = $_POST['view_details_type'];
$esc_config = json_encode($config);
DB::update([
"update problems",
"set", ["extra_config" => $esc_config],
"where", ["id" => $problem['id']]
]);
};
$view_type_form->submit_button_config['class_str'] = 'btn btn-warning d-block w-100 mt-2';
$solution_view_type_form = new UOJBs4Form('solution_view_type');
$solution_view_type_form->addVSelect(
'view_solution_type',
array(
'NONE' => '禁止',
'ALL_AFTER_AC' => 'AC后',
'ALL' => '所有人'
),
'查看题解:',
$problem_extra_config['view_solution_type']
);
$solution_view_type_form->addVSelect(
'submit_solution_type',
array(
'NONE' => '禁止',
'ALL_AFTER_AC' => 'AC后',
'ALL' => '所有人'
),
'提交题解:',
$problem_extra_config['submit_solution_type']
);
$solution_view_type_form->handle = function () {
global $problem, $problem_extra_config;
$config = $problem_extra_config;
$config['view_solution_type'] = $_POST['view_solution_type'];
$config['submit_solution_type'] = $_POST['submit_solution_type'];
$esc_config = json_encode($config);
DB::update([
"update problems",
"set", ["extra_config" => $esc_config],
"where", ["id" => $problem['id']]
]);
};
$solution_view_type_form->submit_button_config['class_str'] = 'btn btn-warning d-block w-100 mt-2';
if ($problem['hackable']) {
$test_std_form = new UOJBs4Form('test_std');
$test_std_form->handle = function () use ($problem, $data_disp) {
@ -618,8 +532,6 @@ if ($problem['hackable']) {
}
$hackable_form->runAtServer();
$view_type_form->runAtServer();
$solution_view_type_form->runAtServer();
$data_form->runAtServer();
$clear_data_form->runAtServer();
$rejudge_form->runAtServer();
@ -753,18 +665,6 @@ $info_form->runAtServer();
<?php $test_std_form->printHTML() ?>
</div>
<?php endif ?>
<div class="mt-2">
<button id="button-display_view_type" type="button" class="btn btn-primary d-block w-100" onclick="$('#div-view_type').toggle('fast');">提交记录可视权限</button>
<div class="mt-2" id="div-view_type" style="display:none; padding-left:5px; padding-right:5px;">
<?php $view_type_form->printHTML(); ?>
</div>
</div>
<div class="mt-2">
<button id="button-solution_view_type" type="button" class="btn btn-primary d-block w-100" onclick="$('#div-solution_view_type').toggle('fast');">题解可视权限</button>
<div class="mt-2" id="div-solution_view_type" style="display:none; padding-left:5px; padding-right:5px;">
<?php $solution_view_type_form->printHTML(); ?>
</div>
</div>
<div class="mt-2">
<?php $data_form->printHTML(); ?>
</div>

View File

@ -91,11 +91,13 @@ if (isSuperUser(Auth::user())) {
管理者
</a>
</li>
<?php if (UOJProblem::info('type') === 'local') : ?>
<li class="nav-item">
<a class="nav-link" href="/problem/<?= UOJProblem::info('id') ?>/manage/data" role="tab">
数据
</a>
</li>
<?php endif ?>
</ul>
<div class="card card-default">

View File

@ -0,0 +1,53 @@
<?php
requireLib('bootstrap5');
requireLib('hljs');
Auth::check() || redirectToLogin();
UOJProblem::init(UOJRequest::get('id')) || UOJResponse::page404();
$problem = UOJProblem::cur()->info;
$problem_content = UOJProblem::cur()->queryContent();
$user_can_view = UOJProblem::cur()->userCanView(Auth::user());
if (!$user_can_view) {
foreach (UOJProblem::cur()->findInContests() as $cp) {
if ($cp->contest->progress() >= CONTEST_IN_PROGRESS && $cp->contest->userHasRegistered(Auth::user())) {
$user_can_view = true;
break;
}
}
}
if (!$user_can_view) {
UOJResponse::page403();
}
// Create directory if not exists
if (!is_dir(UOJProblem::cur()->getResourcesPath())) {
mkdir(UOJProblem::cur()->getResourcesPath(), 0755, true);
}
define('APP_TITLE', '题目资源 - ' . UOJProblem::cur()->getTitle(['with' => false]));
define('FM_EMBED', true);
define('FM_DISABLE_COLS', true);
define('FM_DATETIME_FORMAT', UOJTime::FORMAT);
define('FM_ROOT_PATH', UOJProblem::cur()->getResourcesFolderPath());
define('FM_ROOT_URL', UOJProblem::cur()->getResourcesBaseUri());
$sub_path = UOJRequest::get('sub_path', 'is_string', '');
if ($sub_path) {
$filepath = realpath(UOJProblem::cur()->getResourcesPath($sub_path));
$realbasepath = realpath(UOJProblem::cur()->getResourcesPath());
if (!str_starts_with($filepath, $realbasepath)) {
UOJResponse::page406();
}
UOJResponse::xsendfile($filepath);
}
$global_readonly = !UOJProblem::cur()->userCanManage(Auth::user());
include(__DIR__ . '/tinyfilemanager/tinyfilemanager.php');

View File

@ -77,10 +77,10 @@ EOD;
redirectTo("/problem/{$id}/manage/statement");
die();
};
$new_problem_form->config['submit_container']['class'] = 'text-end';
$new_problem_form->config['submit_button']['class'] = 'btn btn-primary';
$new_problem_form->config['submit_button']['text'] = UOJLocale::get('problems::add new');
$new_problem_form->config['confirm']['smart'] = true;
$new_problem_form->config['submit_container']['class'] = '';
$new_problem_form->config['submit_button']['class'] = 'bg-transparent border-0 d-block w-100 px-3 py-2 text-start';
$new_problem_form->config['submit_button']['text'] = '<i class="bi bi-plus-lg"></i> 新建本地题目';
$new_problem_form->config['confirm']['text'] = '你真的要添加新题吗?';
$new_problem_form->runAtServer();
}
@ -94,6 +94,9 @@ function getProblemTR($info) {
if ($problem->isUserOwnProblem(Auth::user())) {
$html .= ' <a href="/problems?my=on"><span class="badge text-white bg-info">' . UOJLocale::get('problems::my problem') . '</span></a> ';
}
if ($info['type'] == 'remote') {
$html .= ' ' . HTML::tag('a', ['class' => 'badge text-bg-success', 'href' => '/problems/remote'], '远端评测题');
}
if ($info['is_hidden']) {
$html .= ' <a href="/problems?is_hidden=on"><span class="badge text-bg-danger"><i class="bi bi-eye-slash-fill"></i> ' . UOJLocale::get('hidden') . '</span></a> ';
}
@ -141,6 +144,8 @@ $search_is_effective = false;
$cur_tab = UOJRequest::get('tab', 'is_string', 'all');
if ($cur_tab == 'template') {
$search_tag = "模板题";
} else if ($cur_tab == 'remote') {
$cond['type'] = 'remote';
}
if (is_string($search_tag)) {
$cond[] = [
@ -199,16 +204,20 @@ $header .= '<th class="text-center" style="width:4em;">' . UOJLocale::get('probl
$header .= '<th class="text-center" style="width:50px;">' . UOJLocale::get('appraisal') . '</th>';
$header .= '</tr>';
$tabs_info = array(
'all' => array(
$tabs_info = [
'all' => [
'name' => UOJLocale::get('problems::all problems'),
'url' => "/problems"
),
'template' => array(
],
'template' => [
'name' => UOJLocale::get('problems::template problems'),
'url' => "/problems/template"
)
);
],
'remote' => [
'name' => UOJLocale::get('problems::remote problems'),
'url' => "/problems/remote"
],
];
$pag = new Paginator([
'col_names' => ['*'],
@ -253,25 +262,20 @@ $pag = new Paginator([
<!-- left col -->
<div class="col-md-9">
<!-- title -->
<div class="d-flex justify-content-between">
<div class="d-flex justify-content-between flex-wrap">
<h1>
<?= UOJLocale::get('problems') ?>
</h1>
<?php if (isset($new_problem_form)) : ?>
<div class="text-end">
<?php $new_problem_form->printHTML(); ?>
<div>
<?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills') ?>
</div>
<?php endif ?>
</div>
<!-- end title -->
<div class="row">
<div class="col-sm-4 col-12">
<?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills') ?>
</div>
<div class="text-end p-2 col-12 col-sm-8">
<?= $pag->pagination() ?>
<div class="text-end">
<div class="form-check d-inline-block me-2">
<input type="checkbox" id="input-show_tags_mode" class="form-check-input" <?= isset($_COOKIE['show_tags_mode']) ? 'checked="checked" ' : '' ?> />
<label class="form-check-label" for="input-show_tags_mode">
@ -286,9 +290,6 @@ $pag = new Paginator([
</label>
</div>
</div>
</div>
<?= $pag->pagination() ?>
<script type="text/javascript">
$('#input-show_tags_mode').click(function() {
@ -389,6 +390,23 @@ $pag = new Paginator([
});
</script>
<?php if (UOJProblem::userCanCreateProblem(Auth::user())) : ?>
<div class="card mb-3">
<div class="card-header fw-bold">
新建题目
</div>
<div class="list-group list-group-flush">
<div class="list-group-item list-group-item-action p-0">
<?php $new_problem_form->printHTML() ?>
</div>
<a class="list-group-item list-group-item-action" href="/problems/remote/new">
<i class="bi bi-cloud-plus"></i>
新建远端评测题目
</a>
</div>
</div>
<?php endif ?>
<!-- sidebar -->
<?php uojIncludeView('sidebar') ?>
</aside>

View File

@ -2,6 +2,7 @@
requireLib('bootstrap5');
requireLib('hljs');
requireLib('mathjax');
requireLib('pdf.js');
requirePHPLib('form');
requirePHPLib('judger');
@ -119,6 +120,12 @@ if (UOJProblem::cur()->userCanManage(Auth::user()) || UOJProblem::cur()->userPer
$blog_id = DB::insert_id();
DB::insert([
"insert into problems_solutions",
DB::bracketed_fields(["problem_id", "blog_id"]),
"values", DB::tuple([UOJProblem::info('id'), $blog_id]),
]);
redirectTo(HTML::blog_url(Auth::id(), "/post/{$blog_id}/write"));
die();
};

View File

@ -65,9 +65,12 @@ $problem_editor->runAtServer();
$difficulty_form = new UOJForm('difficulty');
$difficulty_form->addSelect('difficulty', [
'div_class' => 'flex-grow-1',
'options' => [-1 => '暂无评定'] + array_combine(UOJProblem::$difficulty, UOJProblem::$difficulty),
'default_value' => UOJProblem::info('difficulty'),
]);
$difficulty_form->config['form']['class'] = 'd-flex';
$difficulty_form->config['submit_container']['class'] = 'ms-2';
$difficulty_form->handle = function () {
DB::update([
"update problems",
@ -80,6 +83,211 @@ $difficulty_form->handle = function () {
]);
};
$difficulty_form->runAtServer();
if (UOJProblem::info('type') == 'remote') {
$remote_online_judge = UOJProblem::cur()->getExtraConfig('remote_online_judge');
$remote_problem_id = UOJProblem::cur()->getExtraConfig('remote_problem_id');
$remote_provider = UOJRemoteProblem::$providers[$remote_online_judge];
$re_crawl_form = new UOJForm('re_crawl');
$re_crawl_form->appendHTML(<<<EOD
<ul>
<li>远程题库:{$remote_provider['name']}</li>
<li>远程题号:{$remote_problem_id}</li>
</ul>
EOD);
$re_crawl_form->config['submit_button']['text'] = '重新爬取';
$re_crawl_form->handle = function () use ($remote_online_judge, $remote_problem_id, $remote_provider) {
try {
$data = UOJRemoteProblem::getProblemBasicInfo($remote_online_judge, $remote_problem_id);
} catch (Exception $e) {
$data = null;
UOJLog::error($e->getMessage());
}
if ($data === null) {
UOJResponse::page500('题目抓取失败,可能是题目不存在或者没有题面!如果题目没有问题,请稍后再试。<a href="">返回</a>');
}
if ($data['difficulty'] == -1) {
$data['difficulty'] = UOJProblem::info('difficulty');
}
$submission_requirement = [
[
"name" => "answer",
"type" => "source code",
"file_name" => "answer.code",
"languages" => $remote_provider['languages'],
]
];
$enc_submission_requirement = json_encode($submission_requirement);
$extra_config = [
'remote_online_judge' => $remote_online_judge,
'remote_problem_id' => $remote_problem_id,
'time_limit' => $data['time_limit'],
'memory_limit' => $data['memory_limit'],
];
$enc_extra_config = json_encode($extra_config);
DB::update([
"update problems",
"set", [
"title" => $data['title'],
"submission_requirement" => $enc_submission_requirement,
"extra_config" => $enc_extra_config,
"difficulty" => $data['difficulty'] ?: -1,
],
"where", [
"id" => UOJProblem::info('id'),
],
]);
if ($data['type'] == 'pdf') {
file_put_contents(UOJContext::storagePath() . "/problem_resources/" . UOJProblem::info('id') . "/statement.pdf", $data['pdf_data']);
$data['statement'] = '<div data-pdf data-src="/problem/' . UOJProblem::info('id') . '/resources/statement.pdf"></div>' . "\n" . $data['statement'];
}
DB::update([
"update problems_contents",
"set", [
"remote_content" => HTML::purifier(['a' => ['target' => 'Enum#_blank']])->purify($data['statement']),
],
"where", [
"id" => UOJProblem::info('id'),
],
]);
UOJRemoteProblem::downloadImagesInRemoteContent(UOJProblem::info('id'));
redirectTo(UOJProblem::cur()->getUri());
};
$re_crawl_form->runAtServer();
$convert_local_form = new UOJForm('convert_local');
$convert_local_form->handle = function () {
DB::update([
"update problems",
"set", [
"type" => 'local',
"submission_requirement" => "{}",
"extra_config" => "{}",
],
"where", [
"id" => UOJProblem::info('id'),
],
]);
DB::update([
"update problems_contents",
"set", [
"remote_content" => '',
],
"where", [
"id" => UOJProblem::info('id'),
],
]);
};
$convert_local_form->config['submit_container']['class'] = '';
$convert_local_form->config['submit_button']['class'] = 'btn btn-danger';
$convert_local_form->config['submit_button']['text'] = '将本题转换为本地题目(不可逆)';
$convert_local_form->config['confirm']['text'] = '您真的要*不可逆*地将本题转换为本地题目吗?';
$convert_local_form->runAtServer();
}
$view_type_form = new UOJForm('view_type');
$view_type_form->addSelect('view_content_type', [
'div_class' => 'row align-items-center g-0',
'label_class' => 'form-label col-auto m-0 flex-grow-1',
'select_class' => 'col-auto form-select w-auto',
'label' => '查看提交文件',
'options' => [
'NONE' => '禁止',
'ALL_AFTER_AC' => 'AC 后',
'ALL' => '所有人',
],
'default_value' => UOJProblem::cur()->getExtraConfig('view_content_type'),
]);
$view_type_form->addSelect('view_all_details_type', [
'div_class' => 'row align-items-center g-0 mt-3',
'label_class' => 'form-label col-auto m-0 flex-grow-1',
'select_class' => 'col-auto form-select w-auto',
'label' => '查看全部详细信息',
'options' => [
'NONE' => '禁止',
'SELF' => '仅自己',
'ALL_AFTER_AC' => 'AC 后',
'ALL' => '所有人'
],
'default_value' => UOJProblem::cur()->getExtraConfig('view_all_details_type'),
]);
$view_type_form->addSelect('view_details_type', [
'div_class' => 'row align-items-center g-0 mt-3',
'label_class' => 'form-label col-auto m-0 flex-grow-1',
'select_class' => 'col-auto form-select w-auto',
'label' => '查看测试点详细信息',
'options' => [
'NONE' => '禁止',
'SELF' => '仅自己',
'ALL_AFTER_AC' => 'AC 后',
'ALL' => '所有人',
],
'default_value' => UOJProblem::cur()->getExtraConfig('view_details_type'),
]);
$view_type_form->handle = function () {
$config = UOJProblem::cur()->getExtraConfig();
$config['view_content_type'] = $_POST['view_content_type'];
$config['view_all_details_type'] = $_POST['view_all_details_type'];
$config['view_details_type'] = $_POST['view_details_type'];
$esc_config = json_encode($config);
DB::update([
"update problems",
"set", ["extra_config" => $esc_config],
"where", ["id" => UOJProblem::info('id')]
]);
};
$view_type_form->runAtServer();
$solution_view_type_form = new UOJForm('solution_view_type');
$solution_view_type_form->addSelect('view_solution_type', [
'div_class' => 'row align-items-center g-0',
'label_class' => 'form-label col-auto m-0 flex-grow-1',
'select_class' => 'col-auto form-select w-auto',
'label' => '查看题解',
'options' => [
'NONE' => '禁止',
'ALL_AFTER_AC' => 'AC 后',
'ALL' => '所有人',
],
'default_value' => UOJProblem::cur()->getExtraConfig('view_solution_type'),
]);
$solution_view_type_form->addSelect('submit_solution_type', [
'div_class' => 'row align-items-center g-0 mt-3',
'label_class' => 'form-label col-auto m-0 flex-grow-1',
'select_class' => 'col-auto form-select w-auto',
'label' => '提交题解',
'options' => [
'NONE' => '禁止',
'ALL_AFTER_AC' => 'AC 后',
'ALL' => '所有人',
],
'default_value' => UOJProblem::cur()->getExtraConfig('submit_solution_type'),
]);
$solution_view_type_form->handle = function () {
$config = UOJProblem::cur()->getExtraConfig();
$config['view_solution_type'] = $_POST['view_solution_type'];
$config['submit_solution_type'] = $_POST['submit_solution_type'];
$esc_config = json_encode($config);
DB::update([
"update problems",
"set", ["extra_config" => $esc_config],
"where", ["id" => UOJProblem::info('id')]
]);
};
$solution_view_type_form->runAtServer();
?>
<?php echoUOJPageHeader('题面编辑 - ' . HTML::stripTags(UOJProblem::info('title'))) ?>
@ -102,11 +310,13 @@ $difficulty_form->runAtServer();
管理者
</a>
</li>
<?php if (UOJProblem::info('type') == 'local') : ?>
<li class="nav-item">
<a class="nav-link" href="/problem/<?= UOJProblem::info('id') ?>/manage/data" role="tab">
数据
</a>
</li>
<?php endif ?>
</ul>
<div class="card card-default">
@ -121,8 +331,8 @@ $difficulty_form->runAtServer();
<h2 class="h3 card-title">提示</h2>
<ol>
<li>请勿引用不稳定的外部资源(如来自个人服务器的图片或文档等),以便备份及后期维护;</li>
<li>请勿在题面中直接插入大段 HTML 代码,这可能会破坏页面的显示,可以考虑使用 <a class="text-decoration-none" href="/html2markdown" target="_blank">转换工具</a> 转换后再作修正;</li>
<li>图片上传推荐使用 <a class="text-decoration-none" href="/image_hosting" target="_blank">S2OJ 图床</a>,以免后续产生外链图片大量失效的情况。</li>
<li>请勿在题面中直接插入大段 HTML 代码,这可能会破坏页面的显示,可以考虑使用 <a class="text-decoration-none" href="/apps/html2markdown" target="_blank">转换工具</a> 转换后再作修正;</li>
<li>图片上传推荐使用 <a class="text-decoration-none" href="/apps/image_hosting" target="_blank">S2OJ 图床</a>,以免后续产生外链图片大量失效的情况。</li>
</ol>
<p class="card-text">
更多内容请查看 S2OJ 用户手册中的「<a class="text-decoration-none" href="https://s2oj.github.io/#/manage/problem?id=%e4%bc%a0%e9%a2%98%e6%8c%87%e5%bc%95">传题指引</a>」部分。
@ -261,6 +471,47 @@ $difficulty_form->runAtServer();
<?php $difficulty_form->printHTML() ?>
</div>
</div>
<?php if (UOJProblem::info('type') == 'remote') : ?>
<div class="card mt-3">
<div class="card-header fw-bold">
重新爬取题目信息
</div>
<div class="card-body">
<?php $re_crawl_form->printHTML() ?>
</div>
</div>
<div class="card mt-3 border-danger">
<div class="card-header fw-bold text-bg-danger border-danger">
转换为本地题目
</div>
<div class="card-body border-danger">
<?php $convert_local_form->printHTML() ?>
</div>
<div class="card-footer bg-transparent small text-muted border-danger">
转换为本地题目之后可以上传自行准备的测试数据进行评测。转换后不再将代码提交至远端 OJ 进行评测。该操作不可逆。
</div>
</div>
<?php endif ?>
<div class="card mt-3">
<div class="card-header fw-bold">
提交记录可视权限
</div>
<div class="card-body">
<?php $view_type_form->printHTML() ?>
</div>
</div>
<div class="card mt-3">
<div class="card-header fw-bold">
题解可视权限
</div>
<div class="card-body">
<?php $solution_view_type_form->printHTML() ?>
</div>
</div>
</aside>
</div>

View File

@ -2,6 +2,7 @@
requireLib('bootstrap5');
requireLib('mathjax');
requireLib('hljs');
requireLib('pdf.js');
requirePHPLib('form');
Auth::check() || redirectToLogin();

View File

@ -107,8 +107,8 @@ $blog_editor->runAtServer();
<ol>
<li>题解发布后还需要返回对应题目的题解页面 <b>手动输入博客 ID</b> 来将本文添加到题目的题解列表中(博客 ID 可以在右上角找到);</li>
<li>请勿引用不稳定的外部资源(如来自个人服务器的图片或文档等),以便备份及后期维护;</li>
<li>请勿在博文中直接插入大段 HTML 代码,这可能会破坏页面的显示,可以考虑使用 <a class="text-decoration-none" href="/html2markdown" target="_blank">转换工具</a> 转换后再作修正;</li>
<li>图片上传推荐使用 <a class="text-decoration-none" href="/image_hosting" target="_blank">S2OJ 图床</a>,以免后续产生外链图片大量失效的情况。</li>
<li>请勿在博文中直接插入大段 HTML 代码,这可能会破坏页面的显示,可以考虑使用 <a class="text-decoration-none" href="/apps/html2markdown" target="_blank">转换工具</a> 转换后再作修正;</li>
<li>图片上传推荐使用 <a class="text-decoration-none" href="/apps/image_hosting" target="_blank">S2OJ 图床</a>,以免后续产生外链图片大量失效的情况。</li>
</ol>
<p class="card-text">
帮助:<a class="text-decoration-none" href="http://uoj.ac/blog/7">UOJ 博客使用教程</a>

View File

@ -0,0 +1,123 @@
<?php
/*
#################################################################################################################
This is an OPTIONAL configuration file.
The role of this file is to make updating of "tinyfilemanager.php" easier.
So you can:
-Feel free to remove completely this file and configure "tinyfilemanager.php" as a single file application.
or
-Put inside this file all the static configuration you want and forgot to configure "tinyfilemanager.php".
#################################################################################################################
*/
// Auth with login/password
// set true/false to enable/disable it
// Is independent from IP white- and blacklisting
$use_auth = false;
// Login user name and password
// Users: array('Username' => 'Password', 'Username2' => 'Password2', ...)
// Generate secure password hash - https://tinyfilemanager.github.io/docs/pwd.html
$auth_users = [];
//set application theme
//options - 'light' and 'dark'
$theme = 'light';
// Readonly users
// e.g. array('users', 'guest', ...)
$readonly_users = [];
// Enable highlight.js (https://highlightjs.org/) on view's page
$use_highlightjs = true;
// highlight.js style
// for dark theme use 'ir-black'
$highlightjs_style = 'vs';
// Enable ace.js (https://ace.c9.io/) on view's page
$edit_files = true;
// Default timezone for date() and time()
// Doc - http://php.net/manual/en/timezones.php
$default_timezone = 'Asia/Shanghai'; // UTC
// Root path for file manager
// use absolute path of directory i.e: '/var/www/folder' or $_SERVER['DOCUMENT_ROOT'].'/folder'
$root_path = UOJContext::storagePath();
// Root url for links in file manager. Relative to $http_host. Variants: '', 'path/to/subfolder'
// Will not working if $root_path will be outside of server document root
$root_url = '';
// Server hostname. Can set manually if wrong
$http_host = UOJContext::httpHost();
// user specific directories
// array('Username' => 'Directory path', 'Username2' => 'Directory path', ...)
$directories_users = [];
// input encoding for iconv
$iconv_input_encoding = 'UTF-8';
// date() format for file modification date
// Doc - https://www.php.net/manual/en/function.date.php
$datetime_format = UOJTime::FORMAT;
// Allowed file extensions for create and rename files
// e.g. 'txt,html,css,js'
$allowed_file_extensions = '';
// Allowed file extensions for upload files
// e.g. 'gif,png,jpg,html,txt'
$allowed_upload_extensions = '';
// Favicon path. This can be either a full url to an .PNG image, or a path based on the document root.
// full path, e.g http://example.com/favicon.png
// local path, e.g images/icons/favicon.png
$favicon_path = '/images/favicon.ico';
// Files and folders to excluded from listing
// e.g. array('myfile.html', 'personal-folder', '*.php', ...)
$exclude_items = [];
// Online office Docs Viewer
// Availabe rules are 'google', 'microsoft' or false
// google => View documents using Google Docs Viewer
// microsoft => View documents using Microsoft Web Apps Viewer
// false => disable online doc viewer
$online_viewer = 'false';
// Sticky Nav bar
// true => enable sticky header
// false => disable sticky header
$sticky_navbar = true;
// max upload file size
$max_upload_size_bytes = 40 * 1024 * 1024;
// Possible rules are 'OFF', 'AND' or 'OR'
// OFF => Don't check connection IP, defaults to OFF
// AND => Connection must be on the whitelist, and not on the blacklist
// OR => Connection must be on the whitelist, or not on the blacklist
$ip_ruleset = 'OFF';
// Should users be notified of their block?
$ip_silent = true;
// IP-addresses, both ipv4 and ipv6
$ip_whitelist = array(
'127.0.0.1', // local ipv4
'::1' // local ipv6
);
// IP-addresses, both ipv4 and ipv6
$ip_blacklist = array(
'0.0.0.0', // non-routable meta ipv4
'::' // non-routable meta ipv6
);
?>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,168 @@
{
"appName": "Tiny File Manager",
"version": "2.5.1",
"language": [
{
"name": "简体中文",
"code": "zh-CN",
"translation": {
"AppName": "文件管理器",
"AppTitle": "文件管理器",
"Login": "登录",
"Username": "账号",
"Password": "密码",
"Logout": "退出",
"Move": "移动",
"Copy": "复制",
"Save": "保存",
"SelectAll": "全选",
"UnSelectAll": "取消全选",
"File": "文件",
"Back": "取消",
"Size": "大小",
"Perms": "权限",
"Modified": "修改时间",
"Owner": "拥有者",
"Search": "查找",
"NewItem": "创建新文件/文件夹",
"Folder": "文件夹",
"Delete": "删除",
"CopyTo": "复制到",
"DirectLink": "直链",
"UploadingFiles": "上传",
"ChangePermissions": "修改权限",
"Copying": "复制中",
"CreateNewItem": "创建新文件",
"Name": "文件名",
"AdvancedEditor": "高级编辑器",
"RememberMe": "记住登录信息",
"Actions": "执行操作",
"Upload": "上传",
"Cancel": "取消",
"InvertSelection": "反向选择",
"DestinationFolder": "目标文件夹",
"ItemType": "文件类型",
"ItemName": "创建名称",
"CreateNow": "创建",
"Download": "下载",
"UnZip": "解压缩",
"UnZipToFolder": "解压至目标文件夹",
"Edit": "编辑",
"NormalEditor": "编辑器",
"BackUp": "备份",
"SourceFolder": "源文件夹",
"Files": "文件",
"Change": "修改",
"Settings": "设置",
"Language": "语言",
"Open": "打开",
"Group": "用户组",
"Other": "其它用户",
"Read": "读取权限",
"Write": "写入权限",
"Execute": "执行权限",
"Rename": "重命名",
"enable": "启用",
"disable": "禁用",
"ErrorReporting": "上传错误报告",
"ShowHiddenFiles": "显示隐藏文件",
"Help": "帮助",
"HideColumns": "隐藏权限&拥有者",
"CalculateFolderSize": "显示文件夹大小",
"FullSize": "所有文件大小",
"MemoryUsed": "使用内存",
"PartitionSize": "可用空间",
"FreeOf": "磁盘大小",
"Check Latest Version": "检查更新",
"Generate new password hash": "生成新的hash密码",
"Report Issue": "报告问题",
"Help Documents": "帮助文档",
"Generate": "生成",
"Renamed from": "生成",
"Preview": "预览",
"Access denied. IP restriction applicable": "访问被拒绝。适用的IP限制",
"You are logged in": "您已登录",
"Login failed. Invalid username or password": "登录失败。用户名或密码无效",
"password_hash not supported, Upgrade PHP version": "不支持password_hash,请升级PHP版本",
"Root path": "根路径",
"not found!": "没有找到!",
"File not found": "找不到文件",
"Deleted": "删除",
"not deleted": "未删除",
"Invalid file or folder name": "无效的文件或文件夹名",
"Created": "已创建",
"File extension is not allowed": "不允许文件扩展名",
"already exists": "已经存在",
"not created": "未创建",
"Invalid characters in file or folder name": "文件或文件夹名称中的无效字符",
"Source path not defined": "未定义源路径",
"Moved from": "移动自",
"to": "至",
"File or folder with this path already exists": "具有此路径的文件或文件夹已存在",
"Error while moving from": "移动时出错",
"Copied from": "复制自",
"Error while copying from": "复制时出错",
"Paths must be not equal": "路径必须不相等",
"Nothing selected": "未选择任何内容",
"Error while renaming from": "重命名时出错",
"Invalid characters in file name": "文件名中的无效字符",
"Invalid Token.": "无效令牌。",
"Selected files and folder deleted": "已删除选定的文件和文件夹",
"Error while deleting items": "删除项目时出错",
"Operations with archives are not available": "存档操作不可用",
"Archive": "存档",
"Archive not created": "未创建存档",
"Archive unpacked": "存档未打包",
"Archive not unpacked": "存档未打开",
"Permissions changed": "权限已更改",
"Permissions not changed": "权限未更改",
"Select folder": "选择文件夹",
"Theme": "主题",
"light": "浅色",
"dark": "深色",
"Error while fetching archive info": "获取存档信息时出错",
"File Saved Successfully": "文件保存成功",
"FILE EXTENSION HAS NOT SUPPORTED": "文件扩展名不受支持",
"Folder is empty": "文件夹为空",
"Delete selected files and folders?": "是否删除选定的文件和文件夹?",
"Create archive?": "创建存档?",
"Zip": "Zip",
"Tar": "Tar",
"Zero byte file! Aborting download": "零字节文件!正在中止下载",
"Cannot open file! Aborting download": "无法打开文件!正在中止下载",
"Filter": "过滤器",
"Advanced Search": "高级搜索",
"Search file in folder and subfolders...": "在文件夹和子文件夹中搜索文件…",
"Are you sure want to": "你确定要",
"Okay": "确定",
"a files": "一个文件",
"Enter here...": "在此处输入...",
"Enter new file name": "输入新文件名",
"Full path": "完整路径",
"File size": "文件大小",
"MIME-type": "MIME类型",
"Image sizes": "图像大小",
"Charset": "编码格式",
"Image": "图片",
"Audio": "音频",
"Video": "视频",
"Upload from URL": "从URL上传",
"Files in archive": "档案文件",
"Total size": "总大小",
"Compression": "压缩",
"Size in archive": "存档中的大小",
"Invalid Token.": "无效令牌",
"Fullscreen": "全屏",
"Search": "搜索",
"Word Wrap": "自动换行",
"Undo": "撤消",
"Redo": "恢复",
"Select Document Type": "选择文档类型",
"Select Mode": "选择模式",
"Select Theme": "选择主题",
"Select Font Size": "选择字体大小",
"Are you sure want to rename?": "是否确实要重命名?"
}
}
]
}

View File

@ -1,100 +1,130 @@
<?php
requireLib('bootstrap5');
if (!Auth::check()) {
redirectToLogin();
}
function handleMsgPost() {
if (!isset($_POST['receiver'])) {
return 'fail';
}
if (!isset($_POST['message'])) {
return 'fail';
}
if (0 > strlen($_POST['message']) || strlen($_POST['message']) > 65535) {
return 'fail';
}
$receiver = $_POST['receiver'];
$esc_message = DB::escape($_POST['message']);
$sender = Auth::id();
$receiver = UOJRequest::user(UOJRequest::POST, 'receiver');
if (!$receiver) {
return 'fail';
}
$message = $_POST['message'];
if (!validateUsername($receiver) || !UOJUser::query($receiver)) {
if ($receiver['username'] === Auth::id()) {
return 'fail';
}
DB::query("insert into user_msg (sender, receiver, message, send_time) values ('$sender', '$receiver', '$esc_message', now())");
DB::insert([
"insert into user_msg",
"(sender, receiver, message, send_time)",
"values", DB::tuple([Auth::id(), $receiver['username'], $message, DB::now()])
]);
return "ok";
}
function getConversations() {
$username = Auth::id();
$result = DB::query("select * from user_msg where sender = '$username' or receiver = '$username' order by send_time DESC");
$ret = array();
while ($msg = DB::fetch($result)) {
$res = DB::selectAll([
"select * from user_msg",
"where", DB::lor([
"sender" => $username,
"receiver" => $username,
]),
"order by send_time DESC"
]);
$ret = [];
foreach ($res as $msg) {
if ($msg['sender'] !== $username) {
if (isset($ret[$msg['sender']])) {
$ret[$msg['sender']][1] |= ($msg['read_time'] == null);
continue;
}
$ret[$msg['sender']] = array($msg['send_time'], ($msg['read_time'] == null));
$ret[$msg['sender']] = [$msg['send_time'], ($msg['read_time'] == null), $msg['message']];
} else {
if (isset($ret[$msg['receiver']])) {
continue;
}
$ret[$msg['receiver']] = array($msg['send_time'], 0);
if (isset($ret[$msg['receiver']])) continue;
$ret[$msg['receiver']] = [$msg['send_time'], 0, $msg['message']];
}
}
$res = [];
foreach ($ret as $name => $con) {
$res[] = [$con[0], $con[1], $name];
$user = UOJUser::query($name);
$res[] = [
$con[0],
$con[1],
$name,
HTML::avatar_addr($user, 128),
UOJUser::getRealname($user),
UOJUser::getUserColor($user),
$con[2],
];
}
usort($res, function ($a, $b) {
return -strcmp($a[0], $b[0]);
});
return json_encode($res);
}
function getHistory() {
$username = Auth::id();
if (!isset($_GET['conversationName']) || !validateUsername($_GET['conversationName'])) {
$receiver = UOJRequest::user(UOJRequest::GET, 'conversationName');
$page_num = UOJRequest::uint(UOJRequest::GET, 'pageNumber');
if (!$receiver || $receiver['username'] === $username) {
return '[]';
}
if (!isset($_GET['pageNumber']) || !validateUInt($_GET['pageNumber'])) {
if (!$page_num) { // false, null, or zero
return '[]';
}
$conversationName = $_GET['conversationName'];
$pageNumber = ($_GET['pageNumber'] - 1) * 10;
DB::query("update user_msg set read_time = now() where sender = '$conversationName' and receiver = '$username' and read_time is null");
DB::update([
"update user_msg",
"set", ["read_time" => DB::now()],
"where", [
"sender" => $receiver['username'],
"receiver" => $username,
"read_time" => null,
]
]);
$result = DB::query("select * from user_msg where (sender = '$username' and receiver = '$conversationName') or (sender = '$conversationName' and receiver = '$username') order by send_time DESC limit $pageNumber, 11");
$ret = array();
while ($msg = DB::fetch($result)) {
$ret[] = array($msg['message'], $msg['send_time'], $msg['read_time'], $msg['id'], ($msg['sender'] == $username));
$result = DB::selectAll([
"select * from user_msg",
"where", DB::lor([
DB::land([
"sender" => $username,
"receiver" => $receiver['username']
]),
DB::land([
"sender" => $receiver['username'],
"receiver" => $username
])
]),
"order by send_time DESC", DB::limit(($page_num - 1) * 10, 11)
]);
$ret = [];
foreach ($result as $msg) {
$ret[] = [
$msg['message'],
$msg['send_time'],
$msg['read_time'],
$msg['id'],
($msg['sender'] === $username),
];
}
return json_encode($ret);
}
/*
function deleteMsg($msgId) {
return 1;
$str = <<<EOD
select * from user_msg
where id = $msgId
and read_time is null
EOD;
$result = DB::query($str);
if (DB::fetch($result)) {
$str = <<<EOD
delete from user_msg
where id = $msgId
EOD;
DB::query($str);
return 1;
}
return 0;
}
*/
if (isset($_POST['user_msg'])) {
die(handleMsgPost());
} elseif (isset($_GET['getConversations'])) {
@ -106,48 +136,76 @@ if (isset($_POST['user_msg'])) {
<?php echoUOJPageHeader('私信') ?>
<h1 class="page-header">私信</h1>
<h1>私信</h1>
<div id="conversations"></div>
<style>
@media (min-width: 768px) {
.chat-container {
height: calc(100ch - 10rem);
}
}
</style>
<div id="history" style="display:none">
<div class="card border-primary">
<div class="card-header bg-primary text-white">
<button type="button" id="goBack" class="btn btn-info btn-sm" style="position:absolute">返回</button>
<div class="card overflow-hidden-md chat-container">
<div class="row gx-0 flex-grow-1 h-100">
<div class="col-md-3 border-end h-100">
<div class="list-group list-group-flush h-100 overflow-auto" id="conversations"></div>
</div>
<div class="col-md-9 h-100" id="history" style="display: none">
<div class="card h-100 border-0 rounded-0">
<div class="card-header">
<button id="goBack" class="btn-close position-absolute" aria-label="关闭对话"></button>
<div id="conversation-name" class="text-center"></div>
</div>
<div class="card-body">
<ul class="pagination top-buffer-no justify-content-between">
<li class="previous"><a class="btn btn-outline-secondary text-primary" href="#" id="pageLeft">&larr; 更早的消息</a></li>
<li class="text-center" id="pageShow" style="line-height:32px"></li>
<li class="next"><a class="btn btn-outline-secondary text-primary" href="#" id="pageRight">更新的消息 &rarr;</a></li>
</ul>
<div id="history-list" style="min-height: 200px;">
<div class="card-body overflow-auto" id="history-list-container">
<div id="history-list" style="min-height: 200px;"></div>
</div>
<ul class="pagination top-buffer-no justify-content-between">
<li class="previous"><a class="btn btn-outline-secondary text-primary" href="#history" id="pageLeft2">&larr; 更早的消息</a></li>
<li class="next"><a class="btn btn-outline-secondary text-primary" href="#history" id="pageRight2">更新的消息 &rarr;</a></li>
<div class="card-footer bg-transparent">
<ul class="pagination pagination-sm justify-content-between mt-1">
<li class="page-item">
<button class="page-link rounded" id="pageLeft">
<i class="bi bi-chevron-left"></i>
更早的消息
</button>
</li>
<li class="page-item">
<button class="page-link rounded" id="pageRight">
更新的消息
<i class="bi bi-chevron-right"></i>
</button>
</li>
</ul>
<hr />
<form id="form-message">
<div class="form-group" id="form-group-message">
<textarea id="input-message" class="form-control"></textarea>
<form id="form-message" class="">
<div id="form-group-message" class="flex-grow-1">
<textarea id="input-message" class="form-control" style="resize: none;" data-no-autosize></textarea>
<span id="help-message" class="help-block"></span>
</div>
<div class="text-right">
<button type="submit" id="message-submit" class="btn btn-info btn-md">发送</button>
<div class="text-end mt-2">
<span class="text-muted small"> Ctrl+Enter 键发送</span>
<button type="submit" id="message-submit" class="btn btn-primary flex-shrink-0 ms-3">
发送
<i class="bi bi-send"></i>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
var REFRESH_INTERVAL = 30 * 1000;
$(document).ready(function() {
$.ajaxSetup({
async: false
});
refreshConversations();
setInterval(refreshConversations, REFRESH_INTERVAL);
<?php if (isset($_GET['enter'])) : ?>
enterConversation(<?= json_encode($_GET['enter']) ?>);
<?php endif ?>
@ -155,17 +213,64 @@ if (isset($_POST['user_msg'])) {
</script>
<script type="text/javascript">
function addButton(conversationName, send_time, type) {
<?php $enter_user = UOJRequest::user(UOJRequest::GET, 'enter'); ?>
var conversations = {};
var intervalId = 0;
var user_avatar = '<?= HTML::avatar_addr(Auth::user(), 80) ?>';
var enter_user = ['<?= $enter_user['username'] ?>', '<?= UOJUser::getRealname($enter_user) ?>', '<?= UOJUser::getUserColor($enter_user) ?>'];
function formatDate(date) {
var d = new Date(date),
month = '' + (d.getMonth() + 1),
day = '' + d.getDate(),
year = d.getFullYear();
if (month.length < 2)
month = '0' + month;
if (day.length < 2)
day = '0' + day;
return [year, month, day].join('-');
}
function formatTime(date) {
var d = new Date(date),
hour = '' + d.getHours(),
minute = '' + d.getMinutes();
if (hour.length < 2)
hour = '0' + hour;
if (minute.length < 2)
minute = '0' + minute;
return [hour, minute].join(':');
}
function addButton(conversationName, send_time, type, avatar_addr, realname, color, last_message) {
var now = new Date();
var time = new Date(send_time);
var timeStr = formatDate(send_time);
if (formatDate(now) === timeStr) {
timeStr = formatTime(send_time);
}
$("#conversations").append(
'<div class="row top-buffer-sm">' +
'<div class="col-sm-3">' +
'<button type="button" class="btn btn-' + (type ? 'warning' : 'primary') + ' btn-block" ' +
'<div class="list-group-item list-group-item-action p-2 d-flex ' + (type ? 'list-group-item-warning' : '') + '" style="cursor: pointer; user-select: none;" ' +
'onclick="enterConversation(\'' + conversationName + '\')">' +
conversationName +
'</button>' +
'<div class="flex-shrink-0 me-3">' +
'<img class="rounded" width="56" height="56" src="' + avatar_addr + '" />' +
'</div>' +
'<div class="flex-grow-1 overflow-hidden">' +
'<div class="d-flex justify-content-between">' +
getUserSpan(conversationName, '', color) +
'<span class="float-end text-muted small flex-shrink-0 lh-lg">' +
timeStr +
'</span>' +
'</div>' +
'<div class="text-muted text-nowrap text-truncate">' +
htmlspecialchars(last_message) +
'</div>' +
'<div class="col-sm-9" style="line-height:34px">' +
'最后发送时间:' + send_time +
'</div>' +
'</div>'
);
@ -173,24 +278,22 @@ if (isset($_POST['user_msg'])) {
function addBubble(content, send_time, read_time, msgId, conversation, page, type) {
$("#history-list").append(
'<div style=' + (!type ? "margin-left:0%;margin-right:20%;" : "margin-left:20%;margin-right:0%;") + '>' +
'<div class="card border-info mb-4">' +
'<div class="card-body" style="background:#17a2b8; word-break: break-all">' +
'<div style="white-space:pre-wrap">' +
'<div class="d-flex align-items-end mt-3" style="' + (type ? 'margin-left:20%;' : 'margin-right:20%;') + '">' +
(type ? '' : '<img class="flex-shrink-0 me-2 rounded" width="32" height="32" src="' + conversations[conversation][1] + '" style="user-select: none;" />') +
'<div class="card flex-grow-1">' +
'<div class="card-body px-3 py-2" style="white-space:pre-wrap">' +
htmlspecialchars(content) +
'</div>' +
'</div>' +
'<div>' +
'<div class="row">' +
'<div class="col-sm-6">' +
'发送时间:' + send_time +
'</div>' +
'<div class="col-sm-6 text-right">' +
'查看时间:' + (read_time == null ? '<strong>未查看</strong>' : read_time) +
'</div>' +
'</div>' +
'<div class="card-footer text-muted px-3 py-1">' +
'<span class="small">' +
'<i class="bi bi-clock"></i> ' + send_time +
'</span>' +
(read_time == null ?
'<span class="float-end" data-bs-toggle="tooltip" data-bs-title="未读"><i class="bi bi-check2"></i></span>' :
'<span class="float-end" data-bs-toggle="tooltip" data-bs-title="' + read_time + '"><i class="bi bi-check2-all"></i></span>') +
'</div>' +
'</div>' +
(type ? '<img class="flex-shrink-0 ms-2 rounded" width="32" height="32" src="' + user_avatar + '" style="user-select: none;" />' : '') +
'</div>'
);
}
@ -204,7 +307,7 @@ if (isset($_POST['user_msg'])) {
$('#help-message').text('');
$('#form-group-message').removeClass('has-error');
$.post('', {
$.post('/user_msg', {
user_msg: 1,
receiver: conversationName,
message: $('#input-message').val()
@ -216,9 +319,9 @@ if (isset($_POST['user_msg'])) {
function refreshHistory(conversation, page) {
$("#history-list").empty();
var ret = false;
$('#conversation-name').text(conversation);
$('#conversation-name').html(getUserLink(conversation, conversation == enter_user[0] ? enter_user[1] : conversations[conversation][2], conversation == enter_user[0] ? enter_user[2] : conversations[conversation][3]));
$('#pageShow').text("" + page.toString() + "");
$.get('', {
$.get('/user_msg', {
getHistory: '',
conversationName: conversation,
pageNumber: page
@ -236,29 +339,39 @@ if (isset($_POST['user_msg'])) {
}
var message = result[msg];
addBubble(message[0], message[1], message[2], message[3], conversation, page, message[4]);
if ((++cnt) + 1 == result.length && F) break;
if ((++cnt) + 1 == result.length && F) {
break;
}
if (result.length == 11) ret = true;
}
if (result.length == 11) {
ret = true;
}
bootstrap.Tooltip.jQueryInterface.call($('#history-list [data-bs-toggle="tooltip"]'), {
container: $('#history-list'),
});
});
return ret;
}
function refreshConversations() {
$("#conversations").empty();
$.get('', {
getConversations: ""
$.get('/user_msg', {
getConversations: 1
}, function(msg) {
var result = JSON.parse(msg);
for (i in result) {
var conversation = result[i];
if (conversation[1] == 1) {
addButton(conversation[2], conversation[0], conversation[1]);
addButton(conversation[2], conversation[0], conversation[1], conversation[3], conversation[4], conversation[5], conversation[6]);
}
conversations[conversation[2]] = [conversation[0], conversation[3], conversation[4], conversation[5]];
}
for (i in result) {
var conversation = result[i];
if (conversation[1] == 0) {
addButton(conversation[2], conversation[0], conversation[1]);
addButton(conversation[2], conversation[0], conversation[1], conversation[3], conversation[4], conversation[5], conversation[6]);
}
}
});
@ -267,40 +380,50 @@ if (isset($_POST['user_msg'])) {
function enterConversation(conversationName) {
var slideTime = 300;
var page = 1;
$("#conversations").hide(slideTime);
var changeAble = refreshHistory(conversationName, page);
$("#history").slideDown(slideTime);
clearInterval(intervalId);
intervalId = setInterval(function() {
changeAble = refreshHistory(conversationName, page);
}, REFRESH_INTERVAL);
$('#history').show();
$('#conversations').addClass('d-none d-md-block')
$("#history-list-container").scrollTop($("#history-list").height());
$('#input-message').unbind('keydown').keydown(function(e) {
if (e.keyCode == 13 && e.ctrlKey) {
$('#message-submit').click();
}
});
$('#form-message').unbind("submit").submit(function() {
submitMessagePost(conversationName);
page = 1;
changeAble = refreshHistory(conversationName, page);
refreshConversations();
$("#history-list-container").scrollTop($("#history-list").height());
return false;
});
$('#goBack').unbind("click").click(function() {
clearInterval(intervalId);
refreshConversations();
$("#history").slideUp(slideTime);
$("#conversations").show(slideTime);
$("#history").hide();
$("#conversations").removeClass('d-none d-md-block');
return;
});
$('#pageLeft').unbind("click").click(function() {
if (changeAble) page++;
if (changeAble) {
page++;
clearInterval(intervalId);
}
changeAble = refreshHistory(conversationName, page);
return false;
});
$('#pageLeft2').unbind("click").click(function() {
if (changeAble) page++;
changeAble = refreshHistory(conversationName, page);
});
$('#pageRight').unbind("click").click(function() {
if (page > 1) page--;
if (page > 1) {
page--;
clearInterval(intervalId);
}
changeAble = refreshHistory(conversationName, page);
return false;
});
$('#pageRight2').unbind("click").click(function() {
if (page > 1) page--;
changeAble = refreshHistory(conversationName, page);
});
}
</script>

View File

@ -5,6 +5,7 @@
function dataNewProblem($id) {
mkdir("/var/uoj_data/upload/$id");
mkdir("/var/uoj_data/$id");
mkdir(UOJContext::storagePath() . "/problem_resources/$id");
UOJLocalRun::execAnd([
['cd', '/var/uoj_data'],

View File

@ -465,6 +465,7 @@ class JudgmentDetailsPrinter {
echo '</div>';
} elseif ($node->nodeName == 'subtask') {
$subtask_info = $node->getAttribute('info');
$subtask_title = $node->getAttribute('title');
$subtask_num = $node->getAttribute('num');
$subtask_score = $node->getAttribute('score');
$subtask_time = $this->_get_attr($node, 'time', -1);
@ -482,10 +483,14 @@ class JudgmentDetailsPrinter {
echo '<div class="row">';
echo '<div class="col-sm-4">';
if ($subtask_title !== '') {
echo '<h3 class="fs-5">', $subtask_title, ': ', '</h3>';
} else {
echo '<h3 class="fs-5">', 'Subtask #', $subtask_num, ': ', '</h3>';
}
echo '</div>';
if ($this->styler->show_score) {
if ($this->styler->show_score && $subtask_score !== '') {
echo '<div class="col-sm-2">';
echo '<i class="bi bi-clipboard-check"></i> ', $subtask_score, ' pts';
echo '</div>';
@ -494,7 +499,7 @@ class JudgmentDetailsPrinter {
echo htmlspecialchars($subtask_info);
echo '</div>';
} else {
echo '<div class="col-sm-4">';
echo '<div class="col-sm-4 uoj-status-text">';
echo $this->styler->getTestInfoIcon($subtask_info);
echo htmlspecialchars($subtask_info);
echo '</div>';
@ -541,6 +546,9 @@ class JudgmentDetailsPrinter {
$accordion_parent .= "_collapse_subtask_{$this->subtask_num}_accordion";
}
$accordion_collapse = "{$accordion_parent}_collapse_test_{$test_num}";
if ($this->subtask_num != null) {
$accordion_collapse .= "_in_subtask_{$this->subtask_num}";
}
if (!$this->styler->shouldFadeDetails($test_info)) {
echo '<div class="card-header uoj-submission-result-item bg-transparent rounded-0 border-0" data-bs-toggle="collapse" data-bs-parent="#', $accordion_parent, '" data-bs-target="#', $accordion_collapse, '">';
} else {
@ -555,7 +563,7 @@ class JudgmentDetailsPrinter {
}
echo '</div>';
if ($this->styler->show_score) {
if ($this->styler->show_score && $test_score !== '') {
echo '<div class="col-sm-2">';
echo '<i class="bi bi-clipboard-check"></i> ', $test_score, ' pts';
echo '</div>';
@ -664,6 +672,11 @@ class JudgmentDetailsPrinter {
echo '<pre class="bg-light p-3 rounded">', "\n";
$this->_print_c($node);
echo "\n</pre>";
} elseif ($node->nodeName == 'ans') {
echo '<h4 class="fs-6"><span>answer: </span></h4>';
echo '<pre class="bg-light p-3 rounded">', "\n";
$this->_print_c($node);
echo "\n</pre>";
} elseif ($node->nodeName == 'res') {
echo '<h4 class="fs-6"><span>result: </span></h4>';
echo '<pre class="bg-light p-3 rounded">', "\n";
@ -687,7 +700,7 @@ class JudgmentDetailsPrinter {
}
echo '<h4 class="mb-2">', $node->getAttribute("title"), ":</h4>";
}
echo '<pre>', "\n";
echo '<pre class="bg-light p-3 rounded">', "\n";
$this->_print_c($node);
echo "\n</pre>";
echo '</div>';
@ -757,7 +770,7 @@ class SubmissionDetailsStyler {
}
}
public function shouldFadeDetails($info) {
return $this->fade_all_details || $info == 'Extra Test Passed';
return $this->fade_all_details || $info == 'Extra Test Passed' || $info == 'Skipped';
}
}
class CustomTestSubmissionDetailsStyler {

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

@ -4,6 +4,7 @@ return [
'problem list' => 'Problem List',
'all problems' => 'All Problems',
'template problems' => 'Template Problems',
'remote problems' => 'Remote Problems',
'add new' => 'Add new problem',
'add new list' => 'Add new problem list',
'my problem' => 'My problem',

View File

@ -4,6 +4,7 @@ return [
'problem list' => '题单',
'all problems' => '总题库',
'template problems' => '模板题库',
'remote problems' => '远程题库',
'add new' => '添加新题',
'add new list' => '添加新题单',
'my problem' => '我的题目',

View File

@ -33,6 +33,7 @@ class Auth {
"select remember_token from user_info",
"where", ["username" => $username]
]);
if ($remember_token == '') {
$remember_token = uojRandString(60);
DB::update([
@ -54,10 +55,10 @@ class Auth {
"where", ["username" => $username]
]);
}
public static function logout() {
unset($_SESSION['username']);
unset($_SESSION['last_login']);
unset($_SESSION['last_visited']);
session_unset();
Cookie::safeUnset(session_name(), '/');
Cookie::safeUnset('uoj_username', '/');
Cookie::safeUnset('uoj_remember_token', '/');
DB::update([

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,18 @@ class HTML {
$def->addElement('header', 'Block', 'Flow', 'Common');
$def->addElement('footer', 'Block', 'Flow', 'Common');
$extra_allowed_html = [
mergeConfig($extra_allowed_html, [
'div' => [
'data-pdf' => 'Text',
'data-src' => 'URI',
],
'span' => [
'class' => 'Enum#uoj-username',
'data-realname' => 'Text',
'data-color' => 'Color',
],
'img' => ['width' => 'Text'],
];
]);
foreach ($extra_allowed_html as $element => $attributes) {
foreach ($attributes as $attribute => $type) {

View File

@ -27,7 +27,7 @@ class UOJBlogEditor {
$REQUIRE_LIB['blog-editor'] = '';
$this->validator = [
'title' => function(&$title) {
'title' => function (&$title) {
if ($title == '') {
return '标题不能为空';
}
@ -39,13 +39,13 @@ class UOJBlogEditor {
}
return '';
},
'content_md' => function(&$content_md) {
'content_md' => function (&$content_md) {
if (strlen($content_md) > 1000000) {
return '内容过长';
}
return '';
},
'tags' => function(&$tags) {
'tags' => function (&$tags) {
$tags = str_replace('', ',', $tags);
$tags_raw = explode(',', $tags);
if (count($tags_raw) > 10) {
@ -58,10 +58,10 @@ class UOJBlogEditor {
continue;
}
if (strlen($tag) > 30) {
return '标签 “' . HTML::escape($tag) .'” 太长';
return '标签 “' . HTML::escape($tag) . '” 太长';
}
if (in_array($tag, $tags, true)) {
return '标签 “' . HTML::escape($tag) .'” 重复出现';
return '标签 “' . HTML::escape($tag) . '” 重复出现';
}
$tags[] = $tag;
}
@ -113,7 +113,7 @@ class UOJBlogEditor {
if (preg_match('/^.*<!--.*readmore.*-->.*$/m', $this->post_data['content'], $matches, PREG_OFFSET_CAPTURE)) {
$content_less = substr($this->post_data['content'], 0, $matches[0][1]);
$content_more = substr($this->post_data['content'], $matches[0][1] + strlen($matches[0][0]));
$this->post_data['content'] = $purifier->purify($content_less).'<!-- readmore -->'.$purifier->purify($content_more);
$this->post_data['content'] = $purifier->purify($content_less) . '<!-- readmore -->' . $purifier->purify($content_more);
} else {
$this->post_data['content'] = $purifier->purify($this->post_data['content']);
}
@ -123,14 +123,16 @@ class UOJBlogEditor {
die(json_encode(array('content_md' => '不合法的 YAML 格式')));
}
$marked = function($md) use ($parsedown, $purifier) {
$marked = function ($md) use ($parsedown, $purifier) {
$dom = new DOMDocument;
$dom->loadHTML(mb_convert_encoding($parsedown->text($md), 'HTML-ENTITIES', 'UTF-8'));
$elements = $dom->getElementsByTagName('li');
foreach ($elements as $element) {
$element->setAttribute('class',
$element->getAttribute('class') . ' fragment');
$element->setAttribute(
'class',
$element->getAttribute('class') . ' fragment'
);
}
return $purifier->purify($dom->saveHTML());
@ -181,7 +183,10 @@ class UOJBlogEditor {
if (isset($_POST['need_preview'])) {
ob_start();
if ($this->type == 'blog') {
$req_lib = array('mathjax' => '');
$req_lib = [
'mathjax' => '',
'pdf.js' => '',
];
if (isset($REQUIRE_LIB['bootstrap5'])) {
$req_lib['bootstrap5'] = '';

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

@ -7,9 +7,12 @@ class UOJMarkdown extends ParsedownMath {
$this->options['username_with_color'] = $options['username_with_color'] ?: false;
// Special Block
$this->inlineMarkerList .= '@';
$this->InlineTypes['@'][] = 'SpecialBlock';
// https://gist.github.com/ShNURoK42/b5ce8baa570975db487c
$this->InlineTypes['@'][] = 'UserMention';
$this->inlineMarkerList .= '@';
}
// https://github.com/taufik-nurrohman/parsedown-extra-plugin/blob/1653418c5a9cf5277cd28b0b23ba2d95d18e9bc4/ParsedownExtraPlugin.php#L340-L345
@ -85,4 +88,35 @@ class UOJMarkdown extends ParsedownMath {
];
}
}
protected function inlineSpecialBlock($Excerpt) {
if (!isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') {
return;
}
$Excerpt['text'] = substr($Excerpt['text'], 1);
$Link = $this->inlineLink($Excerpt);
if ($Link === null) {
return;
}
$Inline = [
'extent' => $Link['extent'] + 1,
'element' => [
'name' => 'div',
'attributes' => [
'data-src' => $Link['element']['attributes']['href'],
"data-{$Link['element']['text']}" => $Link['element']['text'],
],
],
];
$Inline['element']['attributes'] += $Link['element']['attributes'];
unset($Inline['element']['attributes']['href']);
return $Inline;
}
}

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];
@ -646,6 +671,22 @@ class UOJProblem {
return "{$this->getDataFolderPath()}/$name";
}
public function getResourcesFolderPath() {
return UOJContext::storagePath() . "/problem_resources/" . $this->info['id'];
}
public function getResourcesPath($name = '') {
return "{$this->getResourcesFolderPath()}/$name";
}
public function getResourcesBaseUri() {
return "/problem/{$this->info['id']}/resources";
}
public function getResourcesUri($name = '') {
return "{$this->getResourcesBaseUri()}/{$name}";
}
public function getProblemConfArray(string $where = 'data') {
if ($where === 'data') {
return getUOJConf($this->getDataFilePath('problem.conf'));

View File

@ -5,10 +5,13 @@ class UOJRanklist {
$cfg += [
'top10' => false,
'card' => false,
'flush' => false,
'group_id' => null,
'page_len' => 50,
];
$cfg['flush'] |= $cfg['card'];
$conds = [];
if ($cfg['group_id']) {
@ -86,6 +89,9 @@ class UOJRanklist {
if ($cfg['card']) {
echo '<div class="card my-3">';
}
if ($cfg['flush']) {
echo '<div class="list-group list-group-flush">';
} else {
echo '<div class="list-group">';
@ -104,7 +110,18 @@ class UOJRanklist {
if ($cfg['card']) {
echo '</div>';
}
if ($pag->n_pages > 1) {
if ($cfg['flush']) {
echo '<div class="list-group-item">';
}
echo $pag->pagination();
if ($cfg['flush']) {
echo '</div>';
}
}
}
/**

View File

@ -0,0 +1,459 @@
<?php
class UOJRemoteProblem {
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36 S2OJ/3.1.0';
static $providers = [
'codeforces' => [
'name' => 'Codeforces',
'short_name' => 'CF',
'url' => 'https://codeforces.com',
'not_exists_texts' => [
'<th>Actions</th>',
'Statement is not available on English language',
'ограничение по времени на тест',
],
'languages' => ['C', 'C++', 'C++17', 'C++20', 'Java17', 'Pascal', 'Python2', 'Python3'],
],
'atcoder' => [
'name' => 'AtCoder',
'short_name' => 'AT',
'url' => 'https://atcoder.jp',
'not_exists_texts' => [
'Task not found',
'指定されたタスクが見つかりません',
],
'languages' => ['C', 'C++', 'Java11', 'Python3', 'Pascal'],
],
'uoj' => [
'name' => 'UniversalOJ',
'short_name' => 'UOJ',
'url' => 'https://uoj.ac',
'not_exist_texts' => [
'未找到该页面',
],
'languages' => ['C', 'C++03', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java8', 'Java11', 'Java17', 'Pascal'],
],
'loj' => [
'name' => 'LibreOJ',
'short_name' => 'LOJ',
'url' => 'https://loj.ac',
'languages' => ['C', 'C++03', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java17', 'Pascal'],
],
];
static function curl_get($url) {
$curl = new Curl\Curl();
$curl->setUserAgent(static::USER_AGENT);
$res = retry_loop(function () use (&$curl, $url) {
$curl->get($url);
if ($curl->error) {
return false;
}
return [
'content-type' => $curl->responseHeaders['Content-Type'],
'response' => $curl->response,
];
});
return $res;
}
static function getCodeforcesProblemUrl($id) {
if (str_starts_with($id, 'GYM')) {
return static::$providers['codeforces']['url'] . '/gym/' . preg_replace_callback('/GYM([1-9][0-9]{0,5})([A-Z][1-9]?)/', fn ($matches) => $matches[1] . '/problem/' . $matches[2], $id);
}
return static::$providers['codeforces']['url'] . '/problemset/problem/' . preg_replace_callback('/([1-9][0-9]{0,5})([A-Z][1-9]?)/', fn ($matches) => $matches[1] . '/' . $matches[2], $id);
}
static function getAtcoderProblemUrl($id) {
return static::$providers['atcoder']['url'] . '/contests/' . preg_replace_callback('/(\w+)([a-z][1-9]?)/', function ($matches) {
$contest = str_replace('_', '', $matches[1]);
if (str_ends_with($matches[1], '_')) {
return "{$contest}/tasks/{$matches[1]}{$matches[2]}";
}
return "{$contest}/tasks/{$matches[1]}_{$matches[2]}";
}, $id);
}
static function getUojProblemUrl($id) {
return static::$providers['uoj']['url'] . '/problem/' . $id;
}
static function getLojProblemUrl($id) {
return static::$providers['loj']['url'] . '/p/' . $id;
}
static function getCodeforcesProblemBasicInfoFromHtml($id, $html) {
$remote_provider = static::$providers['codeforces'];
$html = preg_replace('/\$\$\$/', '$', $html);
$dom = new \IvoPetkov\HTML5DOMDocument();
$dom->loadHTML($html);
$judgestatement = $dom->querySelector('html')->innerHTML;
foreach ($remote_provider['not_exists_texts'] as $text) {
if (str_contains($judgestatement, $text)) {
return null;
}
}
$statement_dom = $dom->querySelector('.problem-statement');
$title_prefix = str_starts_with($id, 'GYM') ? 'Gym' : 'CF';
$title = explode('. ', trim($statement_dom->querySelector('.title')->innerHTML))[1];
$title_id = str_starts_with($id, 'GYM') ? substr($id, 3) : $id;
$title = "{$title_prefix}{$title_id}{$title}";
$time_limit = intval(substr($statement_dom->querySelector('.time-limit')->innerHTML, 53));
$memory_limit = intval(substr($statement_dom->querySelector('.memory-limit')->innerHTML, 55));
$difficulty = -1;
foreach ($dom->querySelectorAll('.tag-box') as &$elem) {
$matches = [];
if (preg_match('/\*([0-9]{3,4})/', trim($elem->innerHTML), $matches)) {
$difficulty = intval($matches[1]);
break;
}
}
if ($difficulty != -1) {
$closest = null;
foreach (UOJProblem::$difficulty as $val) {
if ($closest === null || abs($val - $difficulty) < abs($closest - $difficulty)) {
$closest = $val;
}
}
$difficulty = $closest;
}
$statement_dom->removeChild($statement_dom->querySelector('.header'));
$statement_dom->childNodes->item(0)->insertBefore($dom->createElement('h3', 'Description'), $statement_dom->childNodes->item(0)->childNodes->item(0));
foreach ($statement_dom->querySelectorAll('.section-title') as &$elem) {
$elem->outerHTML = '<h3>' . $elem->innerHTML . '</h3>';
}
$sample_input_cnt = 0;
$sample_output_cnt = 0;
foreach ($statement_dom->querySelectorAll('.input') as &$input_dom) {
$sample_input_cnt++;
$input_text = '';
if ($input_dom->querySelector('.test-example-line')) {
foreach ($input_dom->querySelectorAll('.test-example-line') as &$line) {
$input_text .= HTML::stripTags($line->innerHTML) . "\n";
}
} else {
$input_text = HTML::stripTags($input_dom->querySelector('pre')->innerHTML);
}
$input_dom->outerHTML = HTML::tag('h4', [], "Input #{$sample_input_cnt}") . HTML::tag('pre', [], HTML::tag('code', [], $input_text));
}
foreach ($statement_dom->querySelectorAll('.output') as &$output_dom) {
$sample_output_cnt++;
$output_text = '';
if ($output_dom->querySelector('.test-example-line')) {
foreach ($output_dom->querySelectorAll('.test-example-line') as &$line) {
$output_text .= HTML::stripTags($line->innerHTML) . "\n";
}
} else {
$output_text = HTML::stripTags($output_dom->querySelector('pre')->innerHTML);
}
$output_dom->outerHTML = HTML::tag('h4', [], "Output #{$sample_output_cnt}") . HTML::tag('pre', [], HTML::tag('code', [], $output_text));
}
return [
'type' => 'html',
'title' => $title,
'time_limit' => $time_limit,
'memory_limit' => $memory_limit,
'difficulty' => $difficulty,
'statement' => $statement_dom->innerHTML,
];
}
static function getCodeforcesProblemBasicInfo($id) {
$res = static::curl_get(static::getCodeforcesProblemUrl($id));
if (!$res) return null;
if (str_starts_with($res['content-type'], 'text/html')) {
return static::getCodeforcesProblemBasicInfoFromHtml($id, $res['response']);
} else if (str_starts_with($res['content-type'], 'application/pdf')) {
$title_prefix = str_starts_with($id, 'GYM') ? 'Gym' : 'CF';
$title_id = str_starts_with($id, 'GYM') ? substr($id, 3) : $id;
$title = "{$title_prefix}{$title_id}{$title_prefix}{$title_id}";
return [
'type' => 'pdf',
'title' => $title,
'time_limit' => null,
'memory_limit' => null,
'difficulty' => -1,
'pdf_data' => $res['response'],
'statement' => HTML::tag('h3', [], '提示') .
HTML::tag(
'p',
[],
'若无法正常加载 PDF请' .
HTML::tag('a', ['href' => static::getCodeforcesProblemUrl($id), 'target' => '_blank'], '点此') .
'查看原题面。'
),
];
} else {
return null;
}
}
static function getAtcoderProblemBasicInfo($id) {
$res = static::curl_get(static::getAtcoderProblemUrl($id));
if (!$res) return null;
$dom = new \IvoPetkov\HTML5DOMDocument();
$dom->loadHTML($res);
$container_dom = $dom->querySelectorAll('#main-container > div.row > div.col-sm-12')->item(1);
if (!$container_dom) return null;
$title_dom = $container_dom->querySelector('span.h2');
$title = '【' . strtoupper($id) . '】' . preg_replace('/([A-Z][1-9]?) - (.*)/', '$2', explode("\n", trim($title_dom->textContent))[0]);
$limit_dom = $container_dom->querySelector('p');
$time_limit_matches = [];
preg_match('/Time Limit: (\d+)/', $limit_dom->textContent, $time_limit_matches);
$time_limit = intval($time_limit_matches[1]);
$memory_limit_matches = [];
preg_match('/Memory Limit: (\d+)/', $limit_dom->textContent, $memory_limit_matches);
$memory_limit = intval($memory_limit_matches[1]);
$statement_container_dom = $container_dom->querySelector('#task-statement');
$statement_dom = $statement_container_dom->querySelector('.lang-en');
if (!$statement_dom) {
$statement_dom = $statement_container_dom->querySelector('.lang-ja');
}
$statement_first_child = $statement_dom->querySelector('p');
$first_child_content = trim($statement_first_child->textContent);
if (str_starts_with($first_child_content, 'Score :') || str_starts_with($first_child_content, '配点 :')) {
$statement_dom->removeChild($statement_first_child);
}
foreach ($statement_dom->querySelectorAll('var') as &$elem) {
$html = $elem->innerHTML;
// <sub> => _{
$html = str_replace('<sub>', '_{', $html);
// </sub> => }
$html = str_replace('</sub>', '}', $html);
// <sup> => ^{
$html = str_replace('<sup>', '^{', $html);
// </sup> => }
$html = str_replace('</sup>', '}', $html);
$elem->innerHTML = $html;
}
$statement = $statement_dom->innerHTML;
// <var> => $
$statement = str_replace('<var>', '\\(', $statement);
// </var> => $
$statement = str_replace('</var>', '\\)', $statement);
return [
'type' => 'html',
'title' => $title,
'time_limit' => $time_limit,
'memory_limit' => $memory_limit,
'difficulty' => -1,
'statement' => $statement,
];
}
static function getUojProblemBasicInfo($id) {
$remote_provider = static::$providers['uoj'];
$res = static::curl_get(static::getUojProblemUrl($id));
if (!$res) return null;
$dom = new \IvoPetkov\HTML5DOMDocument();
$dom->loadHTML($res);
$title_dom = $dom->querySelector('.page-header');
$title_matches = [];
preg_match('/^#[1-9][0-9]*\. (.*)$/', trim($title_dom->textContent), $title_matches);
$title = "{$remote_provider['short_name']}{$id}{$title_matches[1]}";
$statement_dom = $dom->querySelector('.uoj-article');
$statement = HTML::tag('h3', [], '题目描述');
foreach ($statement_dom->querySelectorAll('a') as &$elem) {
$href = $elem->getAttribute('href');
$href = getAbsoluteUrl($href, $remote_provider['url']);
$elem->setAttribute('href', $href);
}
$statement .= $statement_dom->innerHTML;
return [
'type' => 'html',
'title' => $title,
'time_limit' => null,
'memory_limit' => null,
'difficulty' => -1,
'statement' => $statement,
];
}
static function getLojProblemBasicInfo($id) {
$remote_provider = static::$providers['loj'];
$curl = new Curl\Curl();
$curl->setUserAgent(static::USER_AGENT);
$curl->setHeader('Content-Type', 'application/json');
$res = retry_loop(function () use (&$curl, $id) {
$curl->post('https://api.loj.ac.cn/api/problem/getProblem', json_encode([
'displayId' => (int)$id,
'localizedContentsOfLocale' => 'zh_CN',
'samples' => true,
'judgeInfo' => true,
]));
if ($curl->error) {
return false;
}
return $curl->response;
});
if (!$res) return null;
// Convert stdClass to array
$res = json_decode(json_encode($res), true);
if (isset($res['error'])) return null;
$localized_contents = $res['localizedContentsOfLocale'];
$statement = '';
foreach ($localized_contents['contentSections'] as $section) {
$statement .= "\n###" . $section['sectionTitle'] . "\n\n";
if ($section['type'] === 'Text') {
$statement .= $section['text'] . "\n";
} else if ($section['type'] === 'Sample') {
// assert($res['samples'][$section['sampleId']]);
$display_sample_id = $section['sampleId'] + 1;
$sample = $res['samples'][$section['sampleId']];
$statement .= "\n#### 样例输入 #{$display_sample_id}\n\n";
$statement .= "\n```text\n{$sample['inputData']}\n```\n\n";
$statement .= "\n#### 样例输出 #{$display_sample_id}\n\n";
$statement .= "\n```text\n{$sample['outputData']}\n```\n\n";
if (trim($section['text'])) {
$statement .= "\n#### 样例解释 #{$display_sample_id}\n\n";
$statement .= $section['text'] . "\n";
}
} else {
// do nothing...
}
}
return [
'type' => 'html',
'title' => "{$remote_provider['short_name']}{$id}{$localized_contents['title']}",
'time_limit' => (float)$res['judgeInfo']['timeLimit'] / 1000.0,
'memory_limit' => $res['judgeInfo']['memoryLimit'],
'difficulty' => -1,
'statement' => HTML::parsedown()->text($statement),
];
}
public static function getProblemRemoteUrl($oj, $id) {
if ($oj === 'codeforces') {
return static::getCodeforcesProblemUrl($id);
} else if ($oj === 'atcoder') {
return static::getAtcoderProblemUrl($id);
} else if ($oj === 'uoj') {
return static::getUojProblemUrl($id);
} else if ($oj === 'loj') {
return static::getLojProblemUrl($id);
}
return null;
}
// 传入 ID 需确保有效
public static function getProblemBasicInfo($oj, $id) {
if ($oj === 'codeforces') {
return static::getCodeforcesProblemBasicInfo($id);
} else if ($oj === 'atcoder') {
return static::getAtcoderProblemBasicInfo($id);
} else if ($oj === 'uoj') {
return static::getUojProblemBasicInfo($id);
} else if ($oj === 'loj') {
return static::getLojProblemBasicInfo($id);
}
return null;
}
public static function downloadImagesInRemoteContent($problem_id) {
$curl = new Curl\Curl();
$curl->setUserAgent(static::USER_AGENT);
$curl->setRetry(5);
$problem = UOJProblem::query($problem_id);
if ($problem->info['type'] != 'remote') return;
$remote_provider = static::$providers[$problem->getExtraConfig('remote_online_judge')];
$remote_content = $problem->queryContent()['remote_content'];
$dom = new IvoPetkov\HTML5DOMDocument();
$dom->loadHTML($remote_content);
foreach ($dom->querySelectorAll('img') as &$elem) {
$src = $elem->getAttribute('src');
$url = getAbsoluteUrl($src, $remote_provider['url']);
$filename = 'remote_image_' . hash('md5', $url);
$curl->download($url, $problem->getResourcesPath($filename));
$elem->setAttribute('src', $problem->getResourcesUri($filename));
}
DB::update([
"update problems_contents",
"set", [
"remote_content" => HTML::purifier(['a' => ['target' => 'Enum#_blank']])->purify($dom->saveHTML()),
],
"where", [
"id" => $problem->info['id'],
],
]);
}
}

View File

@ -5,6 +5,7 @@ class UOJResponse {
if (UOJContext::isAjax()) {
die($msg);
} else {
requireLib('bootstrap5');
echoUOJPageHeader($title);
echo $msg;
echoUOJPageFooter();

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

@ -8,6 +8,7 @@ Route::pattern('tab', '\S{1,20}');
Route::pattern('rand_str_id', '[0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]{20}');
Route::pattern('image_name', '[0-9a-z]{1,20}');
Route::pattern('upgrade_name', '[a-zA-Z0-9_]{1,50}');
Route::pattern('sub_path', '.*');
Route::group(
[
@ -17,9 +18,12 @@ Route::group(
Route::any('/', '/index.php');
Route::any('/problems', '/problem_set.php');
Route::any('/problems/template', '/problem_set.php?tab=template');
Route::any('/problems/remote', '/problem_set.php?tab=remote');
Route::any('/problems/remote/new', '/new_remote_problem.php');
Route::any('/problem/{id}', '/problem.php');
Route::any('/problem/{id}/solutions', '/problem_solutions.php');
Route::any('/problem/{id}/statistics', '/problem_statistics.php');
Route::any('/problem/{id}/resources(?:/{sub_path})?', '/problem_resources.php');
Route::any('/problem/{id}/manage/statement', '/problem_statement_manage.php');
Route::any('/problem/{id}/manage/managers', '/problem_managers_manage.php');
Route::any('/problem/{id}/manage/data', '/problem_data_manage.php');
@ -85,16 +89,12 @@ Route::group(
Route::any('/super_manage(?:/{tab})?', '/super_manage.php');
Route::any('/download/problem/{id}/data.zip', '/download.php?type=problem');
Route::any('/download/problem/{id}/attachment.zip', '/download.php?type=attachment');
Route::any('/check-notice', '/check_notice.php');
Route::any('/click-zan', '/click_zan.php');
// Apps
Route::any('/image_hosting', '/app/image_hosting/index.php');
Route::get('/image_hosting/{image_name}.png', '/app/image_hosting/get_image.php');
Route::any('/html2markdown', '/app/html2markdown.php');
Route::any('/apps/image_hosting', '/apps/image_hosting.php');
Route::any('/apps/html2markdown', '/apps/html2markdown.php');
}
);

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

@ -0,0 +1,13 @@
<?php
return function ($type) {
if ($type == 'up') {
DB::init();
$problems = DB::selectAll("select id from problems");
foreach ($problems as $row) {
mkdir(UOJContext::storagePath() . "/problem_resources/{$row['id']}");
}
}
};

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,11 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'ParsedownMath' => $vendorDir . '/parsedown-math/ParsedownMath.php',
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);

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,7 +6,9 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
'Gregwar\\' => array($vendorDir . '/gregwar/captcha/src/Gregwar'),
'Curl\\' => array($vendorDir . '/php-curl-class/php-curl-class/src/Curl'),
);

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' =>
@ -23,9 +27,17 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
array (
'Gregwar\\' => 8,
),
'C' =>
array (
'Curl\\' => 5,
),
);
public static $prefixDirsPsr4 = array (
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
),
'Symfony\\Component\\Finder\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/finder',
@ -38,6 +50,10 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
array (
0 => __DIR__ . '/..' . '/gregwar/captcha/src/Gregwar',
),
'Curl\\' =>
array (
0 => __DIR__ . '/..' . '/php-curl-class/php-curl-class/src/Curl',
),
);
public static $prefixesPsr0 = array (
@ -58,7 +74,13 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
);
public static $classMap = array (
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'ParsedownMath' => __DIR__ . '/..' . '/parsedown-math/ParsedownMath.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);
public static function getInitializer(ClassLoader $loader)

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,135 @@
"bot",
"captcha",
"spam"
],
"install-path": "../gregwar/captcha"
},
{
"name": "ivopetkov/html5-dom-document-php",
"version": "v2.4.0",
"version_normalized": "2.4.0.0",
"source": {
"type": "git",
"url": "https://github.com/ivopetkov/html5-dom-document-php.git",
"reference": "32c5ba748d661a9654c190bf70ce2854eaf5ad22"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ivopetkov/html5-dom-document-php/zipball/32c5ba748d661a9654c190bf70ce2854eaf5ad22",
"reference": "32c5ba748d661a9654c190bf70ce2854eaf5ad22",
"shasum": ""
},
"require": {
"ext-dom": "*",
"php": "7.0.*|7.1.*|7.2.*|7.3.*|7.4.*|8.0.*|8.1.*|8.2.*"
},
"require-dev": {
"ivopetkov/docs-generator": "1.*"
},
"time": "2022-12-17T00:20:55+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"autoload.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ivo Petkov",
"email": "ivo@ivopetkov.com",
"homepage": "http://ivopetkov.com"
}
],
"description": "HTML5 DOMDocument PHP library (extends DOMDocument)",
"support": {
"issues": "https://github.com/ivopetkov/html5-dom-document-php/issues",
"source": "https://github.com/ivopetkov/html5-dom-document-php/tree/v2.4.0"
},
"install-path": "../ivopetkov/html5-dom-document-php"
},
{
"name": "php-curl-class/php-curl-class",
"version": "9.13.1",
"version_normalized": "9.13.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-curl-class/php-curl-class.git",
"reference": "1fa71f639275987092c60ac117ff5c8a3771898d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-curl-class/php-curl-class/zipball/1fa71f639275987092c60ac117ff5c8a3771898d",
"reference": "1fa71f639275987092c60ac117ff5c8a3771898d",
"shasum": ""
},
"require": {
"ext-curl": "*",
"php": ">=7.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "*",
"ext-gd": "*",
"phpcompatibility/php-compatibility": "dev-develop",
"phpcsstandards/phpcsutils": "@alpha",
"phpunit/phpunit": "*",
"squizlabs/php_codesniffer": "*",
"vimeo/psalm": "*"
},
"suggest": {
"ext-mbstring": "*"
},
"time": "2023-01-16T01:29:25+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Curl\\": "src/Curl/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Unlicense"
],
"authors": [
{
"name": "Zach Borboa"
}
],
"description": "PHP Curl Class makes it easy to send HTTP requests and integrate with web APIs.",
"homepage": "https://github.com/php-curl-class/php-curl-class",
"keywords": [
"API-Client",
"api",
"class",
"client",
"curl",
"framework",
"http",
"http-client",
"http-proxy",
"json",
"php",
"php-curl",
"php-curl-library",
"proxy",
"requests",
"restful",
"web-scraper",
"web-scraping ",
"web-service",
"xml"
],
"support": {
"issues": "https://github.com/php-curl-class/php-curl-class/issues",
"source": "https://github.com/php-curl-class/php-curl-class/tree/9.13.1"
},
"install-path": "../php-curl-class/php-curl-class"
},
{
"name": "phpmailer/phpmailer",
"version": "v6.6.5",
@ -235,30 +365,97 @@
"url": "https://github.com/Synchro",
"type": "github"
}
]
],
"install-path": "../phpmailer/phpmailer"
},
{
"name": "symfony/finder",
"version": "v6.1.3",
"version_normalized": "6.1.3.0",
"name": "symfony/deprecation-contracts",
"version": "v2.5.2",
"version_normalized": "2.5.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709"
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/39696bff2c2970b3779a5cac7bf9f0b88fc2b709",
"reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
"reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
"shasum": ""
},
"require": {
"php": ">=8.1"
"php": ">=7.1"
},
"require-dev": {
"symfony/filesystem": "^6.0"
"time": "2022-01-02T09:53:40+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.5-dev"
},
"time": "2022-07-29T07:42:06+00:00",
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/deprecation-contracts"
},
{
"name": "symfony/finder",
"version": "v5.4.11",
"version_normalized": "5.4.11.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/7872a66f57caffa2916a584db1aa7f12adc76f8c",
"reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/polyfill-php80": "^1.16"
},
"time": "2022-07-29T07:37:50+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -298,6 +495,93 @@
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
]
],
"install-path": "../symfony/finder"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.26.0",
"version_normalized": "1.26.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"time": "2022-05-10T07:21:04+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
]
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/polyfill-php80"
}
],
"dev": true,
"dev-package-names": []
}

104
web/app/vendor/composer/installed.php vendored Normal file
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' => 'aa6bf6a363501f9836d2bb26a513c2ac3985de83',
'name' => '__root__',
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => 'aa6bf6a363501f9836d2bb26a513c2ac3985de83',
'dev_requirement' => false,
),
'erusev/parsedown' => array(
'pretty_version' => '1.7.4',
'version' => '1.7.4.0',
'type' => 'library',
'install_path' => __DIR__ . '/../erusev/parsedown',
'aliases' => array(),
'reference' => 'cb17b6477dfff935958ba01325f2e8a2bfa6dab3',
'dev_requirement' => false,
),
'ezyang/htmlpurifier' => array(
'pretty_version' => 'v4.16.0',
'version' => '4.16.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../ezyang/htmlpurifier',
'aliases' => array(),
'reference' => '523407fb06eb9e5f3d59889b3978d5bfe94299c8',
'dev_requirement' => false,
),
'gregwar/captcha' => array(
'pretty_version' => 'v1.1.9',
'version' => '1.1.9.0',
'type' => 'captcha',
'install_path' => __DIR__ . '/../gregwar/captcha',
'aliases' => array(),
'reference' => '4bb668e6b40e3205a020ca5ee4ca8cff8b8780c5',
'dev_requirement' => false,
),
'ivopetkov/html5-dom-document-php' => array(
'pretty_version' => 'v2.4.0',
'version' => '2.4.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../ivopetkov/html5-dom-document-php',
'aliases' => array(),
'reference' => '32c5ba748d661a9654c190bf70ce2854eaf5ad22',
'dev_requirement' => false,
),
'php-curl-class/php-curl-class' => array(
'pretty_version' => '9.13.1',
'version' => '9.13.1.0',
'type' => 'library',
'install_path' => __DIR__ . '/../php-curl-class/php-curl-class',
'aliases' => array(),
'reference' => '1fa71f639275987092c60ac117ff5c8a3771898d',
'dev_requirement' => false,
),
'phpmailer/phpmailer' => array(
'pretty_version' => 'v6.6.5',
'version' => '6.6.5.0',
'type' => 'library',
'install_path' => __DIR__ . '/../phpmailer/phpmailer',
'aliases' => array(),
'reference' => '8b6386d7417526d1ea4da9edb70b8352f7543627',
'dev_requirement' => false,
),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v2.5.2',
'version' => '2.5.2.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(),
'reference' => 'e8b495ea28c1d97b5e0c121748d6f9b53d075c66',
'dev_requirement' => false,
),
'symfony/finder' => array(
'pretty_version' => 'v5.4.11',
'version' => '5.4.11.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/finder',
'aliases' => array(),
'reference' => '7872a66f57caffa2916a584db1aa7f12adc76f8c',
'dev_requirement' => false,
),
'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.26.0',
'version' => '1.26.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(),
'reference' => 'cfa0ae98841b9e461207c13ab093d76b0fa7bace',
'dev_requirement' => false,
),
),
);

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,214 @@
# Change Log
PHP Curl Class uses semantic versioning with version numbers written as `MAJOR.MINOR.PATCH`. You may safely update
`MINOR` and `PATCH` version changes. It is recommended to review `MAJOR` changes prior to upgrade as there may be
backwards-incompatible changes that will affect existing usage.
<!-- CHANGELOG_PLACEHOLDER -->
## 9.13.1 - 2023-01-16
- Allow uploads with CURLStringFile type ([#762](https://github.com/php-curl-class/php-curl-class/pull/762))
## 9.13.0 - 2023-01-13
- Implement abstract class BaseCurl for Curl and MultiCurl ([#759](https://github.com/php-curl-class/php-curl-class/pull/759))
- Display error messages found in Curl::diagnose() ([#758](https://github.com/php-curl-class/php-curl-class/pull/758))
- Fix Curl::diagnose() request type output for POST requests ([#757](https://github.com/php-curl-class/php-curl-class/pull/757))
## 9.12.6 - 2023-01-11
- Replace use of #[\AllowDynamicProperties] ([#756](https://github.com/php-curl-class/php-curl-class/pull/756))
- silence PHP 8.2 deprecation notices ([#754](https://github.com/php-curl-class/php-curl-class/pull/754))
## 9.12.5 - 2022-12-20
- Fix static analysis error ([#752](https://github.com/php-curl-class/php-curl-class/pull/752))
## 9.12.4 - 2022-12-17
- Exclude additional files from git archive ([#751](https://github.com/php-curl-class/php-curl-class/pull/751))
## 9.12.3 - 2022-12-13
- Ensure string response before gzip decode ([#749](https://github.com/php-curl-class/php-curl-class/pull/749))
## 9.12.2 - 2022-12-11
- Disable warning when gzip-decoding response errors ([#748](https://github.com/php-curl-class/php-curl-class/pull/748))
## 9.12.1 - 2022-12-08
- Include option constant that uses the CURLINFO_ prefix ([#745](https://github.com/php-curl-class/php-curl-class/pull/745))
## 9.12.0 - 2022-12-07
- Add automatic gzip decoding of response ([#744](https://github.com/php-curl-class/php-curl-class/pull/744))
## 9.11.1 - 2022-12-06
- change: remove unused namespace import ([#743](https://github.com/php-curl-class/php-curl-class/pull/743))
## 9.11.0 - 2022-12-05
- Add Curl::diagnose() HTTP method check matches methods allowed ([#741](https://github.com/php-curl-class/php-curl-class/pull/741))
- Add temporary fix missing template params ([#742](https://github.com/php-curl-class/php-curl-class/pull/742))
## 9.10.0 - 2022-11-07
- Display request options in Curl::diagnose() output ([#739](https://github.com/php-curl-class/php-curl-class/pull/739))
## 9.9.0 - 2022-11-06
- Fix MultiCurl::setCookieString() ([#738](https://github.com/php-curl-class/php-curl-class/pull/738))
- Pass MultiCurl options to new Curl instances earlier ([#737](https://github.com/php-curl-class/php-curl-class/pull/737))
- Add deferred constant curlErrorCodeConstants ([#736](https://github.com/php-curl-class/php-curl-class/pull/736))
## 9.8.0 - 2022-10-01
- Include curl error code constant in curl error message ([#733](https://github.com/php-curl-class/php-curl-class/pull/733))
## 9.7.0 - 2022-09-29
- Implement ArrayUtil::arrayRandomIndex() ([#732](https://github.com/php-curl-class/php-curl-class/pull/732))
## 9.6.3 - 2022-09-24
- Remove filter flag constants deprecated as of PHP 7.3 ([#730](https://github.com/php-curl-class/php-curl-class/pull/730))
## 9.6.2 - 2022-09-24
- Call MultiCurl::beforeSend() before each request is made ([#723](https://github.com/php-curl-class/php-curl-class/pull/723))
- Encode keys for post data with numeric keys ([#726](https://github.com/php-curl-class/php-curl-class/pull/726))
- Fix building post data with object ([#728](https://github.com/php-curl-class/php-curl-class/pull/728))
## 9.6.1 - 2022-06-30
### Fixed
- Attempt to stop active requests when `MultiCurl::stop()` is called
[#714](https://github.com/php-curl-class/php-curl-class/issues/714)
[#718](https://github.com/php-curl-class/php-curl-class/issues/718)
- Retain keys for arrays with null values when building post data
[#712](https://github.com/php-curl-class/php-curl-class/issues/712)
## 9.6.0 - 2022-03-17
### Added
- Method `MultiCurl::stop()` for stopping subsequent requests
[#708](https://github.com/php-curl-class/php-curl-class/issues/708)
## 9.5.1 - 2021-12-14
### Fixed
- Silence PHP 8.1 deprecations [#691](https://github.com/php-curl-class/php-curl-class/issues/691)
- Remove data parameter from additional request types
[#689](https://github.com/php-curl-class/php-curl-class/issues/689)
## 9.5.0 - 2021-11-21
### Added
- Method `Curl::setStop()` for stopping requests early without downloading the full response body
[#681](https://github.com/php-curl-class/php-curl-class/issues/681)
### Fixed
- Fixed constructing request url when using `MultiCurl::addPost()`
[#686](https://github.com/php-curl-class/php-curl-class/issues/686)
## 9.4.0 - 2021-09-04
### Changed
- Method `Url::parseUrl()` is now public
### Fixed
- Fix parsing schemeless urls [#679](https://github.com/php-curl-class/php-curl-class/issues/679)
## 9.3.1 - 2021-08-05
### Changed
- Enabled strict types (`declare(strict_types=1);`)
### Fixed
- Fixed `Curl::downloadFileName` not being set correctly
## 9.3.0 - 2021-07-23
### Added
- Method `Curl::diagnose()` for troubleshooting requests
## 9.2.0 - 2021-06-23
### Added
- Additional Curl::set\* and MultiCurl::set\* helper methods
```
Curl::setAutoReferer()
Curl::setAutoReferrer()
Curl::setFollowLocation()
Curl::setForbidReuse()
Curl::setMaximumRedirects()
MultiCurl::setAutoReferer()
MultiCurl::setAutoReferrer()
MultiCurl::setFollowLocation()
MultiCurl::setForbidReuse()
MultiCurl::setMaximumRedirects()
```
### Fixed
- Closing curl handles [#670](https://github.com/php-curl-class/php-curl-class/issues/670)
- Use of "$this" in non-object context [#671](https://github.com/php-curl-class/php-curl-class/pull/671)
## 9.1.0 - 2021-03-24
### Added
- Support for using relative urls with MultiCurl::add\*() methods [#628](https://github.com/php-curl-class/php-curl-class/issues/628)
## 9.0.0 - 2021-03-19
### Changed
- Use short array syntax
### Removed
- Support for PHP 5.3, 5.4, 5.5, and 5.6 [#380](https://github.com/php-curl-class/php-curl-class/issues/380)
## Manual Review
A manual review of changes is possible using the
[comparison page](https://github.com/php-curl-class/php-curl-class/compare/). For example, visit
[7.4.0...8.0.0](https://github.com/php-curl-class/php-curl-class/compare/7.4.0...8.0.0) to compare the changes for
the `MAJOR` upgrade from 7.4.0 to 8.0.0. Comparing against `HEAD` is also possible using the `tag...HEAD` syntax
([8.3.0...HEAD](https://github.com/php-curl-class/php-curl-class/compare/8.3.0...HEAD)).
View the log between releases:
$ git fetch --tags
$ git log 7.4.0...8.0.0
View the code changes between releases:
$ git fetch --tags
$ git diff 7.4.0...8.0.0
View only the source log and code changes between releases:
$ git log 7.4.0...8.0.0 "src/"
$ git diff 7.4.0...8.0.0 "src/"
View only the source log and code changes between a release and the current checked-out commit:
$ git log 8.0.0...head "src/"
$ git diff 8.0.0...head "src/"

Some files were not shown because too many files have changed in this diff Show More