add server-side code and mobile app code
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"require": {
|
||||
"fabpot/goutte": "^4.0",
|
||||
"clio/clio": "^0.1.8"
|
||||
}
|
||||
}
|
||||
Generated
+1307
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
# API logic
|
||||
|
||||
// 1. make login request
|
||||
// 2. call the desired function (get specific page)
|
||||
// 3. parse that page into a json file
|
||||
// 4. return that json file to the caller
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
use Goutte\Client;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
$client = new Client();
|
||||
|
||||
function make_login_request($username, $password) {
|
||||
global $client;
|
||||
|
||||
$url = "https://aplikace.skolaonline.cz/SOL/Prihlaseni.aspx";
|
||||
$headers = [
|
||||
"__EVENTTARGET" => "dnn\$ctr994\$SOLLogin\$btnODeslat",
|
||||
"__EVENTARGUMENT" => "",
|
||||
"__VIEWSTATE" => "",
|
||||
"__VIEWSTATEGENERATOR" => "",
|
||||
"__VIEWSTATEENCRYPTED" => "",
|
||||
"__PREVIOUSPAGE" => "",
|
||||
"__EVENTVALIDATION" => "",
|
||||
"dnn\$dnnSearch\$txtSearch" => "",
|
||||
"JmenoUzivatele" => $username, # username
|
||||
"HesloUzivatele" => $password, # password
|
||||
"ScrollTop" => "",
|
||||
"__dnnVariable" => "",
|
||||
"__RequestVerificationToken" => ""
|
||||
];
|
||||
|
||||
$client->request("POST", $url, $headers=$headers);
|
||||
|
||||
if ($client->getResponse()->getStatusCode() != 200) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function parse_teacher($onmouseover) {
|
||||
$explode_on_teacher = explode("Učitel:", $onmouseover); // [0] => something, [1] => teacher and something, 0 can be ignored
|
||||
if ($explode_on_teacher == null) {
|
||||
$explode_on_teacher = "~ ~";
|
||||
}
|
||||
$explode_on_first_tilda = explode("~", $explode_on_teacher[1]);
|
||||
$explode_on_second_tilda = explode("~", $explode_on_first_tilda[1]);
|
||||
$teacher = $explode_on_second_tilda[0];
|
||||
if ($teacher == " ") {
|
||||
$teacher = "";
|
||||
}
|
||||
|
||||
if (strstr($teacher, ">")) {
|
||||
$teacher = explode("<", explode(">", $teacher)[1])[0];
|
||||
}
|
||||
|
||||
return $teacher;
|
||||
}
|
||||
|
||||
function get_time($times, $index) {
|
||||
foreach ($times as $cell) {
|
||||
$current_hour = explode(".", $cell->filter(".KuvHeaderNadpis")->text())[0];
|
||||
if ($index == $current_hour) {
|
||||
return explode(" - ", $cell->filter(".KuvHeaderText")->text());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parse_substitute($substitutes) {
|
||||
if ($substitutes->count() == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function parse_cell($cell, $cell_counter, $times) {
|
||||
$is_empty = false;
|
||||
if ($cell->filter(".KuvBunkaRozvrhNadpis")->count() == 0) {
|
||||
$is_empty = true;
|
||||
}
|
||||
|
||||
if (!$is_empty && $cell->filter("table")->filter(".KuvSuplovanaHodina")->count() == 0) {
|
||||
$name = $cell->filter(".KuvBunkaRozvrhNadpis")->first()->text();
|
||||
$place = explode("<br>", $cell->filter(".KuvBunkaRozvrhText")->first()->html())[1];
|
||||
if ($place == null) {
|
||||
$place = "";
|
||||
}
|
||||
$teacher = parse_teacher($cell->filter("table")->filter("td")->first()->attr("onmouseover"));
|
||||
$is_substitute = parse_substitute($cell->filter(".KuvSuplujiciHodina"));
|
||||
$number = $cell_counter;
|
||||
$start = get_time($times, $cell_counter)[0]; // gives [start, end]
|
||||
$end = get_time($times, $cell_counter)[1];
|
||||
|
||||
return [
|
||||
"name" => $name,
|
||||
"place" => $place,
|
||||
"teacher" => $teacher,
|
||||
"is_substitute" => $is_substitute,
|
||||
"number" => $number,
|
||||
"start" => $start,
|
||||
"end" => $end
|
||||
];
|
||||
} else {
|
||||
$number = $cell_counter;
|
||||
$start = get_time($times, $cell_counter)[0];
|
||||
$end = get_time($times, $cell_counter)[1];
|
||||
|
||||
return [
|
||||
"name" => "Volno",
|
||||
"place" => "Volno",
|
||||
"teacher" => "Volno",
|
||||
"is_substitute" => "Volno",
|
||||
"number" => $number,
|
||||
"start" => $start,
|
||||
"end" => $end
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function parse_cal_week($crawler) {
|
||||
$week = [];
|
||||
|
||||
$table = $crawler->filter("#CCADynamicCalendarTable");
|
||||
|
||||
$rows_ = $table->filter("tr");
|
||||
$times_ = $rows_->first()->filter("th");
|
||||
$times = [];
|
||||
$rows = [];
|
||||
|
||||
$rows_->each(function (Crawler $node, $i) use (&$rows) {
|
||||
$rows[] = $node;
|
||||
});
|
||||
array_shift($rows);
|
||||
|
||||
$times_->each(function (Crawler $node, $i) use (&$times) {
|
||||
$times[] = $node;
|
||||
});
|
||||
array_shift($times);
|
||||
|
||||
foreach ($rows as $row) { // for every day do:
|
||||
$filtered_names = $row->filter(".KuvHeaderNadpis");
|
||||
$filtered_dates = $row->filter(".KuvHeaderText");
|
||||
|
||||
if ($filtered_names->count() > 0 && $filtered_dates->count() > 0) {
|
||||
$name = $filtered_names->first()->text();
|
||||
$date = $filtered_dates->first()->text();
|
||||
$subjects = [];
|
||||
$cells_ = $row->filter(".DctCell");
|
||||
$cells = [];
|
||||
$cells_->each(function (Crawler $node, $i) use (&$cells) {
|
||||
$cells[] = $node;
|
||||
});
|
||||
|
||||
$cell_counter = 1; // start from 1
|
||||
foreach ($cells as $cell) {
|
||||
$subject = parse_cell($cell, $cell_counter, $times);
|
||||
$colspan = $cell->attr("colspan");
|
||||
$cell_counter += 1;
|
||||
if ($colspan != null) {
|
||||
array_push($subjects, $subject);
|
||||
for ($i = 1; $i < $colspan; $i++) {
|
||||
$subject = parse_cell($cell, $cell_counter, $times);
|
||||
$cell_counter += 1;
|
||||
array_push($subjects, $subject);
|
||||
}
|
||||
} else {
|
||||
array_push($subjects, $subject);
|
||||
}
|
||||
}
|
||||
|
||||
// get rid of trash lectures from the end
|
||||
while ($subjects[count($subjects)-1]["name"] == "Volno") {
|
||||
array_pop($subjects);
|
||||
}
|
||||
|
||||
$day = [
|
||||
"name" => $name,
|
||||
"date" => $date,
|
||||
"subjects" => $subjects
|
||||
];
|
||||
array_push($week, $day);
|
||||
}
|
||||
}
|
||||
|
||||
return $week;
|
||||
}
|
||||
|
||||
function get_cal_week() {
|
||||
global $client;
|
||||
|
||||
$url = "https://aplikace.skolaonline.cz/SOL/App/Kalendar/KZK001_KalendarTyden.aspx";
|
||||
|
||||
$crawler = $client->request("GET", $url);
|
||||
|
||||
if ($client->getResponse()->getStatusCode() != 200) {
|
||||
return "Could not fetch weelky calendar data";
|
||||
} else {
|
||||
return parse_cal_week($crawler);
|
||||
}
|
||||
}
|
||||
|
||||
function create_json_file($body, $status_code) {
|
||||
$json = ["body" => $body, "status_code" => $status_code];
|
||||
|
||||
return json_encode($json);
|
||||
}
|
||||
|
||||
if (!isset($_POST["username"]) || !isset($_POST["password"])) {
|
||||
echo create_json_file(json_encode("Credentials not supplied"), 1); // status 1 => failure
|
||||
} else {
|
||||
if (!make_login_request($_POST["username"], $_POST["password"])) {
|
||||
echo create_json_file("Invalid credentials", 1);
|
||||
}
|
||||
|
||||
if ($_POST["cal_week"]) {
|
||||
echo create_json_file(get_cal_week(), 0);
|
||||
}
|
||||
|
||||
if ($_POST["cal_tdy_tmr"]) {
|
||||
echo create_json_file(get_cal_tdy_tmr(), 0);
|
||||
}
|
||||
|
||||
if ($_POST["grades"]) {
|
||||
echo create_json_file(get_grades(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
$err,
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInit6f86a52ac31283ed9de8003da3458103::getLoader();
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2012 Nofriandi Ramenta
|
||||
|
||||
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.
|
||||
Vendored
+279
@@ -0,0 +1,279 @@
|
||||
# Clio
|
||||
|
||||
Clio is a lightweight utility and helper classes for CLI applications.
|
||||
It provides colored output, prompts, confirmation inputs, selections, background
|
||||
processes, as well as a way to start and stop daemons.
|
||||
|
||||
## Installation
|
||||
|
||||
The prefered way to install Clio is through [composer][Composer]; the minimum
|
||||
composer.json configuration is:
|
||||
|
||||
```
|
||||
{
|
||||
"require": {
|
||||
"clio/clio": "0.1.8"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
PHP 5.4 is required. This library is developed on and is meant to be used on
|
||||
POSIX systems with the posix, pcntl, and sockets extensions loaded.
|
||||
|
||||
## Console
|
||||
|
||||
The Console class provides helpers for interactive command line input/output.
|
||||
|
||||
### Console::stdin($raw = false)
|
||||
|
||||
Waits for user input. If `$raw` is set to `true`, returns the input without
|
||||
right trimming for `PHP_EOL`.
|
||||
|
||||
### Console::input($prompt = null, $raw = false)
|
||||
|
||||
Asks the user for input which ends when the user types a `PHP_EOL` character.
|
||||
You can optionally provide a prompt string. If `$raw` is set to `true`, returns
|
||||
the input without right trimming for `PHP_EOL`.
|
||||
|
||||
### Console::stdout($text, $raw = false)
|
||||
|
||||
Prints `$text` to STDOUT. The text can contain text color and style specifiers.
|
||||
This method detects whether the text is to be sent out to TTY or to a file
|
||||
through the use of shell redirection and acts accordingly, in the case of the
|
||||
latter, by stripping the text of all color and style specifiers.
|
||||
|
||||
If the second parameter is set to true, then it will print `$text` as is with
|
||||
all text color and style specifiers intact regardless of whether it's printing
|
||||
to TTY or to a file.
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Clio\Console;
|
||||
Console::stdout('Hello, World!');
|
||||
```
|
||||
|
||||
### Console::output($text, $raw = false)
|
||||
|
||||
The same as `Console::stdout` except it automatically appends a `PHP_EOL`.
|
||||
|
||||
### Console::stderr($text, $raw = false)
|
||||
|
||||
Behaves like `Console::stdout` except it's for STDERR.
|
||||
|
||||
### Console::error($text, $raw = false)
|
||||
|
||||
The same as `Console::stderr` except it automatically appends a `PHP_EOL`.
|
||||
|
||||
### Console::prompt($text, $options)
|
||||
|
||||
This function prompts the user for input. Several options are available:
|
||||
|
||||
- `required`: True if input is necessary, false otherwise.
|
||||
- `default`: If the user does not provide an input, this is the default value.
|
||||
- `pattern`: Regular expression pattern to match.
|
||||
- `validator`: Callable to validate input. Must return `true` or `false`.
|
||||
- `error`: Default error message.
|
||||
|
||||
If an input error occurs, the prompt will repeat and will keep asking the user
|
||||
for input until it satisfies all the requirements in the `$options` array. Note
|
||||
that if you supply a `default` option, `required` is not enforced.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$db_host = Console::prompt('database host', ['default' => 'localhost']);
|
||||
```
|
||||
|
||||
If you provide your own validator callable, you can pass a custom error message
|
||||
to the second parameter:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$file = Console::prompt('File', [
|
||||
'required' => true,
|
||||
'validator' => function($input, &$error = null) {
|
||||
if (is_readable($input)) {
|
||||
return true;
|
||||
} else {
|
||||
$error = 'Path given is not a readable file';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
Note that for this to work, the second parameter must be declared a reference.
|
||||
|
||||
### Console::confirm($text)
|
||||
|
||||
Asks the user for a simple y/n answer. The answer can be `'y'`, `'n'`, `'Y'`, or
|
||||
`'N'`. Returns either `true` or `false`.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$sure = Console::confirm('are you sure?');
|
||||
```
|
||||
|
||||
### Console::select($text, $options)
|
||||
|
||||
Asks the user to choose from a selection of options. The `$options` array is a
|
||||
key-value pairs of input and explanation. The `'?'` input option is appended
|
||||
automatically and it serves as the help option showing all other options along
|
||||
with their respective explanations.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$opt = Console::select('apply this patch?',
|
||||
['y' => 'yes', 'n' => 'no', 'a' => 'all']
|
||||
);
|
||||
```
|
||||
|
||||
### Console::work(callable $callable)
|
||||
|
||||
Forks another process to run `$callable` in the background while showing status
|
||||
updates to the standard output. By default the status update is a simple spinner
|
||||
which will stop once the `$callable` returns. By providing `$callable` with a
|
||||
`$socket` parameter, the status update is whatever is sent from the background
|
||||
process to the foreground process using the `socket_write()` function:
|
||||
|
||||
```php
|
||||
<?php
|
||||
Console::stdout('Working ... ');
|
||||
Console::work(function($socket) { // $socket is optional, defaults to a spinner
|
||||
$n = 100;
|
||||
for ($i = 1; $i <= $n; $i++) {
|
||||
// do whatever it is you need to do
|
||||
socket_write($socket, "[$i/$n]\n");
|
||||
sleep(1); // sleep is good for you
|
||||
}
|
||||
});
|
||||
Console::stdout("%g[DONE]%n\n");
|
||||
```
|
||||
|
||||
Messages sent to the foreground process needs to end with a `"\n"` character.
|
||||
|
||||
### Text color and style specifiers
|
||||
|
||||
You can use text color and style specifiers in the format of `%x` where `x` is
|
||||
the specifier:
|
||||
|
||||
```php
|
||||
<?php
|
||||
Console::output('this is %rcolored%n and %Bstyled%n');
|
||||
```
|
||||
|
||||
The `%n` specifier normalizes the color and style of the text to that of the
|
||||
shell's defaults. This specifier is taken from PEAR's Console_Color package.
|
||||
To print a percentage symbol, simply put two `%` characters. The following is
|
||||
the full set of specifiers:
|
||||
|
||||
```
|
||||
text text background
|
||||
------------------------------------------------
|
||||
%k %K %0 black dark grey black
|
||||
%r %R %1 red bold red red
|
||||
%g %G %2 green bold green green
|
||||
%y %Y %3 yellow bold yellow yellow
|
||||
%b %B %4 blue bold blue blue
|
||||
%m %M %5 magenta bold magenta magenta
|
||||
%p %P magenta (think: purple)
|
||||
%c %C %6 cyan bold cyan cyan
|
||||
%w %W %7 white bold white white
|
||||
|
||||
%F Blinking, Flashing
|
||||
%U Underline
|
||||
%8 Reverse
|
||||
%_,%9 Bold
|
||||
|
||||
%n Resets the color
|
||||
%% A single %
|
||||
```
|
||||
|
||||
You can use these specifiers with methods that takes a string and outputs it.
|
||||
|
||||
## Daemon
|
||||
|
||||
The Daemon class provides helpers for starting and killing daemonized processes.
|
||||
|
||||
### Daemon::isRunning($pid)
|
||||
|
||||
Tests if a daemon is currently running or not. Returns true or false:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Clio\Daemon;
|
||||
if (Daemon::isRunning('/path/to/process.pid')) {
|
||||
echo "daemon is running.\n";
|
||||
} else {
|
||||
echo "daemon is not running.\n";
|
||||
}
|
||||
```
|
||||
|
||||
### Daemon::work(array $options, callable $callable)
|
||||
|
||||
Daemonize a `$callable` callable object. The `$options` key-value array must
|
||||
contain `pid` as the path to the PID file:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Clio\Daemon;
|
||||
if (Daemon::isRunning('/path/to/process.pid')) {
|
||||
echo "daemon is already running.\n";
|
||||
} else {
|
||||
Daemon::work(array(
|
||||
'pid' => '/path/to/process.pid', // required
|
||||
'stdin' => '/dev/null', // defaults to /dev/null
|
||||
'stdout' => '/path/to/stdout.txt', // defaults to /dev/null
|
||||
'stderr' => '/path/to/stderr.txt', // defaults to php://stdout
|
||||
),
|
||||
function($stdin, $stdout, $stderr) { // these parameters are optional
|
||||
while (true) {
|
||||
// do whatever it is daemons do
|
||||
sleep(1); // sleep is good for you
|
||||
}
|
||||
}
|
||||
);
|
||||
echo "daemon is now running.\n";
|
||||
}
|
||||
```
|
||||
|
||||
The PID file is an ordinary text file with the process ID as its only content.
|
||||
It will be created by the library automatically if it doesn't exist. It is
|
||||
highly recommended to put a call to `sleep` to ease the system load.
|
||||
|
||||
### Daemon::kill($pid, $delete = false)
|
||||
|
||||
Kill a daemonized process:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Clio\Daemon;
|
||||
|
||||
if (Daemon::isRunning('/path/to/process.pid')) {
|
||||
echo "killing running daemon ...\n";
|
||||
if (Daemon::kill('/path/to/process.pid')) {
|
||||
echo "daemon killed.\n";
|
||||
} else {
|
||||
echo "failed killing daemon.\n";
|
||||
}
|
||||
} else {
|
||||
echo "nothing to kill.\n";
|
||||
}
|
||||
```
|
||||
|
||||
If the second parameter is set to `true`, this function will try to delete the
|
||||
PID file after successfully sending the process a kill signal.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
The text color and style specifiers are taken entirely from PEAR's Console_Color
|
||||
class by Stefan Walk. The Daemon class is heavily inspired from Andy Thompson's
|
||||
blog post on [daemonizing a PHP CLI script on a POSIX system][post].
|
||||
|
||||
## License
|
||||
|
||||
Clio is released under the [MIT License][MIT].
|
||||
|
||||
[Composer]: http://getcomposer.org/
|
||||
[MIT]: http://en.wikipedia.org/wiki/MIT_License
|
||||
[post]: http://andytson.com/blog/2010/05/daemonising-a-php-cli-script-on-a-posix-system/
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "clio/clio",
|
||||
"description": "Lightweight utility and helper classes for CLI applications",
|
||||
"keywords": ["cli", "console", "daemon"],
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nofriandi Ramenta",
|
||||
"email": "nramenta@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {"Clio": "src/"}
|
||||
}
|
||||
}
|
||||
+476
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
|
||||
namespace Clio;
|
||||
|
||||
class Console
|
||||
{
|
||||
/**
|
||||
* Text foreground colors.
|
||||
*/
|
||||
protected static $FGCOLOR = array(
|
||||
'black' => 30,
|
||||
'red' => 31,
|
||||
'green' => 32,
|
||||
'brown' => 33,
|
||||
'blue' => 34,
|
||||
'purple' => 35,
|
||||
'cyan' => 36,
|
||||
'grey' => 37,
|
||||
'yellow' => 33,
|
||||
);
|
||||
|
||||
/**
|
||||
* Text styling.
|
||||
*/
|
||||
protected static $STYLE = array(
|
||||
'normal' => 0,
|
||||
'bold' => 1,
|
||||
'light' => 1,
|
||||
'underscore' => 4,
|
||||
'underline' => 4,
|
||||
'blink' => 5,
|
||||
'inverse' => 6,
|
||||
'hidden' => 8,
|
||||
'concealed' => 8,
|
||||
);
|
||||
|
||||
/**
|
||||
* Text background color.
|
||||
*/
|
||||
protected static $BGCOLOR = array(
|
||||
'black' => 40,
|
||||
'red' => 41,
|
||||
'green' => 42,
|
||||
'brown' => 43,
|
||||
'yellow' => 43,
|
||||
'blue' => 44,
|
||||
'purple' => 45,
|
||||
'cyan' => 46,
|
||||
'grey' => 47,
|
||||
);
|
||||
|
||||
/**
|
||||
* Color specifier conversion table. Taken from PEAR's Console_Color.
|
||||
*/
|
||||
protected static $CONVERSIONS = array(
|
||||
'%y' => array('yellow', null, null),
|
||||
'%g' => array('green', null, null),
|
||||
'%b' => array('blue', null, null),
|
||||
'%r' => array('red', null, null),
|
||||
'%p' => array('purple', null, null),
|
||||
'%m' => array('purple', null, null),
|
||||
'%c' => array('cyan', null, null),
|
||||
'%w' => array('grey', null, null),
|
||||
'%k' => array('black', null, null),
|
||||
'%n' => array('reset', null, null),
|
||||
'%Y' => array('yellow', 'light', null),
|
||||
'%G' => array('green', 'light', null),
|
||||
'%B' => array('blue', 'light', null),
|
||||
'%R' => array('red', 'light', null),
|
||||
'%P' => array('purple', 'light', null),
|
||||
'%M' => array('purple', 'light', null),
|
||||
'%C' => array('cyan', 'light', null),
|
||||
'%W' => array('grey', 'light', null),
|
||||
'%K' => array('black', 'light', null),
|
||||
'%N' => array('reset', 'light', null),
|
||||
'%3' => array(null, null, 'yellow'),
|
||||
'%2' => array(null, null, 'green'),
|
||||
'%4' => array(null, null, 'blue'),
|
||||
'%1' => array(null, null, 'red'),
|
||||
'%5' => array(null, null, 'purple'),
|
||||
'%6' => array(null, null, 'cyan'),
|
||||
'%7' => array(null, null, 'grey'),
|
||||
'%0' => array(null, null, 'black'),
|
||||
// Don't use this, I can't stand flashing text
|
||||
'%F' => array(null, 'blink', null),
|
||||
'%U' => array(null, 'underline', null),
|
||||
'%8' => array(null, 'inverse', null),
|
||||
'%9' => array(null, 'bold', null),
|
||||
'%_' => array(null, 'bold', null),
|
||||
);
|
||||
|
||||
/**
|
||||
* Create ANSI-control codes for text foreground and background colors, and
|
||||
* styling.
|
||||
*
|
||||
* @param string $fgcolor Text foreground color
|
||||
* @param string $style Text style
|
||||
* @param string $bgcolor Text background color
|
||||
*
|
||||
* @return string ANSI-control code
|
||||
*/
|
||||
public static function color($fgcolor, $style, $bgcolor)
|
||||
{
|
||||
$code = array();
|
||||
if ($fgcolor == 'reset') {
|
||||
return "\033[0m";
|
||||
}
|
||||
if (isset(static::$FGCOLOR[$fgcolor])) {
|
||||
$code[] = static::$FGCOLOR[$fgcolor];
|
||||
}
|
||||
if (isset(static::$STYLE[$style])) {
|
||||
$code[] = static::$STYLE[$style];
|
||||
}
|
||||
if (isset(static::$BGCOLOR[$bgcolor])) {
|
||||
$code[] = static::$BGCOLOR[$bgcolor];
|
||||
}
|
||||
if (empty($code)) {
|
||||
$code[] = 0;
|
||||
}
|
||||
return "\033[" . implode(';', $code) . 'm';
|
||||
}
|
||||
|
||||
/**
|
||||
* Taken from PEAR's Console_Color:
|
||||
*
|
||||
* Converts colorcodes in the format %y (for yellow) into ansi-control
|
||||
* codes. The conversion table is: ('bold' meaning 'light' on some
|
||||
* terminals). It's almost the same conversion table irssi uses.
|
||||
* <pre>
|
||||
* text text background
|
||||
* ------------------------------------------------
|
||||
* %k %K %0 black dark grey black
|
||||
* %r %R %1 red bold red red
|
||||
* %g %G %2 green bold green green
|
||||
* %y %Y %3 yellow bold yellow yellow
|
||||
* %b %B %4 blue bold blue blue
|
||||
* %m %M %5 magenta bold magenta magenta
|
||||
* %p %P magenta (think: purple)
|
||||
* %c %C %6 cyan bold cyan cyan
|
||||
* %w %W %7 white bold white white
|
||||
*
|
||||
* %F Blinking, Flashing
|
||||
* %U Underline
|
||||
* %8 Reverse
|
||||
* %_,%9 Bold
|
||||
*
|
||||
* %n Resets the color
|
||||
* %% A single %
|
||||
* </pre>
|
||||
* First param is the string to convert, second is an optional flag if
|
||||
* colors should be used. It defaults to true, if set to false, the
|
||||
* colorcodes will just be removed (And %% will be transformed into %)
|
||||
*
|
||||
* @param string $text String to color
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function colorize($text, $color = true)
|
||||
{
|
||||
$text = str_replace('%%', '% ', $text);
|
||||
foreach (static::$CONVERSIONS as $key => $value) {
|
||||
list($fgcolor, $style, $bgcolor) = $value;
|
||||
$text = str_replace(
|
||||
$key,
|
||||
$color ? static::color($fgcolor, $style, $bgcolor) : '',
|
||||
$text
|
||||
);
|
||||
}
|
||||
return str_replace('% ', '%', $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips a string from color specifiers.
|
||||
*
|
||||
* @param string $text String to strip
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function decolorize($text)
|
||||
{
|
||||
return static::colorize($text, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips a string of ansi-control codes.
|
||||
*
|
||||
* @param string $text String to strip
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function strip($text)
|
||||
{
|
||||
return preg_replace('/\033\[(\d+)(;\d+)*m/', '', $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets input from STDIN and returns a string right-trimmed for EOLs.
|
||||
*
|
||||
* @param bool $raw If set to true, returns the raw string without trimming
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function stdin($raw = false)
|
||||
{
|
||||
return $raw ? fgets(STDIN) : rtrim(fgets(STDIN), PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the user for input. Ends when the user types a PHP_EOL. Optionally
|
||||
* provide a prompt.
|
||||
*
|
||||
* @param string $prompt String prompt (optional)
|
||||
* @param bool $raw If set to true, returns the raw string without trimming
|
||||
*
|
||||
* @return string User input
|
||||
*/
|
||||
public static function input($prompt = null, $raw = false)
|
||||
{
|
||||
if (isset($prompt)) {
|
||||
static::stdout($prompt);
|
||||
}
|
||||
return static::stdin($raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints text to STDOUT.
|
||||
*
|
||||
* @param string $text String to write to STDOUT
|
||||
* @param bool $raw Write string as-is; defaults to false
|
||||
*
|
||||
* @return int|false Number of bytes printed or false on error
|
||||
*/
|
||||
public static function stdout($text, $raw = false)
|
||||
{
|
||||
if ($raw) {
|
||||
return fwrite(STDOUT, $text);
|
||||
} elseif (extension_loaded('posix') && posix_isatty(STDOUT)) {
|
||||
return fwrite(STDOUT, static::colorize($text));
|
||||
} else {
|
||||
return fwrite(STDOUT, static::decolorize($text));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints text to STDOUT appended with a PHP_EOL.
|
||||
*
|
||||
* @param string $text String to write to STDOUT
|
||||
* @param bool $raw Write string as-is; defaults to false
|
||||
*
|
||||
* @return int|false Number of bytes printed or false on error
|
||||
*/
|
||||
public static function output($text = null, $raw = false)
|
||||
{
|
||||
return static::stdout($text . PHP_EOL, $raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints text to STDERR.
|
||||
*
|
||||
* @param string $text String to write to STDERR
|
||||
* @param bool $raw Write string as-is; defaults to false
|
||||
*
|
||||
* @return int|false Number of bytes printed or false on error
|
||||
*/
|
||||
public static function stderr($text, $raw = false)
|
||||
{
|
||||
if ($raw) {
|
||||
return fwrite(STDERR, $text);
|
||||
} elseif (extension_loaded('posix') && posix_isatty(STDERR)) {
|
||||
return fwrite(STDERR, static::colorize($text));
|
||||
} else {
|
||||
return fwrite(STDERR, static::decolorize($text));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints text to STDERR appended with a PHP_EOL.
|
||||
*
|
||||
* @param string $text String to write to STDERR
|
||||
* @param bool $raw Write string as-is; defaults to false
|
||||
*
|
||||
* @return int|false Number of bytes printed or false on error
|
||||
*/
|
||||
public static function error($text = null, $raw = false)
|
||||
{
|
||||
return static::stderr($text . PHP_EOL, $raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user for input
|
||||
*
|
||||
* @param string $text Prompt string
|
||||
* @param array $options Set of options
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function prompt($text, $options = array())
|
||||
{
|
||||
$options = $options + array(
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'pattern' => null,
|
||||
'validator' => null,
|
||||
'error' => 'Input unacceptable.',
|
||||
);
|
||||
|
||||
top:
|
||||
if ($options['default']) {
|
||||
$input = static::input("$text [" . $options['default'] . ']: ');
|
||||
} else {
|
||||
$input = static::input("$text: ");
|
||||
}
|
||||
|
||||
if (!strlen($input)) {
|
||||
if (isset($options['default'])) {
|
||||
$input = $options['default'];
|
||||
} elseif ($options['required']) {
|
||||
static::output($options['error']);
|
||||
goto top;
|
||||
}
|
||||
} elseif ($options['pattern'] && !preg_match($options['pattern'], $input)) {
|
||||
static::output($options['error']);
|
||||
goto top;
|
||||
} elseif ($options['validator'] &&
|
||||
!call_user_func_array($options['validator'], array($input, &$error))) {
|
||||
static::output(isset($error) ? $error : $options['error']);
|
||||
goto top;
|
||||
}
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the user for a simple yes/no confirmation.
|
||||
*
|
||||
* @param string $text Prompt string
|
||||
*
|
||||
* @return bool Either true or false
|
||||
*/
|
||||
public static function confirm($text)
|
||||
{
|
||||
top:
|
||||
$input = strtolower(static::input("$text [y/n]: "));
|
||||
if (!in_array($input, array('y', 'n'))) goto top;
|
||||
return $input === 'y' ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives the user an option to choose from. Giving '?' as an input will show
|
||||
* a list of options to choose from and their explanations.
|
||||
*
|
||||
* @param string $text Prompt string
|
||||
* @param array $options Key-value array of options to choose from
|
||||
*
|
||||
* @return string An option character the user chose
|
||||
*/
|
||||
public static function select($text, $options = array())
|
||||
{
|
||||
top:
|
||||
static::stdout("$text [" . implode(',', array_keys($options)) . ",?]: ");
|
||||
$input = static::stdin();
|
||||
if ($input === '?') {
|
||||
foreach ($options as $key => $value) {
|
||||
echo " $key - $value\n";
|
||||
}
|
||||
echo " ? - Show help\n";
|
||||
goto top;
|
||||
} elseif (!in_array($input, array_keys($options))) goto top;
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Closure as another process in the background while showing a
|
||||
* status update. The status update can be an indefinite spinner or a string
|
||||
* periodically sent from the background process, depending on whether the
|
||||
* provided Closure object has a $socket parameter or not. Messaging to the
|
||||
* main process is done by socket_* functions. The return value is either
|
||||
* the return value of the background process, or false if the process fork
|
||||
* failed.
|
||||
*
|
||||
* @throws \Exception
|
||||
*
|
||||
* @param callable $callable Closure object
|
||||
*
|
||||
* @return int|false Process exit status
|
||||
*/
|
||||
public static function work(callable $callable)
|
||||
{
|
||||
if (!extension_loaded('pcntl')) {
|
||||
throw new \Exception('pcntl extension required');
|
||||
}
|
||||
|
||||
if (!extension_loaded('sockets')) {
|
||||
throw new \Exception('sockets extension required');
|
||||
}
|
||||
|
||||
$spinner = array('|', '/', '-', '\\');
|
||||
$i = 0; $l = count($spinner);
|
||||
$delay = 100000;
|
||||
|
||||
$func = new \ReflectionFunction($callable);
|
||||
|
||||
$socket = (bool)$func->getNumberOfParameters();
|
||||
|
||||
if ($socket) {
|
||||
$sockets = array();
|
||||
if (socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$pid = pcntl_fork();
|
||||
|
||||
if ($pid > 0) {
|
||||
$done = false;
|
||||
$retval = 0;
|
||||
pcntl_signal(SIGCHLD, function() use ($pid, &$done, &$retval) {
|
||||
$child_pid = pcntl_waitpid($pid, $status);
|
||||
if (pcntl_wifexited($status)) {
|
||||
$retval = pcntl_wexitstatus($status);
|
||||
}
|
||||
$done = true;
|
||||
});
|
||||
|
||||
if ($socket) {
|
||||
$text = '';
|
||||
while (!$done) {
|
||||
$r = array($sockets[1]);
|
||||
$w = null;
|
||||
$e = null;
|
||||
if ($status = socket_select($r, $w, $e, 0)) {
|
||||
$data = socket_read($sockets[1], 4096, PHP_NORMAL_READ);
|
||||
if ($data === false) {
|
||||
throw new \Exception(
|
||||
sprintf(
|
||||
'socket write error %s',
|
||||
socket_strerror(socket_last_error($sockets[1]))
|
||||
)
|
||||
);
|
||||
}
|
||||
echo str_repeat(chr(8), strlen($text));
|
||||
$text = rtrim($data, "\n");
|
||||
Console::stdout($text);
|
||||
} else {
|
||||
pcntl_signal_dispatch();
|
||||
}
|
||||
usleep($delay);
|
||||
}
|
||||
echo str_repeat(chr(8), strlen($text));
|
||||
socket_close($sockets[0]);
|
||||
socket_close($sockets[1]);
|
||||
} else {
|
||||
while (!$done) {
|
||||
pcntl_signal_dispatch();
|
||||
echo $spinner[$i];
|
||||
usleep($delay);
|
||||
echo chr(8);
|
||||
$i = $i === $l - 1 ? 0 : $i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $retval;
|
||||
} elseif ($pid === 0) {
|
||||
if ($socket) {
|
||||
call_user_func($callable, $sockets[0]);
|
||||
} else {
|
||||
call_user_func($callable);
|
||||
}
|
||||
exit;
|
||||
} else {
|
||||
// Unable to fork process.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Clio;
|
||||
|
||||
class Daemon
|
||||
{
|
||||
/**
|
||||
* Daemonize a Closure object.
|
||||
*
|
||||
* @throws \Exception
|
||||
*
|
||||
* @param array $options Set of options
|
||||
* @param callable $callable Closure object to daemonize
|
||||
*
|
||||
* @return bool True on success, throws an Exception otherwise
|
||||
*/
|
||||
public static function work(array $options, callable $callable)
|
||||
{
|
||||
if (!extension_loaded('pcntl')) {
|
||||
throw new \Exception('pcntl extension required');
|
||||
}
|
||||
|
||||
if (!extension_loaded('posix')) {
|
||||
throw new \Exception('posix extension required');
|
||||
}
|
||||
|
||||
if (!isset($options['pid'])) {
|
||||
throw new \Exception('pid not specified');
|
||||
}
|
||||
|
||||
$options = $options + array(
|
||||
'stdin' => '/dev/null',
|
||||
'stdout' => '/dev/null',
|
||||
'stderr' => 'php://stdout',
|
||||
);
|
||||
|
||||
if (($lock = @fopen($options['pid'], 'c+')) === false) {
|
||||
throw new \Exception('unable to open pid file ' . $options['pid']);
|
||||
}
|
||||
|
||||
if (!flock($lock, LOCK_EX | LOCK_NB)) {
|
||||
throw new \Exception('could not acquire lock for ' . $options['pid']);
|
||||
}
|
||||
|
||||
switch ($pid = pcntl_fork()) {
|
||||
case -1:
|
||||
throw new \Exception('unable to fork');
|
||||
case 0:
|
||||
break;
|
||||
default:
|
||||
fseek($lock, 0);
|
||||
ftruncate($lock, 0);
|
||||
fwrite($lock ,$pid);
|
||||
fflush($lock);
|
||||
return;
|
||||
}
|
||||
|
||||
if (posix_setsid() === -1) {
|
||||
throw new \Exception('failed to setsid');
|
||||
}
|
||||
|
||||
fclose(STDIN);
|
||||
fclose(STDOUT);
|
||||
fclose(STDERR);
|
||||
|
||||
if (!($stdin = fopen($options['stdin'], 'r'))) {
|
||||
throw new \Exception('failed to open STDIN ' . $options['stdin']);
|
||||
}
|
||||
|
||||
if (!($stdout = fopen($options['stdout'], 'w'))) {
|
||||
throw new \Exception('failed to open STDOUT ' . $options['stdout']);
|
||||
}
|
||||
|
||||
if (!($stderr = fopen($options['stderr'], 'w'))) {
|
||||
throw new \Exception('failed to open STDERR ' . $options['stderr']);
|
||||
}
|
||||
|
||||
pcntl_signal(SIGTSTP, SIG_IGN);
|
||||
pcntl_signal(SIGTTOU, SIG_IGN);
|
||||
pcntl_signal(SIGTTIN, SIG_IGN);
|
||||
pcntl_signal(SIGHUP, SIG_IGN);
|
||||
|
||||
call_user_func($callable, $stdin, $stdout, $stderr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a daemon process specified by its PID file is running.
|
||||
*
|
||||
* @throws \Exception
|
||||
*
|
||||
* @param string $file Daemon PID file
|
||||
*
|
||||
* @return bool True if the daemon is still running, false otherwise
|
||||
*/
|
||||
public static function isRunning($file)
|
||||
{
|
||||
if (!extension_loaded('posix')) {
|
||||
throw new \Exception('posix extension required');
|
||||
}
|
||||
|
||||
if (!is_readable($file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($lock = @fopen($file, 'c+')) === false) {
|
||||
throw new \Exception('unable to open pid file ' . $file);
|
||||
}
|
||||
|
||||
if (flock($lock, LOCK_EX | LOCK_NB)) {
|
||||
return false;
|
||||
} else {
|
||||
flock($lock, LOCK_UN);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills a daemon process specified by its PID file.
|
||||
*
|
||||
* @throws \Exception
|
||||
*
|
||||
* @param string $file Daemon PID file
|
||||
* @param bool $delete Flag to delete PID file after killing
|
||||
*
|
||||
* @return bool True on success, false otherwise
|
||||
*/
|
||||
public static function kill($file, $delete = false)
|
||||
{
|
||||
if (!extension_loaded('posix')) {
|
||||
throw new \Exception('posix extension required');
|
||||
}
|
||||
|
||||
if (!is_readable($file)) {
|
||||
throw new \Exception('unreadable pid file ' . $file);
|
||||
}
|
||||
|
||||
if (($lock = @fopen($file, 'c+')) === false) {
|
||||
throw new \Exception('unable to open pid file ' . $file);
|
||||
}
|
||||
|
||||
if (flock($lock, LOCK_EX | LOCK_NB)) {
|
||||
flock($lock, LOCK_UN);
|
||||
throw new \Exception('process not running');
|
||||
}
|
||||
|
||||
$pid = fgets($lock);
|
||||
|
||||
if (posix_kill($pid, SIGTERM)) {
|
||||
if ($delete) unlink($file);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+579
@@ -0,0 +1,579 @@
|
||||
<?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\Autoload;
|
||||
|
||||
/**
|
||||
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||
*
|
||||
* $loader = new \Composer\Autoload\ClassLoader();
|
||||
*
|
||||
* // register classes with namespaces
|
||||
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||
* $loader->add('Symfony', __DIR__.'/framework');
|
||||
*
|
||||
* // activate the autoloader
|
||||
* $loader->register();
|
||||
*
|
||||
* // to enable searching the include path (eg. for PEAR packages)
|
||||
* $loader->setUseIncludePath(true);
|
||||
*
|
||||
* In this example, if you try to use a class in the Symfony\Component
|
||||
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||
* the autoloader will first look for the class under the component/
|
||||
* directory, and it will then fallback to the framework/ directory if not
|
||||
* found before giving up.
|
||||
*
|
||||
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @see https://www.php-fig.org/psr/psr-0/
|
||||
* @see https://www.php-fig.org/psr/psr-4/
|
||||
*/
|
||||
class ClassLoader
|
||||
{
|
||||
/** @var \Closure(string):void */
|
||||
private static $includeFile;
|
||||
|
||||
/** @var string|null */
|
||||
private $vendorDir;
|
||||
|
||||
// PSR-4
|
||||
/**
|
||||
* @var array<string, array<string, int>>
|
||||
*/
|
||||
private $prefixLengthsPsr4 = array();
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private $prefixDirsPsr4 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
/**
|
||||
* List of PSR-0 prefixes
|
||||
*
|
||||
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
||||
*
|
||||
* @var array<string, array<string, list<string>>>
|
||||
*/
|
||||
private $prefixesPsr0 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
/** @var bool */
|
||||
private $useIncludePath = false;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $classMap = array();
|
||||
|
||||
/** @var bool */
|
||||
private $classMapAuthoritative = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $missingClasses = array();
|
||||
|
||||
/** @var string|null */
|
||||
private $apcuPrefix;
|
||||
|
||||
/**
|
||||
* @var array<string, self>
|
||||
*/
|
||||
private static $registeredLoaders = array();
|
||||
|
||||
/**
|
||||
* @param string|null $vendorDir
|
||||
*/
|
||||
public function __construct($vendorDir = null)
|
||||
{
|
||||
$this->vendorDir = $vendorDir;
|
||||
self::initializeIncludeClosure();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> Array of classname => path
|
||||
*/
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $classMap Class to filename map
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addClassMap(array $classMap)
|
||||
{
|
||||
if ($this->classMap) {
|
||||
$this->classMap = array_merge($this->classMap, $classMap);
|
||||
} else {
|
||||
$this->classMap = $classMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 list<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)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr0
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$this->fallbackDirsPsr0,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first = $prefix[0];
|
||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($prepend) {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixesPsr0[$first][$prefix]
|
||||
);
|
||||
} else {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$this->prefixesPsr0[$first][$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 list<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)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
// Register directories for the root namespace.
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr4
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$this->fallbackDirsPsr4,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||
// Register directories for a new namespace.
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = $paths;
|
||||
} elseif ($prepend) {
|
||||
// Prepend directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixDirsPsr4[$prefix]
|
||||
);
|
||||
} else {
|
||||
// Append directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$this->prefixDirsPsr4[$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 list<string>|string $paths The PSR-0 base directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr0 = (array) $paths;
|
||||
} else {
|
||||
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 list<string>|string $paths The PSR-4 base directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPsr4($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr4 = (array) $paths;
|
||||
} else {
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on searching the include path for class files.
|
||||
*
|
||||
* @param bool $useIncludePath
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUseIncludePath($useIncludePath)
|
||||
{
|
||||
$this->useIncludePath = $useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to check if the autoloader uses the include path to check
|
||||
* for classes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUseIncludePath()
|
||||
{
|
||||
return $this->useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off searching the prefix and fallback directories for classes
|
||||
* that have not been registered with the class map.
|
||||
*
|
||||
* @param bool $classMapAuthoritative
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||
{
|
||||
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should class lookup fail if not found in the current class map?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isClassMapAuthoritative()
|
||||
{
|
||||
return $this->classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getApcuPrefix()
|
||||
{
|
||||
return $this->apcuPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 true|null True if loaded, null otherwise
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
if ($file = $this->findFile($class)) {
|
||||
$includeFile = self::$includeFile;
|
||||
$includeFile($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the file where the class is defined.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
*
|
||||
* @return string|false The path if found, false otherwise
|
||||
*/
|
||||
public function findFile($class)
|
||||
{
|
||||
// class map lookup
|
||||
if (isset($this->classMap[$class])) {
|
||||
return $this->classMap[$class];
|
||||
}
|
||||
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||
return false;
|
||||
}
|
||||
if (null !== $this->apcuPrefix) {
|
||||
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||
if ($hit) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
$file = $this->findFileWithExtension($class, '.php');
|
||||
|
||||
// Search for Hack files if we are running on HHVM
|
||||
if (false === $file && defined('HHVM_VERSION')) {
|
||||
$file = $this->findFileWithExtension($class, '.hh');
|
||||
}
|
||||
|
||||
if (null !== $this->apcuPrefix) {
|
||||
apcu_add($this->apcuPrefix.$class, $file);
|
||||
}
|
||||
|
||||
if (false === $file) {
|
||||
// Remember that this class does not exist.
|
||||
$this->missingClasses[$class] = true;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
||||
*
|
||||
* @return array<string, 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
|
||||
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||
|
||||
$first = $class[0];
|
||||
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||
$subPath = $class;
|
||||
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||
$subPath = substr($subPath, 0, $lastPos);
|
||||
$search = $subPath . '\\';
|
||||
if (isset($this->prefixDirsPsr4[$search])) {
|
||||
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||
if (file_exists($file = $dir . $pathEnd)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-4 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 lookup
|
||||
if (false !== $pos = strrpos($class, '\\')) {
|
||||
// namespaced class name
|
||||
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||
} else {
|
||||
// PEAR-like class name
|
||||
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||
}
|
||||
|
||||
if (isset($this->prefixesPsr0[$first])) {
|
||||
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||
if (0 === strpos($class, $prefix)) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 include paths.
|
||||
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private static function initializeIncludeClosure()
|
||||
{
|
||||
if (self::$includeFile !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
self::$includeFile = \Closure::bind(static function($file) {
|
||||
include $file;
|
||||
}, null, null);
|
||||
}
|
||||
}
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
<?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`
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private static $canGetVendors;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: 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 || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||
}
|
||||
}
|
||||
|
||||
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((string) $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, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||
*/
|
||||
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, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: 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, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: 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, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||
*/
|
||||
public static function reload($data)
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: 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')) {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
$installed[] = self::$installedByVendor[$vendorDir] = $required;
|
||||
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') {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require __DIR__ . '/installed.php';
|
||||
self::$installed = $required;
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array()) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
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.
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
// autoload_classmap.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
|
||||
);
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
// autoload_files.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
|
||||
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
|
||||
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
|
||||
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
||||
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
|
||||
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
|
||||
);
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Clio' => array($vendorDir . '/clio/clio/src'),
|
||||
);
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'),
|
||||
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
|
||||
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
|
||||
'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'),
|
||||
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
|
||||
'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'),
|
||||
'Symfony\\Contracts\\HttpClient\\' => array($vendorDir . '/symfony/http-client-contracts'),
|
||||
'Symfony\\Component\\Mime\\' => array($vendorDir . '/symfony/mime'),
|
||||
'Symfony\\Component\\HttpClient\\' => array($vendorDir . '/symfony/http-client'),
|
||||
'Symfony\\Component\\DomCrawler\\' => array($vendorDir . '/symfony/dom-crawler'),
|
||||
'Symfony\\Component\\CssSelector\\' => array($vendorDir . '/symfony/css-selector'),
|
||||
'Symfony\\Component\\BrowserKit\\' => array($vendorDir . '/symfony/browser-kit'),
|
||||
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
|
||||
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
|
||||
'Masterminds\\' => array($vendorDir . '/masterminds/html5/src'),
|
||||
'Goutte\\' => array($vendorDir . '/fabpot/goutte/Goutte'),
|
||||
);
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInit6f86a52ac31283ed9de8003da3458103
|
||||
{
|
||||
private static $loader;
|
||||
|
||||
public static function loadClassLoader($class)
|
||||
{
|
||||
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||
require __DIR__ . '/ClassLoader.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Composer\Autoload\ClassLoader
|
||||
*/
|
||||
public static function getLoader()
|
||||
{
|
||||
if (null !== self::$loader) {
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
require __DIR__ . '/platform_check.php';
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInit6f86a52ac31283ed9de8003da3458103', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInit6f86a52ac31283ed9de8003da3458103', 'loadClassLoader'));
|
||||
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInit6f86a52ac31283ed9de8003da3458103::getInitializer($loader));
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
$filesToLoad = \Composer\Autoload\ComposerStaticInit6f86a52ac31283ed9de8003da3458103::$files;
|
||||
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
|
||||
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
|
||||
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
|
||||
|
||||
require $file;
|
||||
}
|
||||
}, null, null);
|
||||
foreach ($filesToLoad as $fileIdentifier => $file) {
|
||||
$requireFile($fileIdentifier, $file);
|
||||
}
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
// autoload_static.php @generated by Composer
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
class ComposerStaticInit6f86a52ac31283ed9de8003da3458103
|
||||
{
|
||||
public static $files = array (
|
||||
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
|
||||
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
|
||||
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
|
||||
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
||||
'25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
|
||||
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
|
||||
);
|
||||
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'S' =>
|
||||
array (
|
||||
'Symfony\\Polyfill\\Php72\\' => 23,
|
||||
'Symfony\\Polyfill\\Mbstring\\' => 26,
|
||||
'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33,
|
||||
'Symfony\\Polyfill\\Intl\\Idn\\' => 26,
|
||||
'Symfony\\Polyfill\\Ctype\\' => 23,
|
||||
'Symfony\\Contracts\\Service\\' => 26,
|
||||
'Symfony\\Contracts\\HttpClient\\' => 29,
|
||||
'Symfony\\Component\\Mime\\' => 23,
|
||||
'Symfony\\Component\\HttpClient\\' => 29,
|
||||
'Symfony\\Component\\DomCrawler\\' => 29,
|
||||
'Symfony\\Component\\CssSelector\\' => 30,
|
||||
'Symfony\\Component\\BrowserKit\\' => 29,
|
||||
),
|
||||
'P' =>
|
||||
array (
|
||||
'Psr\\Log\\' => 8,
|
||||
'Psr\\Container\\' => 14,
|
||||
),
|
||||
'M' =>
|
||||
array (
|
||||
'Masterminds\\' => 12,
|
||||
),
|
||||
'G' =>
|
||||
array (
|
||||
'Goutte\\' => 7,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'Symfony\\Polyfill\\Php72\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-php72',
|
||||
),
|
||||
'Symfony\\Polyfill\\Mbstring\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
|
||||
),
|
||||
'Symfony\\Polyfill\\Intl\\Normalizer\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer',
|
||||
),
|
||||
'Symfony\\Polyfill\\Intl\\Idn\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn',
|
||||
),
|
||||
'Symfony\\Polyfill\\Ctype\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
|
||||
),
|
||||
'Symfony\\Contracts\\Service\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/service-contracts',
|
||||
),
|
||||
'Symfony\\Contracts\\HttpClient\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/http-client-contracts',
|
||||
),
|
||||
'Symfony\\Component\\Mime\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/mime',
|
||||
),
|
||||
'Symfony\\Component\\HttpClient\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/http-client',
|
||||
),
|
||||
'Symfony\\Component\\DomCrawler\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/dom-crawler',
|
||||
),
|
||||
'Symfony\\Component\\CssSelector\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/css-selector',
|
||||
),
|
||||
'Symfony\\Component\\BrowserKit\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/browser-kit',
|
||||
),
|
||||
'Psr\\Log\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/log/src',
|
||||
),
|
||||
'Psr\\Container\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/container/src',
|
||||
),
|
||||
'Masterminds\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/masterminds/html5/src',
|
||||
),
|
||||
'Goutte\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/fabpot/goutte/Goutte',
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixesPsr0 = array (
|
||||
'C' =>
|
||||
array (
|
||||
'Clio' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/clio/clio/src',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
{
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->prefixLengthsPsr4 = ComposerStaticInit6f86a52ac31283ed9de8003da3458103::$prefixLengthsPsr4;
|
||||
$loader->prefixDirsPsr4 = ComposerStaticInit6f86a52ac31283ed9de8003da3458103::$prefixDirsPsr4;
|
||||
$loader->prefixesPsr0 = ComposerStaticInit6f86a52ac31283ed9de8003da3458103::$prefixesPsr0;
|
||||
$loader->classMap = ComposerStaticInit6f86a52ac31283ed9de8003da3458103::$classMap;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
}
|
||||
}
|
||||
+1348
File diff suppressed because it is too large
Load Diff
Vendored
+209
@@ -0,0 +1,209 @@
|
||||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => '__root__',
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '5aff4ac5a61935ccb58762958f7f3a39bf9370f9',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev' => true,
|
||||
),
|
||||
'versions' => array(
|
||||
'__root__' => array(
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '5aff4ac5a61935ccb58762958f7f3a39bf9370f9',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'clio/clio' => array(
|
||||
'pretty_version' => '0.1.8',
|
||||
'version' => '0.1.8.0',
|
||||
'reference' => '6a9f600b73fa213e817d5c1e67a4641aecd156ef',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clio/clio',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'fabpot/goutte' => array(
|
||||
'pretty_version' => 'v4.0.3',
|
||||
'version' => '4.0.3.0',
|
||||
'reference' => 'e3f28671c87a48a0f13ada1baea0d95acc2138c3',
|
||||
'type' => 'application',
|
||||
'install_path' => __DIR__ . '/../fabpot/goutte',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'masterminds/html5' => array(
|
||||
'pretty_version' => '2.8.1',
|
||||
'version' => '2.8.1.0',
|
||||
'reference' => 'f47dcf3c70c584de14f21143c55d9939631bc6cf',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../masterminds/html5',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'php-http/async-client-implementation' => array(
|
||||
'dev_requirement' => false,
|
||||
'provided' => array(
|
||||
0 => '*',
|
||||
),
|
||||
),
|
||||
'php-http/client-implementation' => array(
|
||||
'dev_requirement' => false,
|
||||
'provided' => array(
|
||||
0 => '*',
|
||||
),
|
||||
),
|
||||
'psr/container' => array(
|
||||
'pretty_version' => '2.0.2',
|
||||
'version' => '2.0.2.0',
|
||||
'reference' => 'c71ecc56dfe541dbd90c5360474fbc405f8d5963',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../psr/container',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/http-client-implementation' => array(
|
||||
'dev_requirement' => false,
|
||||
'provided' => array(
|
||||
0 => '1.0',
|
||||
),
|
||||
),
|
||||
'psr/log' => array(
|
||||
'pretty_version' => '3.0.0',
|
||||
'version' => '3.0.0.0',
|
||||
'reference' => 'fe5ea303b0887d5caefd3d431c3e61ad47037001',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../psr/log',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/browser-kit' => array(
|
||||
'pretty_version' => 'v6.4.0',
|
||||
'version' => '6.4.0.0',
|
||||
'reference' => 'a3bb210e001580ec75e1d02b27fae3452e6bf502',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/browser-kit',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/css-selector' => array(
|
||||
'pretty_version' => 'v6.4.0',
|
||||
'version' => '6.4.0.0',
|
||||
'reference' => 'd036c6c0d0b09e24a14a35f8292146a658f986e4',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/css-selector',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/deprecation-contracts' => array(
|
||||
'pretty_version' => 'v3.4.0',
|
||||
'version' => '3.4.0.0',
|
||||
'reference' => '7c3aff79d10325257a001fcf92d991f24fc967cf',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/dom-crawler' => array(
|
||||
'pretty_version' => 'v6.4.0',
|
||||
'version' => '6.4.0.0',
|
||||
'reference' => '14ff4fd2a5c8969d6158dbe7ef5b17d6a9c6ba33',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/dom-crawler',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/http-client' => array(
|
||||
'pretty_version' => 'v6.4.2',
|
||||
'version' => '6.4.2.0',
|
||||
'reference' => 'fc0944665bd932cf32a7b8a1d009466afc16528f',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/http-client',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/http-client-contracts' => array(
|
||||
'pretty_version' => 'v3.4.0',
|
||||
'version' => '3.4.0.0',
|
||||
'reference' => '1ee70e699b41909c209a0c930f11034b93578654',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/http-client-contracts',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/http-client-implementation' => array(
|
||||
'dev_requirement' => false,
|
||||
'provided' => array(
|
||||
0 => '3.0',
|
||||
),
|
||||
),
|
||||
'symfony/mime' => array(
|
||||
'pretty_version' => 'v6.4.0',
|
||||
'version' => '6.4.0.0',
|
||||
'reference' => 'ca4f58b2ef4baa8f6cecbeca2573f88cd577d205',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/mime',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-ctype' => array(
|
||||
'pretty_version' => 'v1.28.0',
|
||||
'version' => '1.28.0.0',
|
||||
'reference' => 'ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-intl-idn' => array(
|
||||
'pretty_version' => 'v1.28.0',
|
||||
'version' => '1.28.0.0',
|
||||
'reference' => 'ecaafce9f77234a6a449d29e49267ba10499116d',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-intl-idn',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-intl-normalizer' => array(
|
||||
'pretty_version' => 'v1.28.0',
|
||||
'version' => '1.28.0.0',
|
||||
'reference' => '8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-mbstring' => array(
|
||||
'pretty_version' => 'v1.28.0',
|
||||
'version' => '1.28.0.0',
|
||||
'reference' => '42292d99c55abe617799667f454222c54c60e229',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-php72' => array(
|
||||
'pretty_version' => 'v1.28.0',
|
||||
'version' => '1.28.0.0',
|
||||
'reference' => '70f4aebd92afca2f865444d30a4d2151c13c3179',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-php72',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/service-contracts' => array(
|
||||
'pretty_version' => 'v3.4.1',
|
||||
'version' => '3.4.1.0',
|
||||
'reference' => 'fe07cbc8d837f60caf7018068e350cc5163681a0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/service-contracts',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
// platform_check.php @generated by Composer
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 80100)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". 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
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
composer.lock
|
||||
phpunit.xml
|
||||
vendor
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Goutte 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 Goutte;
|
||||
|
||||
use Symfony\Component\BrowserKit\CookieJar;
|
||||
use Symfony\Component\BrowserKit\History;
|
||||
use Symfony\Component\BrowserKit\HttpBrowser;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @deprecated Use Symfony\Component\BrowserKit\HttpBrowser directly instead
|
||||
*/
|
||||
class Client extends HttpBrowser
|
||||
{
|
||||
public function __construct(HttpClientInterface $client = null, History $history = null, CookieJar $cookieJar = null)
|
||||
{
|
||||
trigger_deprecation('fabpot/goutte', '4.0', 'The "%s" class is deprecated, use "%s" instead.', __CLASS__, HttpBrowser::class);
|
||||
|
||||
parent::__construct($client, $history, $cookieJar);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Goutte utility.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
require_once 'phar://'.__FILE__.'/vendor/autoload.php';
|
||||
|
||||
__HALT_COMPILER();
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Goutte 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 Goutte\Tests;
|
||||
|
||||
use Goutte\Client;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\BrowserKit\HttpBrowser;
|
||||
|
||||
class ClientTest extends TestCase
|
||||
{
|
||||
public function testNew()
|
||||
{
|
||||
$client = new Client();
|
||||
$this->assertInstanceOf(HttpBrowser::class, $client);
|
||||
}
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2010-present 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.
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
Goutte, a simple PHP Web Scraper
|
||||
================================
|
||||
|
||||
Goutte is a screen scraping and web crawling library for PHP.
|
||||
|
||||
Goutte provides a nice API to crawl websites and extract data from the HTML/XML
|
||||
responses.
|
||||
|
||||
**WARNING**: This library is deprecated. As of v4, Goutte became a simple proxy
|
||||
to the `HttpBrowser class
|
||||
<https://symfony.com/doc/current/components/browser_kit.html#making-external-http-requests>`_
|
||||
from the `Symfony BrowserKit <https://symfony.com/browser-kit>`_ component. To
|
||||
migrate, replace ``Goutte\\Client`` by
|
||||
``Symfony\\Component\\BrowserKit\\HttpBrowser`` in your code.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Goutte depends on PHP 7.1+.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Add ``fabpot/goutte`` as a require dependency in your ``composer.json`` file:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
composer require fabpot/goutte
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Create a Goutte Client instance (which extends
|
||||
``Symfony\Component\BrowserKit\HttpBrowser``):
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
use Goutte\Client;
|
||||
|
||||
$client = new Client();
|
||||
|
||||
Make requests with the ``request()`` method:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Go to the symfony.com website
|
||||
$crawler = $client->request('GET', 'https://www.symfony.com/blog/');
|
||||
|
||||
The method returns a ``Crawler`` object
|
||||
(``Symfony\Component\DomCrawler\Crawler``).
|
||||
|
||||
To use your own HTTP settings, you may create and pass an HttpClient
|
||||
instance to Goutte. For example, to add a 60 second request timeout:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
use Goutte\Client;
|
||||
use Symfony\Component\HttpClient\HttpClient;
|
||||
|
||||
$client = new Client(HttpClient::create(['timeout' => 60]));
|
||||
|
||||
Click on links:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Click on the "Security Advisories" link
|
||||
$link = $crawler->selectLink('Security Advisories')->link();
|
||||
$crawler = $client->click($link);
|
||||
|
||||
Extract data:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// Get the latest post in this category and display the titles
|
||||
$crawler->filter('h2 > a')->each(function ($node) {
|
||||
print $node->text()."\n";
|
||||
});
|
||||
|
||||
Submit forms:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$crawler = $client->request('GET', 'https://github.com/');
|
||||
$crawler = $client->click($crawler->selectLink('Sign in')->link());
|
||||
$form = $crawler->selectButton('Sign in')->form();
|
||||
$crawler = $client->submit($form, ['login' => 'fabpot', 'password' => 'xxxxxx']);
|
||||
$crawler->filter('.flash-error')->each(function ($node) {
|
||||
print $node->text()."\n";
|
||||
});
|
||||
|
||||
More Information
|
||||
----------------
|
||||
|
||||
Read the documentation of the `BrowserKit`_, `DomCrawler`_, and `HttpClient`_
|
||||
Symfony Components for more information about what you can do with Goutte.
|
||||
|
||||
Pronunciation
|
||||
-------------
|
||||
|
||||
Goutte is pronounced ``goot`` i.e. it rhymes with ``boot`` and not ``out``.
|
||||
|
||||
Technical Information
|
||||
---------------------
|
||||
|
||||
Goutte is a thin wrapper around the following Symfony Components:
|
||||
`BrowserKit`_, `CssSelector`_, `DomCrawler`_, and `HttpClient`_.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Goutte is licensed under the MIT license.
|
||||
|
||||
.. _`Composer`: https://getcomposer.org
|
||||
.. _`BrowserKit`: https://symfony.com/components/BrowserKit
|
||||
.. _`DomCrawler`: https://symfony.com/doc/current/components/dom_crawler.html
|
||||
.. _`CssSelector`: https://symfony.com/doc/current/components/css_selector.html
|
||||
.. _`HttpClient`: https://symfony.com/doc/current/components/http_client.html
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"output": "goutte.phar",
|
||||
"chmod": "0755",
|
||||
"compactors": [
|
||||
"Herrera\\Box\\Compactor\\Php"
|
||||
],
|
||||
"extract": false,
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"Goutte/Client.php"
|
||||
],
|
||||
"finder": [
|
||||
{
|
||||
"name": ["*.php", "*.pem*"],
|
||||
"exclude": ["Tests", "tests"],
|
||||
"in": "vendor"
|
||||
}
|
||||
],
|
||||
"stub": "Goutte/Resources/phar-stub.php",
|
||||
"web": false
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "fabpot/goutte",
|
||||
"type": "application",
|
||||
"description": "A simple PHP Web Scraper",
|
||||
"keywords": ["scraper"],
|
||||
"homepage": "https://github.com/FriendsOfPHP/Goutte",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1.3",
|
||||
"symfony/deprecation-contracts": "^2.1|^3",
|
||||
"symfony/browser-kit": "^4.4|^5.0|^6.0",
|
||||
"symfony/css-selector": "^4.4|^5.0|^6.0",
|
||||
"symfony/dom-crawler": "^4.4|^5.0|^6.0",
|
||||
"symfony/http-client": "^4.4|^5.0|^6.0",
|
||||
"symfony/mime": "^4.4|^5.0|^6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^6.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Goutte\\": "Goutte" },
|
||||
"exclude-from-classmap": ["Goutte/Tests"]
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
bootstrap="./vendor/autoload.php">
|
||||
<testsuites>
|
||||
<testsuite name="Goutte Test Suite">
|
||||
<directory>./Goutte/Tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
Matt Butcher [technosophos] <technosophos@gmail.com> (lead)
|
||||
Matt Farina [mattfarina] <matt@mattfarina.com> (lead)
|
||||
Asmir Mustafic [goetas] <goetas@lignano.it> (contributor)
|
||||
Edward Z. Yang [ezyang] <ezyang@mit.edu> (contributor)
|
||||
Geoffrey Sneddon [gsnedders] <geoffers@gmail.com> (contributor)
|
||||
Kukhar Vasily [ngreduce] <ngreduce@gmail.com> (contributor)
|
||||
Rune Christensen [MrElectronic] <mrelectronic@example.com> (contributor)
|
||||
Mišo Belica [miso-belica] <miso-belica@example.com> (contributor)
|
||||
Asmir Mustafic [goetas] <goetas@example.com> (contributor)
|
||||
KITAITI Makoto [KitaitiMakoto] <KitaitiMakoto@example.com> (contributor)
|
||||
Jacob Floyd [cognifloyd] <cognifloyd@gmail.com> (contributor)
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
## HTML5-PHP License
|
||||
|
||||
Copyright (c) 2013 The Authors of HTML5-PHP
|
||||
|
||||
Matt Butcher - mattbutcher@google.com
|
||||
Matt Farina - matt@mattfarina.com
|
||||
Asmir Mustafic - goetas@gmail.com
|
||||
|
||||
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.
|
||||
|
||||
## HTML5Lib License
|
||||
|
||||
Portions of this are based on html5lib's PHP version, which was a
|
||||
sub-project of html5lib. The following is the list of contributors from
|
||||
html5lib:
|
||||
|
||||
html5lib:
|
||||
|
||||
Copyright (c) 2006-2009 The Authors
|
||||
|
||||
Contributors:
|
||||
James Graham - jg307@cam.ac.uk
|
||||
Anne van Kesteren - annevankesteren@gmail.com
|
||||
Lachlan Hunt - lachlan.hunt@lachy.id.au
|
||||
Matt McDonald - kanashii@kanashii.ca
|
||||
Sam Ruby - rubys@intertwingly.net
|
||||
Ian Hickson (Google) - ian@hixie.ch
|
||||
Thomas Broyer - t.broyer@ltgt.net
|
||||
Jacques Distler - distler@golem.ph.utexas.edu
|
||||
Henri Sivonen - hsivonen@iki.fi
|
||||
Adam Barth - abarth@webkit.org
|
||||
Eric Seidel - eric@webkit.org
|
||||
The Mozilla Foundation (contributions from Henri Sivonen since 2008)
|
||||
David Flanagan (Mozilla) - dflanagan@mozilla.com
|
||||
|
||||
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.
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
> # UKRAINE NEEDS YOUR HELP NOW!
|
||||
>
|
||||
> On 24 February 2022, Russian [President Vladimir Putin ordered an invasion of Ukraine by Russian Armed Forces](https://www.bbc.com/news/world-europe-60504334).
|
||||
>
|
||||
> Your support is urgently needed.
|
||||
>
|
||||
> - Donate to the volunteers. Here is the volunteer fund helping the Ukrainian army to provide all the necessary equipment:
|
||||
> https://bank.gov.ua/en/news/all/natsionalniy-bank-vidkriv-spetsrahunok-dlya-zboru-koshtiv-na-potrebi-armiyi or https://savelife.in.ua/en/donate/
|
||||
> - Triple-check social media sources. Russian disinformation is attempting to coverup and distort the reality in Ukraine.
|
||||
> - Help Ukrainian refugees who are fleeing Russian attacks and shellings: https://www.globalcitizen.org/en/content/ways-to-help-ukraine-conflict/
|
||||
> - Put pressure on your political representatives to provide help to Ukraine.
|
||||
> - Believe in the Ukrainian people, they will not surrender, they don't have another Ukraine.
|
||||
>
|
||||
> THANK YOU!
|
||||
----
|
||||
|
||||
# HTML5-PHP
|
||||
|
||||
HTML5 is a standards-compliant HTML5 parser and writer written entirely in PHP.
|
||||
It is stable and used in many production websites, and has
|
||||
well over [five million downloads](https://packagist.org/packages/masterminds/html5).
|
||||
|
||||
HTML5 provides the following features.
|
||||
|
||||
- An HTML5 serializer
|
||||
- Support for PHP namespaces
|
||||
- Composer support
|
||||
- Event-based (SAX-like) parser
|
||||
- A DOM tree builder
|
||||
- Interoperability with [QueryPath](https://github.com/technosophos/querypath)
|
||||
- Runs on **PHP** 5.3.0 or newer
|
||||
|
||||
[](https://travis-ci.org/Masterminds/html5-php)
|
||||
[](https://packagist.org/packages/masterminds/html5)
|
||||
[](https://scrutinizer-ci.com/g/Masterminds/html5-php/?branch=master)
|
||||
[](https://scrutinizer-ci.com/g/Masterminds/html5-php/?branch=master)
|
||||
[](https://masterminds.github.io/stability/sustained.html)
|
||||
|
||||
## Installation
|
||||
|
||||
Install HTML5-PHP using [composer](http://getcomposer.org/).
|
||||
|
||||
By adding the `masterminds/html5` dependency to your `composer.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"require" : {
|
||||
"masterminds/html5": "^2.0"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
By invoking require command via composer executable:
|
||||
|
||||
```bash
|
||||
composer require masterminds/html5
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
HTML5-PHP has a high-level API and a low-level API.
|
||||
|
||||
Here is how you use the high-level `HTML5` library API:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Assuming you installed from Composer:
|
||||
require "vendor/autoload.php";
|
||||
|
||||
use Masterminds\HTML5;
|
||||
|
||||
// An example HTML document:
|
||||
$html = <<< 'HERE'
|
||||
<html>
|
||||
<head>
|
||||
<title>TEST</title>
|
||||
</head>
|
||||
<body id='foo'>
|
||||
<h1>Hello World</h1>
|
||||
<p>This is a test of the HTML5 parser.</p>
|
||||
</body>
|
||||
</html>
|
||||
HERE;
|
||||
|
||||
// Parse the document. $dom is a DOMDocument.
|
||||
$html5 = new HTML5();
|
||||
$dom = $html5->loadHTML($html);
|
||||
|
||||
// Render it as HTML5:
|
||||
print $html5->saveHTML($dom);
|
||||
|
||||
// Or save it to a file:
|
||||
$html5->save($dom, 'out.html');
|
||||
```
|
||||
|
||||
The `$dom` created by the parser is a full `DOMDocument` object. And the
|
||||
`save()` and `saveHTML()` methods will take any DOMDocument.
|
||||
|
||||
### Options
|
||||
|
||||
It is possible to pass in an array of configuration options when loading
|
||||
an HTML5 document.
|
||||
|
||||
```php
|
||||
// An associative array of options
|
||||
$options = array(
|
||||
'option_name' => 'option_value',
|
||||
);
|
||||
|
||||
// Provide the options to the constructor
|
||||
$html5 = new HTML5($options);
|
||||
|
||||
$dom = $html5->loadHTML($html);
|
||||
```
|
||||
|
||||
The following options are supported:
|
||||
|
||||
* `encode_entities` (boolean): Indicates that the serializer should aggressively
|
||||
encode characters as entities. Without this, it only encodes the bare
|
||||
minimum.
|
||||
* `disable_html_ns` (boolean): Prevents the parser from automatically
|
||||
assigning the HTML5 namespace to the DOM document. This is for
|
||||
non-namespace aware DOM tools.
|
||||
* `target_document` (\DOMDocument): A DOM document that will be used as the
|
||||
destination for the parsed nodes.
|
||||
* `implicit_namespaces` (array): An assoc array of namespaces that should be
|
||||
used by the parser. Name is tag prefix, value is NS URI.
|
||||
|
||||
## The Low-Level API
|
||||
|
||||
This library provides the following low-level APIs that you can use to
|
||||
create more customized HTML5 tools:
|
||||
|
||||
- A SAX-like event-based parser that you can hook into for special kinds
|
||||
of parsing.
|
||||
- A flexible error-reporting mechanism that can be tuned to document
|
||||
syntax checking.
|
||||
- A DOM implementation that uses PHP's built-in DOM library.
|
||||
|
||||
The unit tests exercise each piece of the API, and every public function
|
||||
is well-documented.
|
||||
|
||||
### Parser Design
|
||||
|
||||
The parser is designed as follows:
|
||||
|
||||
- The `Scanner` handles scanning on behalf of the parser.
|
||||
- The `Tokenizer` requests data off of the scanner, parses it, clasifies
|
||||
it, and sends it to an `EventHandler`. It is a *recursive descent parser.*
|
||||
- The `EventHandler` receives notifications and data for each specific
|
||||
semantic event that occurs during tokenization.
|
||||
- The `DOMBuilder` is an `EventHandler` that listens for tokenizing
|
||||
events and builds a document tree (`DOMDocument`) based on the events.
|
||||
|
||||
### Serializer Design
|
||||
|
||||
The serializer takes a data structure (the `DOMDocument`) and transforms
|
||||
it into a character representation -- an HTML5 document.
|
||||
|
||||
The serializer is broken into three parts:
|
||||
|
||||
- The `OutputRules` contain the rules to turn DOM elements into strings. The
|
||||
rules are an implementation of the interface `RulesInterface` allowing for
|
||||
different rule sets to be used.
|
||||
- The `Traverser`, which is a special-purpose tree walker. It visits
|
||||
each node node in the tree and uses the `OutputRules` to transform the node
|
||||
into a string.
|
||||
- `HTML5` manages the `Traverser` and stores the resultant data
|
||||
in the correct place.
|
||||
|
||||
The serializer (`save()`, `saveHTML()`) follows the
|
||||
[section 8.9 of the HTML 5.0 spec](http://www.w3.org/TR/2012/CR-html5-20121217/syntax.html#serializing-html-fragments).
|
||||
So tags are serialized according to these rules:
|
||||
|
||||
- A tag with children: <foo>CHILDREN</foo>
|
||||
- A tag that cannot have content: <foo> (no closing tag)
|
||||
- A tag that could have content, but doesn't: <foo></foo>
|
||||
|
||||
## Known Issues (Or, Things We Designed Against the Spec)
|
||||
|
||||
Please check the issue queue for a full list, but the following are
|
||||
issues known issues that are not presently on the roadmap:
|
||||
|
||||
- Namespaces: HTML5 only [supports a selected list of namespaces](http://www.w3.org/TR/html5/infrastructure.html#namespaces)
|
||||
and they do not operate in the same way as XML namespaces. A `:` has no special
|
||||
meaning.
|
||||
By default the parser does not support XML style namespaces via `:`;
|
||||
to enable the XML namespaces see the [XML Namespaces section](#xml-namespaces)
|
||||
- Scripts: This parser does not contain a JavaScript or a CSS
|
||||
interpreter. While one may be supplied, not all features will be
|
||||
supported.
|
||||
- Reentrance: The current parser is not re-entrant. (Thus you can't pause
|
||||
the parser to modify the HTML string mid-parse.)
|
||||
- Validation: The current tree builder is **not** a validating parser.
|
||||
While it will correct some HTML, it does not check that the HTML
|
||||
conforms to the standard. (Should you wish, you can build a validating
|
||||
parser by extending DOMTree or building your own EventHandler
|
||||
implementation.)
|
||||
* There is limited support for insertion modes.
|
||||
* Some autocorrection is done automatically.
|
||||
* Per the spec, many legacy tags are admitted and correctly handled,
|
||||
even though they are technically not part of HTML5.
|
||||
- Attribute names and values: Due to the implementation details of the
|
||||
PHP implementation of DOM, attribute names that do not follow the
|
||||
XML 1.0 standard are not inserted into the DOM. (Effectively, they
|
||||
are ignored.) If you've got a clever fix for this, jump in!
|
||||
- Processor Instructions: The HTML5 spec does not allow processor
|
||||
instructions. We do. Since this is a server-side library, we think
|
||||
this is useful. And that means, dear reader, that in some cases you
|
||||
can parse the HTML from a mixed PHP/HTML document. This, however,
|
||||
is an incidental feature, not a core feature.
|
||||
- HTML manifests: Unsupported.
|
||||
- PLAINTEXT: Unsupported.
|
||||
- Adoption Agency Algorithm: Not yet implemented. (8.2.5.4.7)
|
||||
|
||||
## XML Namespaces
|
||||
|
||||
To use XML style namespaces you have to configure well the main `HTML5` instance.
|
||||
|
||||
```php
|
||||
use Masterminds\HTML5;
|
||||
$html = new HTML5(array(
|
||||
"xmlNamespaces" => true
|
||||
));
|
||||
|
||||
$dom = $html->loadHTML('<t:tag xmlns:t="http://www.example.com"/>');
|
||||
|
||||
$dom->documentElement->namespaceURI; // http://www.example.com
|
||||
|
||||
```
|
||||
|
||||
You can also add some default prefixes that will not require the namespace declaration,
|
||||
but its elements will be namespaced.
|
||||
|
||||
```php
|
||||
use Masterminds\HTML5;
|
||||
$html = new HTML5(array(
|
||||
"implicitNamespaces"=>array(
|
||||
"t"=>"http://www.example.com"
|
||||
)
|
||||
));
|
||||
|
||||
$dom = $html->loadHTML('<t:tag/>');
|
||||
|
||||
$dom->documentElement->namespaceURI; // http://www.example.com
|
||||
|
||||
```
|
||||
|
||||
## Thanks to...
|
||||
|
||||
The dedicated (and patient) contributors of patches small and large,
|
||||
who have already made this library better.See the CREDITS file for
|
||||
a list of contributors.
|
||||
|
||||
We owe a huge debt of gratitude to the original authors of html5lib.
|
||||
|
||||
While not much of the original parser remains, we learned a lot from
|
||||
reading the html5lib library. And some pieces remain here. In
|
||||
particular, much of the UTF-8 and Unicode handling is derived from the
|
||||
html5lib project.
|
||||
|
||||
## License
|
||||
|
||||
This software is released under the MIT license. The original html5lib
|
||||
library was also released under the MIT license.
|
||||
|
||||
See LICENSE.txt
|
||||
|
||||
Certain files contain copyright assertions by specific individuals
|
||||
involved with html5lib. Those have been retained where appropriate.
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
# Release Notes
|
||||
|
||||
2.7.6 (2021-08-18)
|
||||
|
||||
- #218: Address comment handling issues
|
||||
|
||||
2.7.5 (2021-07-01)
|
||||
|
||||
- #204: Travis: Enable tests on PHP 8.0
|
||||
- #207: Fix PHP 8.1 deprecations
|
||||
|
||||
2.7.4 (2020-10-01)
|
||||
|
||||
- #191: Fix travisci build
|
||||
- #195: Add .gitattributes file with export-ignore rules
|
||||
- #194: Fix query parameter parsed as character entity
|
||||
|
||||
2.7.3 (2020-07-05)
|
||||
|
||||
- #190: mitigate cyclic reference between output rules and the traverser objects
|
||||
|
||||
2.7.2 (2020-07-01)
|
||||
|
||||
- #187: Fixed memory leak in HTML5::saveHTML()
|
||||
- #186: Add special case for end tag </br>
|
||||
|
||||
2.7.1 (2020-06-14)
|
||||
|
||||
- #171: add PHP 7.4 job
|
||||
- #178: Prevent infinite loop on un-terminated entity declaration at EOF
|
||||
|
||||
2.7.0 (2019-07-25)
|
||||
|
||||
- #164: Drop HHVM support
|
||||
- #168: Set default encoding in the DOMDocument object
|
||||
|
||||
2.6.0 (2019-03-10)
|
||||
|
||||
- #163: Allow to pass a charset to the Scanner
|
||||
|
||||
2.5.0 (2018-12-27)
|
||||
|
||||
- #162, #161, #155, #154, #153, #151: big performance improvements
|
||||
- #156: fixed typos
|
||||
- #160: adopt and enforce code style
|
||||
- #159: remove deprecated php unit base test case
|
||||
- #150: backport changes from old master branch
|
||||
|
||||
2.4.0 (2018-11-17)
|
||||
|
||||
- #148: Improve performance by moving sequence matching
|
||||
- #147: Improve the Tokenizer performance
|
||||
- #146: Improve performance by relying on a native string instead of InputStream
|
||||
- #144: Add DOM extension in composer.json
|
||||
- #145: Add more extensions on composer.json, improve phpdocs and remove dead code
|
||||
- #143: Remove experimental comment
|
||||
|
||||
2.3.1 (2018-10-18)
|
||||
|
||||
- #121: Audio is not a block tag (fixed by #141)
|
||||
- #136: Handle illegal self-closing according to spec (fixed by #137)
|
||||
- #141: Minor fixes in the README
|
||||
|
||||
2.3.0 (2017-09-04)
|
||||
|
||||
- #129: image within inline svg breaks system (fixed by #133)
|
||||
- #131: ² does not work (fixed by #132)
|
||||
- #134: Improve tokenizer performance by 20% (alternative version of #130 thanks to @MichaelHeerklotz)
|
||||
- #135: Raw & in attributes
|
||||
|
||||
2.2.2 (2016-09-22)
|
||||
|
||||
- #116: In XML mode, tags are case sensitive
|
||||
- #115: Fix PHP Notice in OutputRules
|
||||
- #112: fix parsing of options of an optgroup
|
||||
- #111: Adding test for the address tag
|
||||
|
||||
2.2.1 (2016-05-10)
|
||||
|
||||
- #109: Fixed issue where address tag could be written without closing tag (thanks sylus)
|
||||
|
||||
2.2.0 (2016-04-11)
|
||||
|
||||
- #105: Enable composer cache (for CI/CD)
|
||||
- #100: Use mb_substitute_character inset of ini_set for environments where ini_set is disable (e.g., shared hosting)
|
||||
- #98: Allow link, meta, style tags in noscript tags
|
||||
- #96: Fixed xml:href on svgs that use the "use" breaking
|
||||
- #94: Counting UTF8 characters performance improvement
|
||||
- #93: Use newer version of coveralls package
|
||||
- #90: Remove duplicate test
|
||||
- #87: Allow multiple root nodes
|
||||
|
||||
2.1.2 (2015-06-07)
|
||||
- #82: Support for PHP7
|
||||
- #84: Improved boolean attribute handling
|
||||
|
||||
2.1.1 (2015-03-23)
|
||||
- #78: Fixes bug where unmatched entity like string drops everything after &.
|
||||
|
||||
2.1.0 (2015-02-01)
|
||||
- #74: Added `disable_html_ns` and `target_doc` dom parsing options
|
||||
- Unified option names
|
||||
- #73: Fixed alphabet, ß now can be detected
|
||||
- #75 and #76: Allow whitespace in RCDATA tags
|
||||
- #77: Fixed parsing blunder for json embeds
|
||||
- #72: Add options to HTML methods
|
||||
|
||||
2.0.2 (2014-12-17)
|
||||
- #50: empty document handling
|
||||
- #63: tags with strange capitalization
|
||||
- #65: dashes and underscores as allowed characters in tag names
|
||||
- #68: Fixed issue with non-inline elements inside inline containers
|
||||
|
||||
2.0.1 (2014-09-23)
|
||||
- #59: Fixed issue parsing some fragments.
|
||||
- #56: Incorrectly saw 0 as empty string
|
||||
- Sami as new documentation generator
|
||||
|
||||
2.0.0 (2014-07-28)
|
||||
- #53: Improved boolean attributes handling
|
||||
- #52: Facebook HHVM compatibility
|
||||
- #48: Adopted PSR-2 as coding standard
|
||||
- #47: Moved everything to Masterminds namespace
|
||||
- #45: Added custom namespaces
|
||||
- #44: Added support to XML-style namespaces
|
||||
- #37: Refactored HTML5 class removing static methods
|
||||
|
||||
1.0.5 (2014-06-10)
|
||||
- #38: Set the dev-master branch as the 1.0.x branch for composer (goetas)
|
||||
- #34: Tests use PSR-4 for autoloading. (goetas)
|
||||
- #40, #41: Fix entity handling in RCDATA sections. (KitaitiMakoto)
|
||||
- #32: Fixed issue where wharacter references were being incorrectly encoded in style tags.
|
||||
|
||||
1.0.4 (2014-04-29)
|
||||
- #30/#31 Don't throw an exception for invalid tag names.
|
||||
|
||||
1.0.3 (2014-02-28)
|
||||
- #23 and #29: Ignore attributes with illegal chars in name for the PHP DOM.
|
||||
|
||||
1.0.2 (2014-02-12)
|
||||
- #23: Handle missing tag close in attribute list.
|
||||
- #25: Fixed text escaping in the serializer (HTML% 8.3).
|
||||
- #27: Fixed tests on Windows: changed "\n" -> PHP_EOL.
|
||||
- #28: Fixed infinite loop for char "&" in unquoted attribute in parser.
|
||||
- #26: Updated tag name case handling to deal with uppercase usage.
|
||||
- #24: Newlines and tabs are allowed inside quoted attributes (HTML5 8.2.4).
|
||||
- Fixed Travis CI testing.
|
||||
|
||||
1.0.1 (2013-11-07)
|
||||
- CDATA encoding is improved. (Non-standard; Issue #19)
|
||||
- Some parser rules were not returning the new current element. (Issue #20)
|
||||
- Added, to the README, details on code test coverage and to packagist version.
|
||||
- Fixed processor instructions.
|
||||
- Improved test coverage and documentation coverage.
|
||||
|
||||
1.0.0 (2013-10-02)
|
||||
- Initial release.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
From 1.x to 2.x
|
||||
=================
|
||||
|
||||
- All classes uses `Masterminds` namespace.
|
||||
- All public static methods has been removed from `HTML5` class and the general API to access the HTML5 functionalities has changed.
|
||||
|
||||
Before:
|
||||
|
||||
$dom = \HTML5::loadHTML('<html>....');
|
||||
\HTML5::saveHTML($dom);
|
||||
|
||||
After:
|
||||
|
||||
use Masterminds\HTML5;
|
||||
|
||||
$html5 = new HTML5();
|
||||
|
||||
$dom = $html5->loadHTML('<html>....');
|
||||
echo $html5->saveHTML($dom);
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* Fetch the entities.json file and convert to PHP datastructure.
|
||||
*/
|
||||
|
||||
// The URL to the official entities JSON file.
|
||||
$ENTITIES_URL = 'http://www.w3.org/TR/2012/CR-html5-20121217/entities.json';
|
||||
|
||||
$payload = file_get_contents($ENTITIES_URL);
|
||||
$json = json_decode($payload);
|
||||
|
||||
$table = array();
|
||||
foreach ($json as $name => $obj) {
|
||||
$sname = substr($name, 1, -1);
|
||||
$table[$sname] = $obj->characters;
|
||||
}
|
||||
|
||||
echo '<?php
|
||||
namespace Masterminds\\HTML5;
|
||||
/** Entity lookup tables. This class is automatically generated. */
|
||||
class Entities {
|
||||
public static $byName = ';
|
||||
var_export($table);
|
||||
echo ';
|
||||
}' . PHP_EOL;
|
||||
//print serialize($table);
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
"description": "An HTML5 parser and serializer.",
|
||||
"type": "library",
|
||||
"homepage": "http://masterminds.github.io/html5-php",
|
||||
"license": "MIT",
|
||||
"keywords": ["xml", "html", "html5", "dom", "parser", "serializer", "querypath"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Matt Butcher",
|
||||
"email": "technosophos@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Matt Farina",
|
||||
"email": "matt@mattfarina.com"
|
||||
},
|
||||
{
|
||||
"name": "Asmir Mustafic",
|
||||
"email": "goetas@gmail.com"
|
||||
}
|
||||
],
|
||||
"require" : {
|
||||
"ext-dom": "*",
|
||||
"php" : ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit" : "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {"Masterminds\\": "src"}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {"Masterminds\\HTML5\\Tests\\": "test/HTML5"}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.7-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
namespace Masterminds;
|
||||
|
||||
use Masterminds\HTML5\Parser\DOMTreeBuilder;
|
||||
use Masterminds\HTML5\Parser\Scanner;
|
||||
use Masterminds\HTML5\Parser\Tokenizer;
|
||||
use Masterminds\HTML5\Serializer\OutputRules;
|
||||
use Masterminds\HTML5\Serializer\Traverser;
|
||||
|
||||
/**
|
||||
* This class offers convenience methods for parsing and serializing HTML5.
|
||||
* It is roughly designed to mirror the \DOMDocument native class.
|
||||
*/
|
||||
class HTML5
|
||||
{
|
||||
/**
|
||||
* Global options for the parser and serializer.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $defaultOptions = array(
|
||||
// Whether the serializer should aggressively encode all characters as entities.
|
||||
'encode_entities' => false,
|
||||
|
||||
// Prevents the parser from automatically assigning the HTML5 namespace to the DOM document.
|
||||
'disable_html_ns' => false,
|
||||
);
|
||||
|
||||
protected $errors = array();
|
||||
|
||||
public function __construct(array $defaultOptions = array())
|
||||
{
|
||||
$this->defaultOptions = array_merge($this->defaultOptions, $defaultOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current default options.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getOptions()
|
||||
{
|
||||
return $this->defaultOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and parse an HTML file.
|
||||
*
|
||||
* This will apply the HTML5 parser, which is tolerant of many
|
||||
* varieties of HTML, including XHTML 1, HTML 4, and well-formed HTML
|
||||
* 3. Note that in these cases, not all of the old data will be
|
||||
* preserved. For example, XHTML's XML declaration will be removed.
|
||||
*
|
||||
* The rules governing parsing are set out in the HTML 5 spec.
|
||||
*
|
||||
* @param string|resource $file The path to the file to parse. If this is a resource, it is
|
||||
* assumed to be an open stream whose pointer is set to the first
|
||||
* byte of input.
|
||||
* @param array $options Configuration options when parsing the HTML.
|
||||
*
|
||||
* @return \DOMDocument A DOM document. These object type is defined by the libxml
|
||||
* library, and should have been included with your version of PHP.
|
||||
*/
|
||||
public function load($file, array $options = array())
|
||||
{
|
||||
// Handle the case where file is a resource.
|
||||
if (is_resource($file)) {
|
||||
return $this->parse(stream_get_contents($file), $options);
|
||||
}
|
||||
|
||||
return $this->parse(file_get_contents($file), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a HTML Document from a string.
|
||||
*
|
||||
* Take a string of HTML 5 (or earlier) and parse it into a
|
||||
* DOMDocument.
|
||||
*
|
||||
* @param string $string A html5 document as a string.
|
||||
* @param array $options Configuration options when parsing the HTML.
|
||||
*
|
||||
* @return \DOMDocument A DOM document. DOM is part of libxml, which is included with
|
||||
* almost all distribtions of PHP.
|
||||
*/
|
||||
public function loadHTML($string, array $options = array())
|
||||
{
|
||||
return $this->parse($string, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to load an HTML file.
|
||||
*
|
||||
* This is here to provide backwards compatibility with the
|
||||
* PHP DOM implementation. It simply calls load().
|
||||
*
|
||||
* @param string $file The path to the file to parse. If this is a resource, it is
|
||||
* assumed to be an open stream whose pointer is set to the first
|
||||
* byte of input.
|
||||
* @param array $options Configuration options when parsing the HTML.
|
||||
*
|
||||
* @return \DOMDocument A DOM document. These object type is defined by the libxml
|
||||
* library, and should have been included with your version of PHP.
|
||||
*/
|
||||
public function loadHTMLFile($file, array $options = array())
|
||||
{
|
||||
return $this->load($file, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a HTML fragment from a string.
|
||||
*
|
||||
* @param string $string the HTML5 fragment as a string
|
||||
* @param array $options Configuration options when parsing the HTML
|
||||
*
|
||||
* @return \DOMDocumentFragment A DOM fragment. The DOM is part of libxml, which is included with
|
||||
* almost all distributions of PHP.
|
||||
*/
|
||||
public function loadHTMLFragment($string, array $options = array())
|
||||
{
|
||||
return $this->parseFragment($string, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all errors encountered into parsing phase.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getErrors()
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true it some errors were encountered into parsing phase.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasErrors()
|
||||
{
|
||||
return count($this->errors) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an input string.
|
||||
*
|
||||
* @param string $input
|
||||
* @param array $options
|
||||
*
|
||||
* @return \DOMDocument
|
||||
*/
|
||||
public function parse($input, array $options = array())
|
||||
{
|
||||
$this->errors = array();
|
||||
$options = array_merge($this->defaultOptions, $options);
|
||||
$events = new DOMTreeBuilder(false, $options);
|
||||
$scanner = new Scanner($input, !empty($options['encoding']) ? $options['encoding'] : 'UTF-8');
|
||||
$parser = new Tokenizer($scanner, $events, !empty($options['xmlNamespaces']) ? Tokenizer::CONFORMANT_XML : Tokenizer::CONFORMANT_HTML);
|
||||
|
||||
$parser->parse();
|
||||
$this->errors = $events->getErrors();
|
||||
|
||||
return $events->document();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an input stream where the stream is a fragment.
|
||||
*
|
||||
* Lower-level loading function. This requires an input stream instead
|
||||
* of a string, file, or resource.
|
||||
*
|
||||
* @param string $input The input data to parse in the form of a string.
|
||||
* @param array $options An array of options.
|
||||
*
|
||||
* @return \DOMDocumentFragment
|
||||
*/
|
||||
public function parseFragment($input, array $options = array())
|
||||
{
|
||||
$options = array_merge($this->defaultOptions, $options);
|
||||
$events = new DOMTreeBuilder(true, $options);
|
||||
$scanner = new Scanner($input, !empty($options['encoding']) ? $options['encoding'] : 'UTF-8');
|
||||
$parser = new Tokenizer($scanner, $events, !empty($options['xmlNamespaces']) ? Tokenizer::CONFORMANT_XML : Tokenizer::CONFORMANT_HTML);
|
||||
|
||||
$parser->parse();
|
||||
$this->errors = $events->getErrors();
|
||||
|
||||
return $events->fragment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a DOM into a given file as HTML5.
|
||||
*
|
||||
* @param mixed $dom The DOM to be serialized.
|
||||
* @param string|resource $file The filename to be written or resource to write to.
|
||||
* @param array $options Configuration options when serializing the DOM. These include:
|
||||
* - encode_entities: Text written to the output is escaped by default and not all
|
||||
* entities are encoded. If this is set to true all entities will be encoded.
|
||||
* Defaults to false.
|
||||
*/
|
||||
public function save($dom, $file, $options = array())
|
||||
{
|
||||
$close = true;
|
||||
if (is_resource($file)) {
|
||||
$stream = $file;
|
||||
$close = false;
|
||||
} else {
|
||||
$stream = fopen($file, 'wb');
|
||||
}
|
||||
$options = array_merge($this->defaultOptions, $options);
|
||||
$rules = new OutputRules($stream, $options);
|
||||
$trav = new Traverser($dom, $stream, $rules, $options);
|
||||
|
||||
$trav->walk();
|
||||
/*
|
||||
* release the traverser to avoid cyclic references and allow PHP to free memory without waiting for gc_collect_cycles
|
||||
*/
|
||||
$rules->unsetTraverser();
|
||||
if ($close) {
|
||||
fclose($stream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a DOM into an HTML5 string.
|
||||
*
|
||||
* @param mixed $dom The DOM to be serialized.
|
||||
* @param array $options Configuration options when serializing the DOM. These include:
|
||||
* - encode_entities: Text written to the output is escaped by default and not all
|
||||
* entities are encoded. If this is set to true all entities will be encoded.
|
||||
* Defaults to false.
|
||||
*
|
||||
* @return string A HTML5 documented generated from the DOM.
|
||||
*/
|
||||
public function saveHTML($dom, $options = array())
|
||||
{
|
||||
$stream = fopen('php://temp', 'wb');
|
||||
$this->save($dom, $stream, array_merge($this->defaultOptions, $options));
|
||||
|
||||
$html = stream_get_contents($stream, -1, 0);
|
||||
|
||||
fclose($stream);
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
<?php
|
||||
/**
|
||||
* Provide general element functions.
|
||||
*/
|
||||
|
||||
namespace Masterminds\HTML5;
|
||||
|
||||
/**
|
||||
* This class provides general information about HTML5 elements,
|
||||
* including syntactic and semantic issues.
|
||||
* Parsers and serializers can
|
||||
* use this class as a reference point for information about the rules
|
||||
* of various HTML5 elements.
|
||||
*
|
||||
* @todo consider using a bitmask table lookup. There is enough overlap in
|
||||
* naming that this could significantly shrink the size and maybe make it
|
||||
* faster. See the Go teams implementation at https://code.google.com/p/go/source/browse/html/atom.
|
||||
*/
|
||||
class Elements
|
||||
{
|
||||
/**
|
||||
* Indicates an element is described in the specification.
|
||||
*/
|
||||
const KNOWN_ELEMENT = 1;
|
||||
|
||||
// From section 8.1.2: "script", "style"
|
||||
// From 8.2.5.4.7 ("in body" insertion mode): "noembed"
|
||||
// From 8.4 "style", "xmp", "iframe", "noembed", "noframes"
|
||||
/**
|
||||
* Indicates the contained text should be processed as raw text.
|
||||
*/
|
||||
const TEXT_RAW = 2;
|
||||
|
||||
// From section 8.1.2: "textarea", "title"
|
||||
/**
|
||||
* Indicates the contained text should be processed as RCDATA.
|
||||
*/
|
||||
const TEXT_RCDATA = 4;
|
||||
|
||||
/**
|
||||
* Indicates the tag cannot have content.
|
||||
*/
|
||||
const VOID_TAG = 8;
|
||||
|
||||
// "address", "article", "aside", "blockquote", "center", "details", "dialog", "dir", "div", "dl",
|
||||
// "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "menu",
|
||||
// "nav", "ol", "p", "section", "summary", "ul"
|
||||
// "h1", "h2", "h3", "h4", "h5", "h6"
|
||||
// "pre", "listing"
|
||||
// "form"
|
||||
// "plaintext"
|
||||
/**
|
||||
* Indicates that if a previous event is for a P tag, that element
|
||||
* should be considered closed.
|
||||
*/
|
||||
const AUTOCLOSE_P = 16;
|
||||
|
||||
/**
|
||||
* Indicates that the text inside is plaintext (pre).
|
||||
*/
|
||||
const TEXT_PLAINTEXT = 32;
|
||||
|
||||
// See https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements
|
||||
/**
|
||||
* Indicates that the tag is a block.
|
||||
*/
|
||||
const BLOCK_TAG = 64;
|
||||
|
||||
/**
|
||||
* Indicates that the tag allows only inline elements as child nodes.
|
||||
*/
|
||||
const BLOCK_ONLY_INLINE = 128;
|
||||
|
||||
/**
|
||||
* The HTML5 elements as defined in http://dev.w3.org/html5/markup/elements.html.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $html5 = array(
|
||||
'a' => 1,
|
||||
'abbr' => 1,
|
||||
'address' => 65, // NORMAL | BLOCK_TAG
|
||||
'area' => 9, // NORMAL | VOID_TAG
|
||||
'article' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'aside' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'audio' => 1, // NORMAL
|
||||
'b' => 1,
|
||||
'base' => 9, // NORMAL | VOID_TAG
|
||||
'bdi' => 1,
|
||||
'bdo' => 1,
|
||||
'blockquote' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'body' => 1,
|
||||
'br' => 9, // NORMAL | VOID_TAG
|
||||
'button' => 1,
|
||||
'canvas' => 65, // NORMAL | BLOCK_TAG
|
||||
'caption' => 1,
|
||||
'cite' => 1,
|
||||
'code' => 1,
|
||||
'col' => 9, // NORMAL | VOID_TAG
|
||||
'colgroup' => 1,
|
||||
'command' => 9, // NORMAL | VOID_TAG
|
||||
// "data" => 1, // This is highly experimental and only part of the whatwg spec (not w3c). See https://developer.mozilla.org/en-US/docs/HTML/Element/data
|
||||
'datalist' => 1,
|
||||
'dd' => 65, // NORMAL | BLOCK_TAG
|
||||
'del' => 1,
|
||||
'details' => 17, // NORMAL | AUTOCLOSE_P,
|
||||
'dfn' => 1,
|
||||
'dialog' => 17, // NORMAL | AUTOCLOSE_P,
|
||||
'div' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'dl' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'dt' => 1,
|
||||
'em' => 1,
|
||||
'embed' => 9, // NORMAL | VOID_TAG
|
||||
'fieldset' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'figcaption' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'figure' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'footer' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'form' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'h1' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'h2' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'h3' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'h4' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'h5' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'h6' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'head' => 1,
|
||||
'header' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'hgroup' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'hr' => 73, // NORMAL | VOID_TAG
|
||||
'html' => 1,
|
||||
'i' => 1,
|
||||
'iframe' => 3, // NORMAL | TEXT_RAW
|
||||
'img' => 9, // NORMAL | VOID_TAG
|
||||
'input' => 9, // NORMAL | VOID_TAG
|
||||
'kbd' => 1,
|
||||
'ins' => 1,
|
||||
'keygen' => 9, // NORMAL | VOID_TAG
|
||||
'label' => 1,
|
||||
'legend' => 1,
|
||||
'li' => 1,
|
||||
'link' => 9, // NORMAL | VOID_TAG
|
||||
'map' => 1,
|
||||
'mark' => 1,
|
||||
'menu' => 17, // NORMAL | AUTOCLOSE_P,
|
||||
'meta' => 9, // NORMAL | VOID_TAG
|
||||
'meter' => 1,
|
||||
'nav' => 17, // NORMAL | AUTOCLOSE_P,
|
||||
'noscript' => 65, // NORMAL | BLOCK_TAG
|
||||
'object' => 1,
|
||||
'ol' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'optgroup' => 1,
|
||||
'option' => 1,
|
||||
'output' => 65, // NORMAL | BLOCK_TAG
|
||||
'p' => 209, // NORMAL | AUTOCLOSE_P | BLOCK_TAG | BLOCK_ONLY_INLINE
|
||||
'param' => 9, // NORMAL | VOID_TAG
|
||||
'pre' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'progress' => 1,
|
||||
'q' => 1,
|
||||
'rp' => 1,
|
||||
'rt' => 1,
|
||||
'ruby' => 1,
|
||||
's' => 1,
|
||||
'samp' => 1,
|
||||
'script' => 3, // NORMAL | TEXT_RAW
|
||||
'section' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'select' => 1,
|
||||
'small' => 1,
|
||||
'source' => 9, // NORMAL | VOID_TAG
|
||||
'span' => 1,
|
||||
'strong' => 1,
|
||||
'style' => 3, // NORMAL | TEXT_RAW
|
||||
'sub' => 1,
|
||||
'summary' => 17, // NORMAL | AUTOCLOSE_P,
|
||||
'sup' => 1,
|
||||
'table' => 65, // NORMAL | BLOCK_TAG
|
||||
'tbody' => 1,
|
||||
'td' => 1,
|
||||
'textarea' => 5, // NORMAL | TEXT_RCDATA
|
||||
'tfoot' => 65, // NORMAL | BLOCK_TAG
|
||||
'th' => 1,
|
||||
'thead' => 1,
|
||||
'time' => 1,
|
||||
'title' => 5, // NORMAL | TEXT_RCDATA
|
||||
'tr' => 1,
|
||||
'track' => 9, // NORMAL | VOID_TAG
|
||||
'u' => 1,
|
||||
'ul' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG
|
||||
'var' => 1,
|
||||
'video' => 1,
|
||||
'wbr' => 9, // NORMAL | VOID_TAG
|
||||
|
||||
// Legacy?
|
||||
'basefont' => 8, // VOID_TAG
|
||||
'bgsound' => 8, // VOID_TAG
|
||||
'noframes' => 2, // RAW_TEXT
|
||||
'frame' => 9, // NORMAL | VOID_TAG
|
||||
'frameset' => 1,
|
||||
'center' => 16,
|
||||
'dir' => 16,
|
||||
'listing' => 16, // AUTOCLOSE_P
|
||||
'plaintext' => 48, // AUTOCLOSE_P | TEXT_PLAINTEXT
|
||||
'applet' => 0,
|
||||
'marquee' => 0,
|
||||
'isindex' => 8, // VOID_TAG
|
||||
'xmp' => 20, // AUTOCLOSE_P | VOID_TAG | RAW_TEXT
|
||||
'noembed' => 2, // RAW_TEXT
|
||||
);
|
||||
|
||||
/**
|
||||
* The MathML elements.
|
||||
* See http://www.w3.org/wiki/MathML/Elements.
|
||||
*
|
||||
* In our case we are only concerned with presentation MathML and not content
|
||||
* MathML. There is a nice list of this subset at https://developer.mozilla.org/en-US/docs/MathML/Element.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $mathml = array(
|
||||
'maction' => 1,
|
||||
'maligngroup' => 1,
|
||||
'malignmark' => 1,
|
||||
'math' => 1,
|
||||
'menclose' => 1,
|
||||
'merror' => 1,
|
||||
'mfenced' => 1,
|
||||
'mfrac' => 1,
|
||||
'mglyph' => 1,
|
||||
'mi' => 1,
|
||||
'mlabeledtr' => 1,
|
||||
'mlongdiv' => 1,
|
||||
'mmultiscripts' => 1,
|
||||
'mn' => 1,
|
||||
'mo' => 1,
|
||||
'mover' => 1,
|
||||
'mpadded' => 1,
|
||||
'mphantom' => 1,
|
||||
'mroot' => 1,
|
||||
'mrow' => 1,
|
||||
'ms' => 1,
|
||||
'mscarries' => 1,
|
||||
'mscarry' => 1,
|
||||
'msgroup' => 1,
|
||||
'msline' => 1,
|
||||
'mspace' => 1,
|
||||
'msqrt' => 1,
|
||||
'msrow' => 1,
|
||||
'mstack' => 1,
|
||||
'mstyle' => 1,
|
||||
'msub' => 1,
|
||||
'msup' => 1,
|
||||
'msubsup' => 1,
|
||||
'mtable' => 1,
|
||||
'mtd' => 1,
|
||||
'mtext' => 1,
|
||||
'mtr' => 1,
|
||||
'munder' => 1,
|
||||
'munderover' => 1,
|
||||
);
|
||||
|
||||
/**
|
||||
* The svg elements.
|
||||
*
|
||||
* The Mozilla documentation has a good list at https://developer.mozilla.org/en-US/docs/SVG/Element.
|
||||
* The w3c list appears to be lacking in some areas like filter effect elements.
|
||||
* That list can be found at http://www.w3.org/wiki/SVG/Elements.
|
||||
*
|
||||
* Note, FireFox appears to do a better job rendering filter effects than chrome.
|
||||
* While they are in the spec I'm not sure how widely implemented they are.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $svg = array(
|
||||
'a' => 1,
|
||||
'altGlyph' => 1,
|
||||
'altGlyphDef' => 1,
|
||||
'altGlyphItem' => 1,
|
||||
'animate' => 1,
|
||||
'animateColor' => 1,
|
||||
'animateMotion' => 1,
|
||||
'animateTransform' => 1,
|
||||
'circle' => 1,
|
||||
'clipPath' => 1,
|
||||
'color-profile' => 1,
|
||||
'cursor' => 1,
|
||||
'defs' => 1,
|
||||
'desc' => 1,
|
||||
'ellipse' => 1,
|
||||
'feBlend' => 1,
|
||||
'feColorMatrix' => 1,
|
||||
'feComponentTransfer' => 1,
|
||||
'feComposite' => 1,
|
||||
'feConvolveMatrix' => 1,
|
||||
'feDiffuseLighting' => 1,
|
||||
'feDisplacementMap' => 1,
|
||||
'feDistantLight' => 1,
|
||||
'feFlood' => 1,
|
||||
'feFuncA' => 1,
|
||||
'feFuncB' => 1,
|
||||
'feFuncG' => 1,
|
||||
'feFuncR' => 1,
|
||||
'feGaussianBlur' => 1,
|
||||
'feImage' => 1,
|
||||
'feMerge' => 1,
|
||||
'feMergeNode' => 1,
|
||||
'feMorphology' => 1,
|
||||
'feOffset' => 1,
|
||||
'fePointLight' => 1,
|
||||
'feSpecularLighting' => 1,
|
||||
'feSpotLight' => 1,
|
||||
'feTile' => 1,
|
||||
'feTurbulence' => 1,
|
||||
'filter' => 1,
|
||||
'font' => 1,
|
||||
'font-face' => 1,
|
||||
'font-face-format' => 1,
|
||||
'font-face-name' => 1,
|
||||
'font-face-src' => 1,
|
||||
'font-face-uri' => 1,
|
||||
'foreignObject' => 1,
|
||||
'g' => 1,
|
||||
'glyph' => 1,
|
||||
'glyphRef' => 1,
|
||||
'hkern' => 1,
|
||||
'image' => 1,
|
||||
'line' => 1,
|
||||
'linearGradient' => 1,
|
||||
'marker' => 1,
|
||||
'mask' => 1,
|
||||
'metadata' => 1,
|
||||
'missing-glyph' => 1,
|
||||
'mpath' => 1,
|
||||
'path' => 1,
|
||||
'pattern' => 1,
|
||||
'polygon' => 1,
|
||||
'polyline' => 1,
|
||||
'radialGradient' => 1,
|
||||
'rect' => 1,
|
||||
'script' => 3, // NORMAL | RAW_TEXT
|
||||
'set' => 1,
|
||||
'stop' => 1,
|
||||
'style' => 3, // NORMAL | RAW_TEXT
|
||||
'svg' => 1,
|
||||
'switch' => 1,
|
||||
'symbol' => 1,
|
||||
'text' => 1,
|
||||
'textPath' => 1,
|
||||
'title' => 1,
|
||||
'tref' => 1,
|
||||
'tspan' => 1,
|
||||
'use' => 1,
|
||||
'view' => 1,
|
||||
'vkern' => 1,
|
||||
);
|
||||
|
||||
/**
|
||||
* Some attributes in SVG are case sensitive.
|
||||
*
|
||||
* This map contains key/value pairs with the key as the lowercase attribute
|
||||
* name and the value with the correct casing.
|
||||
*/
|
||||
public static $svgCaseSensitiveAttributeMap = array(
|
||||
'attributename' => 'attributeName',
|
||||
'attributetype' => 'attributeType',
|
||||
'basefrequency' => 'baseFrequency',
|
||||
'baseprofile' => 'baseProfile',
|
||||
'calcmode' => 'calcMode',
|
||||
'clippathunits' => 'clipPathUnits',
|
||||
'contentscripttype' => 'contentScriptType',
|
||||
'contentstyletype' => 'contentStyleType',
|
||||
'diffuseconstant' => 'diffuseConstant',
|
||||
'edgemode' => 'edgeMode',
|
||||
'externalresourcesrequired' => 'externalResourcesRequired',
|
||||
'filterres' => 'filterRes',
|
||||
'filterunits' => 'filterUnits',
|
||||
'glyphref' => 'glyphRef',
|
||||
'gradienttransform' => 'gradientTransform',
|
||||
'gradientunits' => 'gradientUnits',
|
||||
'kernelmatrix' => 'kernelMatrix',
|
||||
'kernelunitlength' => 'kernelUnitLength',
|
||||
'keypoints' => 'keyPoints',
|
||||
'keysplines' => 'keySplines',
|
||||
'keytimes' => 'keyTimes',
|
||||
'lengthadjust' => 'lengthAdjust',
|
||||
'limitingconeangle' => 'limitingConeAngle',
|
||||
'markerheight' => 'markerHeight',
|
||||
'markerunits' => 'markerUnits',
|
||||
'markerwidth' => 'markerWidth',
|
||||
'maskcontentunits' => 'maskContentUnits',
|
||||
'maskunits' => 'maskUnits',
|
||||
'numoctaves' => 'numOctaves',
|
||||
'pathlength' => 'pathLength',
|
||||
'patterncontentunits' => 'patternContentUnits',
|
||||
'patterntransform' => 'patternTransform',
|
||||
'patternunits' => 'patternUnits',
|
||||
'pointsatx' => 'pointsAtX',
|
||||
'pointsaty' => 'pointsAtY',
|
||||
'pointsatz' => 'pointsAtZ',
|
||||
'preservealpha' => 'preserveAlpha',
|
||||
'preserveaspectratio' => 'preserveAspectRatio',
|
||||
'primitiveunits' => 'primitiveUnits',
|
||||
'refx' => 'refX',
|
||||
'refy' => 'refY',
|
||||
'repeatcount' => 'repeatCount',
|
||||
'repeatdur' => 'repeatDur',
|
||||
'requiredextensions' => 'requiredExtensions',
|
||||
'requiredfeatures' => 'requiredFeatures',
|
||||
'specularconstant' => 'specularConstant',
|
||||
'specularexponent' => 'specularExponent',
|
||||
'spreadmethod' => 'spreadMethod',
|
||||
'startoffset' => 'startOffset',
|
||||
'stddeviation' => 'stdDeviation',
|
||||
'stitchtiles' => 'stitchTiles',
|
||||
'surfacescale' => 'surfaceScale',
|
||||
'systemlanguage' => 'systemLanguage',
|
||||
'tablevalues' => 'tableValues',
|
||||
'targetx' => 'targetX',
|
||||
'targety' => 'targetY',
|
||||
'textlength' => 'textLength',
|
||||
'viewbox' => 'viewBox',
|
||||
'viewtarget' => 'viewTarget',
|
||||
'xchannelselector' => 'xChannelSelector',
|
||||
'ychannelselector' => 'yChannelSelector',
|
||||
'zoomandpan' => 'zoomAndPan',
|
||||
);
|
||||
|
||||
/**
|
||||
* Some SVG elements are case sensitive.
|
||||
* This map contains these.
|
||||
*
|
||||
* The map contains key/value store of the name is lowercase as the keys and
|
||||
* the correct casing as the value.
|
||||
*/
|
||||
public static $svgCaseSensitiveElementMap = array(
|
||||
'altglyph' => 'altGlyph',
|
||||
'altglyphdef' => 'altGlyphDef',
|
||||
'altglyphitem' => 'altGlyphItem',
|
||||
'animatecolor' => 'animateColor',
|
||||
'animatemotion' => 'animateMotion',
|
||||
'animatetransform' => 'animateTransform',
|
||||
'clippath' => 'clipPath',
|
||||
'feblend' => 'feBlend',
|
||||
'fecolormatrix' => 'feColorMatrix',
|
||||
'fecomponenttransfer' => 'feComponentTransfer',
|
||||
'fecomposite' => 'feComposite',
|
||||
'feconvolvematrix' => 'feConvolveMatrix',
|
||||
'fediffuselighting' => 'feDiffuseLighting',
|
||||
'fedisplacementmap' => 'feDisplacementMap',
|
||||
'fedistantlight' => 'feDistantLight',
|
||||
'feflood' => 'feFlood',
|
||||
'fefunca' => 'feFuncA',
|
||||
'fefuncb' => 'feFuncB',
|
||||
'fefuncg' => 'feFuncG',
|
||||
'fefuncr' => 'feFuncR',
|
||||
'fegaussianblur' => 'feGaussianBlur',
|
||||
'feimage' => 'feImage',
|
||||
'femerge' => 'feMerge',
|
||||
'femergenode' => 'feMergeNode',
|
||||
'femorphology' => 'feMorphology',
|
||||
'feoffset' => 'feOffset',
|
||||
'fepointlight' => 'fePointLight',
|
||||
'fespecularlighting' => 'feSpecularLighting',
|
||||
'fespotlight' => 'feSpotLight',
|
||||
'fetile' => 'feTile',
|
||||
'feturbulence' => 'feTurbulence',
|
||||
'foreignobject' => 'foreignObject',
|
||||
'glyphref' => 'glyphRef',
|
||||
'lineargradient' => 'linearGradient',
|
||||
'radialgradient' => 'radialGradient',
|
||||
'textpath' => 'textPath',
|
||||
);
|
||||
|
||||
/**
|
||||
* Check whether the given element meets the given criterion.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* Elements::isA('script', Elements::TEXT_RAW); // Returns true.
|
||||
*
|
||||
* Elements::isA('script', Elements::TEXT_RCDATA); // Returns false.
|
||||
*
|
||||
* @param string $name The element name.
|
||||
* @param int $mask One of the constants on this class.
|
||||
*
|
||||
* @return bool true if the element matches the mask, false otherwise.
|
||||
*/
|
||||
public static function isA($name, $mask)
|
||||
{
|
||||
return (static::element($name) & $mask) === $mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if an element is a valid html5 element.
|
||||
*
|
||||
* @param string $name The name of the element.
|
||||
*
|
||||
* @return bool true if a html5 element and false otherwise.
|
||||
*/
|
||||
public static function isHtml5Element($name)
|
||||
{
|
||||
// html5 element names are case insensitive. Forcing lowercase for the check.
|
||||
// Do we need this check or will all data passed here already be lowercase?
|
||||
return isset(static::$html5[strtolower($name)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if an element name is a valid MathML presentation element.
|
||||
*
|
||||
* @param string $name The name of the element.
|
||||
*
|
||||
* @return bool true if a MathML name and false otherwise.
|
||||
*/
|
||||
public static function isMathMLElement($name)
|
||||
{
|
||||
// MathML is case-sensitive unlike html5 elements.
|
||||
return isset(static::$mathml[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if an element is a valid SVG element.
|
||||
*
|
||||
* @param string $name The name of the element.
|
||||
*
|
||||
* @return bool true if a SVG element and false otherise.
|
||||
*/
|
||||
public static function isSvgElement($name)
|
||||
{
|
||||
// SVG is case-sensitive unlike html5 elements.
|
||||
return isset(static::$svg[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is an element name valid in an html5 document.
|
||||
* This includes html5 elements along with other allowed embedded content
|
||||
* such as svg and mathml.
|
||||
*
|
||||
* @param string $name The name of the element.
|
||||
*
|
||||
* @return bool true if valid and false otherwise.
|
||||
*/
|
||||
public static function isElement($name)
|
||||
{
|
||||
return static::isHtml5Element($name) || static::isMathMLElement($name) || static::isSvgElement($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the element mask for the given element name.
|
||||
*
|
||||
* @param string $name The name of the element.
|
||||
*
|
||||
* @return int the element mask.
|
||||
*/
|
||||
public static function element($name)
|
||||
{
|
||||
if (isset(static::$html5[$name])) {
|
||||
return static::$html5[$name];
|
||||
}
|
||||
if (isset(static::$svg[$name])) {
|
||||
return static::$svg[$name];
|
||||
}
|
||||
if (isset(static::$mathml[$name])) {
|
||||
return static::$mathml[$name];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a SVG element name to its proper case and form.
|
||||
*
|
||||
* @param string $name The name of the element.
|
||||
*
|
||||
* @return string the normalized form of the element name.
|
||||
*/
|
||||
public static function normalizeSvgElement($name)
|
||||
{
|
||||
$name = strtolower($name);
|
||||
if (isset(static::$svgCaseSensitiveElementMap[$name])) {
|
||||
$name = static::$svgCaseSensitiveElementMap[$name];
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a SVG attribute name to its proper case and form.
|
||||
*
|
||||
* @param string $name The name of the attribute.
|
||||
*
|
||||
* @return string The normalized form of the attribute name.
|
||||
*/
|
||||
public static function normalizeSvgAttribute($name)
|
||||
{
|
||||
$name = strtolower($name);
|
||||
if (isset(static::$svgCaseSensitiveAttributeMap[$name])) {
|
||||
$name = static::$svgCaseSensitiveAttributeMap[$name];
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a MathML attribute name to its proper case and form.
|
||||
* Note, all MathML element names are lowercase.
|
||||
*
|
||||
* @param string $name The name of the attribute.
|
||||
*
|
||||
* @return string The normalized form of the attribute name.
|
||||
*/
|
||||
public static function normalizeMathMlAttribute($name)
|
||||
{
|
||||
$name = strtolower($name);
|
||||
|
||||
// Only one attribute has a mixed case form for MathML.
|
||||
if ('definitionurl' === $name) {
|
||||
$name = 'definitionURL';
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
+2236
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Masterminds\HTML5;
|
||||
|
||||
/**
|
||||
* The base exception for the HTML5 project.
|
||||
*/
|
||||
class Exception extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* A handler for processor instructions.
|
||||
*/
|
||||
|
||||
namespace Masterminds\HTML5;
|
||||
|
||||
/**
|
||||
* Provide an processor to handle embedded instructions.
|
||||
*
|
||||
* XML defines a mechanism for inserting instructions (like PHP) into a
|
||||
* document. These are called "Processor Instructions." The HTML5 parser
|
||||
* provides an opportunity to handle these processor instructions during
|
||||
* the tree-building phase (before the DOM is constructed), which makes
|
||||
* it possible to alter the document as it is being created.
|
||||
*
|
||||
* One could, for example, use this mechanism to execute well-formed PHP
|
||||
* code embedded inside of an HTML5 document.
|
||||
*/
|
||||
interface InstructionProcessor
|
||||
{
|
||||
/**
|
||||
* Process an individual processing instruction.
|
||||
*
|
||||
* The process() function is responsible for doing the following:
|
||||
* - Determining whether $name is an instruction type it can handle.
|
||||
* - Determining what to do with the data passed in.
|
||||
* - Making any subsequent modifications to the DOM by modifying the
|
||||
* DOMElement or its attached DOM tree.
|
||||
*
|
||||
* @param \DOMElement $element The parent element for the current processing instruction.
|
||||
* @param string $name The instruction's name. E.g. `<?php` has the name `php`.
|
||||
* @param string $data All of the data between the opening and closing PI marks.
|
||||
*
|
||||
* @return \DOMElement The element that should be considered "Current". This may just be
|
||||
* the element passed in, but if the processor added more elements,
|
||||
* it may choose to reset the current element to one of the elements
|
||||
* it created. (When in doubt, return the element passed in.)
|
||||
*/
|
||||
public function process(\DOMElement $element, $name, $data);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Masterminds\HTML5\Parser;
|
||||
|
||||
use Masterminds\HTML5\Entities;
|
||||
|
||||
/**
|
||||
* Manage entity references.
|
||||
*
|
||||
* This is a simple resolver for HTML5 character reference entitites. See Entities for the list of supported entities.
|
||||
*/
|
||||
class CharacterReference
|
||||
{
|
||||
protected static $numeric_mask = array(
|
||||
0x0,
|
||||
0x2FFFF,
|
||||
0,
|
||||
0xFFFF,
|
||||
);
|
||||
|
||||
/**
|
||||
* Given a name (e.g. 'amp'), lookup the UTF-8 character ('&').
|
||||
*
|
||||
* @param string $name The name to look up.
|
||||
*
|
||||
* @return string The character sequence. In UTF-8 this may be more than one byte.
|
||||
*/
|
||||
public static function lookupName($name)
|
||||
{
|
||||
// Do we really want to return NULL here? or FFFD
|
||||
return isset(Entities::$byName[$name]) ? Entities::$byName[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a decimal number, return the UTF-8 character.
|
||||
*
|
||||
* @param $int
|
||||
*
|
||||
* @return false|string|string[]|null
|
||||
*/
|
||||
public static function lookupDecimal($int)
|
||||
{
|
||||
$entity = '&#' . $int . ';';
|
||||
|
||||
// UNTESTED: This may fail on some planes. Couldn't find full documentation
|
||||
// on the value of the mask array.
|
||||
return mb_decode_numericentity($entity, static::$numeric_mask, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a hexidecimal number, return the UTF-8 character.
|
||||
*
|
||||
* @param $hexdec
|
||||
*
|
||||
* @return false|string|string[]|null
|
||||
*/
|
||||
public static function lookupHex($hexdec)
|
||||
{
|
||||
return static::lookupDecimal(hexdec($hexdec));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,705 @@
|
||||
<?php
|
||||
|
||||
namespace Masterminds\HTML5\Parser;
|
||||
|
||||
use Masterminds\HTML5\Elements;
|
||||
use Masterminds\HTML5\InstructionProcessor;
|
||||
|
||||
/**
|
||||
* Create an HTML5 DOM tree from events.
|
||||
*
|
||||
* This attempts to create a DOM from events emitted by a parser. This
|
||||
* attempts (but does not guarantee) to up-convert older HTML documents
|
||||
* to HTML5. It does this by applying HTML5's rules, but it will not
|
||||
* change the architecture of the document itself.
|
||||
*
|
||||
* Many of the error correction and quirks features suggested in the specification
|
||||
* are implemented herein; however, not all of them are. Since we do not
|
||||
* assume a graphical user agent, no presentation-specific logic is conducted
|
||||
* during tree building.
|
||||
*
|
||||
* FIXME: The present tree builder does not exactly follow the state machine rules
|
||||
* for insert modes as outlined in the HTML5 spec. The processor needs to be
|
||||
* re-written to accomodate this. See, for example, the Go language HTML5
|
||||
* parser.
|
||||
*/
|
||||
class DOMTreeBuilder implements EventHandler
|
||||
{
|
||||
/**
|
||||
* Defined in http://www.w3.org/TR/html51/infrastructure.html#html-namespace-0.
|
||||
*/
|
||||
const NAMESPACE_HTML = 'http://www.w3.org/1999/xhtml';
|
||||
|
||||
const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML';
|
||||
|
||||
const NAMESPACE_SVG = 'http://www.w3.org/2000/svg';
|
||||
|
||||
const NAMESPACE_XLINK = 'http://www.w3.org/1999/xlink';
|
||||
|
||||
const NAMESPACE_XML = 'http://www.w3.org/XML/1998/namespace';
|
||||
|
||||
const NAMESPACE_XMLNS = 'http://www.w3.org/2000/xmlns/';
|
||||
|
||||
const OPT_DISABLE_HTML_NS = 'disable_html_ns';
|
||||
|
||||
const OPT_TARGET_DOC = 'target_document';
|
||||
|
||||
const OPT_IMPLICIT_NS = 'implicit_namespaces';
|
||||
|
||||
/**
|
||||
* Holds the HTML5 element names that causes a namespace switch.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $nsRoots = array(
|
||||
'html' => self::NAMESPACE_HTML,
|
||||
'svg' => self::NAMESPACE_SVG,
|
||||
'math' => self::NAMESPACE_MATHML,
|
||||
);
|
||||
|
||||
/**
|
||||
* Holds the always available namespaces (which does not require the XMLNS declaration).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $implicitNamespaces = array(
|
||||
'xml' => self::NAMESPACE_XML,
|
||||
'xmlns' => self::NAMESPACE_XMLNS,
|
||||
'xlink' => self::NAMESPACE_XLINK,
|
||||
);
|
||||
|
||||
/**
|
||||
* Holds a stack of currently active namespaces.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $nsStack = array();
|
||||
|
||||
/**
|
||||
* Holds the number of namespaces declared by a node.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $pushes = array();
|
||||
|
||||
/**
|
||||
* Defined in 8.2.5.
|
||||
*/
|
||||
const IM_INITIAL = 0;
|
||||
|
||||
const IM_BEFORE_HTML = 1;
|
||||
|
||||
const IM_BEFORE_HEAD = 2;
|
||||
|
||||
const IM_IN_HEAD = 3;
|
||||
|
||||
const IM_IN_HEAD_NOSCRIPT = 4;
|
||||
|
||||
const IM_AFTER_HEAD = 5;
|
||||
|
||||
const IM_IN_BODY = 6;
|
||||
|
||||
const IM_TEXT = 7;
|
||||
|
||||
const IM_IN_TABLE = 8;
|
||||
|
||||
const IM_IN_TABLE_TEXT = 9;
|
||||
|
||||
const IM_IN_CAPTION = 10;
|
||||
|
||||
const IM_IN_COLUMN_GROUP = 11;
|
||||
|
||||
const IM_IN_TABLE_BODY = 12;
|
||||
|
||||
const IM_IN_ROW = 13;
|
||||
|
||||
const IM_IN_CELL = 14;
|
||||
|
||||
const IM_IN_SELECT = 15;
|
||||
|
||||
const IM_IN_SELECT_IN_TABLE = 16;
|
||||
|
||||
const IM_AFTER_BODY = 17;
|
||||
|
||||
const IM_IN_FRAMESET = 18;
|
||||
|
||||
const IM_AFTER_FRAMESET = 19;
|
||||
|
||||
const IM_AFTER_AFTER_BODY = 20;
|
||||
|
||||
const IM_AFTER_AFTER_FRAMESET = 21;
|
||||
|
||||
const IM_IN_SVG = 22;
|
||||
|
||||
const IM_IN_MATHML = 23;
|
||||
|
||||
protected $options = array();
|
||||
|
||||
protected $stack = array();
|
||||
|
||||
protected $current; // Pointer in the tag hierarchy.
|
||||
protected $rules;
|
||||
protected $doc;
|
||||
|
||||
protected $frag;
|
||||
|
||||
protected $processor;
|
||||
|
||||
protected $insertMode = 0;
|
||||
|
||||
/**
|
||||
* Track if we are in an element that allows only inline child nodes.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $onlyInline;
|
||||
|
||||
/**
|
||||
* Quirks mode is enabled by default.
|
||||
* Any document that is missing the DT will be considered to be in quirks mode.
|
||||
*/
|
||||
protected $quirks = true;
|
||||
|
||||
protected $errors = array();
|
||||
|
||||
public function __construct($isFragment = false, array $options = array())
|
||||
{
|
||||
$this->options = $options;
|
||||
|
||||
if (isset($options[self::OPT_TARGET_DOC])) {
|
||||
$this->doc = $options[self::OPT_TARGET_DOC];
|
||||
} else {
|
||||
$impl = new \DOMImplementation();
|
||||
// XXX:
|
||||
// Create the doctype. For now, we are always creating HTML5
|
||||
// documents, and attempting to up-convert any older DTDs to HTML5.
|
||||
$dt = $impl->createDocumentType('html');
|
||||
// $this->doc = \DOMImplementation::createDocument(NULL, 'html', $dt);
|
||||
$this->doc = $impl->createDocument(null, '', $dt);
|
||||
$this->doc->encoding = !empty($options['encoding']) ? $options['encoding'] : 'UTF-8';
|
||||
}
|
||||
|
||||
$this->errors = array();
|
||||
|
||||
$this->current = $this->doc; // ->documentElement;
|
||||
|
||||
// Create a rules engine for tags.
|
||||
$this->rules = new TreeBuildingRules();
|
||||
|
||||
$implicitNS = array();
|
||||
if (isset($this->options[self::OPT_IMPLICIT_NS])) {
|
||||
$implicitNS = $this->options[self::OPT_IMPLICIT_NS];
|
||||
} elseif (isset($this->options['implicitNamespaces'])) {
|
||||
$implicitNS = $this->options['implicitNamespaces'];
|
||||
}
|
||||
|
||||
// Fill $nsStack with the defalut HTML5 namespaces, plus the "implicitNamespaces" array taken form $options
|
||||
array_unshift($this->nsStack, $implicitNS + array('' => self::NAMESPACE_HTML) + $this->implicitNamespaces);
|
||||
|
||||
if ($isFragment) {
|
||||
$this->insertMode = static::IM_IN_BODY;
|
||||
$this->frag = $this->doc->createDocumentFragment();
|
||||
$this->current = $this->frag;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the document.
|
||||
*/
|
||||
public function document()
|
||||
{
|
||||
return $this->doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DOM fragment for the body.
|
||||
*
|
||||
* This returns a DOMNodeList because a fragment may have zero or more
|
||||
* DOMNodes at its root.
|
||||
*
|
||||
* @see http://www.w3.org/TR/2012/CR-html5-20121217/syntax.html#concept-frag-parse-context
|
||||
*
|
||||
* @return \DOMDocumentFragment
|
||||
*/
|
||||
public function fragment()
|
||||
{
|
||||
return $this->frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide an instruction processor.
|
||||
*
|
||||
* This is used for handling Processor Instructions as they are
|
||||
* inserted. If omitted, PI's are inserted directly into the DOM tree.
|
||||
*
|
||||
* @param InstructionProcessor $proc
|
||||
*/
|
||||
public function setInstructionProcessor(InstructionProcessor $proc)
|
||||
{
|
||||
$this->processor = $proc;
|
||||
}
|
||||
|
||||
public function doctype($name, $idType = 0, $id = null, $quirks = false)
|
||||
{
|
||||
// This is used solely for setting quirks mode. Currently we don't
|
||||
// try to preserve the inbound DT. We convert it to HTML5.
|
||||
$this->quirks = $quirks;
|
||||
|
||||
if ($this->insertMode > static::IM_INITIAL) {
|
||||
$this->parseError('Illegal placement of DOCTYPE tag. Ignoring: ' . $name);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->insertMode = static::IM_BEFORE_HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the start tag.
|
||||
*
|
||||
* @todo - XMLNS namespace handling (we need to parse, even if it's not valid)
|
||||
* - XLink, MathML and SVG namespace handling
|
||||
* - Omission rules: 8.1.2.4 Optional tags
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $attributes
|
||||
* @param bool $selfClosing
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function startTag($name, $attributes = array(), $selfClosing = false)
|
||||
{
|
||||
$lname = $this->normalizeTagName($name);
|
||||
|
||||
// Make sure we have an html element.
|
||||
if (!$this->doc->documentElement && 'html' !== $name && !$this->frag) {
|
||||
$this->startTag('html');
|
||||
}
|
||||
|
||||
// Set quirks mode if we're at IM_INITIAL with no doctype.
|
||||
if ($this->insertMode === static::IM_INITIAL) {
|
||||
$this->quirks = true;
|
||||
$this->parseError('No DOCTYPE specified.');
|
||||
}
|
||||
|
||||
// SPECIAL TAG HANDLING:
|
||||
// Spec says do this, and "don't ask."
|
||||
// find the spec where this is defined... looks problematic
|
||||
if ('image' === $name && !($this->insertMode === static::IM_IN_SVG || $this->insertMode === static::IM_IN_MATHML)) {
|
||||
$name = 'img';
|
||||
}
|
||||
|
||||
// Autoclose p tags where appropriate.
|
||||
if ($this->insertMode >= static::IM_IN_BODY && Elements::isA($name, Elements::AUTOCLOSE_P)) {
|
||||
$this->autoclose('p');
|
||||
}
|
||||
|
||||
// Set insert mode:
|
||||
switch ($name) {
|
||||
case 'html':
|
||||
$this->insertMode = static::IM_BEFORE_HEAD;
|
||||
break;
|
||||
case 'head':
|
||||
if ($this->insertMode > static::IM_BEFORE_HEAD) {
|
||||
$this->parseError('Unexpected head tag outside of head context.');
|
||||
} else {
|
||||
$this->insertMode = static::IM_IN_HEAD;
|
||||
}
|
||||
break;
|
||||
case 'body':
|
||||
$this->insertMode = static::IM_IN_BODY;
|
||||
break;
|
||||
case 'svg':
|
||||
$this->insertMode = static::IM_IN_SVG;
|
||||
break;
|
||||
case 'math':
|
||||
$this->insertMode = static::IM_IN_MATHML;
|
||||
break;
|
||||
case 'noscript':
|
||||
if ($this->insertMode === static::IM_IN_HEAD) {
|
||||
$this->insertMode = static::IM_IN_HEAD_NOSCRIPT;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Special case handling for SVG.
|
||||
if ($this->insertMode === static::IM_IN_SVG) {
|
||||
$lname = Elements::normalizeSvgElement($lname);
|
||||
}
|
||||
|
||||
$pushes = 0;
|
||||
// when we found a tag thats appears inside $nsRoots, we have to switch the defalut namespace
|
||||
if (isset($this->nsRoots[$lname]) && $this->nsStack[0][''] !== $this->nsRoots[$lname]) {
|
||||
array_unshift($this->nsStack, array(
|
||||
'' => $this->nsRoots[$lname],
|
||||
) + $this->nsStack[0]);
|
||||
++$pushes;
|
||||
}
|
||||
$needsWorkaround = false;
|
||||
if (isset($this->options['xmlNamespaces']) && $this->options['xmlNamespaces']) {
|
||||
// when xmlNamespaces is true a and we found a 'xmlns' or 'xmlns:*' attribute, we should add a new item to the $nsStack
|
||||
foreach ($attributes as $aName => $aVal) {
|
||||
if ('xmlns' === $aName) {
|
||||
$needsWorkaround = $aVal;
|
||||
array_unshift($this->nsStack, array(
|
||||
'' => $aVal,
|
||||
) + $this->nsStack[0]);
|
||||
++$pushes;
|
||||
} elseif ('xmlns' === (($pos = strpos($aName, ':')) ? substr($aName, 0, $pos) : '')) {
|
||||
array_unshift($this->nsStack, array(
|
||||
substr($aName, $pos + 1) => $aVal,
|
||||
) + $this->nsStack[0]);
|
||||
++$pushes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->onlyInline && Elements::isA($lname, Elements::BLOCK_TAG)) {
|
||||
$this->autoclose($this->onlyInline);
|
||||
$this->onlyInline = null;
|
||||
}
|
||||
|
||||
try {
|
||||
$prefix = ($pos = strpos($lname, ':')) ? substr($lname, 0, $pos) : '';
|
||||
|
||||
if (false !== $needsWorkaround) {
|
||||
$xml = "<$lname xmlns=\"$needsWorkaround\" " . (strlen($prefix) && isset($this->nsStack[0][$prefix]) ? ("xmlns:$prefix=\"" . $this->nsStack[0][$prefix] . '"') : '') . '/>';
|
||||
|
||||
$frag = new \DOMDocument('1.0', 'UTF-8');
|
||||
$frag->loadXML($xml);
|
||||
|
||||
$ele = $this->doc->importNode($frag->documentElement, true);
|
||||
} else {
|
||||
if (!isset($this->nsStack[0][$prefix]) || ('' === $prefix && isset($this->options[self::OPT_DISABLE_HTML_NS]) && $this->options[self::OPT_DISABLE_HTML_NS])) {
|
||||
$ele = $this->doc->createElement($lname);
|
||||
} else {
|
||||
$ele = $this->doc->createElementNS($this->nsStack[0][$prefix], $lname);
|
||||
}
|
||||
}
|
||||
} catch (\DOMException $e) {
|
||||
$this->parseError("Illegal tag name: <$lname>. Replaced with <invalid>.");
|
||||
$ele = $this->doc->createElement('invalid');
|
||||
}
|
||||
|
||||
if (Elements::isA($lname, Elements::BLOCK_ONLY_INLINE)) {
|
||||
$this->onlyInline = $lname;
|
||||
}
|
||||
|
||||
// When we add some namespacess, we have to track them. Later, when "endElement" is invoked, we have to remove them.
|
||||
// When we are on a void tag, we do not need to care about namesapce nesting.
|
||||
if ($pushes > 0 && !Elements::isA($name, Elements::VOID_TAG)) {
|
||||
// PHP tends to free the memory used by DOM,
|
||||
// to avoid spl_object_hash collisions whe have to avoid garbage collection of $ele storing it into $pushes
|
||||
// see https://bugs.php.net/bug.php?id=67459
|
||||
$this->pushes[spl_object_hash($ele)] = array($pushes, $ele);
|
||||
}
|
||||
|
||||
foreach ($attributes as $aName => $aVal) {
|
||||
// xmlns attributes can't be set
|
||||
if ('xmlns' === $aName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->insertMode === static::IM_IN_SVG) {
|
||||
$aName = Elements::normalizeSvgAttribute($aName);
|
||||
} elseif ($this->insertMode === static::IM_IN_MATHML) {
|
||||
$aName = Elements::normalizeMathMlAttribute($aName);
|
||||
}
|
||||
|
||||
$aVal = (string) $aVal;
|
||||
|
||||
try {
|
||||
$prefix = ($pos = strpos($aName, ':')) ? substr($aName, 0, $pos) : false;
|
||||
|
||||
if ('xmlns' === $prefix) {
|
||||
$ele->setAttributeNS(self::NAMESPACE_XMLNS, $aName, $aVal);
|
||||
} elseif (false !== $prefix && isset($this->nsStack[0][$prefix])) {
|
||||
$ele->setAttributeNS($this->nsStack[0][$prefix], $aName, $aVal);
|
||||
} else {
|
||||
$ele->setAttribute($aName, $aVal);
|
||||
}
|
||||
} catch (\DOMException $e) {
|
||||
$this->parseError("Illegal attribute name for tag $name. Ignoring: $aName");
|
||||
continue;
|
||||
}
|
||||
|
||||
// This is necessary on a non-DTD schema, like HTML5.
|
||||
if ('id' === $aName) {
|
||||
$ele->setIdAttribute('id', true);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->frag !== $this->current && $this->rules->hasRules($name)) {
|
||||
// Some elements have special processing rules. Handle those separately.
|
||||
$this->current = $this->rules->evaluate($ele, $this->current);
|
||||
} else {
|
||||
// Otherwise, it's a standard element.
|
||||
$this->current->appendChild($ele);
|
||||
|
||||
if (!Elements::isA($name, Elements::VOID_TAG)) {
|
||||
$this->current = $ele;
|
||||
}
|
||||
|
||||
// Self-closing tags should only be respected on foreign elements
|
||||
// (and are implied on void elements)
|
||||
// See: https://www.w3.org/TR/html5/syntax.html#start-tags
|
||||
if (Elements::isHtml5Element($name)) {
|
||||
$selfClosing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// This is sort of a last-ditch attempt to correct for cases where no head/body
|
||||
// elements are provided.
|
||||
if ($this->insertMode <= static::IM_BEFORE_HEAD && 'head' !== $name && 'html' !== $name) {
|
||||
$this->insertMode = static::IM_IN_BODY;
|
||||
}
|
||||
|
||||
// When we are on a void tag, we do not need to care about namesapce nesting,
|
||||
// but we have to remove the namespaces pushed to $nsStack.
|
||||
if ($pushes > 0 && Elements::isA($name, Elements::VOID_TAG)) {
|
||||
// remove the namespaced definded by current node
|
||||
for ($i = 0; $i < $pushes; ++$i) {
|
||||
array_shift($this->nsStack);
|
||||
}
|
||||
}
|
||||
|
||||
if ($selfClosing) {
|
||||
$this->endTag($name);
|
||||
}
|
||||
|
||||
// Return the element mask, which the tokenizer can then use to set
|
||||
// various processing rules.
|
||||
return Elements::element($name);
|
||||
}
|
||||
|
||||
public function endTag($name)
|
||||
{
|
||||
$lname = $this->normalizeTagName($name);
|
||||
|
||||
// Special case within 12.2.6.4.7: An end tag whose tag name is "br" should be treated as an opening tag
|
||||
if ('br' === $name) {
|
||||
$this->parseError('Closing tag encountered for void element br.');
|
||||
|
||||
$this->startTag('br');
|
||||
}
|
||||
// Ignore closing tags for other unary elements.
|
||||
elseif (Elements::isA($name, Elements::VOID_TAG)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->insertMode <= static::IM_BEFORE_HTML) {
|
||||
// 8.2.5.4.2
|
||||
if (in_array($name, array(
|
||||
'html',
|
||||
'br',
|
||||
'head',
|
||||
'title',
|
||||
))) {
|
||||
$this->startTag('html');
|
||||
$this->endTag($name);
|
||||
$this->insertMode = static::IM_BEFORE_HEAD;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore the tag.
|
||||
$this->parseError('Illegal closing tag at global scope.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case handling for SVG.
|
||||
if ($this->insertMode === static::IM_IN_SVG) {
|
||||
$lname = Elements::normalizeSvgElement($lname);
|
||||
}
|
||||
|
||||
$cid = spl_object_hash($this->current);
|
||||
|
||||
// XXX: HTML has no parent. What do we do, though,
|
||||
// if this element appears in the wrong place?
|
||||
if ('html' === $lname) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the namespaced definded by current node
|
||||
if (isset($this->pushes[$cid])) {
|
||||
for ($i = 0; $i < $this->pushes[$cid][0]; ++$i) {
|
||||
array_shift($this->nsStack);
|
||||
}
|
||||
unset($this->pushes[$cid]);
|
||||
}
|
||||
|
||||
if (!$this->autoclose($lname)) {
|
||||
$this->parseError('Could not find closing tag for ' . $lname);
|
||||
}
|
||||
|
||||
switch ($lname) {
|
||||
case 'head':
|
||||
$this->insertMode = static::IM_AFTER_HEAD;
|
||||
break;
|
||||
case 'body':
|
||||
$this->insertMode = static::IM_AFTER_BODY;
|
||||
break;
|
||||
case 'svg':
|
||||
case 'mathml':
|
||||
$this->insertMode = static::IM_IN_BODY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function comment($cdata)
|
||||
{
|
||||
// TODO: Need to handle case where comment appears outside of the HTML tag.
|
||||
$node = $this->doc->createComment($cdata);
|
||||
$this->current->appendChild($node);
|
||||
}
|
||||
|
||||
public function text($data)
|
||||
{
|
||||
// XXX: Hmmm.... should we really be this strict?
|
||||
if ($this->insertMode < static::IM_IN_HEAD) {
|
||||
// Per '8.2.5.4.3 The "before head" insertion mode' the characters
|
||||
// " \t\n\r\f" should be ignored but no mention of a parse error. This is
|
||||
// practical as most documents contain these characters. Other text is not
|
||||
// expected here so recording a parse error is necessary.
|
||||
$dataTmp = trim($data, " \t\n\r\f");
|
||||
if (!empty($dataTmp)) {
|
||||
// fprintf(STDOUT, "Unexpected insert mode: %d", $this->insertMode);
|
||||
$this->parseError('Unexpected text. Ignoring: ' . $dataTmp);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
// fprintf(STDOUT, "Appending text %s.", $data);
|
||||
$node = $this->doc->createTextNode($data);
|
||||
$this->current->appendChild($node);
|
||||
}
|
||||
|
||||
public function eof()
|
||||
{
|
||||
// If the $current isn't the $root, do we need to do anything?
|
||||
}
|
||||
|
||||
public function parseError($msg, $line = 0, $col = 0)
|
||||
{
|
||||
$this->errors[] = sprintf('Line %d, Col %d: %s', $line, $col, $msg);
|
||||
}
|
||||
|
||||
public function getErrors()
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
public function cdata($data)
|
||||
{
|
||||
$node = $this->doc->createCDATASection($data);
|
||||
$this->current->appendChild($node);
|
||||
}
|
||||
|
||||
public function processingInstruction($name, $data = null)
|
||||
{
|
||||
// XXX: Ignore initial XML declaration, per the spec.
|
||||
if ($this->insertMode === static::IM_INITIAL && 'xml' === strtolower($name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Important: The processor may modify the current DOM tree however it sees fit.
|
||||
if ($this->processor instanceof InstructionProcessor) {
|
||||
$res = $this->processor->process($this->current, $name, $data);
|
||||
if (!empty($res)) {
|
||||
$this->current = $res;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, this is just a dumb PI element.
|
||||
$node = $this->doc->createProcessingInstruction($name, $data);
|
||||
|
||||
$this->current->appendChild($node);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// UTILITIES
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Apply normalization rules to a tag name.
|
||||
* See sections 2.9 and 8.1.2.
|
||||
*
|
||||
* @param string $tagName
|
||||
*
|
||||
* @return string The normalized tag name.
|
||||
*/
|
||||
protected function normalizeTagName($tagName)
|
||||
{
|
||||
/*
|
||||
* Section 2.9 suggests that we should not do this. if (strpos($name, ':') !== false) { // We know from the grammar that there must be at least one other // char besides :, since : is not a legal tag start. $parts = explode(':', $name); return array_pop($parts); }
|
||||
*/
|
||||
return $tagName;
|
||||
}
|
||||
|
||||
protected function quirksTreeResolver($name)
|
||||
{
|
||||
throw new \Exception('Not implemented.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically climb the tree and close the closest node with the matching $tag.
|
||||
*
|
||||
* @param string $tagName
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function autoclose($tagName)
|
||||
{
|
||||
$working = $this->current;
|
||||
do {
|
||||
if (XML_ELEMENT_NODE !== $working->nodeType) {
|
||||
return false;
|
||||
}
|
||||
if ($working->tagName === $tagName) {
|
||||
$this->current = $working->parentNode;
|
||||
|
||||
return true;
|
||||
}
|
||||
} while ($working = $working->parentNode);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given tagname is an ancestor of the present candidate.
|
||||
*
|
||||
* If $this->current or anything above $this->current matches the given tag
|
||||
* name, this returns true.
|
||||
*
|
||||
* @param string $tagName
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAncestor($tagName)
|
||||
{
|
||||
$candidate = $this->current;
|
||||
while (XML_ELEMENT_NODE === $candidate->nodeType) {
|
||||
if ($candidate->tagName === $tagName) {
|
||||
return true;
|
||||
}
|
||||
$candidate = $candidate->parentNode;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the immediate parent element is of the given tagname.
|
||||
*
|
||||
* @param string $tagName
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isParent($tagName)
|
||||
{
|
||||
return $this->current->tagName === $tagName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Masterminds\HTML5\Parser;
|
||||
|
||||
/**
|
||||
* Standard events for HTML5.
|
||||
*
|
||||
* This is roughly analogous to a SAX2 or expat-style interface.
|
||||
* However, it is tuned specifically for HTML5, according to section 8
|
||||
* of the HTML5 specification.
|
||||
*
|
||||
* An event handler receives parser events. For a concrete
|
||||
* implementation, see DOMTreeBuilder.
|
||||
*
|
||||
* Quirks support in the parser is limited to close-in syntax (malformed
|
||||
* tags or attributes). Higher order syntax and semantic issues with a
|
||||
* document (e.g. mismatched tags, illegal nesting, etc.) are the
|
||||
* responsibility of the event handler implementation.
|
||||
*
|
||||
* See HTML5 spec section 8.2.4
|
||||
*/
|
||||
interface EventHandler
|
||||
{
|
||||
const DOCTYPE_NONE = 0;
|
||||
|
||||
const DOCTYPE_PUBLIC = 1;
|
||||
|
||||
const DOCTYPE_SYSTEM = 2;
|
||||
|
||||
/**
|
||||
* A doctype declaration.
|
||||
*
|
||||
* @param string $name The name of the root element.
|
||||
* @param int $idType One of DOCTYPE_NONE, DOCTYPE_PUBLIC, or DOCTYPE_SYSTEM
|
||||
* @param string $id The identifier. For DOCTYPE_PUBLIC, this is the public ID. If DOCTYPE_SYSTEM,
|
||||
* then this is a system ID.
|
||||
* @param bool $quirks Indicates whether the builder should enter quirks mode.
|
||||
*/
|
||||
public function doctype($name, $idType = 0, $id = null, $quirks = false);
|
||||
|
||||
/**
|
||||
* A start tag.
|
||||
*
|
||||
* IMPORTANT: The parser watches the return value of this event. If this returns
|
||||
* an integer, the parser will switch TEXTMODE patters according to the int.
|
||||
*
|
||||
* This is how the Tree Builder can tell the Tokenizer when a certain tag should
|
||||
* cause the parser to go into RAW text mode.
|
||||
*
|
||||
* The HTML5 standard requires that the builder is the one that initiates this
|
||||
* step, and this is the only way short of a circular reference that we can
|
||||
* do that.
|
||||
*
|
||||
* Example: if a startTag even for a `script` name is fired, and the startTag()
|
||||
* implementation returns Tokenizer::TEXTMODE_RAW, then the tokenizer will
|
||||
* switch into RAW text mode and consume data until it reaches a closing
|
||||
* `script` tag.
|
||||
*
|
||||
* The textmode is automatically reset to Tokenizer::TEXTMODE_NORMAL when the
|
||||
* closing tag is encounter. **This behavior may change.**
|
||||
*
|
||||
* @param string $name The tag name.
|
||||
* @param array $attributes An array with all of the tag's attributes.
|
||||
* @param bool $selfClosing An indicator of whether or not this tag is self-closing (<foo/>).
|
||||
*
|
||||
* @return int one of the Tokenizer::TEXTMODE_* constants
|
||||
*/
|
||||
public function startTag($name, $attributes = array(), $selfClosing = false);
|
||||
|
||||
/**
|
||||
* An end-tag.
|
||||
*/
|
||||
public function endTag($name);
|
||||
|
||||
/**
|
||||
* A comment section (unparsed character data).
|
||||
*/
|
||||
public function comment($cdata);
|
||||
|
||||
/**
|
||||
* A unit of parsed character data.
|
||||
*
|
||||
* Entities in this text are *already decoded*.
|
||||
*/
|
||||
public function text($cdata);
|
||||
|
||||
/**
|
||||
* Indicates that the document has been entirely processed.
|
||||
*/
|
||||
public function eof();
|
||||
|
||||
/**
|
||||
* Emitted when the parser encounters an error condition.
|
||||
*/
|
||||
public function parseError($msg, $line, $col);
|
||||
|
||||
/**
|
||||
* A CDATA section.
|
||||
*
|
||||
* @param string $data
|
||||
* The unparsed character data
|
||||
*/
|
||||
public function cdata($data);
|
||||
|
||||
/**
|
||||
* This is a holdover from the XML spec.
|
||||
*
|
||||
* While user agents don't get PIs, server-side does.
|
||||
*
|
||||
* @param string $name The name of the processor (e.g. 'php').
|
||||
* @param string $data The unparsed data.
|
||||
*/
|
||||
public function processingInstruction($name, $data = null);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Masterminds\HTML5\Parser;
|
||||
|
||||
/**
|
||||
* The FileInputStream loads a file to be parsed.
|
||||
*
|
||||
* So right now we read files into strings and then process the
|
||||
* string. We chose to do this largely for the sake of expediency of
|
||||
* development, and also because we could optimize toward processing
|
||||
* arbitrarily large chunks of the input. But in the future, we'd
|
||||
* really like to rewrite this class to efficiently handle lower level
|
||||
* stream reads (and thus efficiently handle large documents).
|
||||
*
|
||||
* @deprecated since 2.4, to remove in 3.0. Use a string in the scanner instead.
|
||||
*/
|
||||
class FileInputStream extends StringInputStream implements InputStream
|
||||
{
|
||||
/**
|
||||
* Load a file input stream.
|
||||
*
|
||||
* @param string $data The file or url path to load.
|
||||
* @param string $encoding The encoding to use for the data.
|
||||
* @param string $debug A fprintf format to use to echo the data on stdout.
|
||||
*/
|
||||
public function __construct($data, $encoding = 'UTF-8', $debug = '')
|
||||
{
|
||||
// Get the contents of the file.
|
||||
$content = file_get_contents($data);
|
||||
|
||||
parent::__construct($content, $encoding, $debug);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Masterminds\HTML5\Parser;
|
||||
|
||||
/**
|
||||
* Interface for stream readers.
|
||||
*
|
||||
* The parser only reads from streams. Various input sources can write
|
||||
* an adapater to this InputStream.
|
||||
*
|
||||
* Currently provided InputStream implementations include
|
||||
* FileInputStream and StringInputStream.
|
||||
*
|
||||
* @deprecated since 2.4, to remove in 3.0. Use a string in the scanner instead.
|
||||
*/
|
||||
interface InputStream extends \Iterator
|
||||
{
|
||||
/**
|
||||
* Returns the current line that is being consumed.
|
||||
*
|
||||
* TODO: Move this to the scanner.
|
||||
*/
|
||||
public function currentLine();
|
||||
|
||||
/**
|
||||
* Returns the current column of the current line that the tokenizer is at.
|
||||
*
|
||||
* Newlines are column 0. The first char after a newline is column 1.
|
||||
*
|
||||
* @TODO Move this to the scanner.
|
||||
*
|
||||
* @return int The column number.
|
||||
*/
|
||||
public function columnOffset();
|
||||
|
||||
/**
|
||||
* Get all characters until EOF.
|
||||
*
|
||||
* This consumes characters until the EOF.
|
||||
*/
|
||||
public function remainingChars();
|
||||
|
||||
/**
|
||||
* Read to a particular match (or until $max bytes are consumed).
|
||||
*
|
||||
* This operates on byte sequences, not characters.
|
||||
*
|
||||
* Matches as far as possible until we reach a certain set of bytes
|
||||
* and returns the matched substring.
|
||||
*
|
||||
* @see strcspn
|
||||
*
|
||||
* @param string $bytes Bytes to match.
|
||||
* @param int $max Maximum number of bytes to scan.
|
||||
*
|
||||
* @return mixed Index or false if no match is found. You should use strong
|
||||
* equality when checking the result, since index could be 0.
|
||||
*/
|
||||
public function charsUntil($bytes, $max = null);
|
||||
|
||||
/**
|
||||
* Returns the string so long as $bytes matches.
|
||||
*
|
||||
* Matches as far as possible with a certain set of bytes
|
||||
* and returns the matched substring.
|
||||
*
|
||||
* @see strspn
|
||||
*
|
||||
* @param string $bytes A mask of bytes to match. If ANY byte in this mask matches the
|
||||
* current char, the pointer advances and the char is part of the
|
||||
* substring.
|
||||
* @param int $max The max number of chars to read.
|
||||
*/
|
||||
public function charsWhile($bytes, $max = null);
|
||||
|
||||
/**
|
||||
* Unconsume one character.
|
||||
*
|
||||
* @param int $howMany The number of characters to move the pointer back.
|
||||
*/
|
||||
public function unconsume($howMany = 1);
|
||||
|
||||
/**
|
||||
* Retrieve the next character without advancing the pointer.
|
||||
*/
|
||||
public function peek();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Masterminds\HTML5\Parser;
|
||||
|
||||
/**
|
||||
* Emit when the parser has an error.
|
||||
*/
|
||||
class ParseError extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# The Parser Model
|
||||
|
||||
The parser model here follows the model in section
|
||||
[8.2.1](http://www.w3.org/TR/2012/CR-html5-20121217/syntax.html#parsing)
|
||||
of the HTML5 specification, though we do not assume a networking layer.
|
||||
|
||||
[ InputStream ] // Generic support for reading input.
|
||||
||
|
||||
[ Scanner ] // Breaks down the stream into characters.
|
||||
||
|
||||
[ Tokenizer ] // Groups characters into syntactic
|
||||
||
|
||||
[ Tree Builder ] // Organizes units into a tree of objects
|
||||
||
|
||||
[ DOM Document ] // The final state of the parsed document.
|
||||
|
||||
|
||||
## InputStream
|
||||
|
||||
This is an interface with at least two concrete implementations:
|
||||
|
||||
- StringInputStream: Reads an HTML5 string.
|
||||
- FileInputStream: Reads an HTML5 file.
|
||||
|
||||
## Scanner
|
||||
|
||||
This is a mechanical piece of the parser.
|
||||
|
||||
## Tokenizer
|
||||
|
||||
This follows section 8.4 of the HTML5 spec. It is (roughly) a recursive
|
||||
descent parser. (Though there are plenty of optimizations that are less
|
||||
than purely functional.
|
||||
|
||||
## EventHandler and DOMTree
|
||||
|
||||
EventHandler is the interface for tree builders. Since not all
|
||||
implementations will necessarily build trees, we've chosen a more
|
||||
generic name.
|
||||
|
||||
The event handler emits tokens during tokenization.
|
||||
|
||||
The DOMTree is an event handler that builds a DOM tree. The output of
|
||||
the DOMTree builder is a DOMDocument.
|
||||
|
||||
## DOMDocument
|
||||
|
||||
PHP has a DOMDocument class built-in (technically, it's part of libxml.)
|
||||
We use that, thus rendering the output of this process compatible with
|
||||
SimpleXML, QueryPath, and many other XML/HTML processing tools.
|
||||
|
||||
For cases where the HTML5 is a fragment of a HTML5 document a
|
||||
DOMDocumentFragment is returned instead. This is another built-in class.
|
||||
@@ -0,0 +1,416 @@
|
||||
<?php
|
||||
|
||||
namespace Masterminds\HTML5\Parser;
|
||||
|
||||
use Masterminds\HTML5\Exception;
|
||||
|
||||
/**
|
||||
* The scanner scans over a given data input to react appropriately to characters.
|
||||
*/
|
||||
class Scanner
|
||||
{
|
||||
const CHARS_HEX = 'abcdefABCDEF01234567890';
|
||||
const CHARS_ALNUM = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890';
|
||||
const CHARS_ALPHA = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
/**
|
||||
* The string data we're parsing.
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* The current integer byte position we are in $data.
|
||||
*/
|
||||
private $char;
|
||||
|
||||
/**
|
||||
* Length of $data; when $char === $data, we are at the end-of-file.
|
||||
*/
|
||||
private $EOF;
|
||||
|
||||
/**
|
||||
* Parse errors.
|
||||
*/
|
||||
public $errors = array();
|
||||
|
||||
/**
|
||||
* Create a new Scanner.
|
||||
*
|
||||
* @param string $data Data to parse.
|
||||
* @param string $encoding The encoding to use for the data.
|
||||
*
|
||||
* @throws Exception If the given data cannot be encoded to UTF-8.
|
||||
*/
|
||||
public function __construct($data, $encoding = 'UTF-8')
|
||||
{
|
||||
if ($data instanceof InputStream) {
|
||||
@trigger_error('InputStream objects are deprecated since version 2.4 and will be removed in 3.0. Use strings instead.', E_USER_DEPRECATED);
|
||||
$data = (string) $data;
|
||||
}
|
||||
|
||||
$data = UTF8Utils::convertToUTF8($data, $encoding);
|
||||
|
||||
// There is good reason to question whether it makes sense to
|
||||
// do this here, since most of these checks are done during
|
||||
// parsing, and since this check doesn't actually *do* anything.
|
||||
$this->errors = UTF8Utils::checkForIllegalCodepoints($data);
|
||||
|
||||
$data = $this->replaceLinefeeds($data);
|
||||
|
||||
$this->data = $data;
|
||||
$this->char = 0;
|
||||
$this->EOF = strlen($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if upcomming chars match the given sequence.
|
||||
*
|
||||
* This will read the stream for the $sequence. If it's
|
||||
* found, this will return true. If not, return false.
|
||||
* Since this unconsumes any chars it reads, the caller
|
||||
* will still need to read the next sequence, even if
|
||||
* this returns true.
|
||||
*
|
||||
* Example: $this->scanner->sequenceMatches('</script>') will
|
||||
* see if the input stream is at the start of a
|
||||
* '</script>' string.
|
||||
*
|
||||
* @param string $sequence
|
||||
* @param bool $caseSensitive
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function sequenceMatches($sequence, $caseSensitive = true)
|
||||
{
|
||||
$portion = substr($this->data, $this->char, strlen($sequence));
|
||||
|
||||
return $caseSensitive ? $portion === $sequence : 0 === strcasecmp($portion, $sequence);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current position.
|
||||
*
|
||||
* @return int The current intiger byte position.
|
||||
*/
|
||||
public function position()
|
||||
{
|
||||
return $this->char;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a peek at the next character in the data.
|
||||
*
|
||||
* @return string The next character.
|
||||
*/
|
||||
public function peek()
|
||||
{
|
||||
if (($this->char + 1) < $this->EOF) {
|
||||
return $this->data[$this->char + 1];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next character.
|
||||
* Note: This advances the pointer.
|
||||
*
|
||||
* @return string The next character.
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
++$this->char;
|
||||
|
||||
if ($this->char < $this->EOF) {
|
||||
return $this->data[$this->char];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current character.
|
||||
* Note, this does not advance the pointer.
|
||||
*
|
||||
* @return string The current character.
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
if ($this->char < $this->EOF) {
|
||||
return $this->data[$this->char];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Silently consume N chars.
|
||||
*
|
||||
* @param int $count
|
||||
*/
|
||||
public function consume($count = 1)
|
||||
{
|
||||
$this->char += $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unconsume some of the data.
|
||||
* This moves the data pointer backwards.
|
||||
*
|
||||
* @param int $howMany The number of characters to move the pointer back.
|
||||
*/
|
||||
public function unconsume($howMany = 1)
|
||||
{
|
||||
if (($this->char - $howMany) >= 0) {
|
||||
$this->char -= $howMany;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next group of that contains hex characters.
|
||||
* Note, along with getting the characters the pointer in the data will be
|
||||
* moved as well.
|
||||
*
|
||||
* @return string The next group that is hex characters.
|
||||
*/
|
||||
public function getHex()
|
||||
{
|
||||
return $this->doCharsWhile(static::CHARS_HEX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next group of characters that are ASCII Alpha characters.
|
||||
* Note, along with getting the characters the pointer in the data will be
|
||||
* moved as well.
|
||||
*
|
||||
* @return string The next group of ASCII alpha characters.
|
||||
*/
|
||||
public function getAsciiAlpha()
|
||||
{
|
||||
return $this->doCharsWhile(static::CHARS_ALPHA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next group of characters that are ASCII Alpha characters and numbers.
|
||||
* Note, along with getting the characters the pointer in the data will be
|
||||
* moved as well.
|
||||
*
|
||||
* @return string The next group of ASCII alpha characters and numbers.
|
||||
*/
|
||||
public function getAsciiAlphaNum()
|
||||
{
|
||||
return $this->doCharsWhile(static::CHARS_ALNUM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next group of numbers.
|
||||
* Note, along with getting the characters the pointer in the data will be
|
||||
* moved as well.
|
||||
*
|
||||
* @return string The next group of numbers.
|
||||
*/
|
||||
public function getNumeric()
|
||||
{
|
||||
return $this->doCharsWhile('0123456789');
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume whitespace.
|
||||
* Whitespace in HTML5 is: formfeed, tab, newline, space.
|
||||
*
|
||||
* @return int The length of the matched whitespaces.
|
||||
*/
|
||||
public function whitespace()
|
||||
{
|
||||
if ($this->char >= $this->EOF) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$len = strspn($this->data, "\n\t\f ", $this->char);
|
||||
|
||||
$this->char += $len;
|
||||
|
||||
return $len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current line that is being consumed.
|
||||
*
|
||||
* @return int The current line number.
|
||||
*/
|
||||
public function currentLine()
|
||||
{
|
||||
if (empty($this->EOF) || 0 === $this->char) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Add one to $this->char because we want the number for the next
|
||||
// byte to be processed.
|
||||
return substr_count($this->data, "\n", 0, min($this->char, $this->EOF)) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read chars until something in the mask is encountered.
|
||||
*
|
||||
* @param string $mask
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function charsUntil($mask)
|
||||
{
|
||||
return $this->doCharsUntil($mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read chars as long as the mask matches.
|
||||
*
|
||||
* @param string $mask
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function charsWhile($mask)
|
||||
{
|
||||
return $this->doCharsWhile($mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current column of the current line that the tokenizer is at.
|
||||
*
|
||||
* Newlines are column 0. The first char after a newline is column 1.
|
||||
*
|
||||
* @return int The column number.
|
||||
*/
|
||||
public function columnOffset()
|
||||
{
|
||||
// Short circuit for the first char.
|
||||
if (0 === $this->char) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// strrpos is weird, and the offset needs to be negative for what we
|
||||
// want (i.e., the last \n before $this->char). This needs to not have
|
||||
// one (to make it point to the next character, the one we want the
|
||||
// position of) added to it because strrpos's behaviour includes the
|
||||
// final offset byte.
|
||||
$backwardFrom = $this->char - 1 - strlen($this->data);
|
||||
$lastLine = strrpos($this->data, "\n", $backwardFrom);
|
||||
|
||||
// However, for here we want the length up until the next byte to be
|
||||
// processed, so add one to the current byte ($this->char).
|
||||
if (false !== $lastLine) {
|
||||
$findLengthOf = substr($this->data, $lastLine + 1, $this->char - 1 - $lastLine);
|
||||
} else {
|
||||
// After a newline.
|
||||
$findLengthOf = substr($this->data, 0, $this->char);
|
||||
}
|
||||
|
||||
return UTF8Utils::countChars($findLengthOf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all characters until EOF.
|
||||
*
|
||||
* This consumes characters until the EOF.
|
||||
*
|
||||
* @return int The number of characters remaining.
|
||||
*/
|
||||
public function remainingChars()
|
||||
{
|
||||
if ($this->char < $this->EOF) {
|
||||
$data = substr($this->data, $this->char);
|
||||
$this->char = $this->EOF;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
return ''; // false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace linefeed characters according to the spec.
|
||||
*
|
||||
* @param $data
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function replaceLinefeeds($data)
|
||||
{
|
||||
/*
|
||||
* U+000D CARRIAGE RETURN (CR) characters and U+000A LINE FEED (LF) characters are treated specially.
|
||||
* Any CR characters that are followed by LF characters must be removed, and any CR characters not
|
||||
* followed by LF characters must be converted to LF characters. Thus, newlines in HTML DOMs are
|
||||
* represented by LF characters, and there are never any CR characters in the input to the tokenization
|
||||
* stage.
|
||||
*/
|
||||
$crlfTable = array(
|
||||
"\0" => "\xEF\xBF\xBD",
|
||||
"\r\n" => "\n",
|
||||
"\r" => "\n",
|
||||
);
|
||||
|
||||
return strtr($data, $crlfTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read to a particular match (or until $max bytes are consumed).
|
||||
*
|
||||
* This operates on byte sequences, not characters.
|
||||
*
|
||||
* Matches as far as possible until we reach a certain set of bytes
|
||||
* and returns the matched substring.
|
||||
*
|
||||
* @param string $bytes Bytes to match.
|
||||
* @param int $max Maximum number of bytes to scan.
|
||||
*
|
||||
* @return mixed Index or false if no match is found. You should use strong
|
||||
* equality when checking the result, since index could be 0.
|
||||
*/
|
||||
private function doCharsUntil($bytes, $max = null)
|
||||
{
|
||||
if ($this->char >= $this->EOF) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (0 === $max || $max) {
|
||||
$len = strcspn($this->data, $bytes, $this->char, $max);
|
||||
} else {
|
||||
$len = strcspn($this->data, $bytes, $this->char);
|
||||
}
|
||||
|
||||
$string = (string) substr($this->data, $this->char, $len);
|
||||
$this->char += $len;
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string so long as $bytes matches.
|
||||
*
|
||||
* Matches as far as possible with a certain set of bytes
|
||||
* and returns the matched substring.
|
||||
*
|
||||
* @param string $bytes A mask of bytes to match. If ANY byte in this mask matches the
|
||||
* current char, the pointer advances and the char is part of the
|
||||
* substring.
|
||||
* @param int $max The max number of chars to read.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function doCharsWhile($bytes, $max = null)
|
||||
{
|
||||
if ($this->char >= $this->EOF) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (0 === $max || $max) {
|
||||
$len = strspn($this->data, $bytes, $this->char, $max);
|
||||
} else {
|
||||
$len = strspn($this->data, $bytes, $this->char);
|
||||
}
|
||||
|
||||
$string = (string) substr($this->data, $this->char, $len);
|
||||
$this->char += $len;
|
||||
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
/**
|
||||
* Loads a string to be parsed.
|
||||
*/
|
||||
|
||||
namespace Masterminds\HTML5\Parser;
|
||||
|
||||
/*
|
||||
*
|
||||
* Based on code from html5lib:
|
||||
|
||||
Copyright 2009 Geoffrey Sneddon <http://gsnedders.com/>
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
// Some conventions:
|
||||
// - /* */ indicates verbatim text from the HTML 5 specification
|
||||
// MPB: Not sure which version of the spec. Moving from HTML5lib to
|
||||
// HTML5-PHP, I have been using this version:
|
||||
// http://www.w3.org/TR/2012/CR-html5-20121217/Overview.html#contents
|
||||
//
|
||||
// - // indicates regular comments
|
||||
|
||||
/**
|
||||
* @deprecated since 2.4, to remove in 3.0. Use a string in the scanner instead.
|
||||
*/
|
||||
class StringInputStream implements InputStream
|
||||
{
|
||||
/**
|
||||
* The string data we're parsing.
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* The current integer byte position we are in $data.
|
||||
*/
|
||||
private $char;
|
||||
|
||||
/**
|
||||
* Length of $data; when $char === $data, we are at the end-of-file.
|
||||
*/
|
||||
private $EOF;
|
||||
|
||||
/**
|
||||
* Parse errors.
|
||||
*/
|
||||
public $errors = array();
|
||||
|
||||
/**
|
||||
* Create a new InputStream wrapper.
|
||||
*
|
||||
* @param string $data Data to parse.
|
||||
* @param string $encoding The encoding to use for the data.
|
||||
* @param string $debug A fprintf format to use to echo the data on stdout.
|
||||
*/
|
||||
public function __construct($data, $encoding = 'UTF-8', $debug = '')
|
||||
{
|
||||
$data = UTF8Utils::convertToUTF8($data, $encoding);
|
||||
if ($debug) {
|
||||
fprintf(STDOUT, $debug, $data, strlen($data));
|
||||
}
|
||||
|
||||
// There is good reason to question whether it makes sense to
|
||||
// do this here, since most of these checks are done during
|
||||
// parsing, and since this check doesn't actually *do* anything.
|
||||
$this->errors = UTF8Utils::checkForIllegalCodepoints($data);
|
||||
|
||||
$data = $this->replaceLinefeeds($data);
|
||||
|
||||
$this->data = $data;
|
||||
$this->char = 0;
|
||||
$this->EOF = strlen($data);
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace linefeed characters according to the spec.
|
||||
*/
|
||||
protected function replaceLinefeeds($data)
|
||||
{
|
||||
/*
|
||||
* U+000D CARRIAGE RETURN (CR) characters and U+000A LINE FEED (LF) characters are treated specially.
|
||||
* Any CR characters that are followed by LF characters must be removed, and any CR characters not
|
||||
* followed by LF characters must be converted to LF characters. Thus, newlines in HTML DOMs are
|
||||
* represented by LF characters, and there are never any CR characters in the input to the tokenization
|
||||
* stage.
|
||||
*/
|
||||
$crlfTable = array(
|
||||
"\0" => "\xEF\xBF\xBD",
|
||||
"\r\n" => "\n",
|
||||
"\r" => "\n",
|
||||
);
|
||||
|
||||
return strtr($data, $crlfTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current line that the tokenizer is at.
|
||||
*/
|
||||
public function currentLine()
|
||||
{
|
||||
if (empty($this->EOF) || 0 === $this->char) {
|
||||
return 1;
|
||||
}
|
||||
// Add one to $this->char because we want the number for the next
|
||||
// byte to be processed.
|
||||
return substr_count($this->data, "\n", 0, min($this->char, $this->EOF)) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function getCurrentLine()
|
||||
{
|
||||
return $this->currentLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current column of the current line that the tokenizer is at.
|
||||
* Newlines are column 0. The first char after a newline is column 1.
|
||||
*
|
||||
* @return int The column number.
|
||||
*/
|
||||
public function columnOffset()
|
||||
{
|
||||
// Short circuit for the first char.
|
||||
if (0 === $this->char) {
|
||||
return 0;
|
||||
}
|
||||
// strrpos is weird, and the offset needs to be negative for what we
|
||||
// want (i.e., the last \n before $this->char). This needs to not have
|
||||
// one (to make it point to the next character, the one we want the
|
||||
// position of) added to it because strrpos's behaviour includes the
|
||||
// final offset byte.
|
||||
$backwardFrom = $this->char - 1 - strlen($this->data);
|
||||
$lastLine = strrpos($this->data, "\n", $backwardFrom);
|
||||
|
||||
// However, for here we want the length up until the next byte to be
|
||||
// processed, so add one to the current byte ($this->char).
|
||||
if (false !== $lastLine) {
|
||||
$findLengthOf = substr($this->data, $lastLine + 1, $this->char - 1 - $lastLine);
|
||||
} else {
|
||||
// After a newline.
|
||||
$findLengthOf = substr($this->data, 0, $this->char);
|
||||
}
|
||||
|
||||
return UTF8Utils::countChars($findLengthOf);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function getColumnOffset()
|
||||
{
|
||||
return $this->columnOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current character.
|
||||
*
|
||||
* @return string The current character.
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->data[$this->char];
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance the pointer.
|
||||
* This is part of the Iterator interface.
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
++$this->char;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind to the start of the string.
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->char = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the current pointer location valid.
|
||||
*
|
||||
* @return bool Whether the current pointer location is valid.
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return $this->char < $this->EOF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all characters until EOF.
|
||||
*
|
||||
* This reads to the end of the file, and sets the read marker at the
|
||||
* end of the file.
|
||||
*
|
||||
* Note this performs bounds checking.
|
||||
*
|
||||
* @return string Returns the remaining text. If called when the InputStream is
|
||||
* already exhausted, it returns an empty string.
|
||||
*/
|
||||
public function remainingChars()
|
||||
{
|
||||
if ($this->char < $this->EOF) {
|
||||
$data = substr($this->data, $this->char);
|
||||
$this->char = $this->EOF;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
return ''; // false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read to a particular match (or until $max bytes are consumed).
|
||||
*
|
||||
* This operates on byte sequences, not characters.
|
||||
*
|
||||
* Matches as far as possible until we reach a certain set of bytes
|
||||
* and returns the matched substring.
|
||||
*
|
||||
* @param string $bytes Bytes to match.
|
||||
* @param int $max Maximum number of bytes to scan.
|
||||
*
|
||||
* @return mixed Index or false if no match is found. You should use strong
|
||||
* equality when checking the result, since index could be 0.
|
||||
*/
|
||||
public function charsUntil($bytes, $max = null)
|
||||
{
|
||||
if ($this->char >= $this->EOF) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (0 === $max || $max) {
|
||||
$len = strcspn($this->data, $bytes, $this->char, $max);
|
||||
} else {
|
||||
$len = strcspn($this->data, $bytes, $this->char);
|
||||
}
|
||||
|
||||
$string = (string) substr($this->data, $this->char, $len);
|
||||
$this->char += $len;
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string so long as $bytes matches.
|
||||
*
|
||||
* Matches as far as possible with a certain set of bytes
|
||||
* and returns the matched substring.
|
||||
*
|
||||
* @param string $bytes A mask of bytes to match. If ANY byte in this mask matches the
|
||||
* current char, the pointer advances and the char is part of the
|
||||
* substring.
|
||||
* @param int $max The max number of chars to read.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function charsWhile($bytes, $max = null)
|
||||
{
|
||||
if ($this->char >= $this->EOF) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (0 === $max || $max) {
|
||||
$len = strspn($this->data, $bytes, $this->char, $max);
|
||||
} else {
|
||||
$len = strspn($this->data, $bytes, $this->char);
|
||||
}
|
||||
$string = (string) substr($this->data, $this->char, $len);
|
||||
$this->char += $len;
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unconsume characters.
|
||||
*
|
||||
* @param int $howMany The number of characters to unconsume.
|
||||
*/
|
||||
public function unconsume($howMany = 1)
|
||||
{
|
||||
if (($this->char - $howMany) >= 0) {
|
||||
$this->char -= $howMany;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look ahead without moving cursor.
|
||||
*/
|
||||
public function peek()
|
||||
{
|
||||
if (($this->char + 1) <= $this->EOF) {
|
||||
return $this->data[$this->char + 1];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function key()
|
||||
{
|
||||
return $this->char;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Masterminds\HTML5\Parser;
|
||||
|
||||
/**
|
||||
* Handles special-case rules for the DOM tree builder.
|
||||
*
|
||||
* Many tags have special rules that need to be accomodated on an
|
||||
* individual basis. This class handles those rules.
|
||||
*
|
||||
* See section 8.1.2.4 of the spec.
|
||||
*
|
||||
* @todo - colgroup and col special behaviors
|
||||
* - body and head special behaviors
|
||||
*/
|
||||
class TreeBuildingRules
|
||||
{
|
||||
protected static $tags = array(
|
||||
'li' => 1,
|
||||
'dd' => 1,
|
||||
'dt' => 1,
|
||||
'rt' => 1,
|
||||
'rp' => 1,
|
||||
'tr' => 1,
|
||||
'th' => 1,
|
||||
'td' => 1,
|
||||
'thead' => 1,
|
||||
'tfoot' => 1,
|
||||
'tbody' => 1,
|
||||
'table' => 1,
|
||||
'optgroup' => 1,
|
||||
'option' => 1,
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the given tagname has special processing rules.
|
||||
*/
|
||||
public function hasRules($tagname)
|
||||
{
|
||||
return isset(static::$tags[$tagname]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate the rule for the current tag name.
|
||||
*
|
||||
* This may modify the existing DOM.
|
||||
*
|
||||
* @return \DOMElement The new Current DOM element.
|
||||
*/
|
||||
public function evaluate($new, $current)
|
||||
{
|
||||
switch ($new->tagName) {
|
||||
case 'li':
|
||||
return $this->handleLI($new, $current);
|
||||
case 'dt':
|
||||
case 'dd':
|
||||
return $this->handleDT($new, $current);
|
||||
case 'rt':
|
||||
case 'rp':
|
||||
return $this->handleRT($new, $current);
|
||||
case 'optgroup':
|
||||
return $this->closeIfCurrentMatches($new, $current, array(
|
||||
'optgroup',
|
||||
));
|
||||
case 'option':
|
||||
return $this->closeIfCurrentMatches($new, $current, array(
|
||||
'option',
|
||||
));
|
||||
case 'tr':
|
||||
return $this->closeIfCurrentMatches($new, $current, array(
|
||||
'tr',
|
||||
));
|
||||
case 'td':
|
||||
case 'th':
|
||||
return $this->closeIfCurrentMatches($new, $current, array(
|
||||
'th',
|
||||
'td',
|
||||
));
|
||||
case 'tbody':
|
||||
case 'thead':
|
||||
case 'tfoot':
|
||||
case 'table': // Spec isn't explicit about this, but it's necessary.
|
||||
|
||||
return $this->closeIfCurrentMatches($new, $current, array(
|
||||
'thead',
|
||||
'tfoot',
|
||||
'tbody',
|
||||
));
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
protected function handleLI($ele, $current)
|
||||
{
|
||||
return $this->closeIfCurrentMatches($ele, $current, array(
|
||||
'li',
|
||||
));
|
||||
}
|
||||
|
||||
protected function handleDT($ele, $current)
|
||||
{
|
||||
return $this->closeIfCurrentMatches($ele, $current, array(
|
||||
'dt',
|
||||
'dd',
|
||||
));
|
||||
}
|
||||
|
||||
protected function handleRT($ele, $current)
|
||||
{
|
||||
return $this->closeIfCurrentMatches($ele, $current, array(
|
||||
'rt',
|
||||
'rp',
|
||||
));
|
||||
}
|
||||
|
||||
protected function closeIfCurrentMatches($ele, $current, $match)
|
||||
{
|
||||
if (in_array($current->tagName, $match, true)) {
|
||||
$current->parentNode->appendChild($ele);
|
||||
} else {
|
||||
$current->appendChild($ele);
|
||||
}
|
||||
|
||||
return $ele;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Masterminds\HTML5\Parser;
|
||||
|
||||
/*
|
||||
Portions based on code from html5lib files with the following copyright:
|
||||
|
||||
Copyright 2009 Geoffrey Sneddon <http://gsnedders.com/>
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
use Masterminds\HTML5\Exception;
|
||||
|
||||
class UTF8Utils
|
||||
{
|
||||
/**
|
||||
* The Unicode replacement character.
|
||||
*/
|
||||
const FFFD = "\xEF\xBF\xBD";
|
||||
|
||||
/**
|
||||
* Count the number of characters in a string.
|
||||
* UTF-8 aware. This will try (in order) iconv, MB, and finally a custom counter.
|
||||
*
|
||||
* @param string $string
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function countChars($string)
|
||||
{
|
||||
// Get the length for the string we need.
|
||||
if (function_exists('mb_strlen')) {
|
||||
return mb_strlen($string, 'utf-8');
|
||||
}
|
||||
|
||||
if (function_exists('iconv_strlen')) {
|
||||
return iconv_strlen($string, 'utf-8');
|
||||
}
|
||||
|
||||
$count = count_chars($string);
|
||||
|
||||
// 0x80 = 0x7F - 0 + 1 (one added to get inclusive range)
|
||||
// 0x33 = 0xF4 - 0x2C + 1 (one added to get inclusive range)
|
||||
return array_sum(array_slice($count, 0, 0x80)) + array_sum(array_slice($count, 0xC2, 0x33));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert data from the given encoding to UTF-8.
|
||||
*
|
||||
* This has not yet been tested with charactersets other than UTF-8.
|
||||
* It should work with ISO-8859-1/-13 and standard Latin Win charsets.
|
||||
*
|
||||
* @param string $data The data to convert
|
||||
* @param string $encoding A valid encoding. Examples: http://www.php.net/manual/en/mbstring.supported-encodings.php
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function convertToUTF8($data, $encoding = 'UTF-8')
|
||||
{
|
||||
/*
|
||||
* From the HTML5 spec: Given an encoding, the bytes in the input stream must be converted
|
||||
* to Unicode characters for the tokeniser, as described by the rules for that encoding,
|
||||
* except that the leading U+FEFF BYTE ORDER MARK character, if any, must not be stripped
|
||||
* by the encoding layer (it is stripped by the rule below). Bytes or sequences of bytes
|
||||
* in the original byte stream that could not be converted to Unicode characters must be
|
||||
* converted to U+FFFD REPLACEMENT CHARACTER code points.
|
||||
*/
|
||||
|
||||
// mb_convert_encoding is chosen over iconv because of a bug. The best
|
||||
// details for the bug are on http://us1.php.net/manual/en/function.iconv.php#108643
|
||||
// which contains links to the actual but reports as well as work around
|
||||
// details.
|
||||
if (function_exists('mb_convert_encoding')) {
|
||||
// mb library has the following behaviors:
|
||||
// - UTF-16 surrogates result in false.
|
||||
// - Overlongs and outside Plane 16 result in empty strings.
|
||||
|
||||
// Before we run mb_convert_encoding we need to tell it what to do with
|
||||
// characters it does not know. This could be different than the parent
|
||||
// application executing this library so we store the value, change it
|
||||
// to our needs, and then change it back when we are done. This feels
|
||||
// a little excessive and it would be great if there was a better way.
|
||||
$save = mb_substitute_character();
|
||||
mb_substitute_character('none');
|
||||
$data = mb_convert_encoding($data, 'UTF-8', $encoding);
|
||||
mb_substitute_character($save);
|
||||
}
|
||||
// @todo Get iconv running in at least some environments if that is possible.
|
||||
elseif (function_exists('iconv') && 'auto' !== $encoding) {
|
||||
// fprintf(STDOUT, "iconv found\n");
|
||||
// iconv has the following behaviors:
|
||||
// - Overlong representations are ignored.
|
||||
// - Beyond Plane 16 is replaced with a lower char.
|
||||
// - Incomplete sequences generate a warning.
|
||||
$data = @iconv($encoding, 'UTF-8//IGNORE', $data);
|
||||
} else {
|
||||
throw new Exception('Not implemented, please install mbstring or iconv');
|
||||
}
|
||||
|
||||
/*
|
||||
* One leading U+FEFF BYTE ORDER MARK character must be ignored if any are present.
|
||||
*/
|
||||
if ("\xEF\xBB\xBF" === substr($data, 0, 3)) {
|
||||
$data = substr($data, 3);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for Unicode code points that are not valid in a document.
|
||||
*
|
||||
* @param string $data A string to analyze
|
||||
*
|
||||
* @return array An array of (string) error messages produced by the scanning
|
||||
*/
|
||||
public static function checkForIllegalCodepoints($data)
|
||||
{
|
||||
// Vestigal error handling.
|
||||
$errors = array();
|
||||
|
||||
/*
|
||||
* All U+0000 null characters in the input must be replaced by U+FFFD REPLACEMENT CHARACTERs.
|
||||
* Any occurrences of such characters is a parse error.
|
||||
*/
|
||||
for ($i = 0, $count = substr_count($data, "\0"); $i < $count; ++$i) {
|
||||
$errors[] = 'null-character';
|
||||
}
|
||||
|
||||
/*
|
||||
* Any occurrences of any characters in the ranges U+0001 to U+0008, U+000B, U+000E to U+001F, U+007F
|
||||
* to U+009F, U+D800 to U+DFFF , U+FDD0 to U+FDEF, and characters U+FFFE, U+FFFF, U+1FFFE, U+1FFFF,
|
||||
* U+2FFFE, U+2FFFF, U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE, U+6FFFF, U+7FFFE,
|
||||
* U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF, U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF,
|
||||
* U+DFFFE, U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, and U+10FFFF are parse errors.
|
||||
* (These are all control characters or permanently undefined Unicode characters.)
|
||||
*/
|
||||
// Check PCRE is loaded.
|
||||
$count = preg_match_all(
|
||||
'/(?:
|
||||
[\x01-\x08\x0B\x0E-\x1F\x7F] # U+0001 to U+0008, U+000B, U+000E to U+001F and U+007F
|
||||
|
|
||||
\xC2[\x80-\x9F] # U+0080 to U+009F
|
||||
|
|
||||
\xED(?:\xA0[\x80-\xFF]|[\xA1-\xBE][\x00-\xFF]|\xBF[\x00-\xBF]) # U+D800 to U+DFFFF
|
||||
|
|
||||
\xEF\xB7[\x90-\xAF] # U+FDD0 to U+FDEF
|
||||
|
|
||||
\xEF\xBF[\xBE\xBF] # U+FFFE and U+FFFF
|
||||
|
|
||||
[\xF0-\xF4][\x8F-\xBF]\xBF[\xBE\xBF] # U+nFFFE and U+nFFFF (1 <= n <= 10_{16})
|
||||
)/x', $data, $matches);
|
||||
for ($i = 0; $i < $count; ++$i) {
|
||||
$errors[] = 'invalid-codepoint';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,553 @@
|
||||
<?php
|
||||
/**
|
||||
* @file
|
||||
* The rules for generating output in the serializer.
|
||||
*
|
||||
* These output rules are likely to generate output similar to the document that
|
||||
* was parsed. It is not intended to output exactly the document that was parsed.
|
||||
*/
|
||||
|
||||
namespace Masterminds\HTML5\Serializer;
|
||||
|
||||
use Masterminds\HTML5\Elements;
|
||||
|
||||
/**
|
||||
* Generate the output html5 based on element rules.
|
||||
*/
|
||||
class OutputRules implements RulesInterface
|
||||
{
|
||||
/**
|
||||
* Defined in http://www.w3.org/TR/html51/infrastructure.html#html-namespace-0.
|
||||
*/
|
||||
const NAMESPACE_HTML = 'http://www.w3.org/1999/xhtml';
|
||||
|
||||
const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML';
|
||||
|
||||
const NAMESPACE_SVG = 'http://www.w3.org/2000/svg';
|
||||
|
||||
const NAMESPACE_XLINK = 'http://www.w3.org/1999/xlink';
|
||||
|
||||
const NAMESPACE_XML = 'http://www.w3.org/XML/1998/namespace';
|
||||
|
||||
const NAMESPACE_XMLNS = 'http://www.w3.org/2000/xmlns/';
|
||||
|
||||
/**
|
||||
* Holds the HTML5 element names that causes a namespace switch.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $implicitNamespaces = array(
|
||||
self::NAMESPACE_HTML,
|
||||
self::NAMESPACE_SVG,
|
||||
self::NAMESPACE_MATHML,
|
||||
self::NAMESPACE_XML,
|
||||
self::NAMESPACE_XMLNS,
|
||||
);
|
||||
|
||||
const IM_IN_HTML = 1;
|
||||
|
||||
const IM_IN_SVG = 2;
|
||||
|
||||
const IM_IN_MATHML = 3;
|
||||
|
||||
/**
|
||||
* Used as cache to detect if is available ENT_HTML5.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $hasHTML5 = false;
|
||||
|
||||
protected $traverser;
|
||||
|
||||
protected $encode = false;
|
||||
|
||||
protected $out;
|
||||
|
||||
protected $outputMode;
|
||||
|
||||
private $xpath;
|
||||
|
||||
protected $nonBooleanAttributes = array(
|
||||
/*
|
||||
array(
|
||||
'nodeNamespace'=>'http://www.w3.org/1999/xhtml',
|
||||
'attrNamespace'=>'http://www.w3.org/1999/xhtml',
|
||||
|
||||
'nodeName'=>'img', 'nodeName'=>array('img', 'a'),
|
||||
'attrName'=>'alt', 'attrName'=>array('title', 'alt'),
|
||||
),
|
||||
*/
|
||||
array(
|
||||
'nodeNamespace' => 'http://www.w3.org/1999/xhtml',
|
||||
'attrName' => array('href',
|
||||
'hreflang',
|
||||
'http-equiv',
|
||||
'icon',
|
||||
'id',
|
||||
'keytype',
|
||||
'kind',
|
||||
'label',
|
||||
'lang',
|
||||
'language',
|
||||
'list',
|
||||
'maxlength',
|
||||
'media',
|
||||
'method',
|
||||
'name',
|
||||
'placeholder',
|
||||
'rel',
|
||||
'rows',
|
||||
'rowspan',
|
||||
'sandbox',
|
||||
'spellcheck',
|
||||
'scope',
|
||||
'seamless',
|
||||
'shape',
|
||||
'size',
|
||||
'sizes',
|
||||
'span',
|
||||
'src',
|
||||
'srcdoc',
|
||||
'srclang',
|
||||
'srcset',
|
||||
'start',
|
||||
'step',
|
||||
'style',
|
||||
'summary',
|
||||
'tabindex',
|
||||
'target',
|
||||
'title',
|
||||
'type',
|
||||
'value',
|
||||
'width',
|
||||
'border',
|
||||
'charset',
|
||||
'cite',
|
||||
'class',
|
||||
'code',
|
||||
'codebase',
|
||||
'color',
|
||||
'cols',
|
||||
'colspan',
|
||||
'content',
|
||||
'coords',
|
||||
'data',
|
||||
'datetime',
|
||||
'default',
|
||||
'dir',
|
||||
'dirname',
|
||||
'enctype',
|
||||
'for',
|
||||
'form',
|
||||
'formaction',
|
||||
'headers',
|
||||
'height',
|
||||
'accept',
|
||||
'accept-charset',
|
||||
'accesskey',
|
||||
'action',
|
||||
'align',
|
||||
'alt',
|
||||
'bgcolor',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'nodeNamespace' => 'http://www.w3.org/1999/xhtml',
|
||||
'xpath' => 'starts-with(local-name(), \'data-\')',
|
||||
),
|
||||
);
|
||||
|
||||
const DOCTYPE = '<!DOCTYPE html>';
|
||||
|
||||
public function __construct($output, $options = array())
|
||||
{
|
||||
if (isset($options['encode_entities'])) {
|
||||
$this->encode = $options['encode_entities'];
|
||||
}
|
||||
|
||||
$this->outputMode = static::IM_IN_HTML;
|
||||
$this->out = $output;
|
||||
$this->hasHTML5 = defined('ENT_HTML5');
|
||||
}
|
||||
|
||||
public function addRule(array $rule)
|
||||
{
|
||||
$this->nonBooleanAttributes[] = $rule;
|
||||
}
|
||||
|
||||
public function setTraverser(Traverser $traverser)
|
||||
{
|
||||
$this->traverser = $traverser;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function unsetTraverser()
|
||||
{
|
||||
$this->traverser = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function document($dom)
|
||||
{
|
||||
$this->doctype();
|
||||
if ($dom->documentElement) {
|
||||
foreach ($dom->childNodes as $node) {
|
||||
$this->traverser->node($node);
|
||||
}
|
||||
$this->nl();
|
||||
}
|
||||
}
|
||||
|
||||
protected function doctype()
|
||||
{
|
||||
$this->wr(static::DOCTYPE);
|
||||
$this->nl();
|
||||
}
|
||||
|
||||
public function element($ele)
|
||||
{
|
||||
$name = $ele->tagName;
|
||||
|
||||
// Per spec:
|
||||
// If the element has a declared namespace in the HTML, MathML or
|
||||
// SVG namespaces, we use the lname instead of the tagName.
|
||||
if ($this->traverser->isLocalElement($ele)) {
|
||||
$name = $ele->localName;
|
||||
}
|
||||
|
||||
// If we are in SVG or MathML there is special handling.
|
||||
// Using if/elseif instead of switch because it's faster in PHP.
|
||||
if ('svg' == $name) {
|
||||
$this->outputMode = static::IM_IN_SVG;
|
||||
$name = Elements::normalizeSvgElement($name);
|
||||
} elseif ('math' == $name) {
|
||||
$this->outputMode = static::IM_IN_MATHML;
|
||||
}
|
||||
|
||||
$this->openTag($ele);
|
||||
if (Elements::isA($name, Elements::TEXT_RAW)) {
|
||||
foreach ($ele->childNodes as $child) {
|
||||
if ($child instanceof \DOMCharacterData) {
|
||||
$this->wr($child->data);
|
||||
} elseif ($child instanceof \DOMElement) {
|
||||
$this->element($child);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle children.
|
||||
if ($ele->hasChildNodes()) {
|
||||
$this->traverser->children($ele->childNodes);
|
||||
}
|
||||
|
||||
// Close out the SVG or MathML special handling.
|
||||
if ('svg' == $name || 'math' == $name) {
|
||||
$this->outputMode = static::IM_IN_HTML;
|
||||
}
|
||||
}
|
||||
|
||||
// If not unary, add a closing tag.
|
||||
if (!Elements::isA($name, Elements::VOID_TAG)) {
|
||||
$this->closeTag($ele);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a text node.
|
||||
*
|
||||
* @param \DOMText $ele The text node to write.
|
||||
*/
|
||||
public function text($ele)
|
||||
{
|
||||
if (isset($ele->parentNode) && isset($ele->parentNode->tagName) && Elements::isA($ele->parentNode->localName, Elements::TEXT_RAW)) {
|
||||
$this->wr($ele->data);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: This probably needs some flags set.
|
||||
$this->wr($this->enc($ele->data));
|
||||
}
|
||||
|
||||
public function cdata($ele)
|
||||
{
|
||||
// This encodes CDATA.
|
||||
$this->wr($ele->ownerDocument->saveXML($ele));
|
||||
}
|
||||
|
||||
public function comment($ele)
|
||||
{
|
||||
// These produce identical output.
|
||||
// $this->wr('<!--')->wr($ele->data)->wr('-->');
|
||||
$this->wr($ele->ownerDocument->saveXML($ele));
|
||||
}
|
||||
|
||||
public function processorInstruction($ele)
|
||||
{
|
||||
$this->wr('<?')
|
||||
->wr($ele->target)
|
||||
->wr(' ')
|
||||
->wr($ele->data)
|
||||
->wr('?>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the namespace attributes.
|
||||
*
|
||||
* @param \DOMNode $ele The element being written.
|
||||
*/
|
||||
protected function namespaceAttrs($ele)
|
||||
{
|
||||
if (!$this->xpath || $this->xpath->document !== $ele->ownerDocument) {
|
||||
$this->xpath = new \DOMXPath($ele->ownerDocument);
|
||||
}
|
||||
|
||||
foreach ($this->xpath->query('namespace::*[not(.=../../namespace::*)]', $ele) as $nsNode) {
|
||||
if (!in_array($nsNode->nodeValue, $this->implicitNamespaces)) {
|
||||
$this->wr(' ')->wr($nsNode->nodeName)->wr('="')->wr($nsNode->nodeValue)->wr('"');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the opening tag.
|
||||
*
|
||||
* Tags for HTML, MathML, and SVG are in the local name. Otherwise, use the
|
||||
* qualified name (8.3).
|
||||
*
|
||||
* @param \DOMNode $ele The element being written.
|
||||
*/
|
||||
protected function openTag($ele)
|
||||
{
|
||||
$this->wr('<')->wr($this->traverser->isLocalElement($ele) ? $ele->localName : $ele->tagName);
|
||||
|
||||
$this->attrs($ele);
|
||||
$this->namespaceAttrs($ele);
|
||||
|
||||
if ($this->outputMode == static::IM_IN_HTML) {
|
||||
$this->wr('>');
|
||||
} // If we are not in html mode we are in SVG, MathML, or XML embedded content.
|
||||
else {
|
||||
if ($ele->hasChildNodes()) {
|
||||
$this->wr('>');
|
||||
} // If there are no children this is self closing.
|
||||
else {
|
||||
$this->wr(' />');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function attrs($ele)
|
||||
{
|
||||
// FIXME: Needs support for xml, xmlns, xlink, and namespaced elements.
|
||||
if (!$ele->hasAttributes()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// TODO: Currently, this always writes name="value", and does not do
|
||||
// value-less attributes.
|
||||
$map = $ele->attributes;
|
||||
$len = $map->length;
|
||||
for ($i = 0; $i < $len; ++$i) {
|
||||
$node = $map->item($i);
|
||||
$val = $this->enc($node->value, true);
|
||||
|
||||
// XXX: The spec says that we need to ensure that anything in
|
||||
// the XML, XMLNS, or XLink NS's should use the canonical
|
||||
// prefix. It seems that DOM does this for us already, but there
|
||||
// may be exceptions.
|
||||
$name = $node->nodeName;
|
||||
|
||||
// Special handling for attributes in SVG and MathML.
|
||||
// Using if/elseif instead of switch because it's faster in PHP.
|
||||
if ($this->outputMode == static::IM_IN_SVG) {
|
||||
$name = Elements::normalizeSvgAttribute($name);
|
||||
} elseif ($this->outputMode == static::IM_IN_MATHML) {
|
||||
$name = Elements::normalizeMathMlAttribute($name);
|
||||
}
|
||||
|
||||
$this->wr(' ')->wr($name);
|
||||
|
||||
if ((isset($val) && '' !== $val) || $this->nonBooleanAttribute($node)) {
|
||||
$this->wr('="')->wr($val)->wr('"');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function nonBooleanAttribute(\DOMAttr $attr)
|
||||
{
|
||||
$ele = $attr->ownerElement;
|
||||
foreach ($this->nonBooleanAttributes as $rule) {
|
||||
if (isset($rule['nodeNamespace']) && $rule['nodeNamespace'] !== $ele->namespaceURI) {
|
||||
continue;
|
||||
}
|
||||
if (isset($rule['attNamespace']) && $rule['attNamespace'] !== $attr->namespaceURI) {
|
||||
continue;
|
||||
}
|
||||
if (isset($rule['nodeName']) && !is_array($rule['nodeName']) && $rule['nodeName'] !== $ele->localName) {
|
||||
continue;
|
||||
}
|
||||
if (isset($rule['nodeName']) && is_array($rule['nodeName']) && !in_array($ele->localName, $rule['nodeName'], true)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($rule['attrName']) && !is_array($rule['attrName']) && $rule['attrName'] !== $attr->localName) {
|
||||
continue;
|
||||
}
|
||||
if (isset($rule['attrName']) && is_array($rule['attrName']) && !in_array($attr->localName, $rule['attrName'], true)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($rule['xpath'])) {
|
||||
$xp = $this->getXPath($attr);
|
||||
if (isset($rule['prefixes'])) {
|
||||
foreach ($rule['prefixes'] as $nsPrefix => $ns) {
|
||||
$xp->registerNamespace($nsPrefix, $ns);
|
||||
}
|
||||
}
|
||||
if (!$xp->evaluate($rule['xpath'], $attr)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getXPath(\DOMNode $node)
|
||||
{
|
||||
if (!$this->xpath) {
|
||||
$this->xpath = new \DOMXPath($node->ownerDocument);
|
||||
}
|
||||
|
||||
return $this->xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the closing tag.
|
||||
*
|
||||
* Tags for HTML, MathML, and SVG are in the local name. Otherwise, use the
|
||||
* qualified name (8.3).
|
||||
*
|
||||
* @param \DOMNode $ele The element being written.
|
||||
*/
|
||||
protected function closeTag($ele)
|
||||
{
|
||||
if ($this->outputMode == static::IM_IN_HTML || $ele->hasChildNodes()) {
|
||||
$this->wr('</')->wr($this->traverser->isLocalElement($ele) ? $ele->localName : $ele->tagName)->wr('>');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to the output.
|
||||
*
|
||||
* @param string $text The string to put into the output
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function wr($text)
|
||||
{
|
||||
fwrite($this->out, $text);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a new line character.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function nl()
|
||||
{
|
||||
fwrite($this->out, PHP_EOL);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode text.
|
||||
*
|
||||
* When encode is set to false, the default value, the text passed in is
|
||||
* escaped per section 8.3 of the html5 spec. For details on how text is
|
||||
* escaped see the escape() method.
|
||||
*
|
||||
* When encoding is set to true the text is converted to named character
|
||||
* references where appropriate. Section 8.1.4 Character references of the
|
||||
* html5 spec refers to using named character references. This is useful for
|
||||
* characters that can't otherwise legally be used in the text.
|
||||
*
|
||||
* The named character references are listed in section 8.5.
|
||||
*
|
||||
* @see http://www.w3.org/TR/2013/CR-html5-20130806/syntax.html#named-character-references True encoding will turn all named character references into their entities.
|
||||
* This includes such characters as +.# and many other common ones. By default
|
||||
* encoding here will just escape &'<>".
|
||||
*
|
||||
* Note, PHP 5.4+ has better html5 encoding.
|
||||
*
|
||||
* @todo Use the Entities class in php 5.3 to have html5 entities.
|
||||
*
|
||||
* @param string $text Text to encode.
|
||||
* @param bool $attribute True if we are encoding an attrubute, false otherwise.
|
||||
*
|
||||
* @return string The encoded text.
|
||||
*/
|
||||
protected function enc($text, $attribute = false)
|
||||
{
|
||||
// Escape the text rather than convert to named character references.
|
||||
if (!$this->encode) {
|
||||
return $this->escape($text, $attribute);
|
||||
}
|
||||
|
||||
// If we are in PHP 5.4+ we can use the native html5 entity functionality to
|
||||
// convert the named character references.
|
||||
|
||||
if ($this->hasHTML5) {
|
||||
return htmlentities($text, ENT_HTML5 | ENT_SUBSTITUTE | ENT_QUOTES, 'UTF-8', false);
|
||||
} // If a version earlier than 5.4 html5 entities are not entirely handled.
|
||||
// This manually handles them.
|
||||
else {
|
||||
return strtr($text, HTML5Entities::$map);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape test.
|
||||
*
|
||||
* According to the html5 spec section 8.3 Serializing HTML fragments, text
|
||||
* within tags that are not style, script, xmp, iframe, noembed, and noframes
|
||||
* need to be properly escaped.
|
||||
*
|
||||
* The & should be converted to &, no breaking space unicode characters
|
||||
* converted to , when in attribute mode the " should be converted to
|
||||
* ", and when not in attribute mode the < and > should be converted to
|
||||
* < and >.
|
||||
*
|
||||
* @see http://www.w3.org/TR/2013/CR-html5-20130806/syntax.html#escapingString
|
||||
*
|
||||
* @param string $text Text to escape.
|
||||
* @param bool $attribute True if we are escaping an attrubute, false otherwise.
|
||||
*/
|
||||
protected function escape($text, $attribute = false)
|
||||
{
|
||||
// Not using htmlspecialchars because, while it does escaping, it doesn't
|
||||
// match the requirements of section 8.5. For example, it doesn't handle
|
||||
// non-breaking spaces.
|
||||
if ($attribute) {
|
||||
$replace = array(
|
||||
'"' => '"',
|
||||
'&' => '&',
|
||||
"\xc2\xa0" => ' ',
|
||||
);
|
||||
} else {
|
||||
$replace = array(
|
||||
'<' => '<',
|
||||
'>' => '>',
|
||||
'&' => '&',
|
||||
"\xc2\xa0" => ' ',
|
||||
);
|
||||
}
|
||||
|
||||
return strtr($text, $replace);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
# The Serializer (Writer) Model
|
||||
|
||||
The serializer roughly follows sections _8.1 Writing HTML documents_ and section
|
||||
_8.3 Serializing HTML fragments_ by converting DOMDocument, DOMDocumentFragment,
|
||||
and DOMNodeList into HTML5.
|
||||
|
||||
[ HTML5 ] // Interface for saving.
|
||||
||
|
||||
[ Traverser ] // Walk the DOM
|
||||
||
|
||||
[ Rules ] // Convert DOM elements into strings.
|
||||
||
|
||||
[ HTML5 ] // HTML5 document or fragment in text.
|
||||
|
||||
|
||||
## HTML5 Class
|
||||
|
||||
Provides the top level interface for saving.
|
||||
|
||||
## The Traverser
|
||||
|
||||
Walks the DOM finding each element and passing it off to the output rules to
|
||||
convert to HTML5.
|
||||
|
||||
## Output Rules
|
||||
|
||||
The output rules are defined in the RulesInterface which can have multiple
|
||||
implementations. Currently, the OutputRules is the default implementation that
|
||||
converts a DOM as is into HTML5.
|
||||
|
||||
## HTML5 String
|
||||
|
||||
The output of the process it HTML5 as a string or saved to a file.
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/**
|
||||
* @file
|
||||
* The interface definition for Rules to generate output.
|
||||
*/
|
||||
|
||||
namespace Masterminds\HTML5\Serializer;
|
||||
|
||||
/**
|
||||
* To create a new rule set for writing output the RulesInterface needs to be implemented.
|
||||
* The resulting class can be specified in the options with the key of rules.
|
||||
*
|
||||
* For an example implementation see Serializer\OutputRules.
|
||||
*/
|
||||
interface RulesInterface
|
||||
{
|
||||
/**
|
||||
* The class constructor.
|
||||
*
|
||||
* Note, before the rules can be used a traverser must be registered.
|
||||
*
|
||||
* @param mixed $output The output stream to write output to.
|
||||
* @param array $options An array of options.
|
||||
*/
|
||||
public function __construct($output, $options = array());
|
||||
|
||||
/**
|
||||
* Register the traverser used in but the rules.
|
||||
*
|
||||
* Note, only one traverser can be used by the rules.
|
||||
*
|
||||
* @param Traverser $traverser The traverser used in the rules.
|
||||
*
|
||||
* @return RulesInterface $this for the current object.
|
||||
*/
|
||||
public function setTraverser(Traverser $traverser);
|
||||
|
||||
/**
|
||||
* Write a document element (\DOMDocument).
|
||||
*
|
||||
* Instead of returning the result write it to the output stream ($output)
|
||||
* that was passed into the constructor.
|
||||
*
|
||||
* @param \DOMDocument $dom
|
||||
*/
|
||||
public function document($dom);
|
||||
|
||||
/**
|
||||
* Write an element.
|
||||
*
|
||||
* Instead of returning the result write it to the output stream ($output)
|
||||
* that was passed into the constructor.
|
||||
*
|
||||
* @param mixed $ele
|
||||
*/
|
||||
public function element($ele);
|
||||
|
||||
/**
|
||||
* Write a text node.
|
||||
*
|
||||
* Instead of returning the result write it to the output stream ($output)
|
||||
* that was passed into the constructor.
|
||||
*
|
||||
* @param mixed $ele
|
||||
*/
|
||||
public function text($ele);
|
||||
|
||||
/**
|
||||
* Write a CDATA node.
|
||||
*
|
||||
* Instead of returning the result write it to the output stream ($output)
|
||||
* that was passed into the constructor.
|
||||
*
|
||||
* @param mixed $ele
|
||||
*/
|
||||
public function cdata($ele);
|
||||
|
||||
/**
|
||||
* Write a comment node.
|
||||
*
|
||||
* Instead of returning the result write it to the output stream ($output)
|
||||
* that was passed into the constructor.
|
||||
*
|
||||
* @param mixed $ele
|
||||
*/
|
||||
public function comment($ele);
|
||||
|
||||
/**
|
||||
* Write a processor instruction.
|
||||
*
|
||||
* To learn about processor instructions see InstructionProcessor
|
||||
*
|
||||
* Instead of returning the result write it to the output stream ($output)
|
||||
* that was passed into the constructor.
|
||||
*
|
||||
* @param mixed $ele
|
||||
*/
|
||||
public function processorInstruction($ele);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Masterminds\HTML5\Serializer;
|
||||
|
||||
/**
|
||||
* Traverser for walking a DOM tree.
|
||||
*
|
||||
* This is a concrete traverser designed to convert a DOM tree into an
|
||||
* HTML5 document. It is not intended to be a generic DOMTreeWalker
|
||||
* implementation.
|
||||
*
|
||||
* @see http://www.w3.org/TR/2012/CR-html5-20121217/syntax.html#serializing-html-fragments
|
||||
*/
|
||||
class Traverser
|
||||
{
|
||||
/**
|
||||
* Namespaces that should be treated as "local" to HTML5.
|
||||
*/
|
||||
protected static $local_ns = array(
|
||||
'http://www.w3.org/1999/xhtml' => 'html',
|
||||
'http://www.w3.org/1998/Math/MathML' => 'math',
|
||||
'http://www.w3.org/2000/svg' => 'svg',
|
||||
);
|
||||
|
||||
protected $dom;
|
||||
|
||||
protected $options;
|
||||
|
||||
protected $encode = false;
|
||||
|
||||
protected $rules;
|
||||
|
||||
protected $out;
|
||||
|
||||
/**
|
||||
* Create a traverser.
|
||||
*
|
||||
* @param \DOMNode|\DOMNodeList $dom The document or node to traverse.
|
||||
* @param resource $out A stream that allows writing. The traverser will output into this
|
||||
* stream.
|
||||
* @param array $options An array of options for the traverser as key/value pairs. These include:
|
||||
* - encode_entities: A bool to specify if full encding should happen for all named
|
||||
* charachter references. Defaults to false which escapes &'<>".
|
||||
* - output_rules: The path to the class handling the output rules.
|
||||
*/
|
||||
public function __construct($dom, $out, RulesInterface $rules, $options = array())
|
||||
{
|
||||
$this->dom = $dom;
|
||||
$this->out = $out;
|
||||
$this->rules = $rules;
|
||||
$this->options = $options;
|
||||
|
||||
$this->rules->setTraverser($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the traverser to walk the DOM.
|
||||
*
|
||||
* @return resource $out Returns the output stream.
|
||||
*/
|
||||
public function walk()
|
||||
{
|
||||
if ($this->dom instanceof \DOMDocument) {
|
||||
$this->rules->document($this->dom);
|
||||
} elseif ($this->dom instanceof \DOMDocumentFragment) {
|
||||
// Document fragments are a special case. Only the children need to
|
||||
// be serialized.
|
||||
if ($this->dom->hasChildNodes()) {
|
||||
$this->children($this->dom->childNodes);
|
||||
}
|
||||
} // If NodeList, loop
|
||||
elseif ($this->dom instanceof \DOMNodeList) {
|
||||
// If this is a NodeList of DOMDocuments this will not work.
|
||||
$this->children($this->dom);
|
||||
} // Else assume this is a DOMNode-like datastructure.
|
||||
else {
|
||||
$this->node($this->dom);
|
||||
}
|
||||
|
||||
return $this->out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a node in the DOM.
|
||||
*
|
||||
* @param mixed $node A node implementing \DOMNode.
|
||||
*/
|
||||
public function node($node)
|
||||
{
|
||||
// A listing of types is at http://php.net/manual/en/dom.constants.php
|
||||
switch ($node->nodeType) {
|
||||
case XML_ELEMENT_NODE:
|
||||
$this->rules->element($node);
|
||||
break;
|
||||
case XML_TEXT_NODE:
|
||||
$this->rules->text($node);
|
||||
break;
|
||||
case XML_CDATA_SECTION_NODE:
|
||||
$this->rules->cdata($node);
|
||||
break;
|
||||
case XML_PI_NODE:
|
||||
$this->rules->processorInstruction($node);
|
||||
break;
|
||||
case XML_COMMENT_NODE:
|
||||
$this->rules->comment($node);
|
||||
break;
|
||||
// Currently we don't support embedding DTDs.
|
||||
default:
|
||||
//print '<!-- Skipped -->';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk through all the nodes on a node list.
|
||||
*
|
||||
* @param \DOMNodeList $nl A list of child elements to walk through.
|
||||
*/
|
||||
public function children($nl)
|
||||
{
|
||||
foreach ($nl as $node) {
|
||||
$this->node($node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is an element local?
|
||||
*
|
||||
* @param mixed $ele An element that implement \DOMNode.
|
||||
*
|
||||
* @return bool true if local and false otherwise.
|
||||
*/
|
||||
public function isLocalElement($ele)
|
||||
{
|
||||
$uri = $ele->namespaceURI;
|
||||
if (empty($uri)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isset(static::$local_ns[$uri]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
composer.lock
|
||||
composer.phar
|
||||
/vendor/
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2016 container-interop
|
||||
Copyright (c) 2016 PHP Framework Interoperability Group
|
||||
|
||||
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.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Container interface
|
||||
==============
|
||||
|
||||
This repository holds all interfaces related to [PSR-11 (Container Interface)][psr-url].
|
||||
|
||||
Note that this is not a Container implementation of its own. It is merely abstractions that describe the components of a Dependency Injection Container.
|
||||
|
||||
The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist.
|
||||
|
||||
[psr-url]: https://www.php-fig.org/psr/psr-11/
|
||||
[package-url]: https://packagist.org/packages/psr/container
|
||||
[implementation-url]: https://packagist.org/providers/psr/container-implementation
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "psr/container",
|
||||
"type": "library",
|
||||
"description": "Common Container Interface (PHP FIG PSR-11)",
|
||||
"keywords": ["psr", "psr-11", "container", "container-interop", "container-interface"],
|
||||
"homepage": "https://github.com/php-fig/container",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.4.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Container\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Container;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Base interface representing a generic exception in a container.
|
||||
*/
|
||||
interface ContainerExceptionInterface extends Throwable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psr\Container;
|
||||
|
||||
/**
|
||||
* Describes the interface of a container that exposes methods to read its entries.
|
||||
*/
|
||||
interface ContainerInterface
|
||||
{
|
||||
/**
|
||||
* Finds an entry of the container by its identifier and returns it.
|
||||
*
|
||||
* @param string $id Identifier of the entry to look for.
|
||||
*
|
||||
* @throws NotFoundExceptionInterface No entry was found for **this** identifier.
|
||||
* @throws ContainerExceptionInterface Error while retrieving the entry.
|
||||
*
|
||||
* @return mixed Entry.
|
||||
*/
|
||||
public function get(string $id);
|
||||
|
||||
/**
|
||||
* Returns true if the container can return an entry for the given identifier.
|
||||
* Returns false otherwise.
|
||||
*
|
||||
* `has($id)` returning true does not mean that `get($id)` will not throw an exception.
|
||||
* It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
|
||||
*
|
||||
* @param string $id Identifier of the entry to look for.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $id): bool;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Container;
|
||||
|
||||
/**
|
||||
* No entry was found in the container.
|
||||
*/
|
||||
interface NotFoundExceptionInterface extends ContainerExceptionInterface
|
||||
{
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2012 PHP Framework Interoperability Group
|
||||
|
||||
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.
|
||||
Vendored
+58
@@ -0,0 +1,58 @@
|
||||
PSR Log
|
||||
=======
|
||||
|
||||
This repository holds all interfaces/classes/traits related to
|
||||
[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md).
|
||||
|
||||
Note that this is not a logger of its own. It is merely an interface that
|
||||
describes a logger. See the specification for more details.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
```bash
|
||||
composer require psr/log
|
||||
```
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
If you need a logger, you can use the interface like this:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class Foo
|
||||
{
|
||||
private $logger;
|
||||
|
||||
public function __construct(LoggerInterface $logger = null)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function doSomething()
|
||||
{
|
||||
if ($this->logger) {
|
||||
$this->logger->info('Doing work');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->doSomethingElse();
|
||||
} catch (Exception $exception) {
|
||||
$this->logger->error('Oh no!', array('exception' => $exception));
|
||||
}
|
||||
|
||||
// do something useful
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can then pick one of the implementations of the interface to get a logger.
|
||||
|
||||
If you want to implement the interface, you can require this package and
|
||||
implement `Psr\Log\LoggerInterface` in your code. Please read the
|
||||
[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md)
|
||||
for details.
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "psr/log",
|
||||
"description": "Common interface for logging libraries",
|
||||
"keywords": ["psr", "psr-3", "log"],
|
||||
"homepage": "https://github.com/php-fig/log",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.0.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Log\\": "src"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.x-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Log;
|
||||
|
||||
/**
|
||||
* This is a simple Logger implementation that other Loggers can inherit from.
|
||||
*
|
||||
* It simply delegates all log-level-specific methods to the `log` method to
|
||||
* reduce boilerplate code that a simple Logger that does the same thing with
|
||||
* messages regardless of the error level has to implement.
|
||||
*/
|
||||
abstract class AbstractLogger implements LoggerInterface
|
||||
{
|
||||
use LoggerTrait;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Log;
|
||||
|
||||
class InvalidArgumentException extends \InvalidArgumentException
|
||||
{
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Log;
|
||||
|
||||
/**
|
||||
* Describes log levels.
|
||||
*/
|
||||
class LogLevel
|
||||
{
|
||||
const EMERGENCY = 'emergency';
|
||||
const ALERT = 'alert';
|
||||
const CRITICAL = 'critical';
|
||||
const ERROR = 'error';
|
||||
const WARNING = 'warning';
|
||||
const NOTICE = 'notice';
|
||||
const INFO = 'info';
|
||||
const DEBUG = 'debug';
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Log;
|
||||
|
||||
/**
|
||||
* Describes a logger-aware instance.
|
||||
*/
|
||||
interface LoggerAwareInterface
|
||||
{
|
||||
/**
|
||||
* Sets a logger instance on the object.
|
||||
*
|
||||
* @param LoggerInterface $logger
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger): void;
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Log;
|
||||
|
||||
/**
|
||||
* Basic Implementation of LoggerAwareInterface.
|
||||
*/
|
||||
trait LoggerAwareTrait
|
||||
{
|
||||
/**
|
||||
* The logger instance.
|
||||
*
|
||||
* @var LoggerInterface|null
|
||||
*/
|
||||
protected ?LoggerInterface $logger = null;
|
||||
|
||||
/**
|
||||
* Sets a logger.
|
||||
*
|
||||
* @param LoggerInterface $logger
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Log;
|
||||
|
||||
/**
|
||||
* Describes a logger instance.
|
||||
*
|
||||
* The message MUST be a string or object implementing __toString().
|
||||
*
|
||||
* The message MAY contain placeholders in the form: {foo} where foo
|
||||
* will be replaced by the context data in key "foo".
|
||||
*
|
||||
* The context array can contain arbitrary data. The only assumption that
|
||||
* can be made by implementors is that if an Exception instance is given
|
||||
* to produce a stack trace, it MUST be in a key named "exception".
|
||||
*
|
||||
* See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
|
||||
* for the full interface specification.
|
||||
*/
|
||||
interface LoggerInterface
|
||||
{
|
||||
/**
|
||||
* System is unusable.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param mixed[] $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function emergency(string|\Stringable $message, array $context = []): void;
|
||||
|
||||
/**
|
||||
* Action must be taken immediately.
|
||||
*
|
||||
* Example: Entire website down, database unavailable, etc. This should
|
||||
* trigger the SMS alerts and wake you up.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param mixed[] $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function alert(string|\Stringable $message, array $context = []): void;
|
||||
|
||||
/**
|
||||
* Critical conditions.
|
||||
*
|
||||
* Example: Application component unavailable, unexpected exception.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param mixed[] $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function critical(string|\Stringable $message, array $context = []): void;
|
||||
|
||||
/**
|
||||
* Runtime errors that do not require immediate action but should typically
|
||||
* be logged and monitored.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param mixed[] $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function error(string|\Stringable $message, array $context = []): void;
|
||||
|
||||
/**
|
||||
* Exceptional occurrences that are not errors.
|
||||
*
|
||||
* Example: Use of deprecated APIs, poor use of an API, undesirable things
|
||||
* that are not necessarily wrong.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param mixed[] $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function warning(string|\Stringable $message, array $context = []): void;
|
||||
|
||||
/**
|
||||
* Normal but significant events.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param mixed[] $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function notice(string|\Stringable $message, array $context = []): void;
|
||||
|
||||
/**
|
||||
* Interesting events.
|
||||
*
|
||||
* Example: User logs in, SQL logs.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param mixed[] $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function info(string|\Stringable $message, array $context = []): void;
|
||||
|
||||
/**
|
||||
* Detailed debug information.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param mixed[] $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function debug(string|\Stringable $message, array $context = []): void;
|
||||
|
||||
/**
|
||||
* Logs with an arbitrary level.
|
||||
*
|
||||
* @param mixed $level
|
||||
* @param string|\Stringable $message
|
||||
* @param mixed[] $context
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Psr\Log\InvalidArgumentException
|
||||
*/
|
||||
public function log($level, string|\Stringable $message, array $context = []): void;
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Log;
|
||||
|
||||
/**
|
||||
* This is a simple Logger trait that classes unable to extend AbstractLogger
|
||||
* (because they extend another class, etc) can include.
|
||||
*
|
||||
* It simply delegates all log-level-specific methods to the `log` method to
|
||||
* reduce boilerplate code that a simple Logger that does the same thing with
|
||||
* messages regardless of the error level has to implement.
|
||||
*/
|
||||
trait LoggerTrait
|
||||
{
|
||||
/**
|
||||
* System is unusable.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function emergency(string|\Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::EMERGENCY, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Action must be taken immediately.
|
||||
*
|
||||
* Example: Entire website down, database unavailable, etc. This should
|
||||
* trigger the SMS alerts and wake you up.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function alert(string|\Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::ALERT, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Critical conditions.
|
||||
*
|
||||
* Example: Application component unavailable, unexpected exception.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function critical(string|\Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::CRITICAL, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime errors that do not require immediate action but should typically
|
||||
* be logged and monitored.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function error(string|\Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::ERROR, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exceptional occurrences that are not errors.
|
||||
*
|
||||
* Example: Use of deprecated APIs, poor use of an API, undesirable things
|
||||
* that are not necessarily wrong.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function warning(string|\Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::WARNING, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normal but significant events.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function notice(string|\Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::NOTICE, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interesting events.
|
||||
*
|
||||
* Example: User logs in, SQL logs.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function info(string|\Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::INFO, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed debug information.
|
||||
*
|
||||
* @param string|\Stringable $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function debug(string|\Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::DEBUG, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs with an arbitrary level.
|
||||
*
|
||||
* @param mixed $level
|
||||
* @param string|\Stringable $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Psr\Log\InvalidArgumentException
|
||||
*/
|
||||
abstract public function log($level, string|\Stringable $message, array $context = []): void;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Log;
|
||||
|
||||
/**
|
||||
* This Logger can be used to avoid conditional log calls.
|
||||
*
|
||||
* Logging should always be optional, and if no logger is provided to your
|
||||
* library creating a NullLogger instance to have something to throw logs at
|
||||
* is a good way to avoid littering your code with `if ($this->logger) { }`
|
||||
* blocks.
|
||||
*/
|
||||
class NullLogger extends AbstractLogger
|
||||
{
|
||||
/**
|
||||
* Logs with an arbitrary level.
|
||||
*
|
||||
* @param mixed $level
|
||||
* @param string|\Stringable $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Psr\Log\InvalidArgumentException
|
||||
*/
|
||||
public function log($level, string|\Stringable $message, array $context = []): void
|
||||
{
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,692 @@
|
||||
<?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\Component\BrowserKit;
|
||||
|
||||
use Symfony\Component\BrowserKit\Exception\BadMethodCallException;
|
||||
use Symfony\Component\BrowserKit\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\BrowserKit\Exception\LogicException;
|
||||
use Symfony\Component\BrowserKit\Exception\RuntimeException;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\DomCrawler\Form;
|
||||
use Symfony\Component\DomCrawler\Link;
|
||||
use Symfony\Component\Process\PhpProcess;
|
||||
|
||||
/**
|
||||
* Simulates a browser.
|
||||
*
|
||||
* To make the actual request, you need to implement the doRequest() method.
|
||||
*
|
||||
* If you want to be able to run requests in their own process (insulated flag),
|
||||
* you need to also implement the getScript() method.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
abstract class AbstractBrowser
|
||||
{
|
||||
protected $history;
|
||||
protected $cookieJar;
|
||||
protected $server = [];
|
||||
protected $internalRequest;
|
||||
protected $request;
|
||||
protected $internalResponse;
|
||||
protected $response;
|
||||
protected $crawler;
|
||||
protected bool $useHtml5Parser = true;
|
||||
protected $insulated = false;
|
||||
protected $redirect;
|
||||
protected $followRedirects = true;
|
||||
protected $followMetaRefresh = false;
|
||||
|
||||
private int $maxRedirects = -1;
|
||||
private int $redirectCount = 0;
|
||||
private array $redirects = [];
|
||||
private bool $isMainRequest = true;
|
||||
|
||||
/**
|
||||
* @param array $server The server parameters (equivalent of $_SERVER)
|
||||
*/
|
||||
public function __construct(array $server = [], History $history = null, CookieJar $cookieJar = null)
|
||||
{
|
||||
$this->setServerParameters($server);
|
||||
$this->history = $history ?? new History();
|
||||
$this->cookieJar = $cookieJar ?? new CookieJar();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to automatically follow redirects or not.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function followRedirects(bool $followRedirects = true)
|
||||
{
|
||||
$this->followRedirects = $followRedirects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to automatically follow meta refresh redirects or not.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function followMetaRefresh(bool $followMetaRefresh = true)
|
||||
{
|
||||
$this->followMetaRefresh = $followMetaRefresh;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether client automatically follows redirects or not.
|
||||
*/
|
||||
public function isFollowingRedirects(): bool
|
||||
{
|
||||
return $this->followRedirects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum number of redirects that crawler can follow.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setMaxRedirects(int $maxRedirects)
|
||||
{
|
||||
$this->maxRedirects = $maxRedirects < 0 ? -1 : $maxRedirects;
|
||||
$this->followRedirects = -1 !== $this->maxRedirects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of redirects that crawler can follow.
|
||||
*/
|
||||
public function getMaxRedirects(): int
|
||||
{
|
||||
return $this->maxRedirects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the insulated flag.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws LogicException When Symfony Process Component is not installed
|
||||
*/
|
||||
public function insulate(bool $insulated = true)
|
||||
{
|
||||
if ($insulated && !class_exists(\Symfony\Component\Process\Process::class)) {
|
||||
throw new LogicException('Unable to isolate requests as the Symfony Process Component is not installed. Try running "composer require symfony/process".');
|
||||
}
|
||||
|
||||
$this->insulated = $insulated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets server parameters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setServerParameters(array $server)
|
||||
{
|
||||
$this->server = array_merge([
|
||||
'HTTP_USER_AGENT' => 'Symfony BrowserKit',
|
||||
], $server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets single server parameter.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setServerParameter(string $key, string $value)
|
||||
{
|
||||
$this->server[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets single server parameter for specified key.
|
||||
*/
|
||||
public function getServerParameter(string $key, mixed $default = ''): mixed
|
||||
{
|
||||
return $this->server[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function xmlHttpRequest(string $method, string $uri, array $parameters = [], array $files = [], array $server = [], string $content = null, bool $changeHistory = true): Crawler
|
||||
{
|
||||
$this->setServerParameter('HTTP_X_REQUESTED_WITH', 'XMLHttpRequest');
|
||||
|
||||
try {
|
||||
return $this->request($method, $uri, $parameters, $files, $server, $content, $changeHistory);
|
||||
} finally {
|
||||
unset($this->server['HTTP_X_REQUESTED_WITH']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the request parameters into a JSON string and uses it as request content.
|
||||
*/
|
||||
public function jsonRequest(string $method, string $uri, array $parameters = [], array $server = [], bool $changeHistory = true): Crawler
|
||||
{
|
||||
$content = json_encode($parameters);
|
||||
|
||||
$this->setServerParameter('CONTENT_TYPE', 'application/json');
|
||||
$this->setServerParameter('HTTP_ACCEPT', 'application/json');
|
||||
|
||||
try {
|
||||
return $this->request($method, $uri, [], [], $server, $content, $changeHistory);
|
||||
} finally {
|
||||
unset($this->server['CONTENT_TYPE']);
|
||||
unset($this->server['HTTP_ACCEPT']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the History instance.
|
||||
*/
|
||||
public function getHistory(): History
|
||||
{
|
||||
return $this->history;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the CookieJar instance.
|
||||
*/
|
||||
public function getCookieJar(): CookieJar
|
||||
{
|
||||
return $this->cookieJar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current Crawler instance.
|
||||
*/
|
||||
public function getCrawler(): Crawler
|
||||
{
|
||||
return $this->crawler ?? throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether parsing should be done using "masterminds/html5".
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function useHtml5Parser(bool $useHtml5Parser): static
|
||||
{
|
||||
$this->useHtml5Parser = $useHtml5Parser;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BrowserKit Response instance.
|
||||
*/
|
||||
public function getInternalResponse(): Response
|
||||
{
|
||||
return $this->internalResponse ?? throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current origin response instance.
|
||||
*
|
||||
* The origin response is the response instance that is returned
|
||||
* by the code that handles requests.
|
||||
*
|
||||
* @see doRequest()
|
||||
*/
|
||||
public function getResponse(): object
|
||||
{
|
||||
return $this->response ?? throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BrowserKit Request instance.
|
||||
*/
|
||||
public function getInternalRequest(): Request
|
||||
{
|
||||
return $this->internalRequest ?? throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current origin Request instance.
|
||||
*
|
||||
* The origin request is the request instance that is sent
|
||||
* to the code that handles requests.
|
||||
*
|
||||
* @see doRequest()
|
||||
*/
|
||||
public function getRequest(): object
|
||||
{
|
||||
return $this->request ?? throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on a given link.
|
||||
*
|
||||
* @param array $serverParameters An array of server parameters
|
||||
*/
|
||||
public function click(Link $link/* , array $serverParameters = [] */): Crawler
|
||||
{
|
||||
$serverParameters = 1 < \func_num_args() ? func_get_arg(1) : [];
|
||||
|
||||
if ($link instanceof Form) {
|
||||
return $this->submit($link, [], $serverParameters);
|
||||
}
|
||||
|
||||
return $this->request($link->getMethod(), $link->getUri(), [], [], $serverParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the first link (or clickable image) that contains the given text.
|
||||
*
|
||||
* @param string $linkText The text of the link or the alt attribute of the clickable image
|
||||
* @param array $serverParameters An array of server parameters
|
||||
*/
|
||||
public function clickLink(string $linkText/* , array $serverParameters = [] */): Crawler
|
||||
{
|
||||
$serverParameters = 1 < \func_num_args() ? func_get_arg(1) : [];
|
||||
|
||||
$crawler = $this->crawler ?? throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
|
||||
|
||||
return $this->click($crawler->selectLink($linkText)->link(), $serverParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits a form.
|
||||
*
|
||||
* @param array $values An array of form field values
|
||||
* @param array $serverParameters An array of server parameters
|
||||
*/
|
||||
public function submit(Form $form, array $values = [], array $serverParameters = []): Crawler
|
||||
{
|
||||
$form->setValues($values);
|
||||
|
||||
return $this->request($form->getMethod(), $form->getUri(), $form->getPhpValues(), $form->getPhpFiles(), $serverParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first form that contains a button with the given content and
|
||||
* uses it to submit the given form field values.
|
||||
*
|
||||
* @param string $button The text content, id, value or name of the form <button> or <input type="submit">
|
||||
* @param array $fieldValues Use this syntax: ['my_form[name]' => '...', 'my_form[email]' => '...']
|
||||
* @param string $method The HTTP method used to submit the form
|
||||
* @param array $serverParameters These values override the ones stored in $_SERVER (HTTP headers must include an HTTP_ prefix as PHP does)
|
||||
*/
|
||||
public function submitForm(string $button, array $fieldValues = [], string $method = 'POST', array $serverParameters = []): Crawler
|
||||
{
|
||||
$crawler = $this->crawler ?? throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
|
||||
$buttonNode = $crawler->selectButton($button);
|
||||
|
||||
if (0 === $buttonNode->count()) {
|
||||
throw new InvalidArgumentException(sprintf('There is no button with "%s" as its content, id, value or name.', $button));
|
||||
}
|
||||
|
||||
$form = $buttonNode->form($fieldValues, $method);
|
||||
|
||||
return $this->submit($form, [], $serverParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a URI.
|
||||
*
|
||||
* @param string $method The request method
|
||||
* @param string $uri The URI to fetch
|
||||
* @param array $parameters The Request parameters
|
||||
* @param array $files The files
|
||||
* @param array $server The server parameters (HTTP headers are referenced with an HTTP_ prefix as PHP does)
|
||||
* @param string $content The raw body data
|
||||
* @param bool $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload())
|
||||
*/
|
||||
public function request(string $method, string $uri, array $parameters = [], array $files = [], array $server = [], string $content = null, bool $changeHistory = true): Crawler
|
||||
{
|
||||
if ($this->isMainRequest) {
|
||||
$this->redirectCount = 0;
|
||||
} else {
|
||||
++$this->redirectCount;
|
||||
}
|
||||
|
||||
$originalUri = $uri;
|
||||
|
||||
$uri = $this->getAbsoluteUri($uri);
|
||||
|
||||
$server = array_merge($this->server, $server);
|
||||
|
||||
if (!empty($server['HTTP_HOST']) && null === parse_url($originalUri, \PHP_URL_HOST)) {
|
||||
$uri = preg_replace('{^(https?\://)'.preg_quote($this->extractHost($uri)).'}', '${1}'.$server['HTTP_HOST'], $uri);
|
||||
}
|
||||
|
||||
if (isset($server['HTTPS']) && null === parse_url($originalUri, \PHP_URL_SCHEME)) {
|
||||
$uri = preg_replace('{^'.parse_url($uri, \PHP_URL_SCHEME).'}', $server['HTTPS'] ? 'https' : 'http', $uri);
|
||||
}
|
||||
|
||||
if (!isset($server['HTTP_REFERER']) && !$this->history->isEmpty()) {
|
||||
$server['HTTP_REFERER'] = $this->history->current()->getUri();
|
||||
}
|
||||
|
||||
if (empty($server['HTTP_HOST'])) {
|
||||
$server['HTTP_HOST'] = $this->extractHost($uri);
|
||||
}
|
||||
|
||||
$server['HTTPS'] = 'https' === parse_url($uri, \PHP_URL_SCHEME);
|
||||
|
||||
$this->internalRequest = new Request($uri, $method, $parameters, $files, $this->cookieJar->allValues($uri), $server, $content);
|
||||
|
||||
$this->request = $this->filterRequest($this->internalRequest);
|
||||
|
||||
if (true === $changeHistory) {
|
||||
$this->history->add($this->internalRequest);
|
||||
}
|
||||
|
||||
if ($this->insulated) {
|
||||
$this->response = $this->doRequestInProcess($this->request);
|
||||
} else {
|
||||
$this->response = $this->doRequest($this->request);
|
||||
}
|
||||
|
||||
$this->internalResponse = $this->filterResponse($this->response);
|
||||
|
||||
$this->cookieJar->updateFromResponse($this->internalResponse, $uri);
|
||||
|
||||
$status = $this->internalResponse->getStatusCode();
|
||||
|
||||
if ($status >= 300 && $status < 400) {
|
||||
$this->redirect = $this->internalResponse->getHeader('Location');
|
||||
} else {
|
||||
$this->redirect = null;
|
||||
}
|
||||
|
||||
if ($this->followRedirects && $this->redirect) {
|
||||
$this->redirects[serialize($this->history->current())] = true;
|
||||
|
||||
return $this->crawler = $this->followRedirect();
|
||||
}
|
||||
|
||||
$this->crawler = $this->createCrawlerFromContent($this->internalRequest->getUri(), $this->internalResponse->getContent(), $this->internalResponse->getHeader('Content-Type') ?? '');
|
||||
|
||||
// Check for meta refresh redirect
|
||||
if ($this->followMetaRefresh && null !== $redirect = $this->getMetaRefreshUrl()) {
|
||||
$this->redirect = $redirect;
|
||||
$this->redirects[serialize($this->history->current())] = true;
|
||||
$this->crawler = $this->followRedirect();
|
||||
}
|
||||
|
||||
return $this->crawler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request in another process.
|
||||
*
|
||||
* @return object
|
||||
*
|
||||
* @throws \RuntimeException When processing returns exit code
|
||||
*/
|
||||
protected function doRequestInProcess(object $request)
|
||||
{
|
||||
$deprecationsFile = tempnam(sys_get_temp_dir(), 'deprec');
|
||||
putenv('SYMFONY_DEPRECATIONS_SERIALIZE='.$deprecationsFile);
|
||||
$_ENV['SYMFONY_DEPRECATIONS_SERIALIZE'] = $deprecationsFile;
|
||||
$process = new PhpProcess($this->getScript($request), null, null);
|
||||
$process->run();
|
||||
|
||||
if (file_exists($deprecationsFile)) {
|
||||
$deprecations = file_get_contents($deprecationsFile);
|
||||
unlink($deprecationsFile);
|
||||
foreach ($deprecations ? unserialize($deprecations) : [] as $deprecation) {
|
||||
if ($deprecation[0]) {
|
||||
// unsilenced on purpose
|
||||
trigger_error($deprecation[1], \E_USER_DEPRECATED);
|
||||
} else {
|
||||
@trigger_error($deprecation[1], \E_USER_DEPRECATED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$process->isSuccessful() || !preg_match('/^O\:\d+\:/', $process->getOutput())) {
|
||||
throw new RuntimeException(sprintf('OUTPUT: %s ERROR OUTPUT: %s.', $process->getOutput(), $process->getErrorOutput()));
|
||||
}
|
||||
|
||||
return unserialize($process->getOutput());
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
abstract protected function doRequest(object $request);
|
||||
|
||||
/**
|
||||
* Returns the script to execute when the request must be insulated.
|
||||
*
|
||||
* @param object $request An origin request instance
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws LogicException When this abstract class is not implemented
|
||||
*/
|
||||
protected function getScript(object $request)
|
||||
{
|
||||
throw new LogicException('To insulate requests, you need to override the getScript() method.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the BrowserKit request to the origin one.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
protected function filterRequest(Request $request)
|
||||
{
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the origin response to the BrowserKit one.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
protected function filterResponse(object $response)
|
||||
{
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a crawler.
|
||||
*
|
||||
* This method returns null if the DomCrawler component is not available.
|
||||
*/
|
||||
protected function createCrawlerFromContent(string $uri, string $content, string $type): ?Crawler
|
||||
{
|
||||
if (!class_exists(Crawler::class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$crawler = new Crawler(null, $uri, null, $this->useHtml5Parser);
|
||||
$crawler->addContent($content, $type);
|
||||
|
||||
return $crawler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes back in the browser history.
|
||||
*/
|
||||
public function back(): Crawler
|
||||
{
|
||||
do {
|
||||
$request = $this->history->back();
|
||||
} while (\array_key_exists(serialize($request), $this->redirects));
|
||||
|
||||
return $this->requestFromRequest($request, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes forward in the browser history.
|
||||
*/
|
||||
public function forward(): Crawler
|
||||
{
|
||||
do {
|
||||
$request = $this->history->forward();
|
||||
} while (\array_key_exists(serialize($request), $this->redirects));
|
||||
|
||||
return $this->requestFromRequest($request, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the current browser.
|
||||
*/
|
||||
public function reload(): Crawler
|
||||
{
|
||||
return $this->requestFromRequest($this->history->current(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow redirects?
|
||||
*
|
||||
* @throws LogicException If request was not a redirect
|
||||
*/
|
||||
public function followRedirect(): Crawler
|
||||
{
|
||||
if (empty($this->redirect)) {
|
||||
throw new LogicException('The request was not redirected.');
|
||||
}
|
||||
|
||||
if (-1 !== $this->maxRedirects) {
|
||||
if ($this->redirectCount > $this->maxRedirects) {
|
||||
$this->redirectCount = 0;
|
||||
throw new LogicException(sprintf('The maximum number (%d) of redirections was reached.', $this->maxRedirects));
|
||||
}
|
||||
}
|
||||
|
||||
$request = $this->internalRequest;
|
||||
|
||||
if (\in_array($this->internalResponse->getStatusCode(), [301, 302, 303])) {
|
||||
$method = 'GET';
|
||||
$files = [];
|
||||
$content = null;
|
||||
} else {
|
||||
$method = $request->getMethod();
|
||||
$files = $request->getFiles();
|
||||
$content = $request->getContent();
|
||||
}
|
||||
|
||||
if ('GET' === strtoupper($method)) {
|
||||
// Don't forward parameters for GET request as it should reach the redirection URI
|
||||
$parameters = [];
|
||||
} else {
|
||||
$parameters = $request->getParameters();
|
||||
}
|
||||
|
||||
$server = $request->getServer();
|
||||
$server = $this->updateServerFromUri($server, $this->redirect);
|
||||
|
||||
$this->isMainRequest = false;
|
||||
|
||||
$response = $this->request($method, $this->redirect, $parameters, $files, $server, $content);
|
||||
|
||||
$this->isMainRequest = true;
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://dev.w3.org/html5/spec-preview/the-meta-element.html#attr-meta-http-equiv-refresh
|
||||
*/
|
||||
private function getMetaRefreshUrl(): ?string
|
||||
{
|
||||
$metaRefresh = $this->getCrawler()->filter('head meta[http-equiv="refresh"]');
|
||||
foreach ($metaRefresh->extract(['content']) as $content) {
|
||||
if (preg_match('/^\s*0\s*;\s*URL\s*=\s*(?|\'([^\']++)|"([^"]++)|([^\'"].*))/i', $content, $m)) {
|
||||
return str_replace("\t\r\n", '', rtrim($m[1]));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts the client.
|
||||
*
|
||||
* It flushes history and all cookies.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function restart()
|
||||
{
|
||||
$this->cookieJar->clear();
|
||||
$this->history->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a URI and converts it to absolute if it is not already absolute.
|
||||
*/
|
||||
protected function getAbsoluteUri(string $uri): string
|
||||
{
|
||||
// already absolute?
|
||||
if (str_starts_with($uri, 'http://') || str_starts_with($uri, 'https://')) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
if (!$this->history->isEmpty()) {
|
||||
$currentUri = $this->history->current()->getUri();
|
||||
} else {
|
||||
$currentUri = sprintf('http%s://%s/',
|
||||
isset($this->server['HTTPS']) ? 's' : '',
|
||||
$this->server['HTTP_HOST'] ?? 'localhost'
|
||||
);
|
||||
}
|
||||
|
||||
// protocol relative URL
|
||||
if ('' !== trim($uri, '/') && str_starts_with($uri, '//')) {
|
||||
return parse_url($currentUri, \PHP_URL_SCHEME).':'.$uri;
|
||||
}
|
||||
|
||||
// anchor or query string parameters?
|
||||
if (!$uri || '#' === $uri[0] || '?' === $uri[0]) {
|
||||
return preg_replace('/[#?].*?$/', '', $currentUri).$uri;
|
||||
}
|
||||
|
||||
if ('/' !== $uri[0]) {
|
||||
$path = parse_url($currentUri, \PHP_URL_PATH);
|
||||
|
||||
if (!str_ends_with($path, '/')) {
|
||||
$path = substr($path, 0, strrpos($path, '/') + 1);
|
||||
}
|
||||
|
||||
$uri = $path.$uri;
|
||||
}
|
||||
|
||||
return preg_replace('#^(.*?//[^/]+)\/.*$#', '$1', $currentUri).$uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request from a Request object directly.
|
||||
*
|
||||
* @param bool $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload())
|
||||
*/
|
||||
protected function requestFromRequest(Request $request, bool $changeHistory = true): Crawler
|
||||
{
|
||||
return $this->request($request->getMethod(), $request->getUri(), $request->getParameters(), $request->getFiles(), $request->getServer(), $request->getContent(), $changeHistory);
|
||||
}
|
||||
|
||||
private function updateServerFromUri(array $server, string $uri): array
|
||||
{
|
||||
$server['HTTP_HOST'] = $this->extractHost($uri);
|
||||
$scheme = parse_url($uri, \PHP_URL_SCHEME);
|
||||
$server['HTTPS'] = null === $scheme ? $server['HTTPS'] : 'https' === $scheme;
|
||||
unset($server['HTTP_IF_NONE_MATCH'], $server['HTTP_IF_MODIFIED_SINCE']);
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
private function extractHost(string $uri): ?string
|
||||
{
|
||||
$host = parse_url($uri, \PHP_URL_HOST);
|
||||
|
||||
if ($port = parse_url($uri, \PHP_URL_PORT)) {
|
||||
return $host.':'.$port;
|
||||
}
|
||||
|
||||
return $host;
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
6.4
|
||||
---
|
||||
|
||||
* Add argument `$serverParameters` to `AbstractBrowser::click()` and `AbstractBrowser::clickLink()`
|
||||
|
||||
6.3
|
||||
---
|
||||
|
||||
* Add `AbstractBrowser::useHtml5Parser()`
|
||||
|
||||
6.1
|
||||
---
|
||||
|
||||
* Add `toArray` method to `Response`
|
||||
|
||||
5.3
|
||||
---
|
||||
|
||||
* Added `jsonRequest` method to `AbstractBrowser`
|
||||
* Allowed sending a body with GET requests when a content-type is defined
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
||||
* [BC BREAK] Request parameters are now casted to string in `Request::__construct()`.
|
||||
|
||||
4.3.0
|
||||
-----
|
||||
|
||||
* Added PHPUnit constraints: `BrowserCookieValueSame` and `BrowserHasCookie`
|
||||
* Added `HttpBrowser`, an implementation of a browser with the HttpClient component
|
||||
* Renamed `Client` to `AbstractBrowser`
|
||||
* Marked `Response` final.
|
||||
* Deprecated `Response::buildHeader()`
|
||||
* Deprecated `Response::getStatus()`, use `Response::getStatusCode()` instead
|
||||
|
||||
4.2.0
|
||||
-----
|
||||
|
||||
* The method `Client::submit()` will have a new `$serverParameters` argument
|
||||
in version 5.0, not defining it is deprecated
|
||||
* Added ability to read the "samesite" attribute of cookies using `Cookie::getSameSite()`
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
|
||||
* [BC BREAK] Client will skip redirects during history navigation
|
||||
(back and forward calls) according to W3C Browsers recommendation
|
||||
|
||||
3.3.0
|
||||
-----
|
||||
|
||||
* [BC BREAK] The request method is dropped from POST to GET when the response
|
||||
status code is 301.
|
||||
|
||||
3.2.0
|
||||
-----
|
||||
|
||||
* Client HTTP user agent has been changed to 'Symfony BrowserKit'
|
||||
|
||||
2.3.0
|
||||
-----
|
||||
|
||||
* [BC BREAK] `Client::followRedirect()` won't redirect responses with
|
||||
a non-3xx Status Code and `Location` header anymore, as per
|
||||
http://tools.ietf.org/html/rfc2616#section-14.30
|
||||
|
||||
* added `Client::getInternalRequest()` and `Client::getInternalResponse()` to
|
||||
have access to the BrowserKit internal request and response objects
|
||||
|
||||
2.1.0
|
||||
-----
|
||||
|
||||
* [BC BREAK] The CookieJar internals have changed to allow cookies with the
|
||||
same name on different sub-domains/sub-paths
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
<?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\Component\BrowserKit;
|
||||
|
||||
use Symfony\Component\BrowserKit\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\BrowserKit\Exception\UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* Cookie represents an HTTP cookie.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class Cookie
|
||||
{
|
||||
/**
|
||||
* Handles dates as defined by RFC 2616 section 3.3.1, and also some other
|
||||
* non-standard, but common formats.
|
||||
*/
|
||||
private const DATE_FORMATS = [
|
||||
'D, d M Y H:i:s T',
|
||||
'D, d-M-y H:i:s T',
|
||||
'D, d-M-Y H:i:s T',
|
||||
'D, d-m-y H:i:s T',
|
||||
'D, d-m-Y H:i:s T',
|
||||
'D M j G:i:s Y',
|
||||
'D M d H:i:s Y T',
|
||||
];
|
||||
|
||||
protected $name;
|
||||
protected $value;
|
||||
protected $expires;
|
||||
protected $path;
|
||||
protected $domain;
|
||||
protected $secure;
|
||||
protected $httponly;
|
||||
protected $rawValue;
|
||||
private ?string $samesite;
|
||||
|
||||
/**
|
||||
* Sets a cookie.
|
||||
*
|
||||
* @param string $name The cookie name
|
||||
* @param string|null $value The value of the cookie
|
||||
* @param string|null $expires The time the cookie expires
|
||||
* @param string|null $path The path on the server in which the cookie will be available on
|
||||
* @param string $domain The domain that the cookie is available
|
||||
* @param bool $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client
|
||||
* @param bool $httponly The cookie httponly flag
|
||||
* @param bool $encodedValue Whether the value is encoded or not
|
||||
* @param string|null $samesite The cookie samesite attribute
|
||||
*/
|
||||
public function __construct(string $name, ?string $value, string $expires = null, string $path = null, string $domain = '', bool $secure = false, bool $httponly = true, bool $encodedValue = false, string $samesite = null)
|
||||
{
|
||||
if ($encodedValue) {
|
||||
$this->rawValue = $value ?? '';
|
||||
$this->value = urldecode($this->rawValue);
|
||||
} else {
|
||||
$this->value = $value ?? '';
|
||||
$this->rawValue = rawurlencode($this->value);
|
||||
}
|
||||
$this->name = $name;
|
||||
$this->path = empty($path) ? '/' : $path;
|
||||
$this->domain = $domain;
|
||||
$this->secure = $secure;
|
||||
$this->httponly = $httponly;
|
||||
$this->samesite = $samesite;
|
||||
|
||||
if (null !== $expires) {
|
||||
$timestampAsDateTime = \DateTimeImmutable::createFromFormat('U', $expires);
|
||||
if (false === $timestampAsDateTime) {
|
||||
throw new UnexpectedValueException(sprintf('The cookie expiration time "%s" is not valid.', $expires));
|
||||
}
|
||||
|
||||
$this->expires = $timestampAsDateTime->format('U');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTTP representation of the Cookie.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$cookie = sprintf('%s=%s', $this->name, $this->rawValue);
|
||||
|
||||
if (null !== $this->expires) {
|
||||
$dateTime = \DateTimeImmutable::createFromFormat('U', $this->expires, new \DateTimeZone('GMT'));
|
||||
$cookie .= '; expires='.str_replace('+0000', '', $dateTime->format(self::DATE_FORMATS[0]));
|
||||
}
|
||||
|
||||
if ('' !== $this->domain) {
|
||||
$cookie .= '; domain='.$this->domain;
|
||||
}
|
||||
|
||||
if ($this->path) {
|
||||
$cookie .= '; path='.$this->path;
|
||||
}
|
||||
|
||||
if ($this->secure) {
|
||||
$cookie .= '; secure';
|
||||
}
|
||||
|
||||
if ($this->httponly) {
|
||||
$cookie .= '; httponly';
|
||||
}
|
||||
|
||||
if (null !== $this->samesite) {
|
||||
$cookie .= '; samesite='.$this->samesite;
|
||||
}
|
||||
|
||||
return $cookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Cookie instance from a Set-Cookie header value.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public static function fromString(string $cookie, string $url = null): static
|
||||
{
|
||||
$parts = explode(';', $cookie);
|
||||
|
||||
if (!str_contains($parts[0], '=')) {
|
||||
throw new InvalidArgumentException(sprintf('The cookie string "%s" is not valid.', $parts[0]));
|
||||
}
|
||||
|
||||
[$name, $value] = explode('=', array_shift($parts), 2);
|
||||
|
||||
$values = [
|
||||
'name' => trim($name),
|
||||
'value' => trim($value),
|
||||
'expires' => null,
|
||||
'path' => '/',
|
||||
'domain' => '',
|
||||
'secure' => false,
|
||||
'httponly' => false,
|
||||
'passedRawValue' => true,
|
||||
'samesite' => null,
|
||||
];
|
||||
|
||||
if (null !== $url) {
|
||||
if ((false === $urlParts = parse_url($url)) || !isset($urlParts['host'])) {
|
||||
throw new InvalidArgumentException(sprintf('The URL "%s" is not valid.', $url));
|
||||
}
|
||||
|
||||
$values['domain'] = $urlParts['host'];
|
||||
$values['path'] = isset($urlParts['path']) ? substr($urlParts['path'], 0, strrpos($urlParts['path'], '/')) : '';
|
||||
}
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$part = trim($part);
|
||||
|
||||
if ('secure' === strtolower($part)) {
|
||||
// Ignore the secure flag if the original URI is not given or is not HTTPS
|
||||
if (!$url || !isset($urlParts['scheme']) || 'https' !== $urlParts['scheme']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values['secure'] = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('httponly' === strtolower($part)) {
|
||||
$values['httponly'] = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (2 === \count($elements = explode('=', $part, 2))) {
|
||||
if ('expires' === strtolower($elements[0])) {
|
||||
$elements[1] = self::parseDate($elements[1]);
|
||||
}
|
||||
|
||||
$values[strtolower($elements[0])] = $elements[1];
|
||||
}
|
||||
}
|
||||
|
||||
return new static(
|
||||
$values['name'],
|
||||
$values['value'],
|
||||
$values['expires'],
|
||||
$values['path'],
|
||||
$values['domain'],
|
||||
$values['secure'],
|
||||
$values['httponly'],
|
||||
$values['passedRawValue'],
|
||||
$values['samesite']
|
||||
);
|
||||
}
|
||||
|
||||
private static function parseDate(string $dateValue): ?string
|
||||
{
|
||||
// trim single quotes around date if present
|
||||
if (($length = \strlen($dateValue)) > 1 && "'" === $dateValue[0] && "'" === $dateValue[$length - 1]) {
|
||||
$dateValue = substr($dateValue, 1, -1);
|
||||
}
|
||||
|
||||
foreach (self::DATE_FORMATS as $dateFormat) {
|
||||
if (false !== $date = \DateTimeImmutable::createFromFormat($dateFormat, $dateValue, new \DateTimeZone('GMT'))) {
|
||||
return $date->format('U');
|
||||
}
|
||||
}
|
||||
|
||||
// attempt a fallback for unusual formatting
|
||||
if (false !== $date = date_create_immutable($dateValue, new \DateTimeZone('GMT'))) {
|
||||
return $date->format('U');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the cookie.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the cookie.
|
||||
*/
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the raw value of the cookie.
|
||||
*/
|
||||
public function getRawValue(): string
|
||||
{
|
||||
return $this->rawValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the expires time of the cookie.
|
||||
*/
|
||||
public function getExpiresTime(): ?string
|
||||
{
|
||||
return $this->expires;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path of the cookie.
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the domain of the cookie.
|
||||
*/
|
||||
public function getDomain(): string
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the secure flag of the cookie.
|
||||
*/
|
||||
public function isSecure(): bool
|
||||
{
|
||||
return $this->secure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the httponly flag of the cookie.
|
||||
*/
|
||||
public function isHttpOnly(): bool
|
||||
{
|
||||
return $this->httponly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the cookie has expired.
|
||||
*/
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return null !== $this->expires && 0 != $this->expires && $this->expires <= time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the samesite attribute of the cookie.
|
||||
*/
|
||||
public function getSameSite(): ?string
|
||||
{
|
||||
return $this->samesite;
|
||||
}
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
<?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\Component\BrowserKit;
|
||||
|
||||
use Symfony\Component\BrowserKit\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* CookieJar.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class CookieJar
|
||||
{
|
||||
protected $cookieJar = [];
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function set(Cookie $cookie)
|
||||
{
|
||||
$this->cookieJar[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a cookie by name.
|
||||
*
|
||||
* You should never use an empty domain, but if you do so,
|
||||
* this method returns the first cookie for the given name/path
|
||||
* (this behavior ensures a BC behavior with previous versions of
|
||||
* Symfony).
|
||||
*/
|
||||
public function get(string $name, string $path = '/', string $domain = null): ?Cookie
|
||||
{
|
||||
$this->flushExpiredCookies();
|
||||
|
||||
foreach ($this->cookieJar as $cookieDomain => $pathCookies) {
|
||||
if ($cookieDomain && $domain) {
|
||||
$cookieDomain = '.'.ltrim($cookieDomain, '.');
|
||||
if (!str_ends_with('.'.$domain, $cookieDomain)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pathCookies as $cookiePath => $namedCookies) {
|
||||
if (!str_starts_with($path, $cookiePath)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($namedCookies[$name])) {
|
||||
return $namedCookies[$name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a cookie by name.
|
||||
*
|
||||
* You should never use an empty domain, but if you do so,
|
||||
* all cookies for the given name/path expire (this behavior
|
||||
* ensures a BC behavior with previous versions of Symfony).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function expire(string $name, ?string $path = '/', string $domain = null)
|
||||
{
|
||||
$path ??= '/';
|
||||
|
||||
if (empty($domain)) {
|
||||
// an empty domain means any domain
|
||||
// this should never happen but it allows for a better BC
|
||||
$domains = array_keys($this->cookieJar);
|
||||
} else {
|
||||
$domains = [$domain];
|
||||
}
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
unset($this->cookieJar[$domain][$path][$name]);
|
||||
|
||||
if (empty($this->cookieJar[$domain][$path])) {
|
||||
unset($this->cookieJar[$domain][$path]);
|
||||
|
||||
if (empty($this->cookieJar[$domain])) {
|
||||
unset($this->cookieJar[$domain]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all the cookies from the jar.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
$this->cookieJar = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cookie jar from a response Set-Cookie headers.
|
||||
*
|
||||
* @param string[] $setCookies Set-Cookie headers from an HTTP response
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function updateFromSetCookie(array $setCookies, string $uri = null)
|
||||
{
|
||||
$cookies = [];
|
||||
|
||||
foreach ($setCookies as $cookie) {
|
||||
foreach (explode(',', $cookie) as $i => $part) {
|
||||
if (0 === $i || preg_match('/^(?P<token>\s*[0-9A-Za-z!#\$%\&\'\*\+\-\.^_`\|~]+)=/', $part)) {
|
||||
$cookies[] = ltrim($part);
|
||||
} else {
|
||||
$cookies[\count($cookies) - 1] .= ','.$part;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($cookies as $cookie) {
|
||||
try {
|
||||
$this->set(Cookie::fromString($cookie, $uri));
|
||||
} catch (InvalidArgumentException) {
|
||||
// invalid cookies are just ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cookie jar from a Response object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function updateFromResponse(Response $response, string $uri = null)
|
||||
{
|
||||
$this->updateFromSetCookie($response->getHeader('Set-Cookie', false), $uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns not yet expired cookies.
|
||||
*
|
||||
* @return Cookie[]
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
$this->flushExpiredCookies();
|
||||
|
||||
$flattenedCookies = [];
|
||||
foreach ($this->cookieJar as $path) {
|
||||
foreach ($path as $cookies) {
|
||||
foreach ($cookies as $cookie) {
|
||||
$flattenedCookies[] = $cookie;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $flattenedCookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns not yet expired cookie values for the given URI.
|
||||
*/
|
||||
public function allValues(string $uri, bool $returnsRawValue = false): array
|
||||
{
|
||||
$this->flushExpiredCookies();
|
||||
|
||||
$parts = array_replace(['path' => '/'], parse_url($uri));
|
||||
$cookies = [];
|
||||
foreach ($this->cookieJar as $domain => $pathCookies) {
|
||||
if ($domain) {
|
||||
$domain = '.'.ltrim($domain, '.');
|
||||
if (!str_ends_with('.'.$parts['host'], $domain)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pathCookies as $path => $namedCookies) {
|
||||
if (!str_starts_with($parts['path'], $path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($namedCookies as $cookie) {
|
||||
if ($cookie->isSecure() && 'https' !== $parts['scheme']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cookies[$cookie->getName()] = $returnsRawValue ? $cookie->getRawValue() : $cookie->getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns not yet expired raw cookie values for the given URI.
|
||||
*/
|
||||
public function allRawValues(string $uri): array
|
||||
{
|
||||
return $this->allValues($uri, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all expired cookies.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function flushExpiredCookies()
|
||||
{
|
||||
foreach ($this->cookieJar as $domain => $pathCookies) {
|
||||
foreach ($pathCookies as $path => $namedCookies) {
|
||||
foreach ($namedCookies as $name => $cookie) {
|
||||
if ($cookie->isExpired()) {
|
||||
unset($this->cookieJar[$domain][$path][$name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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\Component\BrowserKit\Exception;
|
||||
|
||||
class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Component\BrowserKit\Exception;
|
||||
|
||||
/**
|
||||
* Base ExceptionInterface for the BrowserKit component.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface ExceptionInterface extends \Throwable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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\Component\BrowserKit\Exception;
|
||||
|
||||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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\Component\BrowserKit\Exception;
|
||||
|
||||
class JsonException extends \JsonException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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\Component\BrowserKit\Exception;
|
||||
|
||||
class LogicException extends \LogicException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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\Component\BrowserKit\Exception;
|
||||
|
||||
class RuntimeException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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\Component\BrowserKit\Exception;
|
||||
|
||||
class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
<?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\Component\BrowserKit;
|
||||
|
||||
use Symfony\Component\BrowserKit\Exception\LogicException;
|
||||
|
||||
/**
|
||||
* History.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class History
|
||||
{
|
||||
protected $stack = [];
|
||||
protected $position = -1;
|
||||
|
||||
/**
|
||||
* Clears the history.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
$this->stack = [];
|
||||
$this->position = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Request to the history.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add(Request $request)
|
||||
{
|
||||
$this->stack = \array_slice($this->stack, 0, $this->position + 1);
|
||||
$this->stack[] = clone $request;
|
||||
$this->position = \count($this->stack) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the history is empty.
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return 0 === \count($this->stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes back in the history.
|
||||
*
|
||||
* @throws LogicException if the stack is already on the first page
|
||||
*/
|
||||
public function back(): Request
|
||||
{
|
||||
if ($this->position < 1) {
|
||||
throw new LogicException('You are already on the first page.');
|
||||
}
|
||||
|
||||
return clone $this->stack[--$this->position];
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes forward in the history.
|
||||
*
|
||||
* @throws LogicException if the stack is already on the last page
|
||||
*/
|
||||
public function forward(): Request
|
||||
{
|
||||
if ($this->position > \count($this->stack) - 2) {
|
||||
throw new LogicException('You are already on the last page.');
|
||||
}
|
||||
|
||||
return clone $this->stack[++$this->position];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current element in the history.
|
||||
*
|
||||
* @throws LogicException if the stack is empty
|
||||
*/
|
||||
public function current(): Request
|
||||
{
|
||||
if (-1 === $this->position) {
|
||||
throw new LogicException('The page history is empty.');
|
||||
}
|
||||
|
||||
return clone $this->stack[$this->position];
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
<?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\Component\BrowserKit;
|
||||
|
||||
use Symfony\Component\BrowserKit\Exception\LogicException;
|
||||
use Symfony\Component\HttpClient\HttpClient;
|
||||
use Symfony\Component\Mime\Part\AbstractPart;
|
||||
use Symfony\Component\Mime\Part\DataPart;
|
||||
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
|
||||
use Symfony\Component\Mime\Part\TextPart;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* An implementation of a browser using the HttpClient component
|
||||
* to make real HTTP requests.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class HttpBrowser extends AbstractBrowser
|
||||
{
|
||||
private HttpClientInterface $client;
|
||||
|
||||
public function __construct(HttpClientInterface $client = null, History $history = null, CookieJar $cookieJar = null)
|
||||
{
|
||||
if (!$client && !class_exists(HttpClient::class)) {
|
||||
throw new LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__));
|
||||
}
|
||||
|
||||
$this->client = $client ?? HttpClient::create();
|
||||
|
||||
parent::__construct([], $history, $cookieJar);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*/
|
||||
protected function doRequest(object $request): Response
|
||||
{
|
||||
$headers = $this->getHeaders($request);
|
||||
[$body, $extraHeaders] = $this->getBodyAndExtraHeaders($request, $headers);
|
||||
|
||||
$response = $this->client->request($request->getMethod(), $request->getUri(), [
|
||||
'headers' => array_merge($headers, $extraHeaders),
|
||||
'body' => $body,
|
||||
'max_redirects' => 0,
|
||||
]);
|
||||
|
||||
return new Response($response->getContent(false), $response->getStatusCode(), $response->getHeaders(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array [$body, $headers]
|
||||
*/
|
||||
private function getBodyAndExtraHeaders(Request $request, array $headers): array
|
||||
{
|
||||
if (\in_array($request->getMethod(), ['GET', 'HEAD']) && !isset($headers['content-type'])) {
|
||||
return ['', []];
|
||||
}
|
||||
|
||||
if (!class_exists(AbstractPart::class)) {
|
||||
throw new LogicException('You cannot pass non-empty bodies as the Mime component is not installed. Try running "composer require symfony/mime".');
|
||||
}
|
||||
|
||||
if (null !== $content = $request->getContent()) {
|
||||
if (isset($headers['content-type'])) {
|
||||
return [$content, []];
|
||||
}
|
||||
|
||||
$part = new TextPart($content, 'utf-8', 'plain', '8bit');
|
||||
|
||||
return [$part->bodyToString(), $part->getPreparedHeaders()->toArray()];
|
||||
}
|
||||
|
||||
$fields = $request->getParameters();
|
||||
|
||||
if ($uploadedFiles = $this->getUploadedFiles($request->getFiles())) {
|
||||
$part = new FormDataPart(array_replace_recursive($fields, $uploadedFiles));
|
||||
|
||||
return [$part->bodyToIterable(), $part->getPreparedHeaders()->toArray()];
|
||||
}
|
||||
|
||||
if (!$fields) {
|
||||
return ['', []];
|
||||
}
|
||||
|
||||
array_walk_recursive($fields, $caster = static function (&$v) use (&$caster) {
|
||||
if (\is_object($v)) {
|
||||
if ($vars = get_object_vars($v)) {
|
||||
array_walk_recursive($vars, $caster);
|
||||
$v = $vars;
|
||||
} elseif (method_exists($v, '__toString')) {
|
||||
$v = (string) $v;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return [http_build_query($fields, '', '&'), ['Content-Type' => 'application/x-www-form-urlencoded']];
|
||||
}
|
||||
|
||||
protected function getHeaders(Request $request): array
|
||||
{
|
||||
$headers = [];
|
||||
foreach ($request->getServer() as $key => $value) {
|
||||
$key = strtolower(str_replace('_', '-', $key));
|
||||
$contentHeaders = ['content-length' => true, 'content-md5' => true, 'content-type' => true];
|
||||
if (str_starts_with($key, 'http-')) {
|
||||
$headers[substr($key, 5)] = $value;
|
||||
} elseif (isset($contentHeaders[$key])) {
|
||||
// CONTENT_* are not prefixed with HTTP_
|
||||
$headers[$key] = $value;
|
||||
}
|
||||
}
|
||||
$cookies = [];
|
||||
foreach ($this->getCookieJar()->allRawValues($request->getUri()) as $name => $value) {
|
||||
$cookies[] = $name.'='.$value;
|
||||
}
|
||||
if ($cookies) {
|
||||
$headers['cookie'] = implode('; ', $cookies);
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively go through the list. If the file has a tmp_name, convert it to a DataPart.
|
||||
* Keep the original hierarchy.
|
||||
*/
|
||||
private function getUploadedFiles(array $files): array
|
||||
{
|
||||
$uploadedFiles = [];
|
||||
foreach ($files as $name => $file) {
|
||||
if (!\is_array($file)) {
|
||||
return $uploadedFiles;
|
||||
}
|
||||
if (!isset($file['tmp_name'])) {
|
||||
$uploadedFiles[$name] = $this->getUploadedFiles($file);
|
||||
}
|
||||
if (isset($file['tmp_name'])) {
|
||||
$uploadedFiles[$name] = DataPart::fromPath($file['tmp_name'], $file['name']);
|
||||
}
|
||||
}
|
||||
|
||||
return $uploadedFiles;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2004-present 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.
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
BrowserKit Component
|
||||
====================
|
||||
|
||||
The BrowserKit component simulates the behavior of a web browser, allowing you
|
||||
to make requests, click on links and submit forms programmatically.
|
||||
|
||||
The component comes with a concrete implementation that uses the HttpClient
|
||||
component to make real HTTP requests.
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Documentation](https://symfony.com/doc/current/components/browser_kit/introduction.html)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
<?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\Component\BrowserKit;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class Request
|
||||
{
|
||||
protected $uri;
|
||||
protected $method;
|
||||
protected $parameters;
|
||||
protected $files;
|
||||
protected $cookies;
|
||||
protected $server;
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* @param string $uri The request URI
|
||||
* @param string $method The HTTP method request
|
||||
* @param array $parameters The request parameters
|
||||
* @param array $files An array of uploaded files
|
||||
* @param array $cookies An array of cookies
|
||||
* @param array $server An array of server parameters
|
||||
* @param string $content The raw body data
|
||||
*/
|
||||
public function __construct(string $uri, string $method, array $parameters = [], array $files = [], array $cookies = [], array $server = [], string $content = null)
|
||||
{
|
||||
$this->uri = $uri;
|
||||
$this->method = $method;
|
||||
|
||||
array_walk_recursive($parameters, static function (&$value) {
|
||||
$value = (string) $value;
|
||||
});
|
||||
|
||||
$this->parameters = $parameters;
|
||||
$this->files = $files;
|
||||
$this->cookies = $cookies;
|
||||
$this->server = $server;
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request URI.
|
||||
*/
|
||||
public function getUri(): string
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request HTTP method.
|
||||
*/
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request parameters.
|
||||
*/
|
||||
public function getParameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request server files.
|
||||
*/
|
||||
public function getFiles(): array
|
||||
{
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request cookies.
|
||||
*/
|
||||
public function getCookies(): array
|
||||
{
|
||||
return $this->cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request server parameters.
|
||||
*/
|
||||
public function getServer(): array
|
||||
{
|
||||
return $this->server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request raw body data.
|
||||
*/
|
||||
public function getContent(): ?string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<?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\Component\BrowserKit;
|
||||
|
||||
use Symfony\Component\BrowserKit\Exception\JsonException;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class Response
|
||||
{
|
||||
private string $content;
|
||||
private int $status;
|
||||
private array $headers;
|
||||
private array $jsonData;
|
||||
|
||||
/**
|
||||
* The headers array is a set of key/value pairs. If a header is present multiple times
|
||||
* then the value is an array of all the values.
|
||||
*
|
||||
* @param string $content The content of the response
|
||||
* @param int $status The response status code (302 "Found" by default)
|
||||
* @param array $headers An array of headers
|
||||
*/
|
||||
public function __construct(string $content = '', int $status = 200, array $headers = [])
|
||||
{
|
||||
$this->content = $content;
|
||||
$this->status = $status;
|
||||
$this->headers = $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the response object to string containing all headers and the response content.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$headers = '';
|
||||
foreach ($this->headers as $name => $value) {
|
||||
if (\is_string($value)) {
|
||||
$headers .= sprintf("%s: %s\n", $name, $value);
|
||||
} else {
|
||||
foreach ($value as $headerValue) {
|
||||
$headers .= sprintf("%s: %s\n", $name, $headerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $headers."\n".$this->content;
|
||||
}
|
||||
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|array|null The first header value if $first is true, an array of values otherwise
|
||||
*/
|
||||
public function getHeader(string $header, bool $first = true): string|array|null
|
||||
{
|
||||
$normalizedHeader = str_replace('-', '_', strtolower($header));
|
||||
foreach ($this->headers as $key => $value) {
|
||||
if (str_replace('-', '_', strtolower($key)) === $normalizedHeader) {
|
||||
if ($first) {
|
||||
return \is_array($value) ? (\count($value) ? $value[0] : '') : $value;
|
||||
}
|
||||
|
||||
return \is_array($value) ? $value : [$value];
|
||||
}
|
||||
}
|
||||
|
||||
return $first ? null : [];
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
if (isset($this->jsonData)) {
|
||||
return $this->jsonData;
|
||||
}
|
||||
|
||||
try {
|
||||
$content = json_decode($this->content, true, flags: \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $e) {
|
||||
throw new JsonException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
|
||||
if (!\is_array($content)) {
|
||||
throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content)));
|
||||
}
|
||||
|
||||
return $this->jsonData = $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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\Component\BrowserKit\Test\Constraint;
|
||||
|
||||
use PHPUnit\Framework\Constraint\Constraint;
|
||||
use Symfony\Component\BrowserKit\AbstractBrowser;
|
||||
|
||||
final class BrowserCookieValueSame extends Constraint
|
||||
{
|
||||
private string $name;
|
||||
private string $value;
|
||||
private bool $raw;
|
||||
private string $path;
|
||||
private ?string $domain;
|
||||
|
||||
public function __construct(string $name, string $value, bool $raw = false, string $path = '/', string $domain = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->path = $path;
|
||||
$this->domain = $domain;
|
||||
$this->value = $value;
|
||||
$this->raw = $raw;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
$str = sprintf('has cookie "%s"', $this->name);
|
||||
if ('/' !== $this->path) {
|
||||
$str .= sprintf(' with path "%s"', $this->path);
|
||||
}
|
||||
if ($this->domain) {
|
||||
$str .= sprintf(' for domain "%s"', $this->domain);
|
||||
}
|
||||
$str .= sprintf(' with %svalue "%s"', $this->raw ? 'raw ' : '', $this->value);
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AbstractBrowser $browser
|
||||
*/
|
||||
protected function matches($browser): bool
|
||||
{
|
||||
$cookie = $browser->getCookieJar()->get($this->name, $this->path, $this->domain);
|
||||
if (!$cookie) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->value === ($this->raw ? $cookie->getRawValue() : $cookie->getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AbstractBrowser $browser
|
||||
*/
|
||||
protected function failureDescription($browser): string
|
||||
{
|
||||
return 'the Browser '.$this->toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?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\Component\BrowserKit\Test\Constraint;
|
||||
|
||||
use PHPUnit\Framework\Constraint\Constraint;
|
||||
use Symfony\Component\BrowserKit\AbstractBrowser;
|
||||
|
||||
final class BrowserHasCookie extends Constraint
|
||||
{
|
||||
private string $name;
|
||||
private string $path;
|
||||
private ?string $domain;
|
||||
|
||||
public function __construct(string $name, string $path = '/', string $domain = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->path = $path;
|
||||
$this->domain = $domain;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
$str = sprintf('has cookie "%s"', $this->name);
|
||||
if ('/' !== $this->path) {
|
||||
$str .= sprintf(' with path "%s"', $this->path);
|
||||
}
|
||||
if ($this->domain) {
|
||||
$str .= sprintf(' for domain "%s"', $this->domain);
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AbstractBrowser $browser
|
||||
*/
|
||||
protected function matches($browser): bool
|
||||
{
|
||||
return null !== $browser->getCookieJar()->get($this->name, $this->path, $this->domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AbstractBrowser $browser
|
||||
*/
|
||||
protected function failureDescription($browser): string
|
||||
{
|
||||
return 'the Browser '.$this->toString();
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "symfony/browser-kit",
|
||||
"type": "library",
|
||||
"description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
|
||||
"keywords": [],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"symfony/dom-crawler": "^5.4|^6.0|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/css-selector": "^5.4|^6.0|^7.0",
|
||||
"symfony/http-client": "^5.4|^6.0|^7.0",
|
||||
"symfony/mime": "^5.4|^6.0|^7.0",
|
||||
"symfony/process": "^5.4|^6.0|^7.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\BrowserKit\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
6.3
|
||||
-----
|
||||
|
||||
* Add support for `:scope`
|
||||
|
||||
4.4.0
|
||||
-----
|
||||
|
||||
* Added support for `*:only-of-type`
|
||||
|
||||
2.8.0
|
||||
-----
|
||||
|
||||
* Added the `CssSelectorConverter` class as a non-static API for the component.
|
||||
* Deprecated the `CssSelector` static API of the component.
|
||||
|
||||
2.1.0
|
||||
-----
|
||||
|
||||
* none
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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\Component\CssSelector;
|
||||
|
||||
use Symfony\Component\CssSelector\Parser\Shortcut\ClassParser;
|
||||
use Symfony\Component\CssSelector\Parser\Shortcut\ElementParser;
|
||||
use Symfony\Component\CssSelector\Parser\Shortcut\EmptyStringParser;
|
||||
use Symfony\Component\CssSelector\Parser\Shortcut\HashParser;
|
||||
use Symfony\Component\CssSelector\XPath\Extension\HtmlExtension;
|
||||
use Symfony\Component\CssSelector\XPath\Translator;
|
||||
|
||||
/**
|
||||
* CssSelectorConverter is the main entry point of the component and can convert CSS
|
||||
* selectors to XPath expressions.
|
||||
*
|
||||
* @author Christophe Coevoet <stof@notk.org>
|
||||
*/
|
||||
class CssSelectorConverter
|
||||
{
|
||||
private Translator $translator;
|
||||
private array $cache;
|
||||
|
||||
private static array $xmlCache = [];
|
||||
private static array $htmlCache = [];
|
||||
|
||||
/**
|
||||
* @param bool $html Whether HTML support should be enabled. Disable it for XML documents
|
||||
*/
|
||||
public function __construct(bool $html = true)
|
||||
{
|
||||
$this->translator = new Translator();
|
||||
|
||||
if ($html) {
|
||||
$this->translator->registerExtension(new HtmlExtension($this->translator));
|
||||
$this->cache = &self::$htmlCache;
|
||||
} else {
|
||||
$this->cache = &self::$xmlCache;
|
||||
}
|
||||
|
||||
$this->translator
|
||||
->registerParserShortcut(new EmptyStringParser())
|
||||
->registerParserShortcut(new ElementParser())
|
||||
->registerParserShortcut(new ClassParser())
|
||||
->registerParserShortcut(new HashParser())
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a CSS expression to its XPath equivalent.
|
||||
*
|
||||
* Optionally, a prefix can be added to the resulting XPath
|
||||
* expression with the $prefix parameter.
|
||||
*/
|
||||
public function toXPath(string $cssExpr, string $prefix = 'descendant-or-self::'): string
|
||||
{
|
||||
return $this->cache[$prefix][$cssExpr] ??= $this->translator->cssToXPath($cssExpr, $prefix);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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\Component\CssSelector\Exception;
|
||||
|
||||
/**
|
||||
* Interface for exceptions.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
interface ExceptionInterface extends \Throwable
|
||||
{
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user