feat(problem): import remote problem

This commit is contained in:
Baoshuo Ren 2023-01-18 16:20:12 +08:00
parent d6997b8475
commit 8041b49bd8
Signed by: baoshuo
GPG Key ID: 00CB9680AB29F51A
91 changed files with 5960 additions and 520 deletions

View File

@ -622,10 +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',
`judge_type` varchar(20) NOT NULL DEFAULT 'local',
`type` varchar(20) NOT NULL DEFAULT 'local',
`assigned_to_judger` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'any',
PRIMARY KEY (`id`),
KEY `judge_type` (`judge_type`),
KEY `type` (`type`),
KEY `assigned_to_judger` (`assigned_to_judger`),
KEY `uploader` (`uploader`),
KEY `difficulty` (`difficulty`),

View File

@ -6,7 +6,7 @@ ENV USE_MIRROR $USE_MIRROR
SHELL ["/bin/bash", "-c"]
ENV DEBIAN_FRONTEND=noninteractive
ENV PKGS="php7.4 php7.4-yaml php7.4-xml php7.4-dev php7.4-zip php7.4-mysql php7.4-mbstring php7.4-gd php7.4-imagick libseccomp-dev git vim ntp zip unzip curl wget apache2 libapache2-mod-xsendfile php-pear mysql-client build-essential fp-compiler re2c libseccomp-dev libyaml-dev python2.7 python3.10 python3-requests openjdk-8-jdk openjdk-11-jdk openjdk-17-jdk"
ENV PKGS="php7.4 php7.4-yaml php7.4-xml php7.4-dev php7.4-zip php7.4-mysql php7.4-mbstring php7.4-gd php7.4-curl php7.4-imagick libseccomp-dev git vim ntp zip unzip curl wget apache2 libapache2-mod-xsendfile php-pear mysql-client build-essential fp-compiler re2c libseccomp-dev libyaml-dev python2.7 python3.10 python3-requests openjdk-8-jdk openjdk-11-jdk openjdk-17-jdk"
RUN if [[ "$USE_MIRROR" == "1" ]]; then\
sed -i "s@http://.*archive.ubuntu.com@https://mirrors.aliyun.com@g" /etc/apt/sources.list &&\
sed -i "s@http://.*security.ubuntu.com@https://mirrors.aliyun.com@g" /etc/apt/sources.list ;\

View File

@ -3,7 +3,10 @@
"gregwar/captcha": "^1.1",
"phpmailer/phpmailer": "^6.6",
"ezyang/htmlpurifier": "^4.16",
"erusev/parsedown": "^1.7"
"erusev/parsedown": "^1.7",
"php-curl-class/php-curl-class": "^2.0",
"ext-dom": "20031129",
"ivopetkov/html5-dom-document-php": "2.*"
},
"autoload": {
"classmap": [

View File

@ -148,14 +148,14 @@ if ($_POST['judger_name'] == "remote_judger") {
$problem_ban_list = array_map(fn ($x) => $x['id'], DB::selectAll([
"select id from problems",
"where", [
["judge_type", "!=", "remote"],
["type", "!=", "remote"],
],
]));
} else {
$problem_ban_list = array_map(fn ($x) => $x['id'], DB::selectAll([
"select id from problems",
"where", [
["judge_type", "!=", "local"],
["type", "!=", "local"],
],
]));
}

View File

@ -0,0 +1,115 @@
<?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' => [
'codeforces' => 'Codeforces',
],
]);
$new_remote_problem_form->addInput('remote_problem_id', [
'div_class' => 'mt-3',
'label' => '远程 OJ 上的题目 ID',
'validator_php' => function ($id, &$vdata) {
if ($_POST['remote_online_judge'] === 'codeforces') {
$id = trim(strtoupper($id));
if (!validateCodeforcesProblemId($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, type)",
"values", DB::tuple([$data['title'], Auth::id(), 1, $enc_submission_requirement, $enc_extra_config, "remote"])
]);
$id = DB::insert_id();
DB::insert([
"insert into problems_contents",
"(id, remote_content, statement, statement_md)",
"values",
DB::tuple([$id, HTML::purifier()->purify($data['statement']), '', ''])
]);
dataNewProblem($id);
redirectTo("/problem/{$id}");
die();
};
$new_remote_problem_form->runAtServer();
?>
<?php echoUOJPageHeader('导入远程题库') ?>
<h1>导入远程题库</h1>
<div class="row">
<div class="col-md-9">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<?php $new_remote_problem_form->printHTML() ?>
</div>
<div class="col-md-6 mt-3 mt-md-0">
<h4>使用帮助</h4>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
TODO: 侧边栏
</div>
</div>
<?php echoUOJPageFooter() ?>

View File

@ -223,7 +223,6 @@ if (UOJContest::cur()) {
<div class="row">
<!-- Left col -->
<div class="col-lg-9">
<?php if (isset($tabs_info)) : ?>
<!-- 比赛导航 -->
<div class="mb-2">
@ -233,7 +232,6 @@ if (UOJContest::cur()) {
<div class="card card-default mb-2">
<div class="card-body">
<h1 class="card-title text-center">
<?php if (UOJContest::cur()) : ?>
<?= UOJProblem::cur()->getTitle(['with' => 'letter', 'simplify' => true]) ?>
@ -243,8 +241,13 @@ if (UOJContest::cur()) {
</h1>
<?php
$time_limit = $conf instanceof UOJProblemConf ? $conf->getVal('time_limit', 1) : null;
$memory_limit = $conf instanceof UOJProblemConf ? $conf->getVal('memory_limit', 256) : null;
if (UOJProblem::cur()->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::cur()->type() == 'remote') {
$time_limit = UOJProblem::cur()->getExtraConfig('time_limit');
$memory_limit = UOJProblem::cur()->getExtraConfig('memory_limit');
}
?>
<div class="text-center small">
时间限制: <?= $time_limit ? "$time_limit s" : "N/A" ?>
@ -259,6 +262,12 @@ if (UOJContest::cur()) {
<article class="mt-3 markdown-body">
<?= $problem_content['statement'] ?>
</article>
<?php if (UOJProblem::info('type') == 'remote') : ?>
<article class="mt-3 markdown-body remote-content">
<?= UOJProblem::cur()->queryContent()['remote_content'] ?>
</article>
<?php endif ?>
</div>
<div class="tab-pane" id="submit">
<?php if ($pre_submit_check_ret !== true) : ?>
@ -392,6 +401,14 @@ if (UOJContest::cur()) {
<?= UOJProblem::cur()->getUploaderLink() ?>
</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="flex-shrink-0">
题目来源
</span>
<span>
<?= UOJProblem::cur()->getProviderLink() ?>
</span>
</li>
<?php if (!UOJContest::cur() || UOJContest::cur()->progress() >= CONTEST_FINISHED) : ?>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="flex-shrink-0">

View File

@ -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('/[1-9][0-9]{0,5}[A-Z][1-9]?/', $str) !== true;
}

View File

@ -423,7 +423,7 @@ class HTML {
return implode("&", $r);
}
public static function purifier() {
public static function purifier($extra_allowed_html = []) {
$config = HTMLPurifier_Config::createDefault();
$config->set('Output.Newline', true);
$def = $config->getHTMLDefinition(true);
@ -435,14 +435,14 @@ class HTML {
$def->addElement('header', 'Block', 'Flow', 'Common');
$def->addElement('footer', 'Block', 'Flow', 'Common');
$extra_allowed_html = [
mergeConfig($extra_allowed_html, [
'span' => [
'class' => 'Enum#uoj-username',
'data-realname' => 'Text',
'data-color' => 'Color',
],
'img' => ['width' => 'Text'],
];
]);
foreach ($extra_allowed_html as $element => $attributes) {
foreach ($attributes as $attribute => $type) {

View File

@ -120,6 +120,53 @@ class UOJForm {
$this->add($name, $html, $validator_php, $validator_js);
}
public function addInput($name, $config) {
$config += [
'type' => 'text',
'div_class' => '',
'input_class' => 'form-control',
'default_value' => '',
'label' => '',
'label_class' => 'form-label',
'placeholder' => '',
'help' => '',
'help_class' => 'form-text',
'validator_php' => function ($x) {
return '';
},
'validator_js' => null,
];
$html = '';
$html .= HTML::tag_begin('div', ['class' => $config['div_class'], 'id' => "div-$name"]);
if ($config['label']) {
$html .= HTML::tag('label', [
'class' => $config['label_class'],
'for' => "input-$name",
'id' => "label-$name"
], $config['label']);
}
$html .= HTML::empty_tag('input', [
'class' => $config['input_class'],
'type' => $config['type'],
'name' => $name,
'id' => "input-$name",
'value' => $config['default_value'],
'placeholder' => $config['placeholder'],
]);
$html .= HTML::tag('div', ['class' => 'invalid-feedback', 'id' => "help-$name"], '');
if ($config['help']) {
$html .= HTML::tag('div', ['class' => $config['help_class']], $config['help']);
}
$html .= HTML::tag_end('div');
$this->add($name, $html, $config['validator_php'], $config['validator_js']);
}
public function addCheckbox($name, $config) {
$config += [
'checked' => false,

View File

@ -65,7 +65,7 @@ class UOJLang {
}
$is_avail = [];
$dep_list = [
['C++', 'C++11', 'C++14', 'C++17', 'C++20'],
['C++98', 'C++03', 'C++11', 'C++', 'C++17', 'C++20'],
['Java8', 'Java11', 'Java17']
];
foreach ($list as $lang) {

View File

@ -344,6 +344,10 @@ class UOJProblem {
$this->info = $info;
}
public function type() {
return $this->info['type'];
}
public function getTitle(array $cfg = []) {
$cfg += [
'with' => 'id',
@ -382,6 +386,22 @@ class UOJProblem {
return UOJUser::getLink($this->info['uploader'] ?: "root");
}
public function getProviderLink() {
if ($this->type() == 'local') {
return HTML::tag('a', ['href' => HTML::url('/')], UOJConfig::$data['profile']['oj-name-short']);
}
$remote_oj = $this->getExtraConfig('remote_online_judge');
if (!$remote_oj || !array_key_exists($remote_oj, UOJRemoteProblem::$providers)) {
return 'Error';
}
$provider = UOJRemoteProblem::$providers[$remote_oj];
return HTML::tag('a', ['href' => $provider['url'], 'target' => '_blank'], $provider['name']);
}
public function getDifficultyHTML() {
$difficulty = (int)$this->info['difficulty'];
$difficulty_text = in_array($difficulty, static::$difficulty) ? $difficulty : '?';

View File

@ -0,0 +1,132 @@
<?php
class UOJRemoteProblem {
static $providers = [
'codeforces' => [
'name' => 'Codeforces',
'short_name' => 'CF',
'url' => 'https://codeforces.com',
'host' => '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'],
],
];
// 传入 ID 需确保有效
static function getCodeforcesProblemBasicInfo($id) {
$curl = new Curl();
$curl->setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36 S2OJ/3.1.0');
$remote_provider = static::$providers['codeforces'];
$url_id = preg_replace_callback('/([1-9][0-9]{0,5})([A-Z][1-9]?)/', fn ($matches) => $matches[1] . '/' . $matches[2], $id);
$html = retry_loop(function () use ($curl, $url_id, $remote_provider) {
$curl->get($remote_provider['host'] . '/problemset/problem/' . $url_id);
if ($curl->error) {
return false;
}
return $curl->response;
});
if (!$html) return null;
$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 = explode('. ', trim($statement_dom->querySelector('.title')->innerHTML))[1];
$title = "{$remote_provider['short_name']}{$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) !== false) {
$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_cnt = 0;
foreach ($statement_dom->querySelectorAll('.sample-test') as &$sample) {
$sample_cnt++;
$input_dom = &$sample->querySelector('.input');
$output_dom = &$sample->querySelector('.output');
$input_text = '';
$output_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);
}
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);
}
$input_dom->outerHTML = HTML::tag('h4', [], "Input #{$sample_cnt}") . HTML::tag('pre', [], HTML::tag('code', [], $input_text));
$output_dom->outerHTML = HTML::tag('h4', [], "Output #{$sample_cnt}") . HTML::tag('pre', [], HTML::tag('code', [], $output_text));
}
return [
'title' => $title,
'time_limit' => $time_limit,
'memory_limit' => $memory_limit,
'difficulty' => $difficulty,
'statement' => $statement_dom->innerHTML,
];
}
public static function getProblemBasicInfo($oj, $id) {
if ($oj === 'codeforces') {
return static::getCodeforcesProblemBasicInfo($id);
}
return null;
}
}

View File

@ -88,7 +88,7 @@ class UOJSubmission {
$content['config'][] = ['problem_id', UOJProblem::info('id')];
if (UOJProblem::info('judge_type') == 'remote') {
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')];
}
@ -414,7 +414,7 @@ class UOJSubmission {
}
public function userCanRejudge(array $user = null) {
if ($this->problem->info['judge_type'] == 'remote') {
if ($this->problem->info['type'] == 'remote') {
return false;
}

View File

@ -17,6 +17,7 @@ Route::group(
Route::any('/', '/index.php');
Route::any('/problems', '/problem_set.php');
Route::any('/problems/template', '/problem_set.php?tab=template');
Route::any('/problems/new/remote', '/new_remote_problem.php');
Route::any('/problem/{id}', '/problem.php');
Route::any('/problem/{id}/solutions', '/problem_solutions.php');
Route::any('/problem/{id}/statistics', '/problem_statistics.php');

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)
{
@ -102,9 +175,11 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix, either
* 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 bool $prepend Whether to prepend the directories
* @param string $prefix The prefix
* @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)
{
@ -147,11 +222,13 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace, either
* 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 bool $prepend Whether to prepend the directories
* @param string $prefix The prefix/namespace, with trailing '\\'
* @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)
{
@ -195,8 +272,10 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
@ -211,10 +290,12 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace,
* 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 $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
@ -234,6 +315,8 @@ class ClassLoader
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
@ -256,6 +339,8 @@ class ClassLoader
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
@ -276,6 +361,8 @@ class ClassLoader
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
@ -296,25 +383,44 @@ class ClassLoader
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
@ -323,6 +429,8 @@ class ClassLoader
return true;
}
return null;
}
/**
@ -367,6 +475,21 @@ class ClassLoader
return $file;
}
/**
* Returns the currently registered loaders indexed by their corresponding vendor directories.
*
* @return self[]
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
@ -438,6 +561,10 @@ class ClassLoader
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
* @private
*/
function includeFile($file)
{

View File

@ -0,0 +1,350 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints($constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = require __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
$installed[] = self::$installed;
return $installed;
}
}

View File

@ -6,5 +6,13 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'CaseInsensitiveArray' => $vendorDir . '/php-curl-class/php-curl-class/src/Curl.class.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Curl' => $vendorDir . '/php-curl-class/php-curl-class/src/Curl.class.php',
'ParsedownMath' => $vendorDir . '/parsedown-math/ParsedownMath.php',
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);

View File

@ -6,5 +6,8 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
'16eed290c5592c18dc3f16802ad3d0e4' => $vendorDir . '/ivopetkov/html5-dom-document-php/autoload.php',
);

View File

@ -6,6 +6,7 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
'Gregwar\\' => array($vendorDir . '/gregwar/captcha/src/Gregwar'),

View File

@ -22,13 +22,15 @@ class ComposerAutoloaderInit0d7c2cd5c2dbf2120e4372996869e900
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit0d7c2cd5c2dbf2120e4372996869e900', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInit0d7c2cd5c2dbf2120e4372996869e900', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900::getInitializer($loader));
} else {
@ -63,11 +65,16 @@ class ComposerAutoloaderInit0d7c2cd5c2dbf2120e4372996869e900
}
}
/**
* @param string $fileIdentifier
* @param string $file
* @return void
*/
function composerRequire0d7c2cd5c2dbf2120e4372996869e900($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}

View File

@ -7,12 +7,16 @@ namespace Composer\Autoload;
class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
{
public static $files = array (
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
'16eed290c5592c18dc3f16802ad3d0e4' => __DIR__ . '/..' . '/ivopetkov/html5-dom-document-php/autoload.php',
);
public static $prefixLengthsPsr4 = array (
'S' =>
array (
'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Component\\Finder\\' => 25,
),
'P' =>
@ -26,6 +30,10 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
);
public static $prefixDirsPsr4 = array (
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
),
'Symfony\\Component\\Finder\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/finder',
@ -58,7 +66,15 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
);
public static $classMap = array (
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'CaseInsensitiveArray' => __DIR__ . '/..' . '/php-curl-class/php-curl-class/src/Curl.class.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Curl' => __DIR__ . '/..' . '/php-curl-class/php-curl-class/src/Curl.class.php',
'ParsedownMath' => __DIR__ . '/..' . '/parsedown-math/ParsedownMath.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);
public static function getInitializer(ClassLoader $loader)

View File

@ -1,303 +1,540 @@
[
{
"name": "erusev/parsedown",
"version": "1.7.4",
"version_normalized": "1.7.4.0",
"source": {
"type": "git",
"url": "https://github.com/erusev/parsedown.git",
"reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
"reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35"
},
"time": "2019-12-30T22:54:17+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"Parsedown": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Emanuil Rusev",
"email": "hello@erusev.com",
"homepage": "http://erusev.com"
}
],
"description": "Parser for Markdown.",
"homepage": "http://parsedown.org",
"keywords": [
"markdown",
"parser"
]
},
{
"name": "ezyang/htmlpurifier",
"version": "v4.16.0",
"version_normalized": "4.16.0.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8",
"reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8",
"shasum": ""
},
"require": {
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0"
},
"require-dev": {
"cerdic/css-tidy": "^1.7 || ^2.0",
"simpletest/simpletest": "dev-master"
},
"suggest": {
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
"ext-bcmath": "Used for unit conversion and imagecrash protection",
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
"ext-tidy": "Used for pretty-printing HTML"
},
"time": "2022-09-18T07:06:19+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"library/HTMLPurifier.composer.php"
{
"packages": [
{
"name": "erusev/parsedown",
"version": "1.7.4",
"version_normalized": "1.7.4.0",
"source": {
"type": "git",
"url": "https://github.com/erusev/parsedown.git",
"reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
"reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35"
},
"time": "2019-12-30T22:54:17+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"Parsedown": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"psr-0": {
"HTMLPurifier": "library/"
"authors": [
{
"name": "Emanuil Rusev",
"email": "hello@erusev.com",
"homepage": "http://erusev.com"
}
],
"description": "Parser for Markdown.",
"homepage": "http://parsedown.org",
"keywords": [
"markdown",
"parser"
],
"install-path": "../erusev/parsedown"
},
{
"name": "ezyang/htmlpurifier",
"version": "v4.16.0",
"version_normalized": "4.16.0.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8"
},
"exclude-from-classmap": [
"/library/HTMLPurifier/Language/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Edward Z. Yang",
"email": "admin@htmlpurifier.org",
"homepage": "http://ezyang.com"
}
],
"description": "Standards compliant HTML filter written in PHP",
"homepage": "http://htmlpurifier.org/",
"keywords": [
"html"
]
},
{
"name": "gregwar/captcha",
"version": "v1.1.9",
"version_normalized": "1.1.9.0",
"source": {
"type": "git",
"url": "https://github.com/Gregwar/Captcha.git",
"reference": "4bb668e6b40e3205a020ca5ee4ca8cff8b8780c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Gregwar/Captcha/zipball/4bb668e6b40e3205a020ca5ee4ca8cff8b8780c5",
"reference": "4bb668e6b40e3205a020ca5ee4ca8cff8b8780c5",
"shasum": ""
},
"require": {
"ext-gd": "*",
"ext-mbstring": "*",
"php": ">=5.3.0",
"symfony/finder": "*"
},
"require-dev": {
"phpunit/phpunit": "^6.4"
},
"time": "2020-03-24T14:39:05+00:00",
"type": "captcha",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Gregwar\\": "src/Gregwar"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Grégoire Passault",
"email": "g.passault@gmail.com",
"homepage": "http://www.gregwar.com/"
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8",
"reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8",
"shasum": ""
},
{
"name": "Jeremy Livingston",
"email": "jeremy.j.livingston@gmail.com"
}
],
"description": "Captcha generator",
"homepage": "https://github.com/Gregwar/Captcha",
"keywords": [
"bot",
"captcha",
"spam"
]
},
{
"name": "phpmailer/phpmailer",
"version": "v6.6.5",
"version_normalized": "6.6.5.0",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "8b6386d7417526d1ea4da9edb70b8352f7543627"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/8b6386d7417526d1ea4da9edb70b8352f7543627",
"reference": "8b6386d7417526d1ea4da9edb70b8352f7543627",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-filter": "*",
"ext-hash": "*",
"php": ">=5.5.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
"doctrine/annotations": "^1.2",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.6.2",
"yoast/phpunit-polyfills": "^1.0.0"
},
"suggest": {
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
},
"time": "2022-10-07T12:23:10+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"PHPMailer\\PHPMailer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-only"
],
"authors": [
{
"name": "Marcus Bointon",
"email": "phpmailer@synchromedia.co.uk"
"require": {
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0"
},
{
"name": "Jim Jagielski",
"email": "jimjag@gmail.com"
"require-dev": {
"cerdic/css-tidy": "^1.7 || ^2.0",
"simpletest/simpletest": "dev-master"
},
{
"name": "Andy Prevost",
"email": "codeworxtech@users.sourceforge.net"
"suggest": {
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
"ext-bcmath": "Used for unit conversion and imagecrash protection",
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
"ext-tidy": "Used for pretty-printing HTML"
},
{
"name": "Brent R. Matzelle"
}
],
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"funding": [
{
"url": "https://github.com/Synchro",
"type": "github"
}
]
},
{
"name": "symfony/finder",
"version": "v6.1.3",
"version_normalized": "6.1.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709"
"time": "2022-09-18T07:06:19+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"library/HTMLPurifier.composer.php"
],
"psr-0": {
"HTMLPurifier": "library/"
},
"exclude-from-classmap": [
"/library/HTMLPurifier/Language/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Edward Z. Yang",
"email": "admin@htmlpurifier.org",
"homepage": "http://ezyang.com"
}
],
"description": "Standards compliant HTML filter written in PHP",
"homepage": "http://htmlpurifier.org/",
"keywords": [
"html"
],
"install-path": "../ezyang/htmlpurifier"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/39696bff2c2970b3779a5cac7bf9f0b88fc2b709",
"reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"require-dev": {
"symfony/filesystem": "^6.0"
},
"time": "2022-07-29T07:42:06+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Component\\Finder\\": ""
{
"name": "gregwar/captcha",
"version": "v1.1.9",
"version_normalized": "1.1.9.0",
"source": {
"type": "git",
"url": "https://github.com/Gregwar/Captcha.git",
"reference": "4bb668e6b40e3205a020ca5ee4ca8cff8b8780c5"
},
"exclude-from-classmap": [
"/Tests/"
]
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Gregwar/Captcha/zipball/4bb668e6b40e3205a020ca5ee4ca8cff8b8780c5",
"reference": "4bb668e6b40e3205a020ca5ee4ca8cff8b8780c5",
"shasum": ""
},
"require": {
"ext-gd": "*",
"ext-mbstring": "*",
"php": ">=5.3.0",
"symfony/finder": "*"
},
"require-dev": {
"phpunit/phpunit": "^6.4"
},
"time": "2020-03-24T14:39:05+00:00",
"type": "captcha",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Gregwar\\": "src/Gregwar"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Grégoire Passault",
"email": "g.passault@gmail.com",
"homepage": "http://www.gregwar.com/"
},
{
"name": "Jeremy Livingston",
"email": "jeremy.j.livingston@gmail.com"
}
],
"description": "Captcha generator",
"homepage": "https://github.com/Gregwar/Captcha",
"keywords": [
"bot",
"captcha",
"spam"
],
"install-path": "../gregwar/captcha"
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
{
"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"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ivopetkov/html5-dom-document-php/zipball/32c5ba748d661a9654c190bf70ce2854eaf5ad22",
"reference": "32c5ba748d661a9654c190bf70ce2854eaf5ad22",
"shasum": ""
},
{
"url": "https://github.com/fabpot",
"type": "github"
"require": {
"ext-dom": "*",
"php": "7.0.*|7.1.*|7.2.*|7.3.*|7.4.*|8.0.*|8.1.*|8.2.*"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
]
}
]
"require-dev": {
"ivopetkov/docs-generator": "1.*"
},
"time": "2022-12-17T00:20:55+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"autoload.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ivo Petkov",
"email": "ivo@ivopetkov.com",
"homepage": "http://ivopetkov.com"
}
],
"description": "HTML5 DOMDocument PHP library (extends DOMDocument)",
"support": {
"issues": "https://github.com/ivopetkov/html5-dom-document-php/issues",
"source": "https://github.com/ivopetkov/html5-dom-document-php/tree/v2.4.0"
},
"install-path": "../ivopetkov/html5-dom-document-php"
},
{
"name": "php-curl-class/php-curl-class",
"version": "2.0.0",
"version_normalized": "2.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-curl-class/php-curl-class.git",
"reference": "24a93bdc51058ad50d219842b63f7f2e0cb350ac"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-curl-class/php-curl-class/zipball/24a93bdc51058ad50d219842b63f7f2e0cb350ac",
"reference": "24a93bdc51058ad50d219842b63f7f2e0cb350ac",
"shasum": ""
},
"time": "2014-04-12T09:46:33+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"description": "PHP Curl Class is an object-oriented wrapper of the PHP cURL extension.",
"support": {
"issues": "https://github.com/php-curl-class/php-curl-class/issues",
"source": "https://github.com/php-curl-class/php-curl-class/tree/2.0.0"
},
"install-path": "../php-curl-class/php-curl-class"
},
{
"name": "phpmailer/phpmailer",
"version": "v6.6.5",
"version_normalized": "6.6.5.0",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "8b6386d7417526d1ea4da9edb70b8352f7543627"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/8b6386d7417526d1ea4da9edb70b8352f7543627",
"reference": "8b6386d7417526d1ea4da9edb70b8352f7543627",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-filter": "*",
"ext-hash": "*",
"php": ">=5.5.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
"doctrine/annotations": "^1.2",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.6.2",
"yoast/phpunit-polyfills": "^1.0.0"
},
"suggest": {
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
},
"time": "2022-10-07T12:23:10+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"PHPMailer\\PHPMailer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-only"
],
"authors": [
{
"name": "Marcus Bointon",
"email": "phpmailer@synchromedia.co.uk"
},
{
"name": "Jim Jagielski",
"email": "jimjag@gmail.com"
},
{
"name": "Andy Prevost",
"email": "codeworxtech@users.sourceforge.net"
},
{
"name": "Brent R. Matzelle"
}
],
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"funding": [
{
"url": "https://github.com/Synchro",
"type": "github"
}
],
"install-path": "../phpmailer/phpmailer"
},
{
"name": "symfony/deprecation-contracts",
"version": "v2.5.2",
"version_normalized": "2.5.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
"reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"time": "2022-01-02T09:53:40+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.5-dev"
},
"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": {
"psr-4": {
"Symfony\\Component\\Finder\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Finds files and directories via an intuitive fluent interface",
"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/finder"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.26.0",
"version_normalized": "1.26.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"time": "2022-05-10T07:21:04+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/polyfill-php80"
}
],
"dev": true,
"dev-package-names": []
}

104
web/app/vendor/composer/installed.php vendored Normal file
View File

@ -0,0 +1,104 @@
<?php return array(
'root' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => 'd6997b84758fbe52d9d90a2d5fe2f2e06806b176',
'name' => '__root__',
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => 'd6997b84758fbe52d9d90a2d5fe2f2e06806b176',
'dev_requirement' => false,
),
'erusev/parsedown' => array(
'pretty_version' => '1.7.4',
'version' => '1.7.4.0',
'type' => 'library',
'install_path' => __DIR__ . '/../erusev/parsedown',
'aliases' => array(),
'reference' => 'cb17b6477dfff935958ba01325f2e8a2bfa6dab3',
'dev_requirement' => false,
),
'ezyang/htmlpurifier' => array(
'pretty_version' => 'v4.16.0',
'version' => '4.16.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../ezyang/htmlpurifier',
'aliases' => array(),
'reference' => '523407fb06eb9e5f3d59889b3978d5bfe94299c8',
'dev_requirement' => false,
),
'gregwar/captcha' => array(
'pretty_version' => 'v1.1.9',
'version' => '1.1.9.0',
'type' => 'captcha',
'install_path' => __DIR__ . '/../gregwar/captcha',
'aliases' => array(),
'reference' => '4bb668e6b40e3205a020ca5ee4ca8cff8b8780c5',
'dev_requirement' => false,
),
'ivopetkov/html5-dom-document-php' => array(
'pretty_version' => 'v2.4.0',
'version' => '2.4.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../ivopetkov/html5-dom-document-php',
'aliases' => array(),
'reference' => '32c5ba748d661a9654c190bf70ce2854eaf5ad22',
'dev_requirement' => false,
),
'php-curl-class/php-curl-class' => array(
'pretty_version' => '2.0.0',
'version' => '2.0.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../php-curl-class/php-curl-class',
'aliases' => array(),
'reference' => '24a93bdc51058ad50d219842b63f7f2e0cb350ac',
'dev_requirement' => false,
),
'phpmailer/phpmailer' => array(
'pretty_version' => 'v6.6.5',
'version' => '6.6.5.0',
'type' => 'library',
'install_path' => __DIR__ . '/../phpmailer/phpmailer',
'aliases' => array(),
'reference' => '8b6386d7417526d1ea4da9edb70b8352f7543627',
'dev_requirement' => false,
),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v2.5.2',
'version' => '2.5.2.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(),
'reference' => 'e8b495ea28c1d97b5e0c121748d6f9b53d075c66',
'dev_requirement' => false,
),
'symfony/finder' => array(
'pretty_version' => 'v5.4.11',
'version' => '5.4.11.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/finder',
'aliases' => array(),
'reference' => '7872a66f57caffa2916a584db1aa7f12adc76f8c',
'dev_requirement' => false,
),
'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.26.0',
'version' => '1.26.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(),
'reference' => 'cfa0ae98841b9e461207c13ab093d76b0fa7bace',
'dev_requirement' => false,
),
),
);

View File

@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70205)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.5". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@ -0,0 +1,21 @@
The MIT License
Copyright (c) Ivo Petkov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,22 @@
<?php
/*
* HTML5 DOMDocument PHP library (extends DOMDocument)
* https://github.com/ivopetkov/html5-dom-document-php
* Copyright (c) Ivo Petkov
* Free to use under the MIT license.
*/
$classes = array(
'IvoPetkov\HTML5DOMDocument' => __DIR__ . '/src/HTML5DOMDocument.php',
'IvoPetkov\HTML5DOMDocument\Internal\QuerySelectors' => __DIR__ . '/src/HTML5DOMDocument/Internal/QuerySelectors.php',
'IvoPetkov\HTML5DOMElement' => __DIR__ . '/src/HTML5DOMElement.php',
'IvoPetkov\HTML5DOMNodeList' => __DIR__ . '/src/HTML5DOMNodeList.php',
'IvoPetkov\HTML5DOMTokenList' => __DIR__ . '/src/HTML5DOMTokenList.php'
);
spl_autoload_register(function ($class) use ($classes) {
if (isset($classes[$class])) {
require $classes[$class];
}
});

View File

@ -0,0 +1,24 @@
{
"name": "ivopetkov/html5-dom-document-php",
"description": "HTML5 DOMDocument PHP library (extends DOMDocument)",
"license": "MIT",
"authors": [
{
"name": "Ivo Petkov",
"email": "ivo@ivopetkov.com",
"homepage": "http://ivopetkov.com"
}
],
"require": {
"php": "7.0.*|7.1.*|7.2.*|7.3.*|7.4.*|8.0.*|8.1.*|8.2.*",
"ext-dom": "*"
},
"require-dev": {
"ivopetkov/docs-generator": "1.*"
},
"autoload": {
"files": [
"autoload.php"
]
}
}

View File

@ -0,0 +1,747 @@
<?php
/*
* HTML5 DOMDocument PHP library (extends DOMDocument)
* https://github.com/ivopetkov/html5-dom-document-php
* Copyright (c) Ivo Petkov
* Free to use under the MIT license.
*/
namespace IvoPetkov;
use IvoPetkov\HTML5DOMDocument\Internal\QuerySelectors;
/**
* Represents a live (can be manipulated) representation of a HTML5 document.
*
* @method \IvoPetkov\HTML5DOMElement|false createElement(string $localName, string $value = '') Create new element node.
* @method \IvoPetkov\HTML5DOMElement|false createElementNS(?string $namespace, string $qualifiedName, string $value = '') Create new element node with an associated namespace.
* @method ?\IvoPetkov\HTML5DOMElement getElementById(string $elementId) Searches for an element with a certain id.
*/
class HTML5DOMDocument extends \DOMDocument
{
use QuerySelectors;
/**
* An option passed to loadHTML() and loadHTMLFile() to disable duplicate element IDs exception.
*/
const ALLOW_DUPLICATE_IDS = 67108864;
/**
* A modification (passed to modify()) that removes all but the last title elements.
*/
const FIX_MULTIPLE_TITLES = 2;
/**
* A modification (passed to modify()) that removes all but the last metatags with matching name or property attributes.
*/
const FIX_DUPLICATE_METATAGS = 4;
/**
* A modification (passed to modify()) that merges multiple head elements.
*/
const FIX_MULTIPLE_HEADS = 8;
/**
* A modification (passed to modify()) that merges multiple body elements.
*/
const FIX_MULTIPLE_BODIES = 16;
/**
* A modification (passed to modify()) that moves charset metatag and title elements first.
*/
const OPTIMIZE_HEAD = 32;
/**
*
* @var array
*/
static private $newObjectsCache = [];
/**
* Indicates whether an HTML code is loaded.
*
* @var boolean
*/
private $loaded = false;
/**
* Creates a new HTML5DOMDocument object.
*
* @param string $version The version number of the document as part of the XML declaration.
* @param string $encoding The encoding of the document as part of the XML declaration.
*/
public function __construct(string $version = '1.0', string $encoding = '')
{
parent::__construct($version, $encoding);
$this->registerNodeClass('DOMElement', '\IvoPetkov\HTML5DOMElement');
}
/**
* Load HTML from a string.
*
* @param string $source The HTML code.
* @param int $options Additional Libxml parameters.
* @return boolean TRUE on success or FALSE on failure.
*/
public function loadHTML($source, $options = 0)
{
// Enables libxml errors handling
$internalErrorsOptionValue = libxml_use_internal_errors();
if ($internalErrorsOptionValue === false) {
libxml_use_internal_errors(true);
}
$source = trim($source);
// Add CDATA around script tags content
$matches = null;
preg_match_all('/<script(.*?)>/', $source, $matches);
if (isset($matches[0])) {
$matches[0] = array_unique($matches[0]);
foreach ($matches[0] as $match) {
if (substr($match, -2, 1) !== '/') { // check if ends with />
$source = str_replace($match, $match . '<![CDATA[-html5-dom-document-internal-cdata', $source); // Add CDATA after the open tag
}
}
}
$source = str_replace('</script>', '-html5-dom-document-internal-cdata]]></script>', $source); // Add CDATA before the end tag
$source = str_replace('<![CDATA[-html5-dom-document-internal-cdata-html5-dom-document-internal-cdata]]>', '', $source); // Clean empty script tags
$matches = null;
preg_match_all('/\<!\[CDATA\[-html5-dom-document-internal-cdata.*?-html5-dom-document-internal-cdata\]\]>/s', $source, $matches);
if (isset($matches[0])) {
$matches[0] = array_unique($matches[0]);
foreach ($matches[0] as $match) {
if (strpos($match, '</') !== false) { // check if contains </
$source = str_replace($match, str_replace('</', '<-html5-dom-document-internal-cdata-endtagfix/', $match), $source);
}
}
}
$autoAddHtmlAndBodyTags = !defined('LIBXML_HTML_NOIMPLIED') || ($options & LIBXML_HTML_NOIMPLIED) === 0;
$autoAddDoctype = !defined('LIBXML_HTML_NODEFDTD') || ($options & LIBXML_HTML_NODEFDTD) === 0;
$allowDuplicateIDs = ($options & self::ALLOW_DUPLICATE_IDS) !== 0;
// Add body tag if missing
if ($autoAddHtmlAndBodyTags && $source !== '' && preg_match('/\<!DOCTYPE.*?\>/', $source) === 0 && preg_match('/\<html.*?\>/', $source) === 0 && preg_match('/\<body.*?\>/', $source) === 0 && preg_match('/\<head.*?\>/', $source) === 0) {
$source = '<body>' . $source . '</body>';
}
// Add DOCTYPE if missing
if ($autoAddDoctype && strtoupper(substr($source, 0, 9)) !== '<!DOCTYPE') {
$source = "<!DOCTYPE html>\n" . $source;
}
// Adds temporary head tag
$charsetTag = '<meta data-html5-dom-document-internal-attribute="charset-meta" http-equiv="content-type" content="text/html; charset=utf-8" />';
$matches = [];
preg_match('/\<head.*?\>/', $source, $matches);
$removeHeadTag = false;
$removeHtmlTag = false;
if (isset($matches[0])) { // has head tag
$insertPosition = strpos($source, $matches[0]) + strlen($matches[0]);
$source = substr($source, 0, $insertPosition) . $charsetTag . substr($source, $insertPosition);
} else {
$matches = [];
preg_match('/\<html.*?\>/', $source, $matches);
if (isset($matches[0])) { // has html tag
$source = str_replace($matches[0], $matches[0] . '<head>' . $charsetTag . '</head>', $source);
} else {
$source = '<head>' . $charsetTag . '</head>' . $source;
$removeHtmlTag = true;
}
$removeHeadTag = true;
}
// Preserve html entities
$source = preg_replace('/&([a-zA-Z]*);/', 'html5-dom-document-internal-entity1-$1-end', $source);
$source = preg_replace('/&#([0-9]*);/', 'html5-dom-document-internal-entity2-$1-end', $source);
$result = parent::loadHTML('<?xml encoding="utf-8" ?>' . $source, $options);
if ($internalErrorsOptionValue === false) {
libxml_use_internal_errors(false);
}
if ($result === false) {
return false;
}
$this->encoding = 'utf-8';
foreach ($this->childNodes as $item) {
if ($item->nodeType === XML_PI_NODE) {
$this->removeChild($item);
break;
}
}
/** @var HTML5DOMElement|null */
$metaTagElement = $this->getElementsByTagName('meta')->item(0);
if ($metaTagElement !== null) {
if ($metaTagElement->getAttribute('data-html5-dom-document-internal-attribute') === 'charset-meta') {
$headElement = $metaTagElement->parentNode;
$htmlElement = $headElement->parentNode;
$metaTagElement->parentNode->removeChild($metaTagElement);
if ($removeHeadTag && $headElement !== null && $headElement->parentNode !== null && ($headElement->firstChild === null || ($headElement->childNodes->length === 1 && $headElement->firstChild instanceof \DOMText))) {
$headElement->parentNode->removeChild($headElement);
}
if ($removeHtmlTag && $htmlElement !== null && $htmlElement->parentNode !== null && $htmlElement->firstChild === null) {
$htmlElement->parentNode->removeChild($htmlElement);
}
}
}
if (!$allowDuplicateIDs) {
$matches = [];
preg_match_all('/\sid[\s]*=[\s]*(["\'])(.*?)\1/', $source, $matches);
if (!empty($matches[2]) && max(array_count_values($matches[2])) > 1) {
$elementIDs = [];
$walkChildren = function ($element) use (&$walkChildren, &$elementIDs) {
foreach ($element->childNodes as $child) {
if ($child instanceof \DOMElement) {
if ($child->attributes->length > 0) { // Performance optimization
$id = $child->getAttribute('id');
if ($id !== '') {
if (isset($elementIDs[$id])) {
throw new \Exception('A DOM node with an ID value "' . $id . '" already exists! Pass the HTML5DOMDocument::ALLOW_DUPLICATE_IDS option to disable this check.');
} else {
$elementIDs[$id] = true;
}
}
}
$walkChildren($child);
}
}
};
$walkChildren($this);
}
}
$this->loaded = true;
return true;
}
/**
* Load HTML from a file.
*
* @param string $filename The path to the HTML file.
* @param int $options Additional Libxml parameters.
*/
public function loadHTMLFile($filename, $options = 0)
{
return $this->loadHTML(file_get_contents($filename), $options);
}
/**
* Adds the HTML tag to the document if missing.
*
* @return boolean TRUE on success, FALSE otherwise.
*/
private function addHtmlElementIfMissing(): bool
{
if ($this->getElementsByTagName('html')->length === 0) {
if (!isset(self::$newObjectsCache['htmlelement'])) {
self::$newObjectsCache['htmlelement'] = new \DOMElement('html');
}
$this->appendChild(clone (self::$newObjectsCache['htmlelement']));
return true;
}
return false;
}
/**
* Adds the HEAD tag to the document if missing.
*
* @return boolean TRUE on success, FALSE otherwise.
*/
private function addHeadElementIfMissing(): bool
{
if ($this->getElementsByTagName('head')->length === 0) {
$htmlElement = $this->getElementsByTagName('html')->item(0);
if (!isset(self::$newObjectsCache['headelement'])) {
self::$newObjectsCache['headelement'] = new \DOMElement('head');
}
$headElement = clone (self::$newObjectsCache['headelement']);
if ($htmlElement->firstChild === null) {
$htmlElement->appendChild($headElement);
} else {
$htmlElement->insertBefore($headElement, $htmlElement->firstChild);
}
return true;
}
return false;
}
/**
* Adds the BODY tag to the document if missing.
*
* @return boolean TRUE on success, FALSE otherwise.
*/
private function addBodyElementIfMissing(): bool
{
if ($this->getElementsByTagName('body')->length === 0) {
if (!isset(self::$newObjectsCache['bodyelement'])) {
self::$newObjectsCache['bodyelement'] = new \DOMElement('body');
}
$this->getElementsByTagName('html')->item(0)->appendChild(clone (self::$newObjectsCache['bodyelement']));
return true;
}
return false;
}
/**
* Dumps the internal document into a string using HTML formatting.
*
* @param \DOMNode $node Optional parameter to output a subset of the document.
* @return string The document (or node) HTML code as string.
*/
public function saveHTML(\DOMNode $node = null): string
{
$nodeMode = $node !== null;
if ($nodeMode && $node instanceof \DOMDocument) {
$nodeMode = false;
}
if ($nodeMode) {
if (!isset(self::$newObjectsCache['html5domdocument'])) {
self::$newObjectsCache['html5domdocument'] = new HTML5DOMDocument();
}
$tempDomDocument = clone (self::$newObjectsCache['html5domdocument']);
if ($node->nodeName === 'html') {
$tempDomDocument->loadHTML('<!DOCTYPE html>');
$tempDomDocument->appendChild($tempDomDocument->importNode(clone ($node), true));
$html = $tempDomDocument->saveHTML();
$html = substr($html, 16); // remove the DOCTYPE + the new line after
} elseif ($node->nodeName === 'head' || $node->nodeName === 'body') {
$tempDomDocument->loadHTML("<!DOCTYPE html>\n<html></html>");
$tempDomDocument->childNodes[1]->appendChild($tempDomDocument->importNode(clone ($node), true));
$html = $tempDomDocument->saveHTML();
$html = substr($html, 22, -7); // remove the DOCTYPE + the new line after + html tag
} else {
$isInHead = false;
$parentNode = $node;
for ($i = 0; $i < 1000; $i++) {
$parentNode = $parentNode->parentNode;
if ($parentNode === null) {
break;
}
if ($parentNode->nodeName === 'body') {
break;
} elseif ($parentNode->nodeName === 'head') {
$isInHead = true;
break;
}
}
$tempDomDocument->loadHTML("<!DOCTYPE html>\n<html>" . ($isInHead ? '<head></head>' : '<body></body>') . '</html>');
$tempDomDocument->childNodes[1]->childNodes[0]->appendChild($tempDomDocument->importNode(clone ($node), true));
$html = $tempDomDocument->saveHTML();
$html = substr($html, 28, -14); // remove the DOCTYPE + the new line + html + body or head tags
}
$html = trim($html);
} else {
$removeHtmlElement = false;
$removeHeadElement = false;
$headElement = $this->getElementsByTagName('head')->item(0);
if ($headElement === null) {
if ($this->addHtmlElementIfMissing()) {
$removeHtmlElement = true;
}
if ($this->addHeadElementIfMissing()) {
$removeHeadElement = true;
}
$headElement = $this->getElementsByTagName('head')->item(0);
}
$meta = $this->createElement('meta');
$meta->setAttribute('data-html5-dom-document-internal-attribute', 'charset-meta');
$meta->setAttribute('http-equiv', 'content-type');
$meta->setAttribute('content', 'text/html; charset=utf-8');
if ($headElement->firstChild !== null) {
$headElement->insertBefore($meta, $headElement->firstChild);
} else {
$headElement->appendChild($meta);
}
$html = parent::saveHTML();
$html = rtrim($html, "\n");
if ($removeHeadElement) {
$headElement->parentNode->removeChild($headElement);
} else {
$meta->parentNode->removeChild($meta);
}
if (strpos($html, 'html5-dom-document-internal-entity') !== false) {
$html = preg_replace('/html5-dom-document-internal-entity1-(.*?)-end/', '&$1;', $html);
$html = preg_replace('/html5-dom-document-internal-entity2-(.*?)-end/', '&#$1;', $html);
}
$codeToRemove = [
'html5-dom-document-internal-content',
'<meta data-html5-dom-document-internal-attribute="charset-meta" http-equiv="content-type" content="text/html; charset=utf-8">',
'</area>', '</base>', '</br>', '</col>', '</command>', '</embed>', '</hr>', '</img>', '</input>', '</keygen>', '</link>', '</meta>', '</param>', '</source>', '</track>', '</wbr>',
'<![CDATA[-html5-dom-document-internal-cdata', '-html5-dom-document-internal-cdata]]>', '-html5-dom-document-internal-cdata-endtagfix'
];
if ($removeHeadElement) {
$codeToRemove[] = '<head></head>';
}
if ($removeHtmlElement) {
$codeToRemove[] = '<html></html>';
}
$html = str_replace($codeToRemove, '', $html);
}
return $html;
}
/**
* Dumps the internal document into a file using HTML formatting.
*
* @param string $filename The path to the saved HTML document.
* @return int|false the number of bytes written or FALSE if an error occurred.
*/
#[\ReturnTypeWillChange] // Return type "int|false" is invalid in older supported versions.
public function saveHTMLFile($filename)
{
if (!is_writable($filename)) {
return false;
}
$result = $this->saveHTML();
file_put_contents($filename, $result);
$bytesWritten = filesize($filename);
if ($bytesWritten === strlen($result)) {
return $bytesWritten;
}
return false;
}
/**
* Returns the first document element matching the selector.
*
* @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname.classname.classname2, .classname.classname2, tagname[attribute-selector], [attribute-selector], "div, p", div p, div > p, div + p and p ~ ul.
* @return HTML5DOMElement|null The result DOMElement or null if not found.
* @throws \InvalidArgumentException
*/
public function querySelector(string $selector)
{
return $this->internalQuerySelector($selector);
}
/**
* Returns a list of document elements matching the selector.
*
* @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname.classname.classname2, .classname.classname2, tagname[attribute-selector], [attribute-selector], "div, p", div p, div > p, div + p and p ~ ul.
* @return HTML5DOMNodeList Returns a list of DOMElements matching the criteria.
* @throws \InvalidArgumentException
*/
public function querySelectorAll(string $selector)
{
return $this->internalQuerySelectorAll($selector);
}
/**
* Creates an element that will be replaced by the new body in insertHTML.
*
* @param string $name The name of the insert target.
* @return HTML5DOMElement A new DOMElement that must be set in the place where the new body will be inserted.
*/
public function createInsertTarget(string $name)
{
if (!$this->loaded) {
$this->loadHTML('');
}
$element = $this->createElement('html5-dom-document-insert-target');
$element->setAttribute('name', $name);
return $element;
}
/**
* Inserts a HTML document into the current document. The elements from the head and the body will be moved to their proper locations.
*
* @param string $source The HTML code to be inserted.
* @param string $target Body target position. Available values: afterBodyBegin, beforeBodyEnd or insertTarget name.
*/
public function insertHTML(string $source, string $target = 'beforeBodyEnd')
{
$this->insertHTMLMulti([['source' => $source, 'target' => $target]]);
}
/**
* Inserts multiple HTML documents into the current document. The elements from the head and the body will be moved to their proper locations.
*
* @param array $sources An array containing the source of the document to be inserted in the following format: [ ['source'=>'', 'target'=>''], ['source'=>'', 'target'=>''], ... ]
* @throws \Exception
*/
public function insertHTMLMulti(array $sources)
{
if (!$this->loaded) {
$this->loadHTML('');
}
if (!isset(self::$newObjectsCache['html5domdocument'])) {
self::$newObjectsCache['html5domdocument'] = new HTML5DOMDocument();
}
$currentDomDocument = &$this;
$copyAttributes = function ($sourceNode, $targetNode) {
foreach ($sourceNode->attributes as $attributeName => $attribute) {
$targetNode->setAttribute($attributeName, $attribute->value);
}
};
$currentDomHTMLElement = null;
$currentDomHeadElement = null;
$currentDomBodyElement = null;
$insertTargetsList = null;
$prepareInsertTargetsList = function () use (&$insertTargetsList) {
if ($insertTargetsList === null) {
$insertTargetsList = [];
$targetElements = $this->getElementsByTagName('html5-dom-document-insert-target');
foreach ($targetElements as $targetElement) {
$insertTargetsList[$targetElement->getAttribute('name')] = $targetElement;
}
}
};
foreach ($sources as $sourceData) {
if (!isset($sourceData['source'])) {
throw new \Exception('Missing source key');
}
$source = $sourceData['source'];
$target = isset($sourceData['target']) ? $sourceData['target'] : 'beforeBodyEnd';
$domDocument = clone (self::$newObjectsCache['html5domdocument']);
$domDocument->loadHTML($source, self::ALLOW_DUPLICATE_IDS);
$htmlElement = $domDocument->getElementsByTagName('html')->item(0);
if ($htmlElement !== null) {
if ($htmlElement->attributes->length > 0) {
if ($currentDomHTMLElement === null) {
$currentDomHTMLElement = $this->getElementsByTagName('html')->item(0);
if ($currentDomHTMLElement === null) {
$this->addHtmlElementIfMissing();
$currentDomHTMLElement = $this->getElementsByTagName('html')->item(0);
}
}
$copyAttributes($htmlElement, $currentDomHTMLElement);
}
}
$headElement = $domDocument->getElementsByTagName('head')->item(0);
if ($headElement !== null) {
if ($currentDomHeadElement === null) {
$currentDomHeadElement = $this->getElementsByTagName('head')->item(0);
if ($currentDomHeadElement === null) {
$this->addHtmlElementIfMissing();
$this->addHeadElementIfMissing();
$currentDomHeadElement = $this->getElementsByTagName('head')->item(0);
}
}
foreach ($headElement->childNodes as $headElementChild) {
$newNode = $currentDomDocument->importNode($headElementChild, true);
if ($newNode !== null) {
$currentDomHeadElement->appendChild($newNode);
}
}
if ($headElement->attributes->length > 0) {
$copyAttributes($headElement, $currentDomHeadElement);
}
}
$bodyElement = $domDocument->getElementsByTagName('body')->item(0);
if ($bodyElement !== null) {
if ($currentDomBodyElement === null) {
$currentDomBodyElement = $this->getElementsByTagName('body')->item(0);
if ($currentDomBodyElement === null) {
$this->addHtmlElementIfMissing();
$this->addBodyElementIfMissing();
$currentDomBodyElement = $this->getElementsByTagName('body')->item(0);
}
}
$bodyElementChildren = $bodyElement->childNodes;
if ($target === 'afterBodyBegin') {
$bodyElementChildrenCount = $bodyElementChildren->length;
for ($i = $bodyElementChildrenCount - 1; $i >= 0; $i--) {
$newNode = $currentDomDocument->importNode($bodyElementChildren->item($i), true);
if ($newNode !== null) {
if ($currentDomBodyElement->firstChild === null) {
$currentDomBodyElement->appendChild($newNode);
} else {
$currentDomBodyElement->insertBefore($newNode, $currentDomBodyElement->firstChild);
}
}
}
} elseif ($target === 'beforeBodyEnd') {
foreach ($bodyElementChildren as $bodyElementChild) {
$newNode = $currentDomDocument->importNode($bodyElementChild, true);
if ($newNode !== null) {
$currentDomBodyElement->appendChild($newNode);
}
}
} else {
$prepareInsertTargetsList();
if (isset($insertTargetsList[$target])) {
$targetElement = $insertTargetsList[$target];
$targetElementParent = $targetElement->parentNode;
foreach ($bodyElementChildren as $bodyElementChild) {
$newNode = $currentDomDocument->importNode($bodyElementChild, true);
if ($newNode !== null) {
$targetElementParent->insertBefore($newNode, $targetElement);
}
}
$targetElementParent->removeChild($targetElement);
}
}
if ($bodyElement->attributes->length > 0) {
$copyAttributes($bodyElement, $currentDomBodyElement);
}
} else { // clear the insert target when there is no body element
$prepareInsertTargetsList();
if (isset($insertTargetsList[$target])) {
$targetElement = $insertTargetsList[$target];
$targetElement->parentNode->removeChild($targetElement);
}
}
}
}
/**
* Applies the modifications specified to the DOM document.
*
* @param int $modifications The modifications to apply. Available values:
* - HTML5DOMDocument::FIX_MULTIPLE_TITLES - removes all but the last title elements.
* - HTML5DOMDocument::FIX_DUPLICATE_METATAGS - removes all but the last metatags with matching name or property attributes.
* - HTML5DOMDocument::FIX_MULTIPLE_HEADS - merges multiple head elements.
* - HTML5DOMDocument::FIX_MULTIPLE_BODIES - merges multiple body elements.
* - HTML5DOMDocument::OPTIMIZE_HEAD - moves charset metatag and title elements first.
*/
public function modify($modifications = 0)
{
$fixMultipleTitles = ($modifications & self::FIX_MULTIPLE_TITLES) !== 0;
$fixDuplicateMetatags = ($modifications & self::FIX_DUPLICATE_METATAGS) !== 0;
$fixMultipleHeads = ($modifications & self::FIX_MULTIPLE_HEADS) !== 0;
$fixMultipleBodies = ($modifications & self::FIX_MULTIPLE_BODIES) !== 0;
$optimizeHead = ($modifications & self::OPTIMIZE_HEAD) !== 0;
/** @var \DOMNodeList<HTML5DOMElement> */
$headElements = $this->getElementsByTagName('head');
if ($fixMultipleHeads) { // Merges multiple head elements.
if ($headElements->length > 1) {
$firstHeadElement = $headElements->item(0);
while ($headElements->length > 1) {
$nextHeadElement = $headElements->item(1);
$nextHeadElementChildren = $nextHeadElement->childNodes;
$nextHeadElementChildrenCount = $nextHeadElementChildren->length;
for ($i = 0; $i < $nextHeadElementChildrenCount; $i++) {
$firstHeadElement->appendChild($nextHeadElementChildren->item(0));
}
$nextHeadElement->parentNode->removeChild($nextHeadElement);
}
$headElements = [$firstHeadElement];
}
}
foreach ($headElements as $headElement) {
if ($fixMultipleTitles) { // Remove all title elements except the last one.
$titleTags = $headElement->getElementsByTagName('title');
$titleTagsCount = $titleTags->length;
for ($i = 0; $i < $titleTagsCount - 1; $i++) {
$node = $titleTags->item($i);
$node->parentNode->removeChild($node);
}
}
if ($fixDuplicateMetatags) { // Remove all meta tags that has matching name or property attributes.
$metaTags = $headElement->getElementsByTagName('meta');
if ($metaTags->length > 0) {
$list = [];
$idsList = [];
foreach ($metaTags as $metaTag) {
$id = $metaTag->getAttribute('name');
if ($id !== '') {
$id = 'name:' . $id;
} else {
$id = $metaTag->getAttribute('property');
if ($id !== '') {
$id = 'property:' . $id;
} else {
$id = $metaTag->getAttribute('charset');
if ($id !== '') {
$id = 'charset';
}
}
}
if (!isset($idsList[$id])) {
$idsList[$id] = 0;
}
$idsList[$id]++;
$list[] = [$metaTag, $id];
}
foreach ($idsList as $id => $count) {
if ($count > 1 && $id !== '') {
foreach ($list as $i => $item) {
if ($item[1] === $id) {
$node = $item[0];
$node->parentNode->removeChild($node);
unset($list[$i]);
$count--;
}
if ($count === 1) {
break;
}
}
}
}
}
}
if ($optimizeHead) { // Moves charset metatag and title elements first.
$titleElement = $headElement->getElementsByTagName('title')->item(0);
$hasTitleElement = false;
if ($titleElement !== null && $titleElement->previousSibling !== null) {
$headElement->insertBefore($titleElement, $headElement->firstChild);
$hasTitleElement = true;
}
$metaTags = $headElement->getElementsByTagName('meta');
$metaTagsLength = $metaTags->length;
if ($metaTagsLength > 0) {
$charsetMetaTag = null;
$nodesToMove = [];
for ($i = $metaTagsLength - 1; $i >= 0; $i--) {
$nodesToMove[$i] = $metaTags->item($i);
}
for ($i = $metaTagsLength - 1; $i >= 0; $i--) {
$nodeToMove = $nodesToMove[$i];
if ($charsetMetaTag === null && $nodeToMove->getAttribute('charset') !== '') {
$charsetMetaTag = $nodeToMove;
}
$referenceNode = $headElement->childNodes->item($hasTitleElement ? 1 : 0);
if ($nodeToMove !== $referenceNode) {
$headElement->insertBefore($nodeToMove, $referenceNode);
}
}
if ($charsetMetaTag !== null && $charsetMetaTag->previousSibling !== null) {
$headElement->insertBefore($charsetMetaTag, $headElement->firstChild);
}
}
}
}
if ($fixMultipleBodies) { // Merges multiple body elements.
$bodyElements = $this->getElementsByTagName('body');
if ($bodyElements->length > 1) {
$firstBodyElement = $bodyElements->item(0);
while ($bodyElements->length > 1) {
$nextBodyElement = $bodyElements->item(1);
$nextBodyElementChildren = $nextBodyElement->childNodes;
$nextBodyElementChildrenCount = $nextBodyElementChildren->length;
for ($i = 0; $i < $nextBodyElementChildrenCount; $i++) {
$firstBodyElement->appendChild($nextBodyElementChildren->item(0));
}
$nextBodyElement->parentNode->removeChild($nextBodyElement);
}
}
}
}
}

View File

@ -0,0 +1,514 @@
<?php
namespace IvoPetkov\HTML5DOMDocument\Internal;
use IvoPetkov\HTML5DOMElement;
trait QuerySelectors
{
/**
* Returns the first element matching the selector.
*
* @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname[attribute-selector] and [attribute-selector].
* @return HTML5DOMElement|null The result DOMElement or null if not found
*/
private function internalQuerySelector(string $selector)
{
$result = $this->internalQuerySelectorAll($selector, 1);
return $result->item(0);
}
/**
* Returns a list of document elements matching the selector.
*
* @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname[attribute-selector] and [attribute-selector].
* @param int|null $preferredLimit Preferred maximum number of elements to return.
* @return DOMNodeList Returns a list of DOMElements matching the criteria.
* @throws \InvalidArgumentException
*/
private function internalQuerySelectorAll(string $selector, $preferredLimit = null)
{
$selector = trim($selector);
$cache = [];
$walkChildren = function (\DOMNode $context, $tagNames, callable $callback) use (&$cache) {
if (!empty($tagNames)) {
$children = [];
foreach ($tagNames as $tagName) {
$elements = $context->getElementsByTagName($tagName);
foreach ($elements as $element) {
$children[] = $element;
}
}
} else {
$getChildren = function () use ($context) {
$result = [];
$process = function (\DOMNode $node) use (&$process, &$result) {
foreach ($node->childNodes as $child) {
if ($child instanceof \DOMElement) {
$result[] = $child;
$process($child);
}
}
};
$process($context);
return $result;
};
if ($this === $context) {
$cacheKey = 'walk_children';
if (!isset($cache[$cacheKey])) {
$cache[$cacheKey] = $getChildren();
}
$children = $cache[$cacheKey];
} else {
$children = $getChildren();
}
}
foreach ($children as $child) {
if ($callback($child) === true) {
return true;
}
}
};
$getElementById = function (\DOMNode $context, $id, $tagName) use (&$walkChildren) {
if ($context instanceof \DOMDocument) {
$element = $context->getElementById($id);
if ($element && ($tagName === null || $element->tagName === $tagName)) {
return $element;
}
} else {
$foundElement = null;
$walkChildren($context, $tagName !== null ? [$tagName] : null, function ($element) use ($id, &$foundElement) {
if ($element->attributes->length > 0 && $element->getAttribute('id') === $id) {
$foundElement = $element;
return true;
}
});
return $foundElement;
}
return null;
};
$simpleSelectors = [];
// all
$simpleSelectors['\*'] = function (string $mode, array $matches, \DOMNode $context, callable $add = null) use ($walkChildren) {
if ($mode === 'validate') {
return true;
} else {
$walkChildren($context, [], function ($element) use ($add) {
if ($add($element)) {
return true;
}
});
}
};
// tagname
$simpleSelectors['[a-zA-Z0-9\-]+'] = function (string $mode, array $matches, \DOMNode $context, callable $add = null) use ($walkChildren) {
$tagNames = [];
foreach ($matches as $match) {
$tagNames[] = strtolower($match[0]);
}
if ($mode === 'validate') {
return array_search($context->tagName, $tagNames) !== false;
}
$walkChildren($context, $tagNames, function ($element) use ($add) {
if ($add($element)) {
return true;
}
});
};
// tagname[target] or [target] // Available values for targets: attr, attr="value", attr~="value", attr|="value", attr^="value", attr$="value", attr*="value"
$simpleSelectors['(?:[a-zA-Z0-9\-]*)(?:\[.+?\])'] = function (string $mode, array $matches, \DOMNode $context, callable $add = null) use ($walkChildren) {
$run = function ($match) use ($mode, $context, $add, $walkChildren) {
$attributeSelectors = explode('][', substr($match[2], 1, -1));
foreach ($attributeSelectors as $i => $attributeSelector) {
$attributeSelectorMatches = null;
if (preg_match('/^(.+?)(=|~=|\|=|\^=|\$=|\*=)\"(.+?)\"$/', $attributeSelector, $attributeSelectorMatches) === 1) {
$attributeSelectors[$i] = [
'name' => strtolower($attributeSelectorMatches[1]),
'value' => $attributeSelectorMatches[3],
'operator' => $attributeSelectorMatches[2]
];
} else {
$attributeSelectors[$i] = [
'name' => $attributeSelector
];
}
}
$tagName = strlen($match[1]) > 0 ? strtolower($match[1]) : null;
$check = function ($element) use ($attributeSelectors) {
if ($element->attributes->length > 0) {
foreach ($attributeSelectors as $attributeSelector) {
$isMatch = false;
$attributeValue = $element->getAttribute($attributeSelector['name']);
if (isset($attributeSelector['value'])) {
$valueToMatch = $attributeSelector['value'];
switch ($attributeSelector['operator']) {
case '=':
if ($attributeValue === $valueToMatch) {
$isMatch = true;
}
break;
case '~=':
$words = preg_split("/[\s]+/", $attributeValue);
if (array_search($valueToMatch, $words) !== false) {
$isMatch = true;
}
break;
case '|=':
if ($attributeValue === $valueToMatch || strpos($attributeValue, $valueToMatch . '-') === 0) {
$isMatch = true;
}
break;
case '^=':
if (strpos($attributeValue, $valueToMatch) === 0) {
$isMatch = true;
}
break;
case '$=':
if (substr($attributeValue, -strlen($valueToMatch)) === $valueToMatch) {
$isMatch = true;
}
break;
case '*=':
if (strpos($attributeValue, $valueToMatch) !== false) {
$isMatch = true;
}
break;
}
} else {
if ($attributeValue !== '') {
$isMatch = true;
}
}
if (!$isMatch) {
return false;
}
}
return true;
}
return false;
};
if ($mode === 'validate') {
return ($tagName === null ? true : $context->tagName === $tagName) && $check($context);
} else {
$walkChildren($context, $tagName !== null ? [$tagName] : null, function ($element) use ($check, $add) {
if ($check($element)) {
if ($add($element)) {
return true;
}
}
});
}
};
// todo optimize
foreach ($matches as $match) {
if ($mode === 'validate') {
if ($run($match)) {
return true;
}
} else {
$run($match);
}
}
if ($mode === 'validate') {
return false;
}
};
// tagname#id or #id
$simpleSelectors['(?:[a-zA-Z0-9\-]*)#(?:[a-zA-Z0-9\-\_]+?)'] = function (string $mode, array $matches, \DOMNode $context, callable $add = null) use ($getElementById) {
$run = function ($match) use ($mode, $context, $add, $getElementById) {
$tagName = strlen($match[1]) > 0 ? strtolower($match[1]) : null;
$id = $match[2];
if ($mode === 'validate') {
return ($tagName === null ? true : $context->tagName === $tagName) && $context->getAttribute('id') === $id;
} else {
$element = $getElementById($context, $id, $tagName);
if ($element) {
$add($element);
}
}
};
// todo optimize
foreach ($matches as $match) {
if ($mode === 'validate') {
if ($run($match)) {
return true;
}
} else {
$run($match);
}
}
if ($mode === 'validate') {
return false;
}
};
// tagname.classname, .classname, tagname.classname.classname2, .classname.classname2
$simpleSelectors['(?:[a-zA-Z0-9\-]*)\.(?:[a-zA-Z0-9\-\_\.]+?)'] = function (string $mode, array $matches, \DOMNode $context, callable $add = null) use ($walkChildren) {
$rawData = []; // Array containing [tag, classnames]
$tagNames = [];
foreach ($matches as $match) {
$tagName = strlen($match[1]) > 0 ? $match[1] : null;
$classes = explode('.', $match[2]);
if (empty($classes)) {
continue;
}
$rawData[] = [$tagName, $classes];
if ($tagName !== null) {
$tagNames[] = $tagName;
}
}
$check = function ($element) use ($rawData) {
if ($element->attributes->length > 0) {
$classAttribute = ' ' . $element->getAttribute('class') . ' ';
$tagName = $element->tagName;
foreach ($rawData as $rawMatch) {
if ($rawMatch[0] !== null && $tagName !== $rawMatch[0]) {
continue;
}
$allClassesFound = true;
foreach ($rawMatch[1] as $class) {
if (strpos($classAttribute, ' ' . $class . ' ') === false) {
$allClassesFound = false;
break;
}
}
if ($allClassesFound) {
return true;
}
}
}
return false;
};
if ($mode === 'validate') {
return $check($context);
}
$walkChildren($context, $tagNames, function ($element) use ($check, $add) {
if ($check($element)) {
if ($add($element)) {
return true;
}
}
});
};
$isMatchingElement = function (\DOMNode $context, string $selector) use ($simpleSelectors) {
foreach ($simpleSelectors as $simpleSelector => $callback) {
$match = null;
if (preg_match('/^' . (str_replace('?:', '', $simpleSelector)) . '$/', $selector, $match) === 1) {
return call_user_func($callback, 'validate', [$match], $context);
}
}
};
$complexSelectors = [];
$getMatchingElements = function (\DOMNode $context, string $selector, $preferredLimit = null) use (&$simpleSelectors, &$complexSelectors) {
$processSelector = function (string $mode, string $selector, $operator = null) use (&$processSelector, $simpleSelectors, $complexSelectors, $context, $preferredLimit) {
$supportedSimpleSelectors = array_keys($simpleSelectors);
$supportedSimpleSelectorsExpression = '(?:(?:' . implode(')|(?:', $supportedSimpleSelectors) . '))';
$supportedSelectors = $supportedSimpleSelectors;
$supportedComplexOperators = array_keys($complexSelectors);
if ($operator === null) {
$operator = ',';
foreach ($supportedComplexOperators as $complexOperator) {
array_unshift($supportedSelectors, '(?:(?:(?:' . $supportedSimpleSelectorsExpression . '\s*\\' . $complexOperator . '\s*))+' . $supportedSimpleSelectorsExpression . ')');
}
}
$supportedSelectorsExpression = '(?:(?:' . implode(')|(?:', $supportedSelectors) . '))';
$vallidationExpression = '/^(?:(?:' . $supportedSelectorsExpression . '\s*\\' . $operator . '\s*))*' . $supportedSelectorsExpression . '$/';
if (preg_match($vallidationExpression, $selector) !== 1) {
return false;
}
$selector .= $operator; // append the seprator at the back for easier matching below
$result = [];
if ($mode === 'execute') {
$add = function ($element) use ($preferredLimit, &$result) {
$found = false;
foreach ($result as $addedElement) {
if ($addedElement === $element) {
$found = true;
break;
}
}
if (!$found) {
$result[] = $element;
if ($preferredLimit !== null && sizeof($result) >= $preferredLimit) {
return true;
}
}
return false;
};
}
$selectorsToCall = [];
$addSelectorToCall = function ($type, $selector, $argument) use (&$selectorsToCall) {
$previousIndex = sizeof($selectorsToCall) - 1;
// todo optimize complex too
if ($type === 1 && isset($selectorsToCall[$previousIndex]) && $selectorsToCall[$previousIndex][0] === $type && $selectorsToCall[$previousIndex][1] === $selector) {
$selectorsToCall[$previousIndex][2][] = $argument;
} else {
$selectorsToCall[] = [$type, $selector, [$argument]];
}
};
for ($i = 0; $i < 100000; $i++) {
$matches = null;
preg_match('/^(?<subselector>' . $supportedSelectorsExpression . ')\s*\\' . $operator . '\s*/', $selector, $matches); // getting the next subselector
if (isset($matches['subselector'])) {
$subSelector = $matches['subselector'];
$selectorFound = false;
foreach ($simpleSelectors as $simpleSelector => $callback) {
$match = null;
if (preg_match('/^' . (str_replace('?:', '', $simpleSelector)) . '$/', $subSelector, $match) === 1) { // if simple selector
if ($mode === 'parse') {
$result[] = $match[0];
} else {
$addSelectorToCall(1, $simpleSelector, $match);
//call_user_func($callback, 'execute', $match, $context, $add);
}
$selectorFound = true;
break;
}
}
if (!$selectorFound) {
foreach ($complexSelectors as $complexOperator => $callback) {
$subSelectorParts = $processSelector('parse', $subSelector, $complexOperator);
if ($subSelectorParts !== false) {
$addSelectorToCall(2, $complexOperator, $subSelectorParts);
//call_user_func($callback, $subSelectorParts, $context, $add);
$selectorFound = true;
break;
}
}
}
if (!$selectorFound) {
throw new \Exception('Internal error for selector "' . $selector . '"!');
}
$selector = substr($selector, strlen($matches[0])); // remove the matched subselector and continue parsing
if (strlen($selector) === 0) {
break;
}
}
}
foreach ($selectorsToCall as $selectorToCall) {
if ($selectorToCall[0] === 1) { // is simple selector
call_user_func($simpleSelectors[$selectorToCall[1]], 'execute', $selectorToCall[2], $context, $add);
} else { // is complex selector
call_user_func($complexSelectors[$selectorToCall[1]], $selectorToCall[2][0], $context, $add); // todo optimize and send all arguments
}
}
return $result;
};
return $processSelector('execute', $selector);
};
// div p (space between) - all <p> elements inside <div> elements
$complexSelectors[' '] = function (array $parts, \DOMNode $context, callable $add = null) use (&$getMatchingElements) {
$elements = null;
foreach ($parts as $part) {
if ($elements === null) {
$elements = $getMatchingElements($context, $part);
} else {
$temp = [];
foreach ($elements as $element) {
$temp = array_merge($temp, $getMatchingElements($element, $part));
}
$elements = $temp;
}
}
foreach ($elements as $element) {
$add($element);
}
};
// div > p - all <p> elements where the parent is a <div> element
$complexSelectors['>'] = function (array $parts, \DOMNode $context, callable $add = null) use (&$getMatchingElements, &$isMatchingElement) {
$elements = null;
foreach ($parts as $part) {
if ($elements === null) {
$elements = $getMatchingElements($context, $part);
} else {
$temp = [];
foreach ($elements as $element) {
foreach ($element->childNodes as $child) {
if ($child instanceof \DOMElement && $isMatchingElement($child, $part)) {
$temp[] = $child;
}
}
}
$elements = $temp;
}
}
foreach ($elements as $element) {
$add($element);
}
};
// div + p - all <p> elements that are placed immediately after <div> elements
$complexSelectors['+'] = function (array $parts, \DOMNode $context, callable $add = null) use (&$getMatchingElements, &$isMatchingElement) {
$elements = null;
foreach ($parts as $part) {
if ($elements === null) {
$elements = $getMatchingElements($context, $part);
} else {
$temp = [];
foreach ($elements as $element) {
if ($element->nextSibling !== null && $isMatchingElement($element->nextSibling, $part)) {
$temp[] = $element->nextSibling;
}
}
$elements = $temp;
}
}
foreach ($elements as $element) {
$add($element);
}
};
// p ~ ul - all <ul> elements that are preceded by a <p> element
$complexSelectors['~'] = function (array $parts, \DOMNode $context, callable $add = null) use (&$getMatchingElements, &$isMatchingElement) {
$elements = null;
foreach ($parts as $part) {
if ($elements === null) {
$elements = $getMatchingElements($context, $part);
} else {
$temp = [];
foreach ($elements as $element) {
$nextSibling = $element->nextSibling;
while ($nextSibling !== null) {
if ($isMatchingElement($nextSibling, $part)) {
$temp[] = $nextSibling;
}
$nextSibling = $nextSibling->nextSibling;
}
}
$elements = $temp;
}
}
foreach ($elements as $element) {
$add($element);
}
};
$result = $getMatchingElements($this, $selector, $preferredLimit);
if ($result === false) {
throw new \InvalidArgumentException('Unsupported selector (' . $selector . ')');
}
return new \IvoPetkov\HTML5DOMNodeList($result);
}
}

View File

@ -0,0 +1,240 @@
<?php
/*
* HTML5 DOMDocument PHP library (extends DOMDocument)
* https://github.com/ivopetkov/html5-dom-document-php
* Copyright (c) Ivo Petkov
* Free to use under the MIT license.
*/
namespace IvoPetkov;
use IvoPetkov\HTML5DOMDocument\Internal\QuerySelectors;
/**
* Represents a live (can be manipulated) representation of an element in a HTML5 document.
*
* @property string $innerHTML The HTML code inside the element.
* @property string $outerHTML The HTML code for the element including the code inside.
* @property \IvoPetkov\HTML5DOMTokenList $classList A collection of the class attributes of the element.
*/
class HTML5DOMElement extends \DOMElement
{
use QuerySelectors;
/**
*
* @var array
*/
static private $foundEntitiesCache = [[], []];
/**
*
* @var array
*/
static private $newObjectsCache = [];
/*
*
* @var HTML5DOMTokenList
*/
private $classList = null;
/**
* Returns the value for the property specified.
*
* @param string $name
* @return string
* @throws \Exception
*/
public function __get(string $name)
{
if ($name === 'innerHTML') {
if ($this->firstChild === null) {
return '';
}
$html = $this->ownerDocument->saveHTML($this);
$nodeName = $this->nodeName;
return preg_replace('@^<' . $nodeName . '[^>]*>|</' . $nodeName . '>$@', '', $html);
} elseif ($name === 'outerHTML') {
if ($this->firstChild === null) {
$nodeName = $this->nodeName;
$attributes = $this->getAttributes();
$result = '<' . $nodeName . '';
foreach ($attributes as $name => $value) {
$result .= ' ' . $name . '="' . htmlentities($value) . '"';
}
if (array_search($nodeName, ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']) === false) {
$result .= '></' . $nodeName . '>';
} else {
$result .= '/>';
}
return $result;
}
return $this->ownerDocument->saveHTML($this);
} elseif ($name === 'classList') {
if ($this->classList === null) {
$this->classList = new HTML5DOMTokenList($this, 'class');
}
return $this->classList;
}
throw new \Exception('Undefined property: HTML5DOMElement::$' . $name);
}
/**
* Sets the value for the property specified.
*
* @param string $name
* @param string $value
* @throws \Exception
*/
public function __set(string $name, $value)
{
if ($name === 'innerHTML') {
while ($this->hasChildNodes()) {
$this->removeChild($this->firstChild);
}
if (!isset(self::$newObjectsCache['html5domdocument'])) {
self::$newObjectsCache['html5domdocument'] = new \IvoPetkov\HTML5DOMDocument();
}
$tmpDoc = clone (self::$newObjectsCache['html5domdocument']);
$tmpDoc->loadHTML('<body>' . $value . '</body>', HTML5DOMDocument::ALLOW_DUPLICATE_IDS);
foreach ($tmpDoc->getElementsByTagName('body')->item(0)->childNodes as $node) {
$node = $this->ownerDocument->importNode($node, true);
$this->appendChild($node);
}
return;
} elseif ($name === 'outerHTML') {
if (!isset(self::$newObjectsCache['html5domdocument'])) {
self::$newObjectsCache['html5domdocument'] = new \IvoPetkov\HTML5DOMDocument();
}
$tmpDoc = clone (self::$newObjectsCache['html5domdocument']);
$tmpDoc->loadHTML('<body>' . $value . '</body>', HTML5DOMDocument::ALLOW_DUPLICATE_IDS);
foreach ($tmpDoc->getElementsByTagName('body')->item(0)->childNodes as $node) {
$node = $this->ownerDocument->importNode($node, true);
$this->parentNode->insertBefore($node, $this);
}
$this->parentNode->removeChild($this);
return;
} elseif ($name === 'classList') {
$this->setAttribute('class', $value);
return;
}
throw new \Exception('Undefined property: HTML5DOMElement::$' . $name);
}
/**
* Updates the result value before returning it.
*
* @param string $value
* @return string The updated value
*/
private function updateResult(string $value): string
{
$value = str_replace(self::$foundEntitiesCache[0], self::$foundEntitiesCache[1], $value);
if (strstr($value, 'html5-dom-document-internal-entity') !== false) {
$search = [];
$replace = [];
$matches = [];
preg_match_all('/html5-dom-document-internal-entity([12])-(.*?)-end/', $value, $matches);
$matches[0] = array_unique($matches[0]);
foreach ($matches[0] as $i => $match) {
$search[] = $match;
$replace[] = html_entity_decode(($matches[1][$i] === '1' ? '&' : '&#') . $matches[2][$i] . ';');
}
$value = str_replace($search, $replace, $value);
self::$foundEntitiesCache[0] = array_merge(self::$foundEntitiesCache[0], $search);
self::$foundEntitiesCache[1] = array_merge(self::$foundEntitiesCache[1], $replace);
unset($search);
unset($replace);
unset($matches);
}
return $value;
}
/**
* Returns the updated nodeValue Property
*
* @return string The updated $nodeValue
*/
public function getNodeValue(): string
{
return $this->updateResult($this->nodeValue);
}
/**
* Returns the updated $textContent Property
*
* @return string The updated $textContent
*/
public function getTextContent(): string
{
return $this->updateResult($this->textContent);
}
/**
* Returns the value for the attribute name specified.
*
* @param string $name The attribute name.
* @return string The attribute value.
* @throws \InvalidArgumentException
*/
public function getAttribute($name): string
{
if ($this->attributes->length === 0) { // Performance optimization
return '';
}
$value = parent::getAttribute($name);
return $value !== '' ? (strstr($value, 'html5-dom-document-internal-entity') !== false ? $this->updateResult($value) : $value) : '';
}
/**
* Returns an array containing all attributes.
*
* @return array An associative array containing all attributes.
*/
public function getAttributes(): array
{
$attributes = [];
foreach ($this->attributes as $attributeName => $attribute) {
$value = $attribute->value;
$attributes[$attributeName] = $value !== '' ? (strstr($value, 'html5-dom-document-internal-entity') !== false ? $this->updateResult($value) : $value) : '';
}
return $attributes;
}
/**
* Returns the element outerHTML.
*
* @return string The element outerHTML.
*/
public function __toString(): string
{
return $this->outerHTML;
}
/**
* Returns the first child element matching the selector.
*
* @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname.classname.classname2, .classname.classname2, tagname[attribute-selector], [attribute-selector], "div, p", div p, div > p, div + p and p ~ ul.
* @return HTML5DOMElement|null The result DOMElement or null if not found.
* @throws \InvalidArgumentException
*/
public function querySelector(string $selector)
{
return $this->internalQuerySelector($selector);
}
/**
* Returns a list of children elements matching the selector.
*
* @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname.classname.classname2, .classname.classname2, tagname[attribute-selector], [attribute-selector], "div, p", div p, div > p, div + p and p ~ ul.
* @return HTML5DOMNodeList Returns a list of DOMElements matching the criteria.
* @throws \InvalidArgumentException
*/
public function querySelectorAll(string $selector)
{
return $this->internalQuerySelectorAll($selector);
}
}

View File

@ -0,0 +1,45 @@
<?php
/*
* HTML5 DOMDocument PHP library (extends DOMDocument)
* https://github.com/ivopetkov/html5-dom-document-php
* Copyright (c) Ivo Petkov
* Free to use under the MIT license.
*/
namespace IvoPetkov;
/**
* Represents a list of DOM nodes.
*
* @property-read int $length The list items count
*/
class HTML5DOMNodeList extends \ArrayObject
{
/**
* Returns the item at the specified index.
*
* @param int $index The item index.
* @return \IvoPetkov\HTML5DOMElement|null The item at the specified index or null if not existent.
*/
public function item(int $index)
{
return $this->offsetExists($index) ? $this->offsetGet($index) : null;
}
/**
* Returns the value for the property specified.
*
* @param string $name The name of the property.
* @return mixed
* @throws \Exception
*/
public function __get(string $name)
{
if ($name === 'length') {
return sizeof($this);
}
throw new \Exception('Undefined property: \IvoPetkov\HTML5DOMNodeList::$' . $name);
}
}

View File

@ -0,0 +1,266 @@
<?php
/*
* HTML5 DOMDocument PHP library (extends DOMDocument)
* https://github.com/ivopetkov/html5-dom-document-php
* Copyright (c) Ivo Petkov
* Free to use under the MIT license.
*/
namespace IvoPetkov;
use ArrayIterator;
use DOMElement;
/**
* Represents a set of space-separated tokens of an element attribute.
*
* @property-read int $length The number of tokens.
* @property-read string $value A space-separated list of the tokens.
*/
class HTML5DOMTokenList
{
/**
* @var string
*/
private $attributeName;
/**
* @var DOMElement
*/
private $element;
/**
* @var string[]
*/
private $tokens;
/**
* @var string
*/
private $previousValue;
/**
* Creates a list of space-separated tokens based on the attribute value of an element.
*
* @param DOMElement $element The DOM element.
* @param string $attributeName The name of the attribute.
*/
public function __construct(DOMElement $element, string $attributeName)
{
$this->element = $element;
$this->attributeName = $attributeName;
$this->previousValue = null;
$this->tokenize();
}
/**
* Adds the given tokens to the list.
*
* @param string[] $tokens The tokens you want to add to the list.
* @return void
*/
public function add(string ...$tokens)
{
if (count($tokens) === 0) {
return;
}
foreach ($tokens as $t) {
if (in_array($t, $this->tokens)) {
continue;
}
$this->tokens[] = $t;
}
$this->setAttributeValue();
}
/**
* Removes the specified tokens from the list. If the string does not exist in the list, no error is thrown.
*
* @param string[] $tokens The token you want to remove from the list.
* @return void
*/
public function remove(string ...$tokens)
{
if (count($tokens) === 0) {
return;
}
if (count($this->tokens) === 0) {
return;
}
foreach ($tokens as $t) {
$i = array_search($t, $this->tokens);
if ($i === false) {
continue;
}
array_splice($this->tokens, $i, 1);
}
$this->setAttributeValue();
}
/**
* Returns an item in the list by its index (returns null if the number is greater than or equal to the length of the list).
*
* @param int $index The zero-based index of the item you want to return.
* @return null|string
*/
public function item(int $index)
{
$this->tokenize();
if ($index >= count($this->tokens)) {
return null;
}
return $this->tokens[$index];
}
/**
* Removes a given token from the list and returns false. If token doesn't exist it's added and the function returns true.
*
* @param string $token The token you want to toggle.
* @param bool $force A Boolean that, if included, turns the toggle into a one way-only operation. If set to false, the token will only be removed but not added again. If set to true, the token will only be added but not removed again.
* @return bool false if the token is not in the list after the call, or true if the token is in the list after the call.
*/
public function toggle(string $token, bool $force = null): bool
{
$this->tokenize();
$isThereAfter = false;
$i = array_search($token, $this->tokens);
if (is_null($force)) {
if ($i === false) {
$this->tokens[] = $token;
$isThereAfter = true;
} else {
array_splice($this->tokens, $i, 1);
}
} else {
if ($force) {
if ($i === false) {
$this->tokens[] = $token;
}
$isThereAfter = true;
} else {
if ($i !== false) {
array_splice($this->tokens, $i, 1);
}
}
}
$this->setAttributeValue();
return $isThereAfter;
}
/**
* Returns true if the list contains the given token, otherwise false.
*
* @param string $token The token you want to check for the existence of in the list.
* @return bool true if the list contains the given token, otherwise false.
*/
public function contains(string $token): bool
{
$this->tokenize();
return in_array($token, $this->tokens);
}
/**
* Replaces an existing token with a new token.
*
* @param string $old The token you want to replace.
* @param string $new The token you want to replace $old with.
* @return void
*/
public function replace(string $old, string $new)
{
if ($old === $new) {
return;
}
$this->tokenize();
$i = array_search($old, $this->tokens);
if ($i !== false) {
$j = array_search($new, $this->tokens);
if ($j === false) {
$this->tokens[$i] = $new;
} else {
array_splice($this->tokens, $i, 1);
}
$this->setAttributeValue();
}
}
/**
*
* @return string
*/
public function __toString(): string
{
$this->tokenize();
return implode(' ', $this->tokens);
}
/**
* Returns an iterator allowing you to go through all tokens contained in the list.
*
* @return ArrayIterator
*/
public function entries(): ArrayIterator
{
$this->tokenize();
return new ArrayIterator($this->tokens);
}
/**
* Returns the value for the property specified
*
* @param string $name The name of the property
* @return string The value of the property specified
* @throws \Exception
*/
public function __get(string $name)
{
if ($name === 'length') {
$this->tokenize();
return count($this->tokens);
} elseif ($name === 'value') {
return $this->__toString();
}
throw new \Exception('Undefined property: HTML5DOMTokenList::$' . $name);
}
/**
*
* @return void
*/
private function tokenize()
{
$current = $this->element->getAttribute($this->attributeName);
if ($this->previousValue === $current) {
return;
}
$this->previousValue = $current;
$tokens = explode(' ', $current);
$finals = [];
foreach ($tokens as $token) {
if ($token === '') {
continue;
}
if (in_array($token, $finals)) {
continue;
}
$finals[] = $token;
}
$this->tokens = $finals;
}
/**
*
* @return void
*/
private function setAttributeValue()
{
$value = implode(' ', $this->tokens);
if ($this->previousValue === $value) {
return;
}
$this->previousValue = $value;
$this->element->setAttribute($this->attributeName, $value);
}
}

View File

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

View File

@ -0,0 +1,19 @@
language: php
php:
- 5.3
- 5.4
- 5.5
- 5.6
- hhvm
matrix:
allow_failures:
- php: 5.6
- php: hhvm
before_script:
- if [[ "$TRAVIS_PHP_VERSION" == "5.4" ]]; then sh -c "php -S 127.0.0.1:8000 -t tests/PHPCurlClass/ &"; fi
- if [[ "$TRAVIS_PHP_VERSION" == "5.5" ]]; then sh -c "php -S 127.0.0.1:8000 -t tests/PHPCurlClass/ &"; fi
- if [[ "$TRAVIS_PHP_VERSION" == "5.6" ]]; then sh -c "php -S 127.0.0.1:8000 -t tests/PHPCurlClass/ &"; fi
- if [[ "$TRAVIS_PHP_VERSION" == "hhvm" ]]; then sh -c "cd tests && hhvm --mode server --port 8000 --config PHPCurlClass/server.hdf &"; fi
script:
- php -l src/*
- if [[ "$TRAVIS_PHP_VERSION" != "5.3" ]]; then sh -c "cd tests && phpunit --configuration phpunit.xml"; fi

View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@ -0,0 +1,136 @@
# php-curl-class
[![Build Status](https://travis-ci.org/php-curl-class/php-curl-class.png?branch=master)](https://travis-ci.org/php-curl-class/php-curl-class)
PHP Curl Class is an object-oriented wrapper of the PHP cURL extension.
### Composer
$ composer require php-curl-class/php-curl-class
### Quick Start and Examples
```php
require 'Curl.class.php';
$curl = new Curl();
$curl->get('http://www.example.com/');
```
```php
$curl = new Curl();
$curl->get('http://www.example.com/search', array(
'q' => 'keyword',
));
```
```php
$curl = new Curl();
$curl->post('http://www.example.com/login/', array(
'username' => 'myusername',
'password' => 'mypassword',
));
```
```php
$curl = new Curl();
$curl->setBasicAuthentication('username', 'password');
$curl->setUserAgent('');
$curl->setReferrer('');
$curl->setHeader('X-Requested-With', 'XMLHttpRequest');
$curl->setCookie('key', 'value');
$curl->get('http://www.example.com/');
if ($curl->error) {
echo $curl->error_code;
}
else {
echo $curl->response;
}
var_dump($curl->request_headers);
var_dump($curl->response_headers);
```
```php
$curl = new Curl();
$curl->setOpt(CURLOPT_SSL_VERIFYPEER, false);
$curl->get('https://encrypted.example.com/');
```
```php
$curl = new Curl();
$curl->put('http://api.example.com/user/', array(
'first_name' => 'Zach',
'last_name' => 'Borboa',
));
```
```php
$curl = new Curl();
$curl->patch('http://api.example.com/profile/', array(
'image' => '@path/to/file.jpg',
));
```
```php
$curl = new Curl();
$curl->delete('http://api.example.com/user/', array(
'id' => '1234',
));
```
```php
// Enable gzip compression.
$curl = new Curl();
$curl->setOpt(CURLOPT_ENCODING , 'gzip');
$curl->get('https://www.example.com/image.png');
```
```php
// Case-insensitive access to headers.
$curl = new Curl();
$curl->get('https://www.example.com/image.png');
echo $curl->response_headers['Content-Type'] . "\n"; // image/png
echo $curl->response_headers['CoNTeNT-TyPE'] . "\n"; // image/png
```
```php
$curl->close();
```
```php
// Example access to curl object.
curl_set_opt($curl->curl, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1');
curl_close($curl->curl);
```
```php
// Requests in parallel with callback functions.
$curl = new Curl();
$curl->setOpt(CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1');
$curl->success(function($instance) {
echo 'call was successful. response was' . "\n";
echo $instance->response . "\n";
});
$curl->error(function($instance) {
echo 'call was unsuccessful.' . "\n";
echo 'error code:' . $instance->error_code . "\n";
echo 'error message:' . $instance->error_message . "\n";
});
$curl->complete(function($instance) {
echo 'call completed' . "\n";
});
$curl->get(array(
'https://duckduckgo.com/',
'https://search.yahoo.com/search',
'https://www.bing.com/search',
'http://www.dogpile.com/search/web',
'https://www.google.com/search',
'https://www.wolframalpha.com/input/',
), array(
'q' => 'hello world',
));
```

View File

@ -0,0 +1,7 @@
{
"name": "php-curl-class/php-curl-class",
"description": "PHP Curl Class is an object-oriented wrapper of the PHP cURL extension.",
"autoload": {
"classmap": ["src/"]
}
}

View File

@ -0,0 +1,22 @@
<?php
require '../src/Curl.class.php';
define('API_KEY', '');
define('API_SECRET', '');
$url = 'https://coinbase.com/api/v1/account/balance';
$nonce = (int)(microtime(true) * 1e6);
$message = $nonce . $url;
$signature = hash_hmac('sha256', $message, API_SECRET);
$curl = new Curl();
$curl->setHeader('ACCESS_KEY', API_KEY);
$curl->setHeader('ACCESS_SIGNATURE', $signature);
$curl->setHeader('ACCESS_NONCE', $nonce);
$curl->get($url);
echo
'My current account balance at Coinbase is ' .
$curl->response->amount . ' ' . $curl->response->currency . '.' . "\n";

View File

@ -0,0 +1,10 @@
<?php
require '../src/Curl.class.php';
$curl = new Curl();
$curl->get('https://coinbase.com/api/v1/prices/spot_rate');
echo
'The current price of bitcoin at Coinbase is ' .
'$' . $curl->response->amount . ' ' . $curl->response->currency . '.' . "\n";

View File

@ -0,0 +1,17 @@
<?php
require '../src/Curl.class.php';
define('INSTAGRAM_CLIENT_ID', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
$curl = new Curl();
$curl->get('https://api.instagram.com/v1/media/search', array(
'client_id' => INSTAGRAM_CLIENT_ID,
'lat' => '37.8296',
'lng' => '-122.4832',
));
foreach ($curl->response->data as $media) {
$image = $media->images->low_resolution;
echo '<img alt="" src="' . $image->url . '" width="' . $image->width . '" height="' . $image->height . '" />';
}

View File

@ -0,0 +1,14 @@
<?php
require '../src/Curl.class.php';
// curl -X PUT -d "id=1&first_name=Zach&last_name=Borboa" "http://httpbin.org/put"
$curl = new Curl();
$curl->put('http://httpbin.org/put', array(
'id' => 1,
'first_name' => 'Zach',
'last_name' => 'Borboa',
));
echo 'Data server received via PUT:' . "\n";
var_dump($curl->response->form);

View File

@ -0,0 +1,35 @@
<?php
require '../src/Curl.class.php';
define('API_KEY', 'XXXXXXXXXXXXXXXXXXXXXXXXX');
define('API_SECRET', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
define('OAUTH_ACCESS_TOKEN', 'XXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
define('OAUTH_TOKEN_SECRET', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
$status = 'I love php curl class. https://github.com/php-curl-class/php-curl-class';
$oauth_data = array(
'oauth_consumer_key' => API_KEY,
'oauth_nonce' => md5(microtime() . mt_rand()),
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_token' => OAUTH_ACCESS_TOKEN,
'oauth_version' => '1.0',
'status' => $status,
);
$url = 'https://api.twitter.com/1.1/statuses/update.json';
$request = implode('&', array(
'POST',
rawurlencode($url),
rawurlencode(http_build_query($oauth_data, '', '&', PHP_QUERY_RFC3986)),
));
$key = implode('&', array(API_SECRET, OAUTH_TOKEN_SECRET));
$oauth_data['oauth_signature'] = base64_encode(hash_hmac('sha1', $request, $key, true));
$data = http_build_query($oauth_data, '', '&');
$curl = new Curl();
$curl->post($url, $data);
echo 'Posted "' . $curl->response->text . '" at ' . $curl->response->created_at . '.' . "\n";

View File

@ -0,0 +1,496 @@
<?php
class Curl
{
const USER_AGENT = 'PHP-Curl-Class/2.0 (+https://github.com/php-curl-class/php-curl-class)';
private $cookies = array();
private $headers = array();
private $options = array();
private $multi_parent = false;
private $multi_child = false;
private $before_send_function = null;
private $success_function = null;
private $error_function = null;
private $complete_function = null;
public $curl;
public $curls;
public $error = false;
public $error_code = 0;
public $error_message = null;
public $curl_error = false;
public $curl_error_code = 0;
public $curl_error_message = null;
public $http_error = false;
public $http_status_code = 0;
public $http_error_message = null;
public $request_headers = null;
public $response_headers = null;
public $response = null;
public function __construct()
{
if (!extension_loaded('curl')) {
throw new \ErrorException('cURL library is not loaded');
}
$this->curl = curl_init();
$this->setUserAgent(self::USER_AGENT);
$this->setOpt(CURLINFO_HEADER_OUT, true);
$this->setOpt(CURLOPT_HEADER, true);
$this->setOpt(CURLOPT_RETURNTRANSFER, true);
}
public function get($url_mixed, $data = array())
{
if (is_array($url_mixed)) {
$curl_multi = curl_multi_init();
$this->multi_parent = true;
$this->curls = array();
foreach ($url_mixed as $url) {
$curl = new Curl();
$curl->multi_child = true;
$curl->setOpt(CURLOPT_URL, $this->buildURL($url, $data), $curl->curl);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'GET');
$curl->setOpt(CURLOPT_HTTPGET, true);
$this->call($this->before_send_function, $curl);
$this->curls[] = $curl;
$curlm_error_code = curl_multi_add_handle($curl_multi, $curl->curl);
if (!($curlm_error_code === CURLM_OK)) {
throw new \ErrorException('cURL multi add handle error: ' . curl_multi_strerror($curlm_error_code));
}
}
foreach ($this->curls as $ch) {
foreach ($this->options as $key => $value) {
$ch->setOpt($key, $value);
}
}
do {
$status = curl_multi_exec($curl_multi, $active);
} while ($status === CURLM_CALL_MULTI_PERFORM || $active);
foreach ($this->curls as $ch) {
$this->exec($ch);
}
} else {
$this->setopt(CURLOPT_URL, $this->buildURL($url_mixed, $data));
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'GET');
$this->setopt(CURLOPT_HTTPGET, true);
return $this->exec();
}
}
public function post($url, $data = array())
{
if (is_array($data) && empty($data)) {
$this->setHeader('Content-Length');
}
$this->setOpt(CURLOPT_URL, $this->buildURL($url));
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
$this->setOpt(CURLOPT_POST, true);
$this->setOpt(CURLOPT_POSTFIELDS, $this->postfields($data));
return $this->exec();
}
public function put($url, $data = array())
{
$this->setOpt(CURLOPT_URL, $url);
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'PUT');
$put_data = http_build_query($data);
if (empty($this->options[CURLOPT_INFILE]) && empty($this->options[CURLOPT_INFILESIZE])) {
$this->setHeader('Content-Length', strlen($put_data));
}
$this->setOpt(CURLOPT_POSTFIELDS, $put_data);
return $this->exec();
}
public function patch($url, $data = array())
{
$this->setHeader('Content-Length');
$this->setOpt(CURLOPT_URL, $this->buildURL($url));
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'PATCH');
$this->setOpt(CURLOPT_POSTFIELDS, $data);
return $this->exec();
}
public function delete($url, $data = array())
{
$this->setHeader('Content-Length');
$this->setOpt(CURLOPT_URL, $this->buildURL($url, $data));
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'DELETE');
return $this->exec();
}
public function head($url, $data = array())
{
$this->setOpt(CURLOPT_URL, $this->buildURL($url, $data));
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'HEAD');
$this->setOpt(CURLOPT_NOBODY, true);
return $this->exec();
}
public function options($url, $data = array())
{
$this->setHeader('Content-Length');
$this->setOpt(CURLOPT_URL, $this->buildURL($url, $data));
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'OPTIONS');
return $this->exec();
}
public function setBasicAuthentication($username, $password)
{
$this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
$this->setOpt(CURLOPT_USERPWD, $username . ':' . $password);
}
public function setHeader($key, $value = '')
{
$this->headers[$key] = $key . ': ' . $value;
$this->setOpt(CURLOPT_HTTPHEADER, array_values($this->headers));
}
public function setUserAgent($user_agent)
{
$this->setOpt(CURLOPT_USERAGENT, $user_agent);
}
public function setReferrer($referrer)
{
$this->setOpt(CURLOPT_REFERER, $referrer);
}
public function setCookie($key, $value)
{
$this->cookies[$key] = $value;
$this->setOpt(CURLOPT_COOKIE, http_build_query($this->cookies, '', '; '));
}
public function setCookieFile($cookie_file)
{
$this->setOpt(CURLOPT_COOKIEFILE, $cookie_file);
}
public function setCookieJar($cookie_jar)
{
$this->setOpt(CURLOPT_COOKIEJAR, $cookie_jar);
}
public function setOpt($option, $value, $_ch = null)
{
$ch = is_null($_ch) ? $this->curl : $_ch;
$required_options = array(
CURLINFO_HEADER_OUT => 'CURLINFO_HEADER_OUT',
CURLOPT_HEADER => 'CURLOPT_HEADER',
CURLOPT_RETURNTRANSFER => 'CURLOPT_RETURNTRANSFER',
);
if (in_array($option, array_keys($required_options), true) && !($value === true)) {
trigger_error($required_options[$option] . ' is a required option', E_USER_WARNING);
}
$this->options[$option] = $value;
return curl_setopt($ch, $option, $value);
}
public function verbose($on = true)
{
$this->setOpt(CURLOPT_VERBOSE, $on);
}
public function close()
{
if ($this->multi_parent) {
foreach ($this->curls as $curl) {
$curl->close();
}
}
if (is_resource($this->curl)) {
curl_close($this->curl);
}
}
public function beforeSend($function)
{
$this->before_send_function = $function;
}
public function success($callback)
{
$this->success_function = $callback;
}
public function error($callback)
{
$this->error_function = $callback;
}
public function complete($callback)
{
$this->complete_function = $callback;
}
private function buildURL($url, $data = array())
{
return $url . (empty($data) ? '' : '?' . http_build_query($data));
}
private function parseHeaders($raw_headers)
{
$raw_headers = preg_split('/\r\n/', $raw_headers, null, PREG_SPLIT_NO_EMPTY);
$http_headers = new CaseInsensitiveArray();
for ($i = 1; $i < count($raw_headers); $i++) {
list($key, $value) = explode(':', $raw_headers[$i], 2);
$key = trim($key);
$value = trim($value);
// Use isset() as array_key_exists() and ArrayAccess are not compatible.
if (isset($http_headers[$key])) {
$http_headers[$key] .= ',' . $value;
} else {
$http_headers[$key] = $value;
}
}
return array(isset($raw_headers['0']) ? $raw_headers['0'] : '', $http_headers);
}
private function parseRequestHeaders($raw_headers)
{
$request_headers = new CaseInsensitiveArray();
list($first_line, $headers) = $this->parseHeaders($raw_headers);
$request_headers['Request-Line'] = $first_line;
foreach ($headers as $key => $value) {
$request_headers[$key] = $value;
}
return $request_headers;
}
private function parseResponseHeaders($raw_headers)
{
$response_headers = new CaseInsensitiveArray();
list($first_line, $headers) = $this->parseHeaders($raw_headers);
$response_headers['Status-Line'] = $first_line;
foreach ($headers as $key => $value) {
$response_headers[$key] = $value;
}
return $response_headers;
}
private function postfields($data)
{
if (is_array($data)) {
if (is_array_multidim($data)) {
$data = http_build_multi_query($data);
} else {
foreach ($data as $key => $value) {
// Fix "Notice: Array to string conversion" when $value in
// curl_setopt($ch, CURLOPT_POSTFIELDS, $value) is an array
// that contains an empty array.
if (is_array($value) && empty($value)) {
$data[$key] = '';
// Fix "curl_setopt(): The usage of the @filename API for
// file uploading is deprecated. Please use the CURLFile
// class instead".
} elseif (is_string($value) && strpos($value, '@') === 0) {
if (class_exists('CURLFile')) {
$data[$key] = new CURLFile(substr($value, 1));
}
}
}
}
}
return $data;
}
protected function exec($_ch = null)
{
$ch = is_null($_ch) ? $this : $_ch;
if ($ch->multi_child) {
$ch->response = curl_multi_getcontent($ch->curl);
} else {
$ch->response = curl_exec($ch->curl);
}
$ch->curl_error_code = curl_errno($ch->curl);
$ch->curl_error_message = curl_error($ch->curl);
$ch->curl_error = !($ch->curl_error_code === 0);
$ch->http_status_code = curl_getinfo($ch->curl, CURLINFO_HTTP_CODE);
$ch->http_error = in_array(floor($ch->http_status_code / 100), array(4, 5));
$ch->error = $ch->curl_error || $ch->http_error;
$ch->error_code = $ch->error ? ($ch->curl_error ? $ch->curl_error_code : $ch->http_status_code) : 0;
$ch->request_headers = $this->parseRequestHeaders(curl_getinfo($ch->curl, CURLINFO_HEADER_OUT));
$ch->response_headers = '';
if (!(strpos($ch->response, "\r\n\r\n") === false)) {
list($response_header, $ch->response) = explode("\r\n\r\n", $ch->response, 2);
if ($response_header === 'HTTP/1.1 100 Continue') {
list($response_header, $ch->response) = explode("\r\n\r\n", $ch->response, 2);
}
$ch->response_headers = $this->parseResponseHeaders($response_header);
if (isset($ch->response_headers['Content-Type'])) {
if (preg_match('/^application\/json/i', $ch->response_headers['Content-Type'])) {
$json_obj = json_decode($ch->response, false);
if (!is_null($json_obj)) {
$ch->response = $json_obj;
}
}
}
}
$ch->http_error_message = '';
if ($ch->error) {
if (isset($ch->response_headers['Status-Line'])) {
$ch->http_error_message = $ch->response_headers['Status-Line'];
}
}
$ch->error_message = $ch->curl_error ? $ch->curl_error_message : $ch->http_error_message;
if (!$ch->error) {
$ch->call($this->success_function, $ch);
} else {
$ch->call($this->error_function, $ch);
}
$ch->call($this->complete_function, $ch);
return $ch->error_code;
}
private function call($function)
{
if (is_callable($function)) {
$args = func_get_args();
array_shift($args);
call_user_func_array($function, $args);
}
}
public function __destruct()
{
$this->close();
}
}
class CaseInsensitiveArray implements ArrayAccess, Countable, Iterator
{
private $container = array();
public function offsetSet($offset, $value)
{
if (is_null($offset)) {
$this->container[] = $value;
} else {
$index = array_search(strtolower($offset), array_keys(array_change_key_case($this->container, CASE_LOWER)));
if (!($index === false)) {
$keys = array_keys($this->container);
unset($this->container[$keys[$index]]);
}
$this->container[$offset] = $value;
}
}
public function offsetExists($offset)
{
return array_key_exists(strtolower($offset), array_change_key_case($this->container, CASE_LOWER));
}
public function offsetUnset($offset)
{
unset($this->container[$offset]);
}
public function offsetGet($offset)
{
$index = array_search(strtolower($offset), array_keys(array_change_key_case($this->container, CASE_LOWER)));
if ($index === false) {
return null;
}
$values = array_values($this->container);
return $values[$index];
}
public function count()
{
return count($this->container);
}
public function current()
{
return current($this->container);
}
public function next()
{
return next($this->container);
}
public function key()
{
return key($this->container);
}
public function valid()
{
return !($this->current() === false);
}
public function rewind()
{
reset($this->container);
}
}
function is_array_assoc($array)
{
return (bool)count(array_filter(array_keys($array), 'is_string'));
}
function is_array_multidim($array)
{
if (!is_array($array)) {
return false;
}
return !(count($array) === count($array, COUNT_RECURSIVE));
}
function http_build_multi_query($data, $key = null)
{
$query = array();
if (empty($data)) {
return $key . '=';
}
$is_array_assoc = is_array_assoc($data);
foreach ($data as $k => $value) {
if (is_string($value) || is_numeric($value)) {
$brackets = $is_array_assoc ? '[' . $k . ']' : '[]';
$query[] = urlencode(is_null($key) ? $k : $key . $brackets) . '=' . rawurlencode($value);
} elseif (is_array($value)) {
$nested = is_null($key) ? $k : $key . '[' . $k . ']';
$query[] = http_build_multi_query($value, $nested);
}
}
return implode('&', $query);
}

View File

@ -0,0 +1,740 @@
<?php
// Usage: phpunit --verbose run.php
require '../src/Curl.class.php';
require 'helper.inc.php';
class CurlTest extends PHPUnit_Framework_TestCase {
public function testExtensionLoaded() {
$this->assertTrue(extension_loaded('curl'));
}
public function testArrayAssociative() {
$this->assertTrue(is_array_assoc(array(
'foo' => 'wibble',
'bar' => 'wubble',
'baz' => 'wobble',
)));
}
public function testArrayIndexed() {
$this->assertFalse(is_array_assoc(array(
'wibble',
'wubble',
'wobble',
)));
}
public function testCaseInsensitiveArrayGet() {
$array = new CaseInsensitiveArray();
$this->assertTrue(is_object($array));
$this->assertCount(0, $array);
$this->assertNull($array[(string)rand()]);
$array['foo'] = 'bar';
$this->assertNotEmpty($array);
$this->assertCount(1, $array);
}
public function testCaseInsensitiveArraySet() {
function assertions($array, $count=1) {
PHPUnit_Framework_Assert::assertCount($count, $array);
PHPUnit_Framework_Assert::assertTrue($array['foo'] === 'bar');
PHPUnit_Framework_Assert::assertTrue($array['Foo'] === 'bar');
PHPUnit_Framework_Assert::assertTrue($array['FOo'] === 'bar');
PHPUnit_Framework_Assert::assertTrue($array['FOO'] === 'bar');
}
$array = new CaseInsensitiveArray();
$array['foo'] = 'bar';
assertions($array);
$array['Foo'] = 'bar';
assertions($array);
$array['FOo'] = 'bar';
assertions($array);
$array['FOO'] = 'bar';
assertions($array);
$array['baz'] = 'qux';
assertions($array, 2);
}
public function testUserAgent() {
$test = new Test();
$test->curl->setUserAgent(Curl::USER_AGENT);
$this->assertTrue($test->server('server', 'GET', array(
'key' => 'HTTP_USER_AGENT',
)) === Curl::USER_AGENT);
}
public function testGet() {
$test = new Test();
$this->assertTrue($test->server('server', 'GET', array(
'key' => 'REQUEST_METHOD',
)) === 'GET');
}
public function testPostRequestMethod() {
$test = new Test();
$this->assertTrue($test->server('server', 'POST', array(
'key' => 'REQUEST_METHOD',
)) === 'POST');
}
public function testPostData() {
$test = new Test();
$this->assertTrue($test->server('post', 'POST', array(
'key' => 'value',
)) === 'key=value');
}
public function testPostAssociativeArrayData() {
$test = new Test();
$this->assertTrue($test->server('post_multidimensional', 'POST', array(
'username' => 'myusername',
'password' => 'mypassword',
'more_data' => array(
'param1' => 'something',
'param2' => 'other thing',
'param3' => 123,
'param4' => 3.14,
),
)) === 'username=myusername&password=mypassword&more_data%5Bparam1%5D=something&more_data%5Bparam2%5D=other%20thing&more_data%5Bparam3%5D=123&more_data%5Bparam4%5D=3.14');
}
public function testPostMultidimensionalData() {
$test = new Test();
$this->assertTrue($test->server('post_multidimensional', 'POST', array(
'key' => 'file',
'file' => array(
'wibble',
'wubble',
'wobble',
),
)) === 'key=file&file%5B%5D=wibble&file%5B%5D=wubble&file%5B%5D=wobble');
}
public function testPostFilePathUpload() {
$file_path = get_png();
$test = new Test();
$this->assertTrue($test->server('post_file_path_upload', 'POST', array(
'key' => 'image',
'image' => '@' . $file_path,
)) === 'image/png');
unlink($file_path);
$this->assertFalse(file_exists($file_path));
}
public function testPostCurlFileUpload() {
if (class_exists('CURLFile')) {
$file_path = get_png();
$test = new Test();
$this->assertTrue($test->server('post_file_path_upload', 'POST', array(
'key' => 'image',
'image' => new CURLFile($file_path),
)) === 'image/png');
unlink($file_path);
$this->assertFalse(file_exists($file_path));
}
}
public function testPutRequestMethod() {
$test = new Test();
$this->assertTrue($test->server('request_method', 'PUT') === 'PUT');
}
public function testPutData() {
$test = new Test();
$this->assertTrue($test->server('put', 'PUT', array(
'key' => 'value',
)) === 'key=value');
}
public function testPutFileHandle() {
$png = create_png();
$tmp_file = create_tmp_file($png);
$test = new Test();
$test->curl->setHeader('X-DEBUG-TEST', 'put_file_handle');
$test->curl->setOpt(CURLOPT_PUT, true);
$test->curl->setOpt(CURLOPT_INFILE, $tmp_file);
$test->curl->setOpt(CURLOPT_INFILESIZE, strlen($png));
$test->curl->put(Test::TEST_URL);
fclose($tmp_file);
$this->assertTrue($test->curl->response === 'image/png');
}
public function testPatchRequestMethod() {
$test = new Test();
$this->assertTrue($test->server('request_method', 'PATCH') === 'PATCH');
}
public function testDelete() {
$test = new Test();
$this->assertTrue($test->server('server', 'DELETE', array(
'key' => 'REQUEST_METHOD',
)) === 'DELETE');
$test = new Test();
$this->assertTrue($test->server('delete', 'DELETE', array(
'test' => 'delete',
'key' => 'test',
)) === 'delete');
}
public function testHeadRequestMethod() {
$test = new Test();
$test->server('request_method', 'HEAD', array(
'key' => 'REQUEST_METHOD',
));
$this->assertEquals($test->curl->response_headers['X-REQUEST-METHOD'], 'HEAD');
$this->assertEmpty($test->curl->response);
}
public function testOptionsRequestMethod() {
$test = new Test();
$test->server('request_method', 'OPTIONS', array(
'key' => 'REQUEST_METHOD',
));
$this->assertEquals($test->curl->response_headers['X-REQUEST-METHOD'], 'OPTIONS');
}
public function testBasicHttpAuth401Unauthorized() {
$test = new Test();
$this->assertTrue($test->server('http_basic_auth', 'GET') === 'canceled');
}
public function testBasicHttpAuthSuccess() {
$username = 'myusername';
$password = 'mypassword';
$test = new Test();
$test->curl->setBasicAuthentication($username, $password);
$test->server('http_basic_auth', 'GET');
$json = $test->curl->response;
$this->assertTrue($json->username === $username);
$this->assertTrue($json->password === $password);
}
public function testReferrer() {
$test = new Test();
$test->curl->setReferrer('myreferrer');
$this->assertTrue($test->server('server', 'GET', array(
'key' => 'HTTP_REFERER',
)) === 'myreferrer');
}
public function testCookies() {
$test = new Test();
$test->curl->setCookie('mycookie', 'yum');
$this->assertTrue($test->server('cookie', 'GET', array(
'key' => 'mycookie',
)) === 'yum');
}
public function testCookieFile() {
$cookie_file = dirname(__FILE__) . '/cookies.txt';
$cookie_data = implode("\t", array(
'127.0.0.1', // domain
'FALSE', // tailmatch
'/', // path
'FALSE', // secure
'0', // expires
'mycookie', // name
'yum', // value
));
file_put_contents($cookie_file, $cookie_data);
$test = new Test();
$test->curl->setCookieFile($cookie_file);
$this->assertTrue($test->server('cookie', 'GET', array(
'key' => 'mycookie',
)) === 'yum');
unlink($cookie_file);
$this->assertFalse(file_exists($cookie_file));
}
public function testCookieJar() {
$cookie_file = dirname(__FILE__) . '/cookies.txt';
$test = new Test();
$test->curl->setCookieJar($cookie_file);
$test->server('cookiejar', 'GET');
$test->curl->close();
$this->assertTrue(!(strpos(file_get_contents($cookie_file), "\t" . 'mycookie' . "\t" . 'yum') === false));
unlink($cookie_file);
$this->assertFalse(file_exists($cookie_file));
}
public function testMultipleCookieResponse() {
$expected_response = 'cookie1=scrumptious,cookie2=mouthwatering';
// github.com/facebook/hhvm/issues/2345
if (defined('HHVM_VERSION')) {
$expected_response = 'cookie2=mouthwatering,cookie1=scrumptious';
}
$test = new Test();
$test->server('multiple_cookie', 'GET');
$this->assertEquals($test->curl->response_headers['Set-Cookie'], $expected_response);
}
public function testError() {
$test = new Test();
$test->curl->setOpt(CURLOPT_CONNECTTIMEOUT_MS, 4000);
$test->curl->get(Test::ERROR_URL);
$this->assertTrue($test->curl->error);
$this->assertTrue($test->curl->curl_error);
$this->assertTrue($test->curl->curl_error_code === CURLE_OPERATION_TIMEOUTED);
}
public function testErrorMessage() {
$test = new Test();
$test->server('error_message', 'GET');
$expected_response = 'HTTP/1.1 401 Unauthorized';
if (defined('HHVM_VERSION')) {
$expected_response = 'HTTP/1.1 401';
}
$this->assertEquals($test->curl->error_message, $expected_response);
}
public function testHeaders() {
$test = new Test();
$test->curl->setHeader('Content-Type', 'application/json');
$test->curl->setHeader('X-Requested-With', 'XMLHttpRequest');
$test->curl->setHeader('Accept', 'application/json');
$this->assertTrue($test->server('server', 'GET', array(
'key' => 'HTTP_CONTENT_TYPE', // OR "CONTENT_TYPE".
)) === 'application/json');
$this->assertTrue($test->server('server', 'GET', array(
'key' => 'HTTP_X_REQUESTED_WITH',
)) === 'XMLHttpRequest');
$this->assertTrue($test->server('server', 'GET', array(
'key' => 'HTTP_ACCEPT',
)) === 'application/json');
}
public function testHeaderCaseSensitivity() {
$content_type = 'application/json';
$test = new Test();
$test->curl->setHeader('Content-Type', $content_type);
$test->server('response_header', 'GET');
$request_headers = $test->curl->request_headers;
$response_headers = $test->curl->response_headers;
$this->assertEquals($request_headers['Content-Type'], $content_type);
$this->assertEquals($request_headers['content-type'], $content_type);
$this->assertEquals($request_headers['CONTENT-TYPE'], $content_type);
$this->assertEquals($request_headers['cOnTeNt-TyPe'], $content_type);
$etag = $response_headers['ETag'];
$this->assertEquals($response_headers['ETAG'], $etag);
$this->assertEquals($response_headers['etag'], $etag);
$this->assertEquals($response_headers['eTAG'], $etag);
$this->assertEquals($response_headers['eTaG'], $etag);
}
public function testRequestURL() {
$test = new Test();
$this->assertFalse(substr($test->server('request_uri', 'GET'), -1) === '?');
$test = new Test();
$this->assertFalse(substr($test->server('request_uri', 'POST'), -1) === '?');
$test = new Test();
$this->assertFalse(substr($test->server('request_uri', 'PUT'), -1) === '?');
$test = new Test();
$this->assertFalse(substr($test->server('request_uri', 'PATCH'), -1) === '?');
$test = new Test();
$this->assertFalse(substr($test->server('request_uri', 'DELETE'), -1) === '?');
}
public function testNestedData() {
$test = new Test();
$data = array(
'username' => 'myusername',
'password' => 'mypassword',
'more_data' => array(
'param1' => 'something',
'param2' => 'other thing',
'another' => array(
'extra' => 'level',
'because' => 'I need it',
),
),
);
$this->assertTrue(
$test->server('post', 'POST', $data) === http_build_query($data)
);
}
public function testPostContentTypes() {
$test = new Test();
$test->server('server', 'POST', 'foo=bar');
$this->assertEquals($test->curl->request_headers['Content-Type'], 'application/x-www-form-urlencoded');
$test = new Test();
$test->server('server', 'POST', array(
'foo' => 'bar',
));
$this->assertEquals($test->curl->request_headers['Expect'], '100-continue');
preg_match('/^multipart\/form-data; boundary=/', $test->curl->request_headers['Content-Type'], $content_type);
$this->assertTrue(!empty($content_type));
}
public function testJSONResponse() {
function assertion($key, $value) {
$test = new Test();
$test->server('json_response', 'POST', array(
'key' => $key,
'value' => $value,
));
$response = $test->curl->response;
PHPUnit_Framework_Assert::assertNotNull($response);
PHPUnit_Framework_Assert::assertNull($response->null);
PHPUnit_Framework_Assert::assertTrue($response->true);
PHPUnit_Framework_Assert::assertFalse($response->false);
PHPUnit_Framework_Assert::assertTrue(is_int($response->integer));
PHPUnit_Framework_Assert::assertTrue(is_float($response->float));
PHPUnit_Framework_Assert::assertEmpty($response->empty);
PHPUnit_Framework_Assert::assertTrue(is_string($response->string));
}
assertion('Content-Type', 'application/json; charset=utf-8');
assertion('content-type', 'application/json; charset=utf-8');
assertion('Content-Type', 'application/json');
assertion('content-type', 'application/json');
assertion('CONTENT-TYPE', 'application/json');
assertion('CONTENT-TYPE', 'APPLICATION/JSON');
}
public function testArrayToStringConversion() {
$test = new Test();
$test->server('post', 'POST', array(
'foo' => 'bar',
'baz' => array(
),
));
$this->assertTrue($test->curl->response === 'foo=bar&baz=');
$test = new Test();
$test->server('post', 'POST', array(
'foo' => 'bar',
'baz' => array(
'qux' => array(
),
),
));
$this->assertTrue(urldecode($test->curl->response) ===
'foo=bar&baz[qux]='
);
$test = new Test();
$test->server('post', 'POST', array(
'foo' => 'bar',
'baz' => array(
'qux' => array(
),
'wibble' => 'wobble',
),
));
$this->assertTrue(urldecode($test->curl->response) ===
'foo=bar&baz[qux]=&baz[wibble]=wobble'
);
}
public function testParallelRequests() {
$test = new Test();
$curl = $test->curl;
$curl->beforeSend(function($instance) {
$instance->setHeader('X-DEBUG-TEST', 'request_uri');
});
$curl->get(array(
Test::TEST_URL . 'a/',
Test::TEST_URL . 'b/',
Test::TEST_URL . 'c/',
), array(
'foo' => 'bar',
));
$len = strlen('/a/?foo=bar');
$this->assertTrue(substr($curl->curls['0']->response, - $len) === '/a/?foo=bar');
$this->assertTrue(substr($curl->curls['1']->response, - $len) === '/b/?foo=bar');
$this->assertTrue(substr($curl->curls['2']->response, - $len) === '/c/?foo=bar');
}
public function testParallelSetOptions() {
$test = new Test();
$curl = $test->curl;
$curl->setHeader('X-DEBUG-TEST', 'server');
$curl->setOpt(CURLOPT_USERAGENT, 'useragent');
$curl->complete(function($instance) {
PHPUnit_Framework_Assert::assertTrue($instance->response === 'useragent');
});
$curl->get(array(
Test::TEST_URL,
), array(
'key' => 'HTTP_USER_AGENT',
));
}
public function testSuccessCallback() {
$success_called = false;
$error_called = false;
$complete_called = false;
$test = new Test();
$curl = $test->curl;
$curl->setHeader('X-DEBUG-TEST', 'get');
$curl->success(function($instance) use (&$success_called, &$error_called, &$complete_called) {
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
PHPUnit_Framework_Assert::assertFalse($success_called);
PHPUnit_Framework_Assert::assertFalse($error_called);
PHPUnit_Framework_Assert::assertFalse($complete_called);
$success_called = true;
});
$curl->error(function($instance) use (&$success_called, &$error_called, &$complete_called, &$curl) {
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
PHPUnit_Framework_Assert::assertFalse($success_called);
PHPUnit_Framework_Assert::assertFalse($error_called);
PHPUnit_Framework_Assert::assertFalse($complete_called);
$error_called = true;
});
$curl->complete(function($instance) use (&$success_called, &$error_called, &$complete_called) {
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
PHPUnit_Framework_Assert::assertTrue($success_called);
PHPUnit_Framework_Assert::assertFalse($error_called);
PHPUnit_Framework_Assert::assertFalse($complete_called);
$complete_called = true;
});
$curl->get(Test::TEST_URL);
$this->assertTrue($success_called);
$this->assertFalse($error_called);
$this->assertTrue($complete_called);
}
public function testParallelSuccessCallback() {
$success_called = false;
$error_called = false;
$complete_called = false;
$success_called_once = false;
$error_called_once = false;
$complete_called_once = false;
$test = new Test();
$curl = $test->curl;
$curl->setHeader('X-DEBUG-TEST', 'get');
$curl->success(function($instance) use (&$success_called,
&$error_called,
&$complete_called,
&$success_called_once) {
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
PHPUnit_Framework_Assert::assertFalse($success_called);
PHPUnit_Framework_Assert::assertFalse($error_called);
PHPUnit_Framework_Assert::assertFalse($complete_called);
$success_called = true;
$success_called_once = true;
});
$curl->error(function($instance) use (&$success_called,
&$error_called,
&$complete_called,
&$curl,
&$error_called_once) {
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
PHPUnit_Framework_Assert::assertFalse($success_called);
PHPUnit_Framework_Assert::assertFalse($error_called);
PHPUnit_Framework_Assert::assertFalse($complete_called);
$error_called = true;
$error_called_once = true;
});
$curl->complete(function($instance) use (&$success_called,
&$error_called,
&$complete_called,
&$complete_called_once) {
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
PHPUnit_Framework_Assert::assertTrue($success_called);
PHPUnit_Framework_Assert::assertFalse($error_called);
PHPUnit_Framework_Assert::assertFalse($complete_called);
$complete_called = true;
$complete_called_once = true;
PHPUnit_Framework_Assert::assertTrue($success_called);
PHPUnit_Framework_Assert::assertFalse($error_called);
PHPUnit_Framework_Assert::assertTrue($complete_called);
$success_called = false;
$error_called = false;
$complete_called = false;
});
$curl->get(array(
Test::TEST_URL . 'a/',
Test::TEST_URL . 'b/',
Test::TEST_URL . 'c/',
));
PHPUnit_Framework_Assert::assertTrue($success_called_once || $error_called_once);
PHPUnit_Framework_Assert::assertTrue($complete_called_once);
}
public function testErrorCallback() {
$success_called = false;
$error_called = false;
$complete_called = false;
$test = new Test();
$curl = $test->curl;
$curl->setHeader('X-DEBUG-TEST', 'get');
$curl->setOpt(CURLOPT_CONNECTTIMEOUT_MS, 2000);
$curl->success(function($instance) use (&$success_called, &$error_called, &$complete_called) {
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
PHPUnit_Framework_Assert::assertFalse($success_called);
PHPUnit_Framework_Assert::assertFalse($error_called);
PHPUnit_Framework_Assert::assertFalse($complete_called);
$success_called = true;
});
$curl->error(function($instance) use (&$success_called, &$error_called, &$complete_called, &$curl) {
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
PHPUnit_Framework_Assert::assertFalse($success_called);
PHPUnit_Framework_Assert::assertFalse($error_called);
PHPUnit_Framework_Assert::assertFalse($complete_called);
$error_called = true;
});
$curl->complete(function($instance) use (&$success_called, &$error_called, &$complete_called) {
PHPUnit_Framework_Assert::assertInstanceOf('Curl', $instance);
PHPUnit_Framework_Assert::assertFalse($success_called);
PHPUnit_Framework_Assert::assertTrue($error_called);
PHPUnit_Framework_Assert::assertFalse($complete_called);
$complete_called = true;
});
$curl->get(Test::ERROR_URL);
$this->assertFalse($success_called);
$this->assertTrue($error_called);
$this->assertTrue($complete_called);
}
public function testClose() {
$test = new Test();
$curl = $test->curl;
$curl->setHeader('X-DEBUG-TEST', 'post');
$curl->post(Test::TEST_URL);
$this->assertTrue(is_resource($curl->curl));
$curl->close();
$this->assertFalse(is_resource($curl->curl));
}
/**
* @expectedException PHPUnit_Framework_Error_Warning
*/
public function testRequiredOptionCurlInfoHeaderOutEmitsWarning() {
$curl = new Curl();
$curl->setOpt(CURLINFO_HEADER_OUT, false);
}
/**
* @expectedException PHPUnit_Framework_Error_Warning
*/
public function testRequiredOptionCurlOptHeaderEmitsWarning() {
$curl = new Curl();
$curl->setOpt(CURLOPT_HEADER, false);
}
/**
* @expectedException PHPUnit_Framework_Error_Warning
*/
public function testRequiredOptionCurlOptReturnTransferEmitsWarning() {
$curl = new Curl();
$curl->setOpt(CURLOPT_RETURNTRANSFER, false);
}
public function testRequestMethodSuccessiveGetRequests() {
$test = new Test();
test($test, 'GET', 'POST');
test($test, 'GET', 'PUT');
test($test, 'GET', 'PATCH');
test($test, 'GET', 'DELETE');
test($test, 'GET', 'HEAD');
test($test, 'GET', 'OPTIONS');
}
public function testRequestMethodSuccessivePostRequests() {
$test = new Test();
test($test, 'POST', 'GET');
test($test, 'POST', 'PUT');
test($test, 'POST', 'PATCH');
test($test, 'POST', 'DELETE');
test($test, 'POST', 'HEAD');
test($test, 'POST', 'OPTIONS');
}
public function testRequestMethodSuccessivePutRequests() {
$test = new Test();
test($test, 'PUT', 'GET');
test($test, 'PUT', 'POST');
test($test, 'PUT', 'PATCH');
test($test, 'PUT', 'DELETE');
test($test, 'PUT', 'HEAD');
test($test, 'PUT', 'OPTIONS');
}
public function testRequestMethodSuccessivePatchRequests() {
$test = new Test();
test($test, 'PATCH', 'GET');
test($test, 'PATCH', 'POST');
test($test, 'PATCH', 'PUT');
test($test, 'PATCH', 'DELETE');
test($test, 'PATCH', 'HEAD');
test($test, 'PATCH', 'OPTIONS');
}
public function testRequestMethodSuccessiveDeleteRequests() {
$test = new Test();
test($test, 'DELETE', 'GET');
test($test, 'DELETE', 'POST');
test($test, 'DELETE', 'PUT');
test($test, 'DELETE', 'PATCH');
test($test, 'DELETE', 'HEAD');
test($test, 'DELETE', 'OPTIONS');
}
public function testRequestMethodSuccessiveHeadRequests() {
$test = new Test();
test($test, 'HEAD', 'GET');
test($test, 'HEAD', 'POST');
test($test, 'HEAD', 'PUT');
test($test, 'HEAD', 'PATCH');
test($test, 'HEAD', 'DELETE');
test($test, 'HEAD', 'OPTIONS');
}
public function testRequestMethodSuccessiveOptionsRequests() {
$test = new Test();
test($test, 'OPTIONS', 'GET');
test($test, 'OPTIONS', 'POST');
test($test, 'OPTIONS', 'PUT');
test($test, 'OPTIONS', 'PATCH');
test($test, 'OPTIONS', 'DELETE');
test($test, 'OPTIONS', 'HEAD');
}
}

View File

@ -0,0 +1,47 @@
<?php
class Test {
const TEST_URL = 'http://127.0.0.1:8000/';
const ERROR_URL = 'https://1.2.3.4/';
function __construct() {
$this->curl = new Curl();
$this->curl->setOpt(CURLOPT_SSL_VERIFYPEER, false);
$this->curl->setOpt(CURLOPT_SSL_VERIFYHOST, false);
}
function server($test, $request_method, $data=array()) {
$this->curl->setHeader('X-DEBUG-TEST', $test);
$request_method = strtolower($request_method);
$this->curl->$request_method(self::TEST_URL, $data);
return $this->curl->response;
}
}
function test($instance, $before, $after) {
$instance->server('request_method', $before);
PHPUnit_Framework_Assert::assertEquals($instance->curl->response_headers['X-REQUEST-METHOD'], $before);
$instance->server('request_method', $after);
PHPUnit_Framework_Assert::assertEquals($instance->curl->response_headers['X-REQUEST-METHOD'], $after);
}
function create_png() {
// PNG image data, 1 x 1, 1-bit colormap, non-interlaced
ob_start();
imagepng(imagecreatefromstring(base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7')));
$raw_image = ob_get_contents();
ob_end_clean();
return $raw_image;
}
function create_tmp_file($data) {
$tmp_file = tmpfile();
fwrite($tmp_file, $data);
rewind($tmp_file);
return $tmp_file;
}
function get_png() {
$tmp_filename = tempnam('/tmp', 'php-curl-class.');
file_put_contents($tmp_filename, create_png());
return $tmp_filename;
}

View File

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

View File

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

View File

@ -0,0 +1,132 @@
<?php
$http_raw_post_data = file_get_contents('php://input');
$_PUT = array();
$_PATCH = array();
$request_method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '';
$content_type = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '';
$data_values = $_GET;
if ($request_method === 'POST') {
$data_values = $_POST;
}
else if ($request_method === 'PUT') {
if (strpos($content_type, 'application/x-www-form-urlencoded') === 0) {
parse_str($http_raw_post_data, $_PUT);
$data_values = $_PUT;
}
}
else if ($request_method === 'PATCH') {
if (strpos($content_type, 'application/x-www-form-urlencoded') === 0) {
parse_str($http_raw_post_data, $_PATCH);
$data_values = $_PATCH;
}
}
$test = isset($_SERVER['HTTP_X_DEBUG_TEST']) ? $_SERVER['HTTP_X_DEBUG_TEST'] : '';
$key = isset($data_values['key']) ? $data_values['key'] : '';
if ($test == 'http_basic_auth') {
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
echo 'canceled';
exit;
}
header('Content-Type: application/json');
echo json_encode(array(
'username' => $_SERVER['PHP_AUTH_USER'],
'password' => $_SERVER['PHP_AUTH_PW'],
));
exit;
}
else if ($test === 'get') {
echo http_build_query($_GET);
exit;
}
else if ($test === 'post') {
echo http_build_query($_POST);
exit;
}
else if ($test === 'put') {
echo $http_raw_post_data;
exit;
}
else if ($test === 'post_multidimensional') {
echo $http_raw_post_data;
exit;
}
else if ($test === 'post_file_path_upload') {
echo mime_content_type($_FILES[$key]['tmp_name']);
exit;
}
else if ($test === 'put_file_handle') {
$tmp_filename = tempnam('/tmp', 'php-curl-class.');
file_put_contents($tmp_filename, $http_raw_post_data);
echo mime_content_type($tmp_filename);
unlink($tmp_filename);
exit;
}
else if ($test === 'request_method') {
header('X-REQUEST-METHOD: ' . $request_method);
echo $request_method;
exit;
}
else if ($test === 'request_uri') {
echo $_SERVER['REQUEST_URI'];
exit;
}
else if ($test === 'cookiejar') {
setcookie('mycookie', 'yum');
exit;
}
else if ($test === 'multiple_cookie') {
setcookie('cookie1', 'scrumptious');
setcookie('cookie2', 'mouthwatering');
exit;
}
else if ($test === 'response_header') {
header('Content-Type: application/json');
header('ETag: ' . md5('worldpeace'));
exit;
}
else if ($test === 'json_response') {
$key = $_POST['key'];
$value = $_POST['value'];
header($key . ': ' . $value);
echo json_encode(array(
'null' => null,
'true' => true,
'false' => false,
'integer' => 1,
'float' => 3.14,
'empty' => '',
'string' => 'string',
));
exit;
}
else if ($test === 'error_message') {
if (function_exists('http_response_code')) {
http_response_code(401);
}
else {
header('HTTP/1.1 401 Unauthorized');
}
exit;
}
header('Content-Type: text/plain');
$data_mapping = array(
'cookie' => '_COOKIE',
'delete' => '_GET',
'get' => '_GET',
'patch' => '_PATCH',
'post' => '_POST',
'put' => '_PUT',
'server' => '_SERVER',
);
$data = $$data_mapping[$test];
$value = isset($data[$key]) ? $data[$key] : '';
echo $value;

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md

View File

@ -0,0 +1,19 @@
Copyright (c) 2020-2022 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,26 @@
Symfony Deprecation Contracts
=============================
A generic function and convention to trigger deprecation notices.
This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
The function requires at least 3 arguments:
- the name of the Composer package that is triggering the deprecation
- the version of the package that introduced the deprecation
- the message of the deprecation
- more arguments can be provided: they will be inserted in the message using `printf()` formatting
Example:
```php
trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
```
This will generate the following message:
`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty
`function trigger_deprecation() {}` in your application.

View File

@ -0,0 +1,35 @@
{
"name": "symfony/deprecation-contracts",
"type": "library",
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.1"
},
"autoload": {
"files": [
"function.php"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "2.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
}
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (!function_exists('trigger_deprecation')) {
/**
* Triggers a silenced deprecation notice.
*
* @param string $package The name of the Composer package that is triggering the deprecation
* @param string $version The version of the package that introduced the deprecation
* @param string $message The message of the deprecation
* @param mixed ...$args Values to insert in the message using printf() formatting
*
* @author Nicolas Grekas <p@tchwork.com>
*/
function trigger_deprecation(string $package, string $version, string $message, ...$args): void
{
@trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
}
}

View File

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

View File

@ -16,47 +16,102 @@ namespace Symfony\Component\Finder\Comparator;
*/
class Comparator
{
private string $target;
private string $operator;
private $target;
private $operator = '==';
public function __construct(string $target, string $operator = '==')
public function __construct(string $target = null, string $operator = '==')
{
if (!\in_array($operator, ['>', '<', '>=', '<=', '==', '!='])) {
throw new \InvalidArgumentException(sprintf('Invalid operator "%s".', $operator));
if (null === $target) {
trigger_deprecation('symfony/finder', '5.4', 'Constructing a "%s" without setting "$target" is deprecated.', __CLASS__);
}
$this->target = $target;
$this->operator = $operator;
$this->doSetOperator($operator);
}
/**
* Gets the target value.
*
* @return string
*/
public function getTarget(): string
public function getTarget()
{
if (null === $this->target) {
trigger_deprecation('symfony/finder', '5.4', 'Calling "%s" without initializing the target is deprecated.', __METHOD__);
}
return $this->target;
}
/**
* Gets the comparison operator.
* @deprecated set the target via the constructor instead
*/
public function getOperator(): string
public function setTarget(string $target)
{
trigger_deprecation('symfony/finder', '5.4', '"%s" is deprecated. Set the target via the constructor instead.', __METHOD__);
$this->target = $target;
}
/**
* Gets the comparison operator.
*
* @return string
*/
public function getOperator()
{
return $this->operator;
}
/**
* Tests against the target.
* Sets the comparison operator.
*
* @throws \InvalidArgumentException
*
* @deprecated set the operator via the constructor instead
*/
public function test(mixed $test): bool
public function setOperator(string $operator)
{
return match ($this->operator) {
'>' => $test > $this->target,
'>=' => $test >= $this->target,
'<' => $test < $this->target,
'<=' => $test <= $this->target,
'!=' => $test != $this->target,
default => $test == $this->target,
};
trigger_deprecation('symfony/finder', '5.4', '"%s" is deprecated. Set the operator via the constructor instead.', __METHOD__);
$this->doSetOperator('' === $operator ? '==' : $operator);
}
/**
* Tests against the target.
*
* @param mixed $test A test value
*
* @return bool
*/
public function test($test)
{
if (null === $this->target) {
trigger_deprecation('symfony/finder', '5.4', 'Calling "%s" without initializing the target is deprecated.', __METHOD__);
}
switch ($this->operator) {
case '>':
return $test > $this->target;
case '>=':
return $test >= $this->target;
case '<':
return $test < $this->target;
case '<=':
return $test <= $this->target;
case '!=':
return $test != $this->target;
}
return $test == $this->target;
}
private function doSetOperator(string $operator): void
{
if (!\in_array($operator, ['>', '<', '>=', '<=', '==', '!='])) {
throw new \InvalidArgumentException(sprintf('Invalid operator "%s".', $operator));
}
$this->operator = $operator;
}
}

View File

@ -32,7 +32,7 @@ class DateComparator extends Comparator
try {
$date = new \DateTime($matches[2]);
$target = $date->format('U');
} catch (\Exception) {
} catch (\Exception $e) {
throw new \InvalidArgumentException(sprintf('"%s" is not a valid date.', $matches[2]));
}

View File

@ -45,27 +45,27 @@ class Finder implements \IteratorAggregate, \Countable
public const IGNORE_DOT_FILES = 2;
public const IGNORE_VCS_IGNORED_FILES = 4;
private int $mode = 0;
private array $names = [];
private array $notNames = [];
private array $exclude = [];
private array $filters = [];
private array $depths = [];
private array $sizes = [];
private bool $followLinks = false;
private bool $reverseSorting = false;
private \Closure|int|false $sort = false;
private int $ignore = 0;
private array $dirs = [];
private array $dates = [];
private array $iterators = [];
private array $contains = [];
private array $notContains = [];
private array $paths = [];
private array $notPaths = [];
private bool $ignoreUnreadableDirs = false;
private $mode = 0;
private $names = [];
private $notNames = [];
private $exclude = [];
private $filters = [];
private $depths = [];
private $sizes = [];
private $followLinks = false;
private $reverseSorting = false;
private $sort = false;
private $ignore = 0;
private $dirs = [];
private $dates = [];
private $iterators = [];
private $contains = [];
private $notContains = [];
private $paths = [];
private $notPaths = [];
private $ignoreUnreadableDirs = false;
private static array $vcsPatterns = ['.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg'];
private static $vcsPatterns = ['.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg'];
public function __construct()
{
@ -74,8 +74,10 @@ class Finder implements \IteratorAggregate, \Countable
/**
* Creates a new Finder.
*
* @return static
*/
public static function create(): static
public static function create()
{
return new static();
}
@ -85,7 +87,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @return $this
*/
public function directories(): static
public function directories()
{
$this->mode = Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES;
@ -97,7 +99,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @return $this
*/
public function files(): static
public function files()
{
$this->mode = Iterator\FileTypeFilterIterator::ONLY_FILES;
@ -120,7 +122,7 @@ class Finder implements \IteratorAggregate, \Countable
* @see DepthRangeFilterIterator
* @see NumberComparator
*/
public function depth(string|int|array $levels): static
public function depth($levels)
{
foreach ((array) $levels as $level) {
$this->depths[] = new Comparator\NumberComparator($level);
@ -148,7 +150,7 @@ class Finder implements \IteratorAggregate, \Countable
* @see DateRangeFilterIterator
* @see DateComparator
*/
public function date(string|array $dates): static
public function date($dates)
{
foreach ((array) $dates as $date) {
$this->dates[] = new Comparator\DateComparator($date);
@ -173,7 +175,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see FilenameFilterIterator
*/
public function name(string|array $patterns): static
public function name($patterns)
{
$this->names = array_merge($this->names, (array) $patterns);
@ -189,7 +191,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see FilenameFilterIterator
*/
public function notName(string|array $patterns): static
public function notName($patterns)
{
$this->notNames = array_merge($this->notNames, (array) $patterns);
@ -211,7 +213,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see FilecontentFilterIterator
*/
public function contains(string|array $patterns): static
public function contains($patterns)
{
$this->contains = array_merge($this->contains, (array) $patterns);
@ -233,7 +235,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see FilecontentFilterIterator
*/
public function notContains(string|array $patterns): static
public function notContains($patterns)
{
$this->notContains = array_merge($this->notContains, (array) $patterns);
@ -257,7 +259,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see FilenameFilterIterator
*/
public function path(string|array $patterns): static
public function path($patterns)
{
$this->paths = array_merge($this->paths, (array) $patterns);
@ -281,7 +283,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see FilenameFilterIterator
*/
public function notPath(string|array $patterns): static
public function notPath($patterns)
{
$this->notPaths = array_merge($this->notPaths, (array) $patterns);
@ -303,7 +305,7 @@ class Finder implements \IteratorAggregate, \Countable
* @see SizeRangeFilterIterator
* @see NumberComparator
*/
public function size(string|int|array $sizes): static
public function size($sizes)
{
foreach ((array) $sizes as $size) {
$this->sizes[] = new Comparator\NumberComparator($size);
@ -325,7 +327,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see ExcludeDirectoryFilterIterator
*/
public function exclude(string|array $dirs): static
public function exclude($dirs)
{
$this->exclude = array_merge($this->exclude, (array) $dirs);
@ -341,7 +343,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see ExcludeDirectoryFilterIterator
*/
public function ignoreDotFiles(bool $ignoreDotFiles): static
public function ignoreDotFiles(bool $ignoreDotFiles)
{
if ($ignoreDotFiles) {
$this->ignore |= static::IGNORE_DOT_FILES;
@ -361,7 +363,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see ExcludeDirectoryFilterIterator
*/
public function ignoreVCS(bool $ignoreVCS): static
public function ignoreVCS(bool $ignoreVCS)
{
if ($ignoreVCS) {
$this->ignore |= static::IGNORE_VCS_FILES;
@ -379,7 +381,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @return $this
*/
public function ignoreVCSIgnored(bool $ignoreVCSIgnored): static
public function ignoreVCSIgnored(bool $ignoreVCSIgnored)
{
if ($ignoreVCSIgnored) {
$this->ignore |= static::IGNORE_VCS_IGNORED_FILES;
@ -397,7 +399,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @param string|string[] $pattern VCS patterns to ignore
*/
public static function addVCSPattern(string|array $pattern)
public static function addVCSPattern($pattern)
{
foreach ((array) $pattern as $p) {
self::$vcsPatterns[] = $p;
@ -417,7 +419,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see SortableIterator
*/
public function sort(\Closure $closure): static
public function sort(\Closure $closure)
{
$this->sort = $closure;
@ -433,7 +435,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see SortableIterator
*/
public function sortByName(bool $useNaturalSort = false): static
public function sortByName(bool $useNaturalSort = false)
{
$this->sort = $useNaturalSort ? Iterator\SortableIterator::SORT_BY_NAME_NATURAL : Iterator\SortableIterator::SORT_BY_NAME;
@ -449,7 +451,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see SortableIterator
*/
public function sortByType(): static
public function sortByType()
{
$this->sort = Iterator\SortableIterator::SORT_BY_TYPE;
@ -467,7 +469,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see SortableIterator
*/
public function sortByAccessedTime(): static
public function sortByAccessedTime()
{
$this->sort = Iterator\SortableIterator::SORT_BY_ACCESSED_TIME;
@ -479,7 +481,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @return $this
*/
public function reverseSorting(): static
public function reverseSorting()
{
$this->reverseSorting = true;
@ -499,7 +501,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see SortableIterator
*/
public function sortByChangedTime(): static
public function sortByChangedTime()
{
$this->sort = Iterator\SortableIterator::SORT_BY_CHANGED_TIME;
@ -517,7 +519,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see SortableIterator
*/
public function sortByModifiedTime(): static
public function sortByModifiedTime()
{
$this->sort = Iterator\SortableIterator::SORT_BY_MODIFIED_TIME;
@ -534,7 +536,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @see CustomFilterIterator
*/
public function filter(\Closure $closure): static
public function filter(\Closure $closure)
{
$this->filters[] = $closure;
@ -546,7 +548,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @return $this
*/
public function followLinks(): static
public function followLinks()
{
$this->followLinks = true;
@ -560,7 +562,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @return $this
*/
public function ignoreUnreadableDirs(bool $ignore = true): static
public function ignoreUnreadableDirs(bool $ignore = true)
{
$this->ignoreUnreadableDirs = $ignore;
@ -576,7 +578,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @throws DirectoryNotFoundException if one of the directories does not exist
*/
public function in(string|array $dirs): static
public function in($dirs)
{
$resolvedDirs = [];
@ -585,7 +587,7 @@ class Finder implements \IteratorAggregate, \Countable
$resolvedDirs[] = [$this->normalizeDir($dir)];
} elseif ($glob = glob($dir, (\defined('GLOB_BRACE') ? \GLOB_BRACE : 0) | \GLOB_ONLYDIR | \GLOB_NOSORT)) {
sort($glob);
$resolvedDirs[] = array_map($this->normalizeDir(...), $glob);
$resolvedDirs[] = array_map([$this, 'normalizeDir'], $glob);
} else {
throw new DirectoryNotFoundException(sprintf('The "%s" directory does not exist.', $dir));
}
@ -605,7 +607,8 @@ class Finder implements \IteratorAggregate, \Countable
*
* @throws \LogicException if the in() method has not been called
*/
public function getIterator(): \Iterator
#[\ReturnTypeWillChange]
public function getIterator()
{
if (0 === \count($this->dirs) && 0 === \count($this->iterators)) {
throw new \LogicException('You must call one of in() or append() methods before iterating over a Finder.');
@ -648,7 +651,7 @@ class Finder implements \IteratorAggregate, \Countable
*
* @throws \InvalidArgumentException when the given argument is not iterable
*/
public function append(iterable $iterator): static
public function append(iterable $iterator)
{
if ($iterator instanceof \IteratorAggregate) {
$this->iterators[] = $iterator->getIterator();
@ -670,8 +673,10 @@ class Finder implements \IteratorAggregate, \Countable
/**
* Check if any results were found.
*
* @return bool
*/
public function hasResults(): bool
public function hasResults()
{
foreach ($this->getIterator() as $_) {
return true;
@ -682,8 +687,11 @@ class Finder implements \IteratorAggregate, \Countable
/**
* Counts all the results collected by the iterators.
*
* @return int
*/
public function count(): int
#[\ReturnTypeWillChange]
public function count()
{
return iterator_count($this->getIterator());
}

View File

@ -43,7 +43,7 @@ class Gitignore
foreach ($gitignoreLines as $line) {
$line = preg_replace('~(?<!\\\\)[ \t]+$~', '', $line);
if (str_starts_with($line, '!')) {
if ('!' === substr($line, 0, 1)) {
$line = substr($line, 1);
$isNegative = true;
} else {

View File

@ -37,8 +37,10 @@ class Glob
{
/**
* Returns a regexp which is the equivalent of the glob pattern.
*
* @return string
*/
public static function toRegex(string $glob, bool $strictLeadingDot = true, bool $strictWildcardSlash = true, string $delimiter = '#'): string
public static function toRegex(string $glob, bool $strictLeadingDot = true, bool $strictWildcardSlash = true, string $delimiter = '#')
{
$firstByte = true;
$escaping = false;

View File

@ -23,7 +23,7 @@ namespace Symfony\Component\Finder\Iterator;
*/
class CustomFilterIterator extends \FilterIterator
{
private array $filters = [];
private $filters = [];
/**
* @param \Iterator<string, \SplFileInfo> $iterator The Iterator to filter
@ -45,8 +45,11 @@ class CustomFilterIterator extends \FilterIterator
/**
* Filters the iterator values.
*
* @return bool
*/
public function accept(): bool
#[\ReturnTypeWillChange]
public function accept()
{
$fileinfo = $this->current();

View File

@ -22,7 +22,7 @@ use Symfony\Component\Finder\Comparator\DateComparator;
*/
class DateRangeFilterIterator extends \FilterIterator
{
private array $comparators = [];
private $comparators = [];
/**
* @param \Iterator<string, \SplFileInfo> $iterator
@ -37,8 +37,11 @@ class DateRangeFilterIterator extends \FilterIterator
/**
* Filters the iterator values.
*
* @return bool
*/
public function accept(): bool
#[\ReturnTypeWillChange]
public function accept()
{
$fileinfo = $this->current();

View File

@ -23,7 +23,7 @@ namespace Symfony\Component\Finder\Iterator;
*/
class DepthRangeFilterIterator extends \FilterIterator
{
private int $minDepth = 0;
private $minDepth = 0;
/**
* @param \RecursiveIteratorIterator<\RecursiveIterator<TKey, TValue>> $iterator The Iterator to filter
@ -40,8 +40,11 @@ class DepthRangeFilterIterator extends \FilterIterator
/**
* Filters the iterator values.
*
* @return bool
*/
public function accept(): bool
#[\ReturnTypeWillChange]
public function accept()
{
return $this->getInnerIterator()->getDepth() >= $this->minDepth;
}

View File

@ -11,27 +11,24 @@
namespace Symfony\Component\Finder\Iterator;
use Symfony\Component\Finder\SplFileInfo;
/**
* ExcludeDirectoryFilterIterator filters out directories.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @extends \FilterIterator<string, SplFileInfo>
* @implements \RecursiveIterator<string, SplFileInfo>
* @extends \FilterIterator<string, \SplFileInfo>
* @implements \RecursiveIterator<string, \SplFileInfo>
*/
class ExcludeDirectoryFilterIterator extends \FilterIterator implements \RecursiveIterator
{
/** @var \Iterator<string, SplFileInfo> */
private \Iterator $iterator;
private bool $isRecursive;
private array $excludedDirs = [];
private ?string $excludedPattern = null;
private $iterator;
private $isRecursive;
private $excludedDirs = [];
private $excludedPattern;
/**
* @param \Iterator<string, SplFileInfo> $iterator The Iterator to filter
* @param string[] $directories An array of directories to exclude
* @param \Iterator $iterator The Iterator to filter
* @param string[] $directories An array of directories to exclude
*/
public function __construct(\Iterator $iterator, array $directories)
{
@ -55,8 +52,11 @@ class ExcludeDirectoryFilterIterator extends \FilterIterator implements \Recursi
/**
* Filters the iterator values.
*
* @return bool
*/
public function accept(): bool
#[\ReturnTypeWillChange]
public function accept()
{
if ($this->isRecursive && isset($this->excludedDirs[$this->getFilename()]) && $this->isDir()) {
return false;
@ -72,12 +72,20 @@ class ExcludeDirectoryFilterIterator extends \FilterIterator implements \Recursi
return true;
}
public function hasChildren(): bool
/**
* @return bool
*/
#[\ReturnTypeWillChange]
public function hasChildren()
{
return $this->isRecursive && $this->iterator->hasChildren();
}
public function getChildren(): self
/**
* @return self
*/
#[\ReturnTypeWillChange]
public function getChildren()
{
$children = new self($this->iterator->getChildren(), []);
$children->excludedDirs = $this->excludedDirs;

View File

@ -23,11 +23,11 @@ class FileTypeFilterIterator extends \FilterIterator
public const ONLY_FILES = 1;
public const ONLY_DIRECTORIES = 2;
private int $mode;
private $mode;
/**
* @param \Iterator<string, \SplFileInfo> $iterator The Iterator to filter
* @param int $mode The mode (self::ONLY_FILES or self::ONLY_DIRECTORIES)
* @param \Iterator $iterator The Iterator to filter
* @param int $mode The mode (self::ONLY_FILES or self::ONLY_DIRECTORIES)
*/
public function __construct(\Iterator $iterator, int $mode)
{
@ -38,8 +38,11 @@ class FileTypeFilterIterator extends \FilterIterator
/**
* Filters the iterator values.
*
* @return bool
*/
public function accept(): bool
#[\ReturnTypeWillChange]
public function accept()
{
$fileinfo = $this->current();
if (self::ONLY_DIRECTORIES === (self::ONLY_DIRECTORIES & $this->mode) && $fileinfo->isFile()) {

View File

@ -11,22 +11,23 @@
namespace Symfony\Component\Finder\Iterator;
use Symfony\Component\Finder\SplFileInfo;
/**
* FilecontentFilterIterator filters files by their contents using patterns (regexps or strings).
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Włodzimierz Gajda <gajdaw@gajdaw.pl>
*
* @extends MultiplePcreFilterIterator<string, SplFileInfo>
* @extends MultiplePcreFilterIterator<string, \SplFileInfo>
*/
class FilecontentFilterIterator extends MultiplePcreFilterIterator
{
/**
* Filters the iterator values.
*
* @return bool
*/
public function accept(): bool
#[\ReturnTypeWillChange]
public function accept()
{
if (!$this->matchRegexps && !$this->noMatchRegexps) {
return true;
@ -50,8 +51,10 @@ class FilecontentFilterIterator extends MultiplePcreFilterIterator
* Converts string to regexp if necessary.
*
* @param string $str Pattern: string or regexp
*
* @return string
*/
protected function toRegex(string $str): string
protected function toRegex(string $str)
{
return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/';
}

View File

@ -24,8 +24,11 @@ class FilenameFilterIterator extends MultiplePcreFilterIterator
{
/**
* Filters the iterator values.
*
* @return bool
*/
public function accept(): bool
#[\ReturnTypeWillChange]
public function accept()
{
return $this->isAccepted($this->current()->getFilename());
}
@ -37,8 +40,10 @@ class FilenameFilterIterator extends MultiplePcreFilterIterator
* Glob strings are transformed with Glob::toRegex().
*
* @param string $str Pattern: glob or regexp
*
* @return string
*/
protected function toRegex(string $str): string
protected function toRegex(string $str)
{
return $this->isRegex($str) ? $str : Glob::toRegex($str);
}

View File

@ -18,11 +18,11 @@ namespace Symfony\Component\Finder\Iterator;
*/
class LazyIterator implements \IteratorAggregate
{
private \Closure $iteratorFactory;
private $iteratorFactory;
public function __construct(callable $iteratorFactory)
{
$this->iteratorFactory = $iteratorFactory(...);
$this->iteratorFactory = $iteratorFactory;
}
public function getIterator(): \Traversable

View File

@ -27,9 +27,9 @@ abstract class MultiplePcreFilterIterator extends \FilterIterator
protected $noMatchRegexps = [];
/**
* @param \Iterator<TKey, TValue> $iterator The Iterator to filter
* @param string[] $matchPatterns An array of patterns that need to match
* @param string[] $noMatchPatterns An array of patterns that need to not match
* @param \Iterator $iterator The Iterator to filter
* @param string[] $matchPatterns An array of patterns that need to match
* @param string[] $noMatchPatterns An array of patterns that need to not match
*/
public function __construct(\Iterator $iterator, array $matchPatterns, array $noMatchPatterns)
{
@ -50,8 +50,10 @@ abstract class MultiplePcreFilterIterator extends \FilterIterator
* If there is no regexps defined in the class, this method will accept the string.
* Such case can be handled by child classes before calling the method if they want to
* apply a different behavior.
*
* @return bool
*/
protected function isAccepted(string $string): bool
protected function isAccepted(string $string)
{
// should at least not match one rule to exclude
foreach ($this->noMatchRegexps as $regex) {
@ -77,8 +79,10 @@ abstract class MultiplePcreFilterIterator extends \FilterIterator
/**
* Checks whether the string is a regex.
*
* @return bool
*/
protected function isRegex(string $str): bool
protected function isRegex(string $str)
{
$availableModifiers = 'imsxuADU';
@ -106,6 +110,8 @@ abstract class MultiplePcreFilterIterator extends \FilterIterator
/**
* Converts string into regexp.
*
* @return string
*/
abstract protected function toRegex(string $str): string;
abstract protected function toRegex(string $str);
}

View File

@ -11,22 +11,23 @@
namespace Symfony\Component\Finder\Iterator;
use Symfony\Component\Finder\SplFileInfo;
/**
* PathFilterIterator filters files by path patterns (e.g. some/special/dir).
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Włodzimierz Gajda <gajdaw@gajdaw.pl>
*
* @extends MultiplePcreFilterIterator<string, SplFileInfo>
* @extends MultiplePcreFilterIterator<string, \SplFileInfo>
*/
class PathFilterIterator extends MultiplePcreFilterIterator
{
/**
* Filters the iterator values.
*
* @return bool
*/
public function accept(): bool
#[\ReturnTypeWillChange]
public function accept()
{
$filename = $this->current()->getRelativePathname();
@ -48,8 +49,10 @@ class PathFilterIterator extends MultiplePcreFilterIterator
* Use only / as directory separator (on Windows also).
*
* @param string $str Pattern: regexp or dirname
*
* @return string
*/
protected function toRegex(string $str): string
protected function toRegex(string $str)
{
return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/';
}

View File

@ -18,17 +18,23 @@ use Symfony\Component\Finder\SplFileInfo;
* Extends the \RecursiveDirectoryIterator to support relative paths.
*
* @author Victor Berchet <victor@suumit.com>
* @extends \RecursiveDirectoryIterator<string, SplFileInfo>
*/
class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator
{
private bool $ignoreUnreadableDirs;
private ?bool $rewindable = null;
/**
* @var bool
*/
private $ignoreUnreadableDirs;
/**
* @var bool
*/
private $rewindable;
// these 3 properties take part of the performance optimization to avoid redoing the same work in all iterations
private string $rootPath;
private string $subPath;
private string $directorySeparator = '/';
private $rootPath;
private $subPath;
private $directorySeparator = '/';
/**
* @throws \RuntimeException
@ -49,15 +55,17 @@ class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator
/**
* Return an instance of SplFileInfo with support for relative paths.
*
* @return SplFileInfo
*/
public function current(): SplFileInfo
#[\ReturnTypeWillChange]
public function current()
{
// the logic here avoids redoing the same work in all iterations
if (!isset($this->subPath)) {
$this->subPath = $this->getSubPath();
if (null === $subPathname = $this->subPath) {
$subPathname = $this->subPath = $this->getSubPath();
}
$subPathname = $this->subPath;
if ('' !== $subPathname) {
$subPathname .= $this->directorySeparator;
}
@ -70,7 +78,13 @@ class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator
return new SplFileInfo($basePath.$subPathname, $this->subPath, $subPathname);
}
public function hasChildren(bool $allowLinks = false): bool
/**
* @param bool $allowLinks
*
* @return bool
*/
#[\ReturnTypeWillChange]
public function hasChildren($allowLinks = false)
{
$hasChildren = parent::hasChildren($allowLinks);
@ -82,16 +96,19 @@ class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator
parent::getChildren();
return true;
} catch (\UnexpectedValueException) {
} catch (\UnexpectedValueException $e) {
// If directory is unreadable and finder is set to ignore it, skip children
return false;
}
}
/**
* @return \RecursiveDirectoryIterator
*
* @throws AccessDeniedException
*/
public function getChildren(): \RecursiveDirectoryIterator
#[\ReturnTypeWillChange]
public function getChildren()
{
try {
$children = parent::getChildren();
@ -113,8 +130,11 @@ class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator
/**
* Do nothing for non rewindable stream.
*
* @return void
*/
public function rewind(): void
#[\ReturnTypeWillChange]
public function rewind()
{
if (false === $this->isRewindable()) {
return;
@ -125,8 +145,10 @@ class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator
/**
* Checks if the stream is rewindable.
*
* @return bool
*/
public function isRewindable(): bool
public function isRewindable()
{
if (null !== $this->rewindable) {
return $this->rewindable;

View File

@ -22,7 +22,7 @@ use Symfony\Component\Finder\Comparator\NumberComparator;
*/
class SizeRangeFilterIterator extends \FilterIterator
{
private array $comparators = [];
private $comparators = [];
/**
* @param \Iterator<string, \SplFileInfo> $iterator
@ -37,8 +37,11 @@ class SizeRangeFilterIterator extends \FilterIterator
/**
* Filters the iterator values.
*
* @return bool
*/
public function accept(): bool
#[\ReturnTypeWillChange]
public function accept()
{
$fileinfo = $this->current();
if (!$fileinfo->isFile()) {

View File

@ -28,9 +28,8 @@ class SortableIterator implements \IteratorAggregate
public const SORT_BY_MODIFIED_TIME = 5;
public const SORT_BY_NAME_NATURAL = 6;
/** @var \Traversable<string, \SplFileInfo> */
private \Traversable $iterator;
private \Closure|int $sort;
private $iterator;
private $sort;
/**
* @param \Traversable<string, \SplFileInfo> $iterator
@ -38,7 +37,7 @@ class SortableIterator implements \IteratorAggregate
*
* @throws \InvalidArgumentException
*/
public function __construct(\Traversable $iterator, int|callable $sort, bool $reverseOrder = false)
public function __construct(\Traversable $iterator, $sort, bool $reverseOrder = false)
{
$this->iterator = $iterator;
$order = $reverseOrder ? -1 : 1;
@ -76,13 +75,17 @@ class SortableIterator implements \IteratorAggregate
} elseif (self::SORT_BY_NONE === $sort) {
$this->sort = $order;
} elseif (\is_callable($sort)) {
$this->sort = $reverseOrder ? static function (\SplFileInfo $a, \SplFileInfo $b) use ($sort) { return -$sort($a, $b); } : $sort(...);
$this->sort = $reverseOrder ? static function (\SplFileInfo $a, \SplFileInfo $b) use ($sort) { return -$sort($a, $b); } : $sort;
} else {
throw new \InvalidArgumentException('The SortableIterator takes a PHP callable or a valid built-in sort algorithm as an argument.');
}
}
public function getIterator(): \Traversable
/**
* @return \Traversable<string, \SplFileInfo>
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
if (1 === $this->sort) {
return $this->iterator;

View File

@ -13,9 +13,6 @@ namespace Symfony\Component\Finder\Iterator;
use Symfony\Component\Finder\Gitignore;
/**
* @extends \FilterIterator<string, \SplFileInfo>
*/
final class VcsIgnoredFilterIterator extends \FilterIterator
{
/**
@ -33,20 +30,10 @@ final class VcsIgnoredFilterIterator extends \FilterIterator
*/
private $ignoredPathsCache = [];
/**
* @param \Iterator<string, \SplFileInfo> $iterator
*/
public function __construct(\Iterator $iterator, string $baseDir)
{
$this->baseDir = $this->normalizePath($baseDir);
foreach ($this->parentDirectoriesUpwards($this->baseDir) as $parentDirectory) {
if (@is_dir("{$parentDirectory}/.git")) {
$this->baseDir = $parentDirectory;
break;
}
}
parent::__construct($iterator);
}
@ -71,7 +58,7 @@ final class VcsIgnoredFilterIterator extends \FilterIterator
$ignored = false;
foreach ($this->parentDirectoriesDownwards($fileRealPath) as $parentDirectory) {
foreach ($this->parentsDirectoryDownward($fileRealPath) as $parentDirectory) {
if ($this->isIgnored($parentDirectory)) {
// rules in ignored directories are ignored, no need to check further.
break;
@ -102,11 +89,11 @@ final class VcsIgnoredFilterIterator extends \FilterIterator
/**
* @return list<string>
*/
private function parentDirectoriesUpwards(string $from): array
private function parentsDirectoryDownward(string $fileRealPath): array
{
$parentDirectories = [];
$parentDirectory = $from;
$parentDirectory = $fileRealPath;
while (true) {
$newParentDirectory = \dirname($parentDirectory);
@ -116,30 +103,16 @@ final class VcsIgnoredFilterIterator extends \FilterIterator
break;
}
$parentDirectories[] = $parentDirectory = $newParentDirectory;
$parentDirectory = $newParentDirectory;
if (0 !== strpos($parentDirectory, $this->baseDir)) {
break;
}
$parentDirectories[] = $parentDirectory;
}
return $parentDirectories;
}
private function parentDirectoriesUpTo(string $from, string $upTo): array
{
return array_filter(
$this->parentDirectoriesUpwards($from),
static function (string $directory) use ($upTo): bool {
return str_starts_with($directory, $upTo);
}
);
}
/**
* @return list<string>
*/
private function parentDirectoriesDownwards(string $fileRealPath): array
{
return array_reverse(
$this->parentDirectoriesUpTo($fileRealPath, $this->baseDir)
);
return array_reverse($parentDirectories);
}
/**

View File

@ -18,8 +18,8 @@ namespace Symfony\Component\Finder;
*/
class SplFileInfo extends \SplFileInfo
{
private string $relativePath;
private string $relativePathname;
private $relativePath;
private $relativePathname;
/**
* @param string $file The file name
@ -37,8 +37,10 @@ class SplFileInfo extends \SplFileInfo
* Returns the relative path.
*
* This path does not contain the file name.
*
* @return string
*/
public function getRelativePath(): string
public function getRelativePath()
{
return $this->relativePath;
}
@ -47,8 +49,10 @@ class SplFileInfo extends \SplFileInfo
* Returns the relative path name.
*
* This path contains the file name.
*
* @return string
*/
public function getRelativePathname(): string
public function getRelativePathname()
{
return $this->relativePathname;
}
@ -63,9 +67,11 @@ class SplFileInfo extends \SplFileInfo
/**
* Returns the contents of the file.
*
* @return string
*
* @throws \RuntimeException
*/
public function getContents(): string
public function getContents()
{
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
try {

View File

@ -16,10 +16,9 @@
}
],
"require": {
"php": ">=8.1"
},
"require-dev": {
"symfony/filesystem": "^6.0"
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/polyfill-php80": "^1.16"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Finder\\": "" },

View File

@ -0,0 +1,19 @@
Copyright (c) 2020 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,115 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Polyfill\Php80;
/**
* @author Ion Bazan <ion.bazan@gmail.com>
* @author Nico Oelgart <nicoswd@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class Php80
{
public static function fdiv(float $dividend, float $divisor): float
{
return @($dividend / $divisor);
}
public static function get_debug_type($value): string
{
switch (true) {
case null === $value: return 'null';
case \is_bool($value): return 'bool';
case \is_string($value): return 'string';
case \is_array($value): return 'array';
case \is_int($value): return 'int';
case \is_float($value): return 'float';
case \is_object($value): break;
case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class';
default:
if (null === $type = @get_resource_type($value)) {
return 'unknown';
}
if ('Unknown' === $type) {
$type = 'closed';
}
return "resource ($type)";
}
$class = \get_class($value);
if (false === strpos($class, '@')) {
return $class;
}
return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous';
}
public static function get_resource_id($res): int
{
if (!\is_resource($res) && null === @get_resource_type($res)) {
throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res)));
}
return (int) $res;
}
public static function preg_last_error_msg(): string
{
switch (preg_last_error()) {
case \PREG_INTERNAL_ERROR:
return 'Internal error';
case \PREG_BAD_UTF8_ERROR:
return 'Malformed UTF-8 characters, possibly incorrectly encoded';
case \PREG_BAD_UTF8_OFFSET_ERROR:
return 'The offset did not correspond to the beginning of a valid UTF-8 code point';
case \PREG_BACKTRACK_LIMIT_ERROR:
return 'Backtrack limit exhausted';
case \PREG_RECURSION_LIMIT_ERROR:
return 'Recursion limit exhausted';
case \PREG_JIT_STACKLIMIT_ERROR:
return 'JIT stack limit exhausted';
case \PREG_NO_ERROR:
return 'No error';
default:
return 'Unknown error';
}
}
public static function str_contains(string $haystack, string $needle): bool
{
return '' === $needle || false !== strpos($haystack, $needle);
}
public static function str_starts_with(string $haystack, string $needle): bool
{
return 0 === strncmp($haystack, $needle, \strlen($needle));
}
public static function str_ends_with(string $haystack, string $needle): bool
{
if ('' === $needle || $needle === $haystack) {
return true;
}
if ('' === $haystack) {
return false;
}
$needleLength = \strlen($needle);
return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength);
}
}

View File

@ -0,0 +1,103 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Polyfill\Php80;
/**
* @author Fedonyuk Anton <info@ensostudio.ru>
*
* @internal
*/
class PhpToken implements \Stringable
{
/**
* @var int
*/
public $id;
/**
* @var string
*/
public $text;
/**
* @var int
*/
public $line;
/**
* @var int
*/
public $pos;
public function __construct(int $id, string $text, int $line = -1, int $position = -1)
{
$this->id = $id;
$this->text = $text;
$this->line = $line;
$this->pos = $position;
}
public function getTokenName(): ?string
{
if ('UNKNOWN' === $name = token_name($this->id)) {
$name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text;
}
return $name;
}
/**
* @param int|string|array $kind
*/
public function is($kind): bool
{
foreach ((array) $kind as $value) {
if (\in_array($value, [$this->id, $this->text], true)) {
return true;
}
}
return false;
}
public function isIgnorable(): bool
{
return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true);
}
public function __toString(): string
{
return (string) $this->text;
}
/**
* @return static[]
*/
public static function tokenize(string $code, int $flags = 0): array
{
$line = 1;
$position = 0;
$tokens = token_get_all($code, $flags);
foreach ($tokens as $index => $token) {
if (\is_string($token)) {
$id = \ord($token);
$text = $token;
} else {
[$id, $text, $line] = $token;
}
$tokens[$index] = new static($id, $text, $line, $position);
$position += \strlen($text);
}
return $tokens;
}
}

View File

@ -0,0 +1,25 @@
Symfony Polyfill / Php80
========================
This component provides features added to PHP 8.0 core:
- [`Stringable`](https://php.net/stringable) interface
- [`fdiv`](https://php.net/fdiv)
- [`ValueError`](https://php.net/valueerror) class
- [`UnhandledMatchError`](https://php.net/unhandledmatcherror) class
- `FILTER_VALIDATE_BOOL` constant
- [`get_debug_type`](https://php.net/get_debug_type)
- [`PhpToken`](https://php.net/phptoken) class
- [`preg_last_error_msg`](https://php.net/preg_last_error_msg)
- [`str_contains`](https://php.net/str_contains)
- [`str_starts_with`](https://php.net/str_starts_with)
- [`str_ends_with`](https://php.net/str_ends_with)
- [`get_resource_id`](https://php.net/get_resource_id)
More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
License
=======
This library is released under the [MIT license](LICENSE).

View File

@ -0,0 +1,22 @@
<?php
#[Attribute(Attribute::TARGET_CLASS)]
final class Attribute
{
public const TARGET_CLASS = 1;
public const TARGET_FUNCTION = 2;
public const TARGET_METHOD = 4;
public const TARGET_PROPERTY = 8;
public const TARGET_CLASS_CONSTANT = 16;
public const TARGET_PARAMETER = 32;
public const TARGET_ALL = 63;
public const IS_REPEATABLE = 64;
/** @var int */
public $flags;
public function __construct(int $flags = self::TARGET_ALL)
{
$this->flags = $flags;
}
}

View File

@ -0,0 +1,7 @@
<?php
if (\PHP_VERSION_ID < 80000 && \extension_loaded('tokenizer')) {
class PhpToken extends Symfony\Polyfill\Php80\PhpToken
{
}
}

View File

@ -0,0 +1,11 @@
<?php
if (\PHP_VERSION_ID < 80000) {
interface Stringable
{
/**
* @return string
*/
public function __toString();
}
}

View File

@ -0,0 +1,7 @@
<?php
if (\PHP_VERSION_ID < 80000) {
class UnhandledMatchError extends Error
{
}
}

View File

@ -0,0 +1,7 @@
<?php
if (\PHP_VERSION_ID < 80000) {
class ValueError extends Error
{
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Php80 as p;
if (\PHP_VERSION_ID >= 80000) {
return;
}
if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) {
define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN);
}
if (!function_exists('fdiv')) {
function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); }
}
if (!function_exists('preg_last_error_msg')) {
function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); }
}
if (!function_exists('str_contains')) {
function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); }
}
if (!function_exists('str_starts_with')) {
function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); }
}
if (!function_exists('str_ends_with')) {
function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); }
}
if (!function_exists('get_debug_type')) {
function get_debug_type($value): string { return p\Php80::get_debug_type($value); }
}
if (!function_exists('get_resource_id')) {
function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); }
}

View File

@ -0,0 +1,40 @@
{
"name": "symfony/polyfill-php80",
"type": "library",
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"keywords": ["polyfill", "shim", "compatibility", "portable"],
"homepage": "https://symfony.com",
"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"
}
],
"require": {
"php": ">=7.1"
},
"autoload": {
"psr-4": { "Symfony\\Polyfill\\Php80\\": "" },
"files": [ "bootstrap.php" ],
"classmap": [ "Resources/stubs" ]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
}
}

View File

@ -463,3 +463,8 @@ form.uoj-bs4-form-compressed button {
--bs-btn-hover-bg: #d3d4d570;
--bs-btn-hover-border-color: transparent;
}
.remote-content center > img + span {
display: block;
font-size: 90%;
}